diff --git a/RootHelper/devmode.h b/RootHelper/devmode.h new file mode 100644 index 0000000..9343b61 --- /dev/null +++ b/RootHelper/devmode.h @@ -0,0 +1,4 @@ +#import + +BOOL checkDeveloperMode(void); +BOOL armDeveloperMode(BOOL* alreadyEnabled); \ No newline at end of file diff --git a/RootHelper/devmode.m b/RootHelper/devmode.m new file mode 100644 index 0000000..7020311 --- /dev/null +++ b/RootHelper/devmode.m @@ -0,0 +1,139 @@ +@import Foundation; + +// Types +typedef NSObject* xpc_object_t; +typedef xpc_object_t xpc_connection_t; +typedef void (^xpc_handler_t)(xpc_object_t object); + +// Serialization +extern CFTypeRef _CFXPCCreateCFObjectFromXPCObject(xpc_object_t xpcattrs); +extern xpc_object_t _CFXPCCreateXPCObjectFromCFObject(CFTypeRef attrs); +extern xpc_object_t _CFXPCCreateXPCMessageWithCFObject(CFTypeRef obj); +extern CFTypeRef _CFXPCCreateCFObjectFromXPCMessage(xpc_object_t obj); + +// Communication +extern xpc_connection_t xpc_connection_create_mach_service(const char* name, dispatch_queue_t targetq, uint64_t flags); +extern void xpc_connection_set_event_handler(xpc_connection_t connection, xpc_handler_t handler); +extern void xpc_connection_resume(xpc_connection_t connection); +extern void xpc_connection_send_message_with_reply(xpc_connection_t connection, xpc_object_t message, dispatch_queue_t replyq, xpc_handler_t handler); +extern xpc_object_t xpc_connection_send_message_with_reply_sync(xpc_connection_t connection, xpc_object_t message); +extern xpc_object_t xpc_dictionary_get_value(xpc_object_t xdict, const char *key); + +typedef enum { + kAMFIActionArm = 0, + kAMFIActionDisable = 1, + kAMFIActionStatus = 2, +} AMFIXPCAction; + +xpc_connection_t startConnection(void) { + xpc_connection_t connection = xpc_connection_create_mach_service("com.apple.amfi.xpc", NULL, 0); + if (!connection) { + NSLog(@"[startXPCConnection] Failed to create XPC connection to amfid"); + return nil; + } + xpc_connection_set_event_handler(connection, ^(xpc_object_t event) { + }); + xpc_connection_resume(connection); + return connection; +} + +NSDictionary* sendXPCRequest(xpc_connection_t connection, AMFIXPCAction action) { + xpc_object_t message = _CFXPCCreateXPCMessageWithCFObject((__bridge CFDictionaryRef) @{@"action": @(action)}); + xpc_object_t replyMsg = xpc_connection_send_message_with_reply_sync(connection, message); + if (!replyMsg) { + NSLog(@"[sendXPCRequest] got no reply from amfid"); + return nil; + } + + xpc_object_t replyObj = xpc_dictionary_get_value(replyMsg, "cfreply"); + if (!replyObj) { + NSLog(@"[sendXPCRequest] got reply but no cfreply"); + return nil; + } + + NSDictionary* asCF = (__bridge NSDictionary*)_CFXPCCreateCFObjectFromXPCMessage(replyObj); + return asCF; +} + +BOOL getDeveloperModeState(xpc_connection_t connection) { + NSDictionary* reply = sendXPCRequest(connection, kAMFIActionStatus); + if (!reply) { + NSLog(@"[getDeveloperModeState] failed to get reply"); + return NO; + } + + NSObject* success = reply[@"success"]; + if (!success || ![success isKindOfClass:[NSNumber class]] || ![(NSNumber*)success boolValue]) { + NSLog(@"[getDeveloperModeState] request failed with error %@", reply[@"error"]); + return NO; + } + + NSObject* status = reply[@"status"]; + if (!status || ![status isKindOfClass:[NSNumber class]]) { + NSLog(@"[getDeveloperModeState] request succeeded but no status"); + return NO; + } + + return [(NSNumber*)status boolValue]; +} + +BOOL setDeveloperModeState(xpc_connection_t connection, BOOL enable) { + NSDictionary* reply = sendXPCRequest(connection, enable ? kAMFIActionArm : kAMFIActionDisable); + if (!reply) { + NSLog(@"[setDeveloperModeState] failed to get reply"); + return NO; + } + + NSObject* success = reply[@"success"]; + if (!success || ![success isKindOfClass:[NSNumber class]] || ![(NSNumber*)success boolValue]) { + NSLog(@"[setDeveloperModeState] request failed with error %@", reply[@"error"]); + return NO; + } + + return YES; +} + +BOOL checkDeveloperMode(void) { + // Developer mode does not exist before iOS 16 + if (@available(iOS 16, *)) { + xpc_connection_t connection = startConnection(); + if (!connection) { + NSLog(@"[checkDeveloperMode] failed to start connection"); + // Assume it's disabled + return NO; + } + + return getDeveloperModeState(connection); + } else { + return YES; + } +} + +BOOL armDeveloperMode(BOOL* alreadyEnabled) { + // Developer mode does not exist before iOS 16 + if (@available(iOS 16, *)) { + xpc_connection_t connection = startConnection(); + if (!connection) { + NSLog(@"[armDeveloperMode] failed to start connection"); + return NO; + } + + BOOL enabled = getDeveloperModeState(connection); + if (alreadyEnabled) { + *alreadyEnabled = enabled; + } + + if (enabled) { + // NSLog(@"[armDeveloperMode] already enabled"); + return YES; + } + + BOOL success = setDeveloperModeState(connection, YES); + if (!success) { + NSLog(@"[armDeveloperMode] failed to arm"); + return NO; + } + } + + return YES; +} diff --git a/RootHelper/main.m b/RootHelper/main.m index 74dd0ab..a07d769 100644 --- a/RootHelper/main.m +++ b/RootHelper/main.m @@ -10,6 +10,7 @@ #import #import #import +#import "devmode.h" #ifndef EMBEDDED_ROOT_HELPER #import "codesign.h" #import "coretrust_bug.h" @@ -564,6 +565,10 @@ int signApp(NSString* appPath) } } + // On iOS 16+, any binary with get-task-allow requires developer mode to be enabled, so we will check + // while we're at it + BOOL requiresDevMode = NO; + NSURL* fileURL; NSDirectoryEnumerator *enumerator; @@ -608,6 +613,14 @@ int signApp(NSString* appPath) if (!entitlementsToUse) entitlementsToUse = [NSMutableDictionary new]; + // Developer mode does not exist before iOS 16 + if (@available(iOS 16, *)){ + NSObject *getTaskAllowO = entitlementsToUse[@"get-task-allow"]; + if (getTaskAllowO && [getTaskAllowO isKindOfClass:[NSNumber class]]) { + requiresDevMode |= [(NSNumber *)getTaskAllowO boolValue]; + } + } + NSObject *containerRequiredO = entitlementsToUse[@"com.apple.private.security.container-required"]; BOOL containerRequired = YES; if (containerRequiredO && [containerRequiredO isKindOfClass:[NSNumber class]]) { @@ -681,6 +694,11 @@ int signApp(NSString* appPath) } } + if (requiresDevMode) { + // Postpone trying to enable dev mode until after the app is (successfully) installed + return 180; + } + return 0; } #endif @@ -764,10 +782,19 @@ int installApp(NSString* appPackagePath, BOOL sign, BOOL force, BOOL isTSUpdate, applyPatchesToInfoDictionary(appBundleToInstallPath); } + BOOL requiresDevMode = NO; + if(sign) { int signRet = signApp(appBundleToInstallPath); - if(signRet != 0) return signRet; + // 180: app requires developer mode; non-fatal + if(signRet != 0) { + if (signRet == 180) { + requiresDevMode = YES; + } else { + return signRet; + } + }; } MCMAppContainer* appContainer = [MCMAppContainer containerWithIdentifier:appId createIfNecessary:NO existed:nil error:nil]; @@ -910,6 +937,23 @@ int installApp(NSString* appPackagePath, BOOL sign, BOOL force, BOOL isTSUpdate, NSURL* updatedAppURL = findAppURLInBundleURL(appContainer.url); fixPermissionsOfAppBundle(updatedAppURL.path); registerPath(updatedAppURL.path, 0, YES); + + // Handle developer mode after installing and registering the app, to ensure that we + // don't arm developer mode but then fail to install the app + if (requiresDevMode) { + BOOL alreadyEnabled = NO; + if (armDeveloperMode(&alreadyEnabled)) { + if (!alreadyEnabled) { + NSLog(@"[installApp] app requires developer mode and we have successfully armed it"); + // non-fatal + return 180; + } + } else { + NSLog(@"[installApp] failed to arm developer mode"); + // fatal + return 181; + } + } return 0; } @@ -1460,6 +1504,15 @@ int MAIN_NAME(int argc, char *argv[], char *envp[]) setTSURLSchemeState(newState, nil); } } + else if([cmd isEqualToString:@"check-dev-mode"]) + { + ret = checkDeveloperMode(); + } + else if([cmd isEqualToString:@"arm-dev-mode"]) + { + // assumes that checkDeveloperMode() has already been called + ret = armDeveloperMode(NULL); + } NSLog(@"trollstorehelper returning %d", ret); return ret; diff --git a/TrollStore/TSApplicationsManager.m b/TrollStore/TSApplicationsManager.m index 5b77e9f..5f504fc 100644 --- a/TrollStore/TSApplicationsManager.m +++ b/TrollStore/TSApplicationsManager.m @@ -74,6 +74,12 @@ extern NSUserDefaults* trollStoreUserDefaults(); case 179: errorDescription = @"The app you tried to install has the same identifier as a system app already installed on the device. The installation has been prevented to protect you from possible bootloops or other issues."; break; + case 180: + errorDescription = @"The app was installed successfully, but requires developer mode to be enabled to run."; + break; + case 181: + errorDescription = @"Failed to enable developer mode."; + break; } NSError* error = [NSError errorWithDomain:TrollStoreErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : errorDescription}]; diff --git a/TrollStore/TSInstallationController.m b/TrollStore/TSInstallationController.m index 0b3e674..5b2ad21 100644 --- a/TrollStore/TSInstallationController.m +++ b/TrollStore/TSInstallationController.m @@ -32,42 +32,57 @@ extern NSUserDefaults* trollStoreUserDefaults(void); { [TSPresentationDelegate stopActivityWithCompletion:^ { - if(ret != 0) - { + if (ret == 0) { + // success + if(completionBlock) completionBlock(YES, nil); + } else if (ret == 171) { + // recoverable error UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Install Error %d", ret] message:[error localizedDescription] preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) { - if(ret == 171) - { - if(completionBlock) completionBlock(NO, error); - } + if(completionBlock) completionBlock(NO, error); }]; [errorAlert addAction:closeAction]; - if(ret == 171) + UIAlertAction* forceInstallAction = [UIAlertAction actionWithTitle:@"Force Installation" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) { - UIAlertAction* forceInstallAction = [UIAlertAction actionWithTitle:@"Force Installation" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) - { - [self handleAppInstallFromFile:pathToIPA forceInstall:YES completion:completionBlock]; - }]; - [errorAlert addAction:forceInstallAction]; - } - else - { - UIAlertAction* copyLogAction = [UIAlertAction actionWithTitle:@"Copy Debug Log" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) - { - UIPasteboard* pasteboard = [UIPasteboard generalPasteboard]; - pasteboard.string = log; - }]; - [errorAlert addAction:copyLogAction]; - } + [self handleAppInstallFromFile:pathToIPA forceInstall:YES completion:completionBlock]; + }]; + [errorAlert addAction:forceInstallAction]; [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; - } + } else if (ret == 180) { + // non-fatal informative message + UIAlertController* rebootNotification = [UIAlertController alertControllerWithTitle:@"Reboot Required" message:[error localizedDescription] preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) + { + if(completionBlock) completionBlock(YES, nil); + }]; + [rebootNotification addAction:closeAction]; - if(ret != 171) - { - if(completionBlock) completionBlock((BOOL)error, error); + UIAlertAction* rebootAction = [UIAlertAction actionWithTitle:@"Reboot Now" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) + { + if(completionBlock) completionBlock(YES, nil); + }]; + [rebootNotification addAction:rebootAction]; + + [TSPresentationDelegate presentViewController:rebootNotification animated:YES completion:nil]; + } else { + // unrecoverable error + UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Install Error %d", ret] message:[error localizedDescription] preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]; + [errorAlert addAction:closeAction]; + + UIAlertAction* copyLogAction = [UIAlertAction actionWithTitle:@"Copy Debug Log" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) + { + UIPasteboard* pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = log; + }]; + [errorAlert addAction:copyLogAction]; + + [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; + + if(completionBlock) completionBlock(NO, error); } }]; });