Skip to content

Commit

Permalink
Implement part of signature process and add Windows support
Browse files Browse the repository at this point in the history
  • Loading branch information
Dadoum committed Sep 29, 2023
1 parent fbc491a commit 10f3bc8
Show file tree
Hide file tree
Showing 25 changed files with 1,535 additions and 149 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
compiler: ldc-1.33.0

- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y libz-dev elfutils
run: sudo apt-get update && sudo apt-get install -y libz-dev elfutils gcc-i686-linux-gnu

- name: Write version file
run: echo 'module version_string; enum versionStr = "Official build, branch ${{ github.ref_name }}, commit ${{ github.sha }}";' > source/version_string.d
Expand Down
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ Actions waiting for you — actually, that's more complicated than that, because
architectures automated builds relied on GDC cross-compilers, but GDC does not compile the
code currently :(.

Currently, not every built platform is functional, and Linux support is the main focus.

Dependencies (runtime): libimobiledevice, libplist-2.X (I attempted to support both 2.2
and 2.3). OpenSSL is currently also needed, but I plan to remove that dependency as soon
as possible (only networking is requiring it).

## How do I build it myself?

Get a recent version `ldc2` or `dmd` installed (an installation script is available on
Expand All @@ -49,11 +55,37 @@ In general, never trust anyone to handle your credentials, even more if it is in
closed-source obfuscated application (as-if there were something to hide there ^^).

[^1]: You may wonder if that would allow full iOS application development on Linux, and
the answer is yes! You can compile a native iOS app on Linux with
[theos](https://theos.dev), and then package it into an ipa with `make package ipa` to
the answer is yes! You can compile a native iOS app on Linux with
[theos](https://theos.dev), and then package it into an ipa with `PACKAGE_FORMAT = ipa` to
eventually install it with Sideloader on a real device (or maybe even an emulated one
in the future!) and debug it (with `idevicedebug`). _(TODO: add an option to add the
entitlement for debugging)_
in the future!) and debug it (with `idevicedebug` or remote `lldb`). _(TODO: add an option
to add the entitlement for debugging)_

## Notes on platform support

### Linux

Linux version currently only features a GTK frontend. I made one because I was already
familiar with GTK+. I made it in plain code because I really dislike GTK Builder's XML.

### Windows

Windows version uses a Win32 frontend. On Windows 11, it looks old and legacy. The current
state of GUI libraries on Windows is rather unsatisfying: we have one old well-supported
library across the major versions of Windows, Win32, the one I am using, and then we have
a lot of unsuccessful attempts to supplant it, and finally we have WinUI 3, which looks
good but has no bindings whatsoever (WinRT is supported in C# and C++ only, I would have
to make the bindings myself, which I could do but would take some effort), and is not
supported everywhere (Windows 10+ only). This is why I used DFL, which is a Windows Forms
like wrapper of Win32 APIs.

**Requirements:** a USB muxer, which is generally iTunes or anything
distirbuting AppleMobileDevice. netmuxd should work too. OpenSSL is also required,
unfortunately.

### macOS

It doesn't work yet. Even less with Apple Silicon.

## Acknowledgements and references

Expand Down
191 changes: 189 additions & 2 deletions cli/frontend.d
Original file line number Diff line number Diff line change
@@ -1,14 +1,201 @@
module frontend;

import slf4d;
import slf4d.default_provider;
import slf4d.provider;

import app.frontend;

class CLIFrontend: Frontend {
int run(string configurationPath, string[] args) {
version = X509;
shared class CLIFrontend: Frontend {
override string configurationPath() {
getLogger().error("Not implemented.");
return "";
}

override int run(string[] args) {
import std.algorithm;
import std.array;
import std.datetime;
import std.path;
import std.typecons;
import file = std.file;

import slf4d;

import plist;

import imobiledevice;

import server.developersession;

import sideload.bundle;
import sideload.application;
import sideload.certificateidentity;

import main;

auto log = getLogger();
// auto app = new Application("~/Téléchargements/SideStore.ipa".expandTilde());
auto app = new Application("~/Téléchargements/appux/packages/com.yourcompany.appux_0.0.1-1+debug.ipa".expandTilde());

// create a certificate for the developer
// auto certIdentity = new CertificateIdentity(configurationPath, null);

auto team = DeveloperTeam("iOS devel.", "TEAMID");

// check if we registered an app id for it (if not create it)
string mainAppBundleId = app.bundleIdentifier();
string mainAppIdStr = mainAppBundleId ~ "." ~ team.teamId;
string mainAppName = app.bundleName();

app.appId = mainAppIdStr;
foreach (plugin; app.plugIns) {
string pluginBundleIdentifier = plugin.bundleIdentifier();
assertBundle(
pluginBundleIdentifier.startsWith(mainAppBundleId) &&
pluginBundleIdentifier.length > mainAppBundleId.length,
"Plug-ins are not formed with the main app bundle identifier"
);
plugin.appId = mainAppIdStr ~ pluginBundleIdentifier[mainAppBundleId.length..$];
}
Bundle[] bundlesNeeded = [cast(Bundle) app] ~ app.plugIns;

// Search which App IDs have to be registered (we don't want to start registering App IDs if we don't
// have enough of them to register them all!! otherwise we will waste their precious App IDs)
auto appIdsToRegister = bundlesNeeded;

foreach (bundle; appIdsToRegister) {
log.infoF!"Creating App ID `%s`..."(bundle.appId);
}

auto bundles = bundlesNeeded.map!((bundle) => tuple(bundle, AppId("", bundle.appId, "ApplicationName", null, DateTime()))).array();
auto mainBundle = bundles[0];

// sign the app with all the retrieved material!
foreach (bundlePair; bundles) {
import core.sys.darwin.mach.loader;
import sideload.macho;

auto bundle = bundlePair[0];
auto appId = bundlePair[1];

auto bundlePath = bundle.bundleDir;

// set the bundle identifier to the one with the team id to match the provisioning profile
bundle.appInfo["CFBundleIdentifier"] = appId.identifier.pl;

string executablePath = bundlePath.buildPath(bundle.appInfo["CFBundleExecutable"].str().native());
MachO[] machOs = MachO.parse(cast(ubyte[]) file.read(executablePath), Architecture.aarch64);
log.infoF!"Mach-Os: %s"(machOs);

import cms.cms_dec;
auto provisioningProfilePlist = Plist.fromMemory(dataFromCMS(
cast(ubyte[]) file.read("/home/dadoum/Téléchargements/com.SideStore.SideStore.MK7ZNLPN7B.AltWidget.mobileprovision")
));

auto entitlements = provisioningProfilePlist["Entitlements"].dict;

foreach (machO; machOs) {
auto execSegBase = machO.execSegBase;
auto execSegLimit = machO.execSegLimit;
auto execFlags = machO.execFlags(entitlements);

auto embeddedSignature = new EmbeddedSignature();
embeddedSignature ~= cast(Blob[]) [
new CodeDirectoryBlob(new SHA1(), team.teamId, execSegBase, execSegLimit, execFlags),
new RequirementsBlob(),
new EntitlementsBlob(entitlements.toXml()),
new DerEntitlementsBlob(entitlements),
new CodeDirectoryBlob(new SHA2(), team.teamId, execSegBase, execSegLimit, execFlags),
new SignatureBlob(),
];
machO.replaceCodeSignature(embeddedSignature);
}

file.write("/home/dadoum/Téléchargements/Salut", makeMachO(machOs));

/*
// fabricate entitlements file!!!
string executablePath = bundlePath.buildPath(bundle.appInfo["CFBundleExecutable"].str().native());
MachO[] machOs = MachO.parse(cast(ubyte[]) file.read(executablePath));
// here is the real signing logic:
// we will sign each of the mach-o contained
// and rebuild them.
foreach (machO; machOs) {
linkedit_data_command signatureCommand = void;
symtab_command symtabCommand = void;
foreach (command; machO.loadCommands) {
switch (command.cmd) {
case LC_CODE_SIGNATURE:
signatureCommand = command.read!linkedit_data_command(0);
break;
case LC_SYMTAB:
symtabCommand = command.read!symtab_command(0);
break;
default:
break;
}
}
string entitlementsStr = "";
if (signatureCommand.cmd) {
// get entitlements!!
SuperBlobHeader superBlob = machO.read!SuperBlobHeader(signatureCommand.dataoff);
auto blobArrayStart = signatureCommand.dataoff + SuperBlobHeader.sizeof;
auto blobArrayEnd = blobArrayStart + superBlob.count * BlobIndex.sizeof;
for (auto blobArrayIndex = blobArrayStart; blobArrayIndex < blobArrayEnd; blobArrayIndex += BlobIndex.sizeof) {
auto currentBlob = machO.read!BlobIndex(signatureCommand.dataoff + blobArrayIndex);
if (currentBlob.type == CSSLOT_ENTITLEMENTS) {
Blob entitlementsBlob = machO.read!Blob(currentBlob.offset);
entitlementsStr = cast(string) machO.data[signatureCommand.dataoff + currentBlob.offset + Blob.sizeof..signatureCommand.dataoff + currentBlob.offset + entitlementsBlob.length];
if (entitlementsStr.length) {
log.infoF!"Entitlements: %s"(entitlementsStr);
}
}
}
}
}
/*
auto entitlements = Plist.fromMemory(cast(ubyte[]) entitlementsStr).dict();
entitlements["application-identifier"] = appId.identifier;
entitlements["com.apple.developer.team-identifier"] = team.teamId;
// create app groups for it if needed
if (auto bundleAppGroups = "com.apple.security.application-groups" in entitlements) {
if (!appId.features[AppIdFeatures.appGroup].boolean().native()) {
// We need to enable app groups then !
log.infoF!"Updating the app id %s to enable app groups."(appId.identifier);
appId.features = developer.updateAppId!iOS(team, appId, dict(AppIdFeatures.appGroup, true)).unwrap();
}
auto appGroups = developer.listApplicationGroups!iOS(team).unwrap();
foreach (bundleAppGroup; bundleAppGroups.array()) {
string bundleGroupId = bundleAppGroup.str().native();
auto matchingAppGroups = appGroups.find!((appGroup) => appGroup.identifier == bundleGroupId).array();
ApplicationGroup appGroup;
if (matchingAppGroups.empty) {
log.infoF!"Creating the app group %s."(bundleGroupId);
appGroup = developer.addApplicationGroup!iOS(team, bundleGroupId, mainAppName).unwrap();
} else {
appGroup = matchingAppGroups[0];
}
}
}
// Write the updated Info.plist with the new bundle identifier.
file.write(bundlePath.buildPath("Info.plist"), bundle.appInfo.toXml());
file.write(bundlePath.buildPath("embedded.mobileprovision"), profile.encodedProfile);
// */
}

return 0;
}
}

Frontend makeFrontend() => new CLIFrontend();
shared(LoggingProvider) makeLoggingProvider(Level rootLoggingLevel) => new shared DefaultProvider(true, rootLoggingLevel);
21 changes: 19 additions & 2 deletions dub.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
},
"dynamic-loader": {
"repository": "git+https://github.com/Dadoum/dynamicloader.git",
"version": "334fc41b6491ccba10f6c594f56544862554581f"
"version": "32355c1aae76e0a89c123bc082ec2df8cddc2b0f"
},
"plist-d": {
"repository": "git+https://github.com/Dadoum/libplist-d.git",
"version": "a0f5b393e517ef236263184d9002670b82a00401"
"version": "f04b7ebf2623ff5e3ad608910c3a3ac56639a21f"
},
"provision": {
"repository": "git+https://github.com/Dadoum/Provision.git",
Expand Down Expand Up @@ -73,6 +73,23 @@
"-defaultlib=:libgphobos.a"
]
},
{
"name": "windows-winforms",
"platforms": ["windows"],
"targetType": "executable",

"sourcePaths": [
"windows/common/",
"windows/winforms/"
],

"dependencies": {
"dfl": {
"repository": "git+https://github.com/Rayerd/dfl.git",
"version": "5a3cba4d13d4d962b707d713be6d77c2ccc52a42"
}
}
},
{
"name": "cli",
"targetType": "executable",
Expand Down
7 changes: 5 additions & 2 deletions dub.selections.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@
"botan": {"version":"a0c206639debc7e5726c02fc399267e6a33571a0","repository":"git+https://github.com/Dadoum/botan.git"},
"botan-math": "1.0.4",
"cachetools": "0.4.1",
"dfl": {"version":"5a3cba4d13d4d962b707d713be6d77c2ccc52a42","repository":"git+https://github.com/Rayerd/dfl.git"},
"dlangui": "0.10.4",
"dsfml": "2.1.1",
"dxml": "0.4.3",
"dynamic-loader": {"version":"334fc41b6491ccba10f6c594f56544862554581f","repository":"git+https://github.com/Dadoum/dynamicloader.git"},
"dynamic-loader": {"version":"32355c1aae76e0a89c123bc082ec2df8cddc2b0f","repository":"git+https://github.com/Dadoum/dynamicloader.git"},
"glx-d": "1.1.0",
"gtk_d": "1.0.3",
"icontheme": "1.2.3",
"inilike": "1.2.2",
"isfreedesktop": "0.1.1",
"memutils": "1.0.9",
"plist": "~master",
"plist-d": {"version":"a0f5b393e517ef236263184d9002670b82a00401","repository":"git+https://github.com/Dadoum/libplist-d.git"},
"plist-d": {"version":"f04b7ebf2623ff5e3ad608910c3a3ac56639a21f","repository":"git+https://github.com/Dadoum/libplist-d.git"},
"provision": {"version":"a007cb290da1a21e231a1a4a335a047a151ca24f","repository":"git+https://github.com/Dadoum/Provision.git"},
"requests": "2.0.9",
"silly": "1.2.0-dev.2",
"slf4d": "2.4.2",
"test_allocator": "0.3.4",
"undead": "1.1.8",
"unit-threaded": "0.10.8",
"x11": "1.0.21",
"xdgpaths": "0.2.5"
Expand Down
27 changes: 24 additions & 3 deletions linux/gtk/frontend.d
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
module frontend;

import file = std.file;
import std.path;
import std.process;

import glib.MessageLog;

import slf4d;
import slf4d.default_provider;
import slf4d.provider;

import constants;
import utils;

import app.frontend;
import ui.sideloadergtkapplication;

class GtkFrontend: Frontend {
shared class GtkFrontend: Frontend {
string _configurationPath;

this() {
MessageLog.logSetHandler(null, GLogLevelFlags.LEVEL_MASK | GLogLevelFlags.FLAG_FATAL | GLogLevelFlags.FLAG_RECURSION,
(logDomainC, logLevel, messageC, userData) {
Expand All @@ -32,11 +43,21 @@ class GtkFrontend: Frontend {
import std.string;
logger.log(level, cast(string) messageC.fromStringz(), null, cast(string) logDomainC.fromStringz(), "");
}, null);

_configurationPath = environment.get("XDG_CONFIG_DIR")
.orDefault("~/.config")
.buildPath(applicationName)
.expandTilde();
}

override string configurationPath() {
return _configurationPath;
}

int run(string configurationPath, string[] args) {
return new SideloaderGtkApplication(configurationPath).run(args);
override int run(string[] args) {
return new SideloaderGtkApplication(_configurationPath).run(args);
}
}

Frontend makeFrontend() => new GtkFrontend();
shared(LoggingProvider) makeLoggingProvider(Level rootLoggingLevel) => new shared DefaultProvider(true, rootLoggingLevel);
Loading

0 comments on commit 10f3bc8

Please sign in to comment.