diff --git a/README.md b/README.md index 28c079a..35413a6 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ This installation method unfortunately does **NOT** work on arm64 (A8 - A11) iOS 4. If this app has not appeared, that's a stock iOS bug, reboot your device and the app will appear. -5. Launch the app, and tap "Install TrollStore" +5. Launch the app, tap "Install TrollStore" 6. Wait a few seconds, your device should respring and TrollStore will be installed. @@ -56,7 +56,7 @@ Supports jailbroken devices running 14.0 and above. 1. Open your package manager, and make sure Havoc repo (https://havoc.app) is added under Sources, then search for "TrollStore Helper" and install it. -2. After the installation, respring and a "TrollHelper" app should be on your home screen, launch it. +2. After the installation, respring and the "TrollHelper" app should have appeared on your home screen. 3. Launch the app, tap "Install TrollStore" @@ -66,6 +66,18 @@ Supports jailbroken devices running 14.0 and above. 6. Done, you can now share IPA files with TrollStore and they will be permanently installed on your device. +### Unjailbreaking while retaining TrollStore + +Some people might prefer to use TrollStore in an unjailbroken environment, if that applies to you, follow this guide. + +1. Uninstall TrollHelper from your package manager + +2. Now when you launch TrollStore, it will have an option to install the persistence helper into a System app like on iOS 15, do so. + +3. Now restore rootFS through your jailbreak app, afterwards use the System app to refresh app registrations. + +4. Done, your device will be jailed, but TrollStore will still work. + ## Updating TrollStore When a new TrollStore update is available, a button to install it will appear at the top in the TrollStore settings. After tapping the button, TrollStore will automatically download the update, install it, and respring. @@ -84,6 +96,14 @@ The only way to work around this is to install a persistence helper into a syste On jailbroken iOS 14 when TrollHelper is used for installation, it is located in /Applications and will persist as a "System" app through icon cache reloads, therefore TrollHelper is used as the persistence helper on iOS 14. +## URL Scheme + +As of version 1.3, TrollStore replaces the system URL scheme "apple-magnifier" (this is done so "jailbreak" detections can't detect TrollStore like they could if TrollStore had a unique URL scheme). This URL scheme can be used to install applications right from the browser, the format goes as follows: + +`apple-magnifier://install?url=` + +On devices that don't have TrollStore (1.3+) installed, this will just open the magnifier app. + ## Features The binaries inside an IPA can have arbitrary entitlements, fakesign them with ldid and the entitlements you want (`ldid -S `) and TrollStore will preserve the entitlements when resigning them with the fake root certificate on installation. This gives you a lot of possibilities, some of which are explained below. diff --git a/RootHelper/control b/RootHelper/control index 25219b2..9b9d911 100644 --- a/RootHelper/control +++ b/RootHelper/control @@ -1,6 +1,6 @@ Package: com.opa334.trollstoreroothelper Name: trollstoreroothelper -Version: 1.2.2 +Version: 1.3 Architecture: iphoneos-arm Description: An awesome tool of some sort!! Maintainer: opa334 diff --git a/RootHelper/main.m b/RootHelper/main.m index b5f27b4..85f4501 100644 --- a/RootHelper/main.m +++ b/RootHelper/main.m @@ -27,27 +27,6 @@ extern NSString* BKSOpenApplicationOptionKeyActivateForEvent; extern void BKSTerminateApplicationForReasonAndReportWithDescription(NSString *bundleID, int reasonID, bool report, NSString *description); -typedef CF_OPTIONS(uint32_t, SecCSFlags) { - kSecCSDefaultFlags = 0 -}; - - -#define kSecCSRequirementInformation 1 << 2 -#define kSecCSSigningInformation 1 << 1 - -typedef struct __SecCode const *SecStaticCodeRef; - -extern CFStringRef kSecCodeInfoEntitlementsDict; -extern CFStringRef kSecCodeInfoCertificates; -extern CFStringRef kSecPolicyAppleiPhoneApplicationSigning; -extern CFStringRef kSecPolicyAppleiPhoneProfileApplicationSigning; -extern CFStringRef kSecPolicyLeafMarkerOid; - -OSStatus SecStaticCodeCreateWithPathAndAttributes(CFURLRef path, SecCSFlags flags, CFDictionaryRef attributes, SecStaticCodeRef *staticCode); -OSStatus SecCodeCopySigningInformation(SecStaticCodeRef code, SecCSFlags flags, CFDictionaryRef *information); -CFDataRef SecCertificateCopyExtensionValue(SecCertificateRef certificate, CFTypeRef extensionOID, bool *isCritical); -void SecPolicySetOptionsValue(SecPolicyRef policy, CFStringRef key, CFTypeRef value); - #define kCFPreferencesNoContainer CFSTR("kCFPreferencesNoContainer") typedef CFPropertyListRef (*_CFPreferencesCopyValueWithContainerType)(CFStringRef key, CFStringRef applicationID, CFStringRef userName, CFStringRef hostName, CFStringRef containerPath); @@ -222,97 +201,6 @@ int runLdid(NSArray* args, NSString** output, NSString** errorOutput) return WEXITSTATUS(status); } -SecStaticCodeRef getStaticCodeRef(NSString *binaryPath) -{ - if(binaryPath == nil) - { - return NULL; - } - - CFURLRef binaryURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (__bridge CFStringRef)binaryPath, kCFURLPOSIXPathStyle, false); - if(binaryURL == NULL) - { - NSLog(@"[getStaticCodeRef] failed to get URL to binary %@", binaryPath); - return NULL; - } - - SecStaticCodeRef codeRef = NULL; - OSStatus result; - - result = SecStaticCodeCreateWithPathAndAttributes(binaryURL, kSecCSDefaultFlags, NULL, &codeRef); - - CFRelease(binaryURL); - - if(result != errSecSuccess) - { - NSLog(@"[getStaticCodeRef] failed to create static code for binary %@", binaryPath); - return NULL; - } - - return codeRef; -} - -NSDictionary* dumpEntitlements(SecStaticCodeRef codeRef) -{ - if(codeRef == NULL) - { - NSLog(@"[dumpEntitlements] attempting to dump entitlements without a StaticCodeRef"); - return nil; - } - - CFDictionaryRef signingInfo = NULL; - OSStatus result; - - result = SecCodeCopySigningInformation(codeRef, kSecCSRequirementInformation, &signingInfo); - - if(result != errSecSuccess) - { - NSLog(@"[dumpEntitlements] failed to copy signing info from static code"); - return nil; - } - - NSDictionary *entitlementsNSDict = nil; - - CFDictionaryRef entitlements = CFDictionaryGetValue(signingInfo, kSecCodeInfoEntitlementsDict); - if(entitlements == NULL) - { - NSLog(@"[dumpEntitlements] no entitlements specified"); - } - else if(CFGetTypeID(entitlements) != CFDictionaryGetTypeID()) - { - NSLog(@"[dumpEntitlements] invalid entitlements"); - } - else - { - entitlementsNSDict = (__bridge NSDictionary *)(entitlements); - NSLog(@"[dumpEntitlements] dumped %@", entitlementsNSDict); - } - - CFRelease(signingInfo); - return entitlementsNSDict; -} - -NSDictionary* dumpEntitlementsFromBinaryAtPath(NSString *binaryPath) -{ - // This function is intended for one-shot checks. Main-event functions should retain/release their own SecStaticCodeRefs - - if(binaryPath == nil) - { - return nil; - } - - SecStaticCodeRef codeRef = getStaticCodeRef(binaryPath); - if(codeRef == NULL) - { - return nil; - } - - NSDictionary *entitlements = dumpEntitlements(codeRef); - CFRelease(codeRef); - - return entitlements; -} - BOOL certificateHasDataForExtensionOID(SecCertificateRef certificate, CFStringRef oidString) { if(certificate == NULL || oidString == NULL) @@ -762,7 +650,7 @@ int installApp(NSString* appPath, BOOL sign, BOOL force) if(suc) { NSLog(@"[installApp] App %@ installed, adding to icon cache now...", appId); - registerPath((char*)newAppPath.UTF8String, 0); + registerPath((char*)newAppPath.UTF8String, 0, YES); return 0; } else @@ -816,7 +704,7 @@ int uninstallApp(NSString* appPath, NSString* appId) } // unregister app - registerPath((char*)appPath.UTF8String, 1); + registerPath((char*)appPath.UTF8String, 1, YES); NSLog(@"[uninstallApp] deleting %@", [appPath stringByDeletingLastPathComponent]); // delete app @@ -930,7 +818,7 @@ BOOL uninstallTrollStore(BOOL unregister) if(unregister) { - registerPath((char*)trollStoreAppPath().UTF8String, 1); + registerPath((char*)trollStoreAppPath().UTF8String, 1, YES); } return [[NSFileManager defaultManager] removeItemAtPath:trollStore error:nil]; @@ -988,13 +876,13 @@ BOOL installTrollStore(NSString* pathToTar) void refreshAppRegistrations() { - //registerPath((char*)trollStoreAppPath().UTF8String, 1); - registerPath((char*)trollStoreAppPath().UTF8String, 0); + //registerPath((char*)trollStoreAppPath().UTF8String, 1, YES); + registerPath((char*)trollStoreAppPath().UTF8String, 0, YES); for(NSString* appPath in trollStoreInstalledAppBundlePaths()) { - //registerPath((char*)appPath.UTF8String, 1); - registerPath((char*)appPath.UTF8String, 0); + //registerPath((char*)appPath.UTF8String, 1, YES); + registerPath((char*)appPath.UTF8String, 0, YES); } } @@ -1128,6 +1016,59 @@ void registerUserPersistenceHelper(NSString* userAppId) [[NSFileManager defaultManager] createFileAtPath:markPath contents:[NSData data] attributes:nil]; } +// Apparently there is some odd behaviour where TrollStore installed apps sometimes get restricted +// This works around that issue at least and is triggered when rebuilding icon cache +void cleanRestrictions(void) +{ + NSString* clientTruthPath = @"/private/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/ClientTruth.plist"; + NSURL* clientTruthURL = [NSURL fileURLWithPath:clientTruthPath]; + NSDictionary* clientTruthDictionary = [NSDictionary dictionaryWithContentsOfURL:clientTruthURL]; + + if(!clientTruthDictionary) return; + + NSArray* valuesArr; + + NSDictionary* lsdAppRemoval = clientTruthDictionary[@"com.apple.lsd.appremoval"]; + if(lsdAppRemoval && [lsdAppRemoval isKindOfClass:NSDictionary.class]) + { + NSDictionary* clientRestrictions = lsdAppRemoval[@"clientRestrictions"]; + if(clientRestrictions && [clientRestrictions isKindOfClass:NSDictionary.class]) + { + NSDictionary* unionDict = clientRestrictions[@"union"]; + if(unionDict && [unionDict isKindOfClass:NSDictionary.class]) + { + NSDictionary* removedSystemAppBundleIDs = unionDict[@"removedSystemAppBundleIDs"]; + if(removedSystemAppBundleIDs && [removedSystemAppBundleIDs isKindOfClass:NSDictionary.class]) + { + valuesArr = removedSystemAppBundleIDs[@"values"]; + } + } + } + } + + if(!valuesArr || !valuesArr.count) return; + + NSMutableArray* valuesArrM = valuesArr.mutableCopy; + __block BOOL changed = NO; + + [valuesArrM enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSString* value, NSUInteger idx, BOOL *stop) + { + if(![value hasPrefix:@"com.apple."]) + { + [valuesArrM removeObjectAtIndex:idx]; + changed = YES; + } + }]; + + if(!changed) return; + + NSMutableDictionary* clientTruthDictionaryM = (__bridge_transfer NSMutableDictionary*)CFPropertyListCreateDeepCopy(kCFAllocatorDefault, (__bridge CFDictionaryRef)clientTruthDictionary, kCFPropertyListMutableContainersAndLeaves); + + clientTruthDictionaryM[@"com.apple.lsd.appremoval"][@"clientRestrictions"][@"union"][@"removedSystemAppBundleIDs"][@"values"] = valuesArrM; + + [clientTruthDictionaryM writeToURL:clientTruthURL error:nil]; +} + int MAIN_NAME(int argc, char *argv[], char *envp[]) { @autoreleasepool { @@ -1142,12 +1083,10 @@ int MAIN_NAME(int argc, char *argv[], char *envp[]) NSString* cmd = [NSString stringWithUTF8String:argv[1]]; if([cmd isEqualToString:@"install"]) { - NSLog(@"argc = %d", argc); BOOL force = NO; if(argc <= 2) return -3; if(argc > 3) { - NSLog(@"argv3 = %s", argv[3]); if(!strcmp(argv[3], "force")) { force = YES; @@ -1190,8 +1129,10 @@ int MAIN_NAME(int argc, char *argv[], char *envp[]) refreshAppRegistrations(); } else if([cmd isEqualToString:@"refresh-all"]) { + cleanRestrictions(); [[LSApplicationWorkspace defaultWorkspace] _LSPrivateRebuildApplicationDatabasesForSystemApps:YES internal:YES user:YES]; refreshAppRegistrations(); + killall(@"backboardd"); } else if([cmd isEqualToString:@"install-persistence-helper"]) { if(argc <= 2) return -3; @@ -1205,6 +1146,17 @@ int MAIN_NAME(int argc, char *argv[], char *envp[]) if(argc <= 2) return -3; NSString* userAppId = [NSString stringWithUTF8String:argv[2]]; registerUserPersistenceHelper(userAppId); + } else if([cmd isEqualToString:@"modify-registration"]) + { + if(argc <= 3) return -3; + NSString* appPath = [NSString stringWithUTF8String:argv[2]]; + NSString* newRegistration = [NSString stringWithUTF8String:argv[3]]; + + NSString* trollStoreMark = [[appPath stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"_TrollStore"]; + if([[NSFileManager defaultManager] fileExistsAtPath:trollStoreMark]) + { + registerPath((char*)appPath.UTF8String, 0, [newRegistration isEqualToString:@"System"]); + } } NSLog(@"returning %d", ret); diff --git a/RootHelper/uicache.h b/RootHelper/uicache.h index 841c025..128391e 100644 --- a/RootHelper/uicache.h +++ b/RootHelper/uicache.h @@ -1 +1 @@ -extern void registerPath(char *path, int unregister); \ No newline at end of file +extern void registerPath(char *path, int unregister, BOOL system); \ No newline at end of file diff --git a/RootHelper/uicache.m b/RootHelper/uicache.m index f1554a9..cc4e469 100644 --- a/RootHelper/uicache.m +++ b/RootHelper/uicache.m @@ -89,7 +89,7 @@ NSDictionary* constructEnvironmentVariablesForContainerPath(NSString* containerP }; } -void registerPath(char* cPath, int unregister) +void registerPath(char* cPath, int unregister, BOOL system) { if(!cPath) return; NSString* path = [NSString stringWithUTF8String:cPath]; @@ -129,7 +129,7 @@ void registerPath(char* cPath, int unregister) // Misc - dictToRegister[@"ApplicationType"] = @"System"; + dictToRegister[@"ApplicationType"] = system ? @"System" : @"User"; dictToRegister[@"CFBundleIdentifier"] = appBundleID; dictToRegister[@"CodeInfoIdentifier"] = appBundleID; dictToRegister[@"CompatibilityState"] = @0; diff --git a/Shared/CoreServices.h b/Shared/CoreServices.h index e80af10..1120192 100644 --- a/Shared/CoreServices.h +++ b/Shared/CoreServices.h @@ -6,6 +6,7 @@ @interface LSApplicationProxy : LSBundleProxy + (instancetype)applicationProxyForIdentifier:(NSString*)identifier; ++ (instancetype)applicationProxyForBundleURL:(NSURL*)bundleURL; @property NSURL* bundleURL; @property NSString* bundleType; @property NSString* canonicalExecutablePath; @@ -14,7 +15,8 @@ @property (getter=isInstalled,nonatomic,readonly) BOOL installed; @property (getter=isPlaceholder,nonatomic,readonly) BOOL placeholder; @property (getter=isRestricted,nonatomic,readonly) BOOL restricted; -@property (nonatomic,readonly) NSSet * claimedURLSchemes; +@property (nonatomic,readonly) NSSet* claimedURLSchemes; +@property (nonatomic,readonly) NSString* applicationType; @end @interface LSApplicationWorkspace : NSObject diff --git a/Shared/TSListControllerShared.h b/Shared/TSListControllerShared.h index 1899fda..b78269b 100644 --- a/Shared/TSListControllerShared.h +++ b/Shared/TSListControllerShared.h @@ -9,10 +9,6 @@ - (BOOL)isTrollStore; - (NSString*)getTrollStoreVersion; - -- (void)startActivity:(NSString*)activity; -- (void)stopActivityWithCompletion:(void (^)(void))completion; - - (void)downloadTrollStoreAndDo:(void (^)(NSString* localTrollStoreTarPath))doHandler; - (void)installTrollStorePressed; - (void)updateTrollStorePressed; diff --git a/Shared/TSListControllerShared.m b/Shared/TSListControllerShared.m index 77ca47b..ac4d317 100644 --- a/Shared/TSListControllerShared.m +++ b/Shared/TSListControllerShared.m @@ -1,5 +1,6 @@ #import "TSListControllerShared.h" #import "TSUtil.h" +#import "TSPresentationDelegate.h" @implementation TSListControllerShared @@ -24,34 +25,6 @@ } } -- (void)startActivity:(NSString*)activity -{ - if(_activityController) return; - - _activityController = [UIAlertController alertControllerWithTitle:activity message:@"" preferredStyle:UIAlertControllerStyleAlert]; - UIActivityIndicatorView* activityIndicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(5,5,50,50)]; - activityIndicator.hidesWhenStopped = YES; - activityIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium; - [activityIndicator startAnimating]; - [_activityController.view addSubview:activityIndicator]; - - [self presentViewController:_activityController animated:YES completion:nil]; -} - -- (void)stopActivityWithCompletion:(void (^)(void))completion -{ - if(!_activityController) return; - - [_activityController dismissViewControllerAnimated:YES completion:^ - { - _activityController = nil; - if(completion) - { - completion(); - } - }]; -} - - (void)downloadTrollStoreAndDo:(void (^)(NSString* localTrollStoreTarPath))doHandler { NSURL* trollStoreURL = [NSURL URLWithString:@"https://github.com/opa334/TrollStore/releases/latest/download/TrollStore.tar"]; @@ -67,9 +40,9 @@ dispatch_async(dispatch_get_main_queue(), ^ { - [self stopActivityWithCompletion:^ + [TSPresentationDelegate stopActivityWithCompletion:^ { - [self presentViewController:errorAlert animated:YES completion:nil]; + [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; }]; }); } @@ -89,11 +62,11 @@ { if(update) { - [self startActivity:@"Updating TrollStore"]; + [TSPresentationDelegate startActivity:@"Updating TrollStore"]; } else { - [self startActivity:@"Installing TrollStore"]; + [TSPresentationDelegate startActivity:@"Installing TrollStore"]; } [self downloadTrollStoreAndDo:^(NSString* tmpTarPath) @@ -113,7 +86,7 @@ { dispatch_async(dispatch_get_main_queue(), ^ { - [self stopActivityWithCompletion:^ + [TSPresentationDelegate stopActivityWithCompletion:^ { [self reloadSpecifiers]; }]; @@ -124,12 +97,12 @@ { dispatch_async(dispatch_get_main_queue(), ^ { - [self stopActivityWithCompletion:^ + [TSPresentationDelegate stopActivityWithCompletion:^ { UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:@"Error" message:[NSString stringWithFormat:@"Error installing TrollStore: trollstorehelper returned %d", ret] preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]; [errorAlert addAction:closeAction]; - [self presentViewController:errorAlert animated:YES completion:nil]; + [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; }]; }); } @@ -148,7 +121,7 @@ - (void)rebuildIconCachePressed { - [self startActivity:@"Rebuilding Icon Cache"]; + [TSPresentationDelegate startActivity:@"Rebuilding Icon Cache"]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ { @@ -156,14 +129,14 @@ dispatch_async(dispatch_get_main_queue(), ^ { - [self stopActivityWithCompletion:nil]; + [TSPresentationDelegate stopActivityWithCompletion:nil]; }); }); } - (void)refreshAppRegistrationsPressed { - [self startActivity:@"Refreshing"]; + [TSPresentationDelegate startActivity:@"Refreshing"]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ { @@ -172,7 +145,7 @@ dispatch_async(dispatch_get_main_queue(), ^ { - [self stopActivityWithCompletion:nil]; + [TSPresentationDelegate stopActivityWithCompletion:nil]; }); }); } @@ -198,7 +171,7 @@ }]; [uninstallWarningAlert addAction:continueAction]; - [self presentViewController:uninstallWarningAlert animated:YES completion:nil]; + [TSPresentationDelegate presentViewController:uninstallWarningAlert animated:YES completion:nil]; } } @@ -228,7 +201,7 @@ }]; [uninstallWarningAlert addAction:continueAction]; - [self presentViewController:uninstallWarningAlert animated:YES completion:nil]; + [TSPresentationDelegate presentViewController:uninstallWarningAlert animated:YES completion:nil]; } @end \ No newline at end of file diff --git a/Shared/TSPresentationDelegate.h b/Shared/TSPresentationDelegate.h new file mode 100644 index 0000000..a84deb5 --- /dev/null +++ b/Shared/TSPresentationDelegate.h @@ -0,0 +1,10 @@ +#import + +@interface TSPresentationDelegate : NSObject +@property (class) UIViewController* presentationViewController; +@property (class) UIAlertController* activityController; ++ (void)startActivity:(NSString*)activity withCancelHandler:(void (^)(void))cancelHandler; ++ (void)startActivity:(NSString*)activity; ++ (void)stopActivityWithCompletion:(void (^)(void))completion; ++ (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion; +@end \ No newline at end of file diff --git a/Shared/TSPresentationDelegate.m b/Shared/TSPresentationDelegate.m new file mode 100644 index 0000000..57b0887 --- /dev/null +++ b/Shared/TSPresentationDelegate.m @@ -0,0 +1,81 @@ +#import "TSPresentationDelegate.h" + +@implementation TSPresentationDelegate + +static UIViewController* g_presentationViewController; +static UIAlertController* g_activityController; + ++ (UIViewController*)presentationViewController +{ + return g_presentationViewController; +} + ++ (void)setPresentationViewController:(UIViewController*)vc +{ + g_presentationViewController = vc; +} + ++ (UIAlertController*)activityController +{ + return g_activityController; +} + ++ (void)setActivityController:(UIAlertController*)ac +{ + g_activityController = ac; +} + ++ (void)startActivity:(NSString*)activity withCancelHandler:(void (^)(void))cancelHandler +{ + if(self.activityController) + { + self.activityController.title = activity; + } + else + { + self.activityController = [UIAlertController alertControllerWithTitle:activity message:@"" preferredStyle:UIAlertControllerStyleAlert]; + UIActivityIndicatorView* activityIndicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(5,5,50,50)]; + activityIndicator.hidesWhenStopped = YES; + activityIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium; + [activityIndicator startAnimating]; + [self.activityController.view addSubview:activityIndicator]; + + if(cancelHandler) + { + UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction* action) + { + self.activityController = nil; + cancelHandler(); + }]; + [self.activityController addAction:cancelAction]; + } + + [self presentViewController:self.activityController animated:YES completion:nil]; + } +} + ++ (void)startActivity:(NSString*)activity +{ + [self startActivity:activity withCancelHandler:nil]; +} + ++ (void)stopActivityWithCompletion:(void (^)(void))completion +{ + if(!self.activityController) return; + + [self.activityController dismissViewControllerAnimated:YES completion:^ + { + self.activityController = nil; + if(completion) + { + completion(); + } + }]; +} + ++ (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion +{ + [self.presentationViewController presentViewController:viewControllerToPresent animated:flag completion:completion]; +} + +@end \ No newline at end of file diff --git a/Shared/TSUtil.h b/Shared/TSUtil.h index 4ab538f..1fc0bcc 100644 --- a/Shared/TSUtil.h +++ b/Shared/TSUtil.h @@ -1,6 +1,8 @@ @import Foundation; #import "CoreServices.h" +#define TrollStoreErrorDomain @"TrollStoreErrorDomain" + extern void chineseWifiFixup(void); extern void loadMCMFramework(void); extern NSString* safe_getExecutablePath(); @@ -8,6 +10,7 @@ extern NSString* rootHelperPath(void); extern NSString* getNSStringFromFile(int fd); extern void printMultilineNSString(NSString* stringToPrint); extern int spawnRoot(NSString* path, NSArray* args, NSString** stdOut, NSString** stdErr); +extern void killall(NSString* processName); extern void respring(void); extern void fetchLatestTrollStoreVersion(void (^completionHandler)(NSString* latestVersion)); @@ -16,6 +19,14 @@ extern NSArray* trollStoreInstalledAppContainerPaths(); extern NSString* trollStorePath(); extern NSString* trollStoreAppPath(); +#import + +@interface UIAlertController (Private) +@property (setter=_setAttributedTitle:,getter=_attributedTitle,nonatomic,copy) NSAttributedString* attributedTitle; +@property (setter=_setAttributedMessage:,getter=_attributedMessage,nonatomic,copy) NSAttributedString* attributedMessage; +@property (nonatomic,retain) UIImage* image; +@end + typedef enum { PERSISTENCE_HELPER_TYPE_USER = 1 << 0, @@ -23,4 +34,28 @@ typedef enum PERSISTENCE_HELPER_TYPE_ALL = PERSISTENCE_HELPER_TYPE_USER | PERSISTENCE_HELPER_TYPE_SYSTEM } PERSISTENCE_HELPER_TYPE; -extern LSApplicationProxy* findPersistenceHelperApp(PERSISTENCE_HELPER_TYPE allowedTypes); \ No newline at end of file +extern LSApplicationProxy* findPersistenceHelperApp(PERSISTENCE_HELPER_TYPE allowedTypes); + +typedef struct __SecCode const *SecStaticCodeRef; + +typedef CF_OPTIONS(uint32_t, SecCSFlags) { + kSecCSDefaultFlags = 0 +}; +#define kSecCSRequirementInformation 1 << 2 +#define kSecCSSigningInformation 1 << 1 + +OSStatus SecStaticCodeCreateWithPathAndAttributes(CFURLRef path, SecCSFlags flags, CFDictionaryRef attributes, SecStaticCodeRef *staticCode); +OSStatus SecCodeCopySigningInformation(SecStaticCodeRef code, SecCSFlags flags, CFDictionaryRef *information); +CFDataRef SecCertificateCopyExtensionValue(SecCertificateRef certificate, CFTypeRef extensionOID, bool *isCritical); +void SecPolicySetOptionsValue(SecPolicyRef policy, CFStringRef key, CFTypeRef value); + +extern CFStringRef kSecCodeInfoEntitlementsDict; +extern CFStringRef kSecCodeInfoCertificates; +extern CFStringRef kSecPolicyAppleiPhoneApplicationSigning; +extern CFStringRef kSecPolicyAppleiPhoneProfileApplicationSigning; +extern CFStringRef kSecPolicyLeafMarkerOid; + +extern SecStaticCodeRef getStaticCodeRef(NSString *binaryPath); +extern NSDictionary* dumpEntitlements(SecStaticCodeRef codeRef); +extern NSDictionary* dumpEntitlementsFromBinaryAtPath(NSString *binaryPath); +extern NSDictionary* dumpEntitlementsFromBinaryData(NSData* binaryData); \ No newline at end of file diff --git a/Shared/TSUtil.m b/Shared/TSUtil.m index bd7b29f..a6617e4 100644 --- a/Shared/TSUtil.m +++ b/Shared/TSUtil.m @@ -343,4 +343,108 @@ LSApplicationProxy* findPersistenceHelperApp(PERSISTENCE_HELPER_TYPE allowedType } return outProxy; +} + +SecStaticCodeRef getStaticCodeRef(NSString *binaryPath) +{ + if(binaryPath == nil) + { + return NULL; + } + + CFURLRef binaryURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (__bridge CFStringRef)binaryPath, kCFURLPOSIXPathStyle, false); + if(binaryURL == NULL) + { + NSLog(@"[getStaticCodeRef] failed to get URL to binary %@", binaryPath); + return NULL; + } + + SecStaticCodeRef codeRef = NULL; + OSStatus result; + + result = SecStaticCodeCreateWithPathAndAttributes(binaryURL, kSecCSDefaultFlags, NULL, &codeRef); + + CFRelease(binaryURL); + + if(result != errSecSuccess) + { + NSLog(@"[getStaticCodeRef] failed to create static code for binary %@", binaryPath); + return NULL; + } + + return codeRef; +} + +NSDictionary* dumpEntitlements(SecStaticCodeRef codeRef) +{ + if(codeRef == NULL) + { + NSLog(@"[dumpEntitlements] attempting to dump entitlements without a StaticCodeRef"); + return nil; + } + + CFDictionaryRef signingInfo = NULL; + OSStatus result; + + result = SecCodeCopySigningInformation(codeRef, kSecCSRequirementInformation, &signingInfo); + + if(result != errSecSuccess) + { + NSLog(@"[dumpEntitlements] failed to copy signing info from static code"); + return nil; + } + + NSDictionary *entitlementsNSDict = nil; + + CFDictionaryRef entitlements = CFDictionaryGetValue(signingInfo, kSecCodeInfoEntitlementsDict); + if(entitlements == NULL) + { + NSLog(@"[dumpEntitlements] no entitlements specified"); + } + else if(CFGetTypeID(entitlements) != CFDictionaryGetTypeID()) + { + NSLog(@"[dumpEntitlements] invalid entitlements"); + } + else + { + entitlementsNSDict = (__bridge NSDictionary *)(entitlements); + NSLog(@"[dumpEntitlements] dumped %@", entitlementsNSDict); + } + + CFRelease(signingInfo); + return entitlementsNSDict; +} + +NSDictionary* dumpEntitlementsFromBinaryAtPath(NSString *binaryPath) +{ + // This function is intended for one-shot checks. Main-event functions should retain/release their own SecStaticCodeRefs + + if(binaryPath == nil) + { + return nil; + } + + SecStaticCodeRef codeRef = getStaticCodeRef(binaryPath); + if(codeRef == NULL) + { + return nil; + } + + NSDictionary *entitlements = dumpEntitlements(codeRef); + CFRelease(codeRef); + + return entitlements; +} + +NSDictionary* dumpEntitlementsFromBinaryData(NSData* binaryData) +{ + NSDictionary* entitlements; + NSString* tmpPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; + NSURL* tmpURL = [NSURL fileURLWithPath:tmpPath]; + if([binaryData writeToURL:tmpURL options:NSDataWritingAtomic error:nil]) + { + entitlements = dumpEntitlementsFromBinaryAtPath(tmpPath); + [[NSFileManager defaultManager] removeItemAtURL:tmpURL error:nil]; + } + return entitlements; } \ No newline at end of file diff --git a/TrollHelper/Resources/Info.plist b/TrollHelper/Resources/Info.plist index 857f246..453795d 100644 --- a/TrollHelper/Resources/Info.plist +++ b/TrollHelper/Resources/Info.plist @@ -52,7 +52,7 @@ iPhoneOS CFBundleVersion - 1.2.2 + 1.3 LSRequiresIPhoneOS UIDeviceFamily diff --git a/TrollHelper/TSHRootViewController.m b/TrollHelper/TSHRootViewController.m index 6f4edab..b027fed 100644 --- a/TrollHelper/TSHRootViewController.m +++ b/TrollHelper/TSHRootViewController.m @@ -1,5 +1,6 @@ #import "TSHRootViewController.h" #import +#import @implementation TSHRootViewController @@ -11,6 +12,8 @@ - (void)viewDidLoad { [super viewDidLoad]; + TSPresentationDelegate.presentationViewController = self; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadSpecifiers) name:UIApplicationWillEnterForegroundNotification object:nil]; fetchLatestTrollStoreVersion(^(NSString* latestVersion) @@ -123,9 +126,7 @@ [_specifiers addObject:installTrollStoreSpecifier]; } - NSString* executableName = NSBundle.mainBundle.bundleURL.lastPathComponent; - NSString* backupExecutableName = [executableName stringByAppendingString:@"_TROLLSTORE_BACKUP"]; - NSString* backupPath = [[NSBundle.mainBundle.bundleURL.path stringByDeletingLastPathComponent] stringByAppendingPathComponent:backupExecutableName]; + NSString* backupPath = [safe_getExecutablePath() stringByAppendingString:@"_TROLLSTORE_BACKUP"]; if([[NSFileManager defaultManager] fileExistsAtPath:backupPath]) { PSSpecifier* uninstallHelperGroupSpecifier = [PSSpecifier emptyGroupSpecifier]; diff --git a/TrollHelper/control b/TrollHelper/control index 17ddd9a..fb42126 100644 --- a/TrollHelper/control +++ b/TrollHelper/control @@ -1,6 +1,6 @@ Package: com.opa334.trollstorehelper Name: TrollStore Helper -Version: 1.2.2 +Version: 1.3 Architecture: iphoneos-arm Description: Helper utility to install and manage TrollStore! Maintainer: opa334 diff --git a/TrollStore/Makefile b/TrollStore/Makefile index 9ddbc4a..343bf07 100644 --- a/TrollStore/Makefile +++ b/TrollStore/Makefile @@ -7,7 +7,8 @@ APPLICATION_NAME = TrollStore TrollStore_FILES = $(wildcard *.m) $(wildcard ../Shared/*.m) TrollStore_FRAMEWORKS = UIKit CoreGraphics CoreServices -TrollStore_PRIVATE_FRAMEWORKS = Preferences +TrollStore_PRIVATE_FRAMEWORKS = Preferences MobileIcons +TrollStore_LIBRARIES = archive TrollStore_CFLAGS = -fobjc-arc -I../Shared TrollStore_CODESIGN_FLAGS = -Sentitlements.plist -K../cert.p12 diff --git a/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/Info.plist b/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/Info.plist new file mode 100644 index 0000000..101b217 Binary files /dev/null and b/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/Info.plist differ diff --git a/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/Kx4-55-vNS-view-9BB-B5-Vbi.nib b/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/Kx4-55-vNS-view-9BB-B5-Vbi.nib new file mode 100644 index 0000000..465c134 Binary files /dev/null and b/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/Kx4-55-vNS-view-9BB-B5-Vbi.nib differ diff --git a/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/UITabBarController-9el-pn-lH0.nib b/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/UITabBarController-9el-pn-lH0.nib new file mode 100644 index 0000000..41dbebd Binary files /dev/null and b/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/UITabBarController-9el-pn-lH0.nib differ diff --git a/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/X3T-Aa-nEE-view-vAu-RC-m7d.nib b/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/X3T-Aa-nEE-view-vAu-RC-m7d.nib new file mode 100644 index 0000000..465c134 Binary files /dev/null and b/TrollStore/Resources/Base.lproj/LaunchScreen.storyboardc/X3T-Aa-nEE-view-vAu-RC-m7d.nib differ diff --git a/TrollStore/Resources/Info.plist b/TrollStore/Resources/Info.plist index adb7f9c..5180442 100644 --- a/TrollStore/Resources/Info.plist +++ b/TrollStore/Resources/Info.plist @@ -50,7 +50,7 @@ iPhoneOS CFBundleVersion - 1.2.2 + 1.3 LSRequiresIPhoneOS UIDeviceFamily @@ -62,81 +62,8 @@ armv7 - UILaunchImageFile - LaunchImage - UILaunchImages - - - UILaunchImageMinimumOSVersion - 7.0 - UILaunchImageName - LaunchImage - UILaunchImageOrientation - Portrait - UILaunchImageSize - {320, 480} - - - UILaunchImageMinimumOSVersion - 7.0 - UILaunchImageName - LaunchImage-700-568h - UILaunchImageOrientation - Portrait - UILaunchImageSize - {320, 568} - - - UILaunchImageMinimumOSVersion - 7.0 - UILaunchImageName - LaunchImage-Portrait - UILaunchImageOrientation - Portrait - UILaunchImageSize - {768, 1024} - - - UILaunchImageMinimumOSVersion - 7.0 - UILaunchImageName - LaunchImage-Landscape - UILaunchImageOrientation - Landscape - UILaunchImageSize - {768, 1024} - - - UILaunchImageMinimumOSVersion - 8.0 - UILaunchImageName - LaunchImage-800-667h - UILaunchImageOrientation - Portrait - UILaunchImageSize - {375, 667} - - - UILaunchImageMinimumOSVersion - 8.0 - UILaunchImageName - LaunchImage-800-Portrait-736h - UILaunchImageOrientation - Portrait - UILaunchImageSize - {414, 736} - - - UILaunchImageMinimumOSVersion - 8.0 - UILaunchImageName - LaunchImage-800-Landscape-736h - UILaunchImageOrientation - Landscape - UILaunchImageSize - {414, 736} - - + UILaunchStoryboardName + LaunchScreen UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -248,6 +175,17 @@ + CFBundleURLTypes + + + CFBundleURLName + com.apple.Magnifier + CFBundleURLSchemes + + apple-magnifier + + + LSSupportsOpeningDocumentsInPlace TSRootBinaries diff --git a/TrollStore/Resources/LaunchImage-700-568h@2x.png b/TrollStore/Resources/LaunchImage-700-568h@2x.png deleted file mode 100644 index e69de29..0000000 diff --git a/TrollStore/Resources/LaunchImage-700-Landscape@2x~ipad.png b/TrollStore/Resources/LaunchImage-700-Landscape@2x~ipad.png deleted file mode 100644 index e69de29..0000000 diff --git a/TrollStore/Resources/LaunchImage-700-Landscape~ipad.png b/TrollStore/Resources/LaunchImage-700-Landscape~ipad.png deleted file mode 100644 index e69de29..0000000 diff --git a/TrollStore/Resources/LaunchImage-700-Portrait@2x~ipad.png b/TrollStore/Resources/LaunchImage-700-Portrait@2x~ipad.png deleted file mode 100644 index e69de29..0000000 diff --git a/TrollStore/Resources/LaunchImage-700-Portrait~ipad.png b/TrollStore/Resources/LaunchImage-700-Portrait~ipad.png deleted file mode 100644 index e69de29..0000000 diff --git a/TrollStore/Resources/LaunchImage-800-667h@2x.png b/TrollStore/Resources/LaunchImage-800-667h@2x.png deleted file mode 100644 index e69de29..0000000 diff --git a/TrollStore/Resources/LaunchImage-800-Landscape-736h@3x.png b/TrollStore/Resources/LaunchImage-800-Landscape-736h@3x.png deleted file mode 100644 index e69de29..0000000 diff --git a/TrollStore/Resources/LaunchImage-800-Portrait-736h@3x.png b/TrollStore/Resources/LaunchImage-800-Portrait-736h@3x.png deleted file mode 100644 index e69de29..0000000 diff --git a/TrollStore/Resources/LaunchImage.png b/TrollStore/Resources/LaunchImage.png deleted file mode 100644 index e69de29..0000000 diff --git a/TrollStore/Resources/LaunchImage@2x.png b/TrollStore/Resources/LaunchImage@2x.png deleted file mode 100644 index e69de29..0000000 diff --git a/TrollStore/TSAppInfo.h b/TrollStore/TSAppInfo.h new file mode 100644 index 0000000..14ae0c8 --- /dev/null +++ b/TrollStore/TSAppInfo.h @@ -0,0 +1,55 @@ +// +// TSIPAInfo.h +// IPAInfo +// +// Created by Lars Fröder on 22.10.22. +// + +#import +#import +#import +@import UIKit; + +@interface TSAppInfo : NSObject +{ + NSString* _path; + BOOL _isArchive; + struct archive* _archive; + + NSString* _cachedAppBundleName; + NSString* _cachedRegistrationState; + NSDictionary* _cachedInfoDictionary; + NSDictionary* _cachedInfoDictionariesByPluginSubpaths; + NSDictionary* _cachedEntitlementsByBinarySubpaths; + UIImage* _cachedPreviewIcon; + int64_t _cachedSize; +} + +- (instancetype)initWithIPAPath:(NSString*)ipaPath; +- (instancetype)initWithAppBundlePath:(NSString*)bundlePath; +- (NSError*)determineAppBundleName; +- (NSError*)loadInfoDictionary; +- (NSError*)loadEntitlements; +- (NSError*)loadPreviewIcon; + +- (NSError*)sync_loadBasicInfo; +- (NSError*)sync_loadInfo; + +- (void)loadBasicInfoWithCompletion:(void (^)(NSError*))completionHandler; +- (void)loadInfoWithCompletion:(void (^)(NSError*))completionHandler; + +- (NSString*)displayName; +- (NSString*)bundleIdentifier; +- (NSString*)versionString; +- (NSString*)sizeString; +- (NSString*)bundlePath; +- (NSString*)registrationState; + +- (UIImage*)iconForSize:(CGSize)size; + +- (NSAttributedString*)detailedInfoTitle; +- (NSAttributedString*)detailedInfoDescription; +//- (UIImage*)image; +- (void)log; + +@end diff --git a/TrollStore/TSAppInfo.m b/TrollStore/TSAppInfo.m new file mode 100644 index 0000000..9a85454 --- /dev/null +++ b/TrollStore/TSAppInfo.m @@ -0,0 +1,1092 @@ +#import "TSAppInfo.h" +#import + +extern CGImageRef LICreateIconForImage(CGImageRef image, int variant, int precomposed); +extern UIImage* imageWithSize(UIImage* image, CGSize size); + +@implementation TSAppInfo + +- (instancetype)initWithIPAPath:(NSString*)ipaPath +{ + self = [super init]; + + if(self) + { + _path = ipaPath; + _isArchive = YES; + _archive = nil; + } + + return self; +} + +- (instancetype)initWithAppBundlePath:(NSString*)bundlePath +{ + self = [super init]; + + if(self) + { + _path = bundlePath; + _isArchive = NO; + _archive = nil; + } + + return self; +} + +- (void)dealloc +{ + [self closeArchive]; +} + +- (void)enumerateArchive:(void (^)(struct archive_entry* entry, BOOL* stop))enumerateBlock +{ + [self openArchive]; + + struct archive_entry *entry; + int r; + for (;;) + { + r = archive_read_next_header(_archive, &entry); + if (r == ARCHIVE_EOF) + break; + if (r < ARCHIVE_OK) + fprintf(stderr, "%s\n", archive_error_string(_archive)); + if (r < ARCHIVE_WARN) + return; + + BOOL stop = NO; + enumerateBlock(entry, &stop); + if(stop) break; + } +} + +- (struct archive_entry*)archiveEntryForSubpath:(NSString*)subpath +{ + __block struct archive_entry* outEntry = nil; + [self enumerateArchive:^(struct archive_entry *entry, BOOL *stop) { + NSString* currentSubpath = [NSString stringWithUTF8String:archive_entry_pathname(entry)]; + if([currentSubpath isEqualToString:subpath]) + { + outEntry = entry; + *stop = YES; + } + }]; + return outEntry; +} + +- (NSError*)determineAppBundleName +{ + NSError* outError; + + if(!_cachedAppBundleName) + { + if(_isArchive) + { + [self enumerateArchive:^(struct archive_entry *entry, BOOL *stop) + { + NSString* currentSubpath = [NSString stringWithUTF8String:archive_entry_pathname(entry)]; + if(currentSubpath.pathComponents.count == 3) + { + if([currentSubpath.pathComponents[0] isEqualToString:@"Payload"] && [currentSubpath.pathComponents[1].pathExtension isEqualToString:@"app"]) + { + self->_cachedAppBundleName = currentSubpath.pathComponents[1]; + *stop = YES; + } + } + }]; + + if(!_cachedAppBundleName) + { + NSString* errorDescription = @"Unable to locate app bundle inside the .IPA archive."; + outError = [NSError errorWithDomain:TrollStoreErrorDomain code:301 userInfo:@{NSLocalizedDescriptionKey : errorDescription}]; + } + } + } + + return outError; +} + +- (NSError*)loadInfoDictionary +{ + if(_isArchive && _cachedAppBundleName) + { + NSString* mainInfoPlistPath = [NSString stringWithFormat:@"Payload/%@/Info.plist", _cachedAppBundleName]; + struct archive_entry* infoDictEntry = [self archiveEntryForSubpath:mainInfoPlistPath]; + if(infoDictEntry) + { + size_t size = archive_entry_size(infoDictEntry); + void* buf = malloc(size); + size_t read = archive_read_data(_archive, buf, size); + + if(read == size) + { + NSData* infoPlistData = [NSData dataWithBytes:buf length:size]; + _cachedInfoDictionary = [NSPropertyListSerialization propertyListWithData:infoPlistData options:NSPropertyListImmutable format:nil error:nil]; + } + free(buf); + } + + __block NSMutableDictionary* pluginInfoDictionaries = [NSMutableDictionary new]; + + [self enumerateArchive:^(struct archive_entry *entry, BOOL *stop) { + NSString* currentSubpath = [NSString stringWithUTF8String:archive_entry_pathname(entry)]; + if([currentSubpath isEqualToString:mainInfoPlistPath]) return; + + if([currentSubpath.lastPathComponent isEqualToString:@"Info.plist"] && currentSubpath.pathComponents.count == 5) + { + if([currentSubpath.pathComponents[2] isEqualToString:@"PlugIns"]) + { + size_t size = archive_entry_size(entry); + void* buf = malloc(size); + size_t read = archive_read_data(self->_archive, buf, size); + + if(read == size) + { + NSData* infoPlistData = [NSData dataWithBytes:buf length:size]; + NSDictionary* pluginPlist = [NSPropertyListSerialization propertyListWithData:infoPlistData options:NSPropertyListImmutable format:nil error:nil]; + pluginInfoDictionaries[currentSubpath.stringByDeletingLastPathComponent] = pluginPlist; + } + free(buf); + } + } + }]; + + _cachedInfoDictionariesByPluginSubpaths = pluginInfoDictionaries.copy; + } + else + { + NSString* mainInfoPlistPath = [_path stringByAppendingPathComponent:@"Info.plist"]; + if([[NSFileManager defaultManager] fileExistsAtPath:mainInfoPlistPath]) + { + _cachedInfoDictionary = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:mainInfoPlistPath] error:nil]; + } + + __block NSMutableDictionary* pluginInfoDictionaries = [NSMutableDictionary new]; + NSArray* plugIns = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[_path stringByAppendingPathComponent:@"PlugIns"] error:nil]; + for(NSString* plugIn in plugIns) + { + NSString* pluginSubpath = [NSString stringWithFormat:@"PlugIns/%@", plugIn]; + NSString* pluginInfoDictionaryPath = [[_path stringByAppendingPathComponent:pluginSubpath] stringByAppendingPathComponent:@"Info.plist"]; + NSDictionary* pluginInfoDictionary = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:pluginInfoDictionaryPath] error:nil]; + if(pluginInfoDictionary) + { + pluginInfoDictionaries[pluginSubpath] = pluginInfoDictionary; + } + } + + _cachedInfoDictionariesByPluginSubpaths = pluginInfoDictionaries.copy; + } + + if(!_cachedInfoDictionary) + { + NSString* errorDescription = @"Unable to locate Info.plist inside app bundle."; + return [NSError errorWithDomain:TrollStoreErrorDomain code:302 userInfo:@{NSLocalizedDescriptionKey : errorDescription}]; + } + + return nil; +} + +- (NSError*)loadInstalledState +{ + if(!_isArchive) + { + NSURL* bundleURL = [NSURL fileURLWithPath:_path]; + LSApplicationProxy* appProxy = [LSApplicationProxy applicationProxyForBundleURL:bundleURL]; + if(appProxy) + { + if(appProxy && appProxy.isInstalled) + { + _cachedRegistrationState = appProxy.applicationType; + } + } + } + return nil; +} + +- (NSError*)loadEntitlements +{ + if(!_cachedEntitlementsByBinarySubpaths) + { + NSMutableDictionary* entitlementsByBinarySubpaths = [NSMutableDictionary new]; + + if(_isArchive) + { + if(_cachedInfoDictionary) + { + NSString* bundleExecutable = _cachedInfoDictionary[@"CFBundleExecutable"]; + NSString* bundleExecutableSubpath = [NSString stringWithFormat:@"Payload/%@/%@", _cachedAppBundleName, bundleExecutable]; + struct archive_entry* mainBinaryEntry = [self archiveEntryForSubpath:bundleExecutableSubpath]; + if(!mainBinaryEntry) + { + NSString* errorDescription = @"Unable to locate main binary inside app bundle."; + return [NSError errorWithDomain:TrollStoreErrorDomain code:303 userInfo:@{NSLocalizedDescriptionKey : errorDescription}]; + } + + size_t size = archive_entry_size(mainBinaryEntry); + void* buf = malloc(size); + size_t read = archive_read_data(_archive, buf, size); + + if(read == size) + { + NSData* binaryData = [NSData dataWithBytes:buf length:size]; + entitlementsByBinarySubpaths[bundleExecutableSubpath] = dumpEntitlementsFromBinaryData(binaryData); + } + free(buf); + } + + [_cachedInfoDictionariesByPluginSubpaths enumerateKeysAndObjectsUsingBlock:^(NSString* pluginSubpath, NSDictionary* infoDictionary, BOOL * _Nonnull stop) { + NSString* pluginExecutable = infoDictionary[@"CFBundleExecutable"]; + NSString* pluginExecutableSubpath = [NSString stringWithFormat:@"%@/%@", pluginSubpath, pluginExecutable]; + struct archive_entry* pluginBinaryEntry = [self archiveEntryForSubpath:pluginExecutableSubpath]; + if(!pluginBinaryEntry) return; + + size_t size = archive_entry_size(pluginBinaryEntry); + void* buf = malloc(size); + size_t read = archive_read_data(_archive, buf, size); + + if(read == size) + { + NSData* binaryData = [NSData dataWithBytes:buf length:size]; + entitlementsByBinarySubpaths[pluginExecutableSubpath] = dumpEntitlementsFromBinaryData(binaryData); + } + free(buf); + }]; + } + else + { + if(_cachedInfoDictionary) + { + NSString* bundleExecutable = _cachedInfoDictionary[@"CFBundleExecutable"]; + NSString* bundleExecutablePath = [_path stringByAppendingPathComponent:bundleExecutable]; + + if(![[NSFileManager defaultManager] fileExistsAtPath:bundleExecutablePath]) + { + NSString* errorDescription = @"Unable to locate main binary inside app bundle."; + return [NSError errorWithDomain:TrollStoreErrorDomain code:303 userInfo:@{NSLocalizedDescriptionKey : errorDescription}]; + } + + entitlementsByBinarySubpaths[bundleExecutable] = dumpEntitlementsFromBinaryAtPath(bundleExecutablePath); + } + + [_cachedInfoDictionariesByPluginSubpaths enumerateKeysAndObjectsUsingBlock:^(NSString* pluginSubpath, NSDictionary* infoDictionary, BOOL * _Nonnull stop) { + NSString* pluginExecutable = infoDictionary[@"CFBundleExecutable"]; + NSString* pluginExecutableSubpath = [NSString stringWithFormat:@"%@/%@", pluginSubpath, pluginExecutable]; + + NSString* pluginExecutablePath = [_path stringByAppendingPathComponent:pluginExecutableSubpath]; + entitlementsByBinarySubpaths[pluginExecutableSubpath] = dumpEntitlementsFromBinaryAtPath(pluginExecutablePath); + }]; + } + + _cachedEntitlementsByBinarySubpaths = entitlementsByBinarySubpaths.copy; + } + return 0; +} + +- (NSError*)loadSize +{ + if(_isArchive) + { + [self enumerateArchive:^(struct archive_entry* entry, BOOL* stop) + { + int64_t size = archive_entry_size(entry); + _cachedSize += size; + }]; + } + else + { + NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager] enumeratorAtURL:[NSURL fileURLWithPath:_path] + includingPropertiesForKeys:@[NSURLIsRegularFileKey,NSURLFileAllocatedSizeKey,NSURLTotalFileAllocatedSizeKey] + options:0 + errorHandler:nil]; + + for(NSURL* itemURL in enumerator) + { + NSNumber* isRegularFile; + NSError* error; + [itemURL getResourceValue:&isRegularFile forKey:NSURLIsRegularFileKey error:&error]; + + if(isRegularFile.boolValue) + { + NSNumber* totalFileAllocatedSize; + [itemURL getResourceValue:&totalFileAllocatedSize forKey:NSURLTotalFileAllocatedSizeKey error:nil]; + if(totalFileAllocatedSize) + { + _cachedSize += totalFileAllocatedSize.integerValue; + } + else + { + NSNumber* fileAllocatedSize; + [itemURL getResourceValue:&fileAllocatedSize forKey:NSURLFileAllocatedSizeKey error:nil]; + if(fileAllocatedSize) + { + _cachedSize += fileAllocatedSize.integerValue; + } + } + } + } + } + + return nil; +} + +- (NSError*)loadPreviewIcon +{ + int imageVariant; + CGFloat screenScale = UIScreen.mainScreen.scale; + + if(screenScale >= 3.0) + { + imageVariant = 34; + } + else if(screenScale >= 2.0) + { + imageVariant = 17; + } + else + { + imageVariant = 4; + } + + CGImageRef liIcon = LICreateIconForImage([[self iconForSize:CGSizeMake(29,29)] CGImage], imageVariant, 0); + _cachedPreviewIcon = [[UIImage alloc] initWithCGImage:liIcon scale:screenScale orientation:0];; + return nil; +} + +- (int)openArchive +{ + if(_archive) + { + [self closeArchive]; + } + NSLog(@"open"); + _archive = archive_read_new(); + archive_read_support_format_all(_archive); + archive_read_support_filter_all(_archive); + int r = archive_read_open_filename(_archive, _path.UTF8String, 10240); + return r ? r : 0; +} + +- (void)closeArchive +{ + if(_archive) + { + archive_read_close(_archive); + archive_read_free(_archive); + _archive = nil; + } +} + +- (NSError*)sync_loadBasicInfo +{ + NSError* e; + + e = [self determineAppBundleName]; + if(e) return e; + + e = [self loadInfoDictionary]; + if(e) return e; + + e = [self loadInstalledState]; + if(e) return e; + + return nil; +} + +- (NSError*)sync_loadInfo +{ + NSError* e; + + e = [self sync_loadBasicInfo]; + if(e) return e; + + e = [self loadEntitlements]; + if(e) return e; + + e = [self loadSize]; + if(e) return e; + + e = [self loadPreviewIcon]; + if(e) return e; + + return nil; +} + +- (void)loadBasicInfoWithCompletion:(void (^)(NSError*))completionHandler +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completionHandler([self sync_loadBasicInfo]); + }); +} + +- (void)loadInfoWithCompletion:(void (^)(NSError*))completionHandler +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completionHandler([self sync_loadInfo]); + }); +} + +- (void)enumerateAllInfoDictionaries:(void (^)(NSString* key, NSObject* value, BOOL* stop))enumerateBlock +{ + __block BOOL b_stop = NO; + + [_cachedInfoDictionary enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSObject* value, BOOL* stop) { + enumerateBlock(key, value, &b_stop); + if(b_stop) *stop = YES; + }]; + + if(b_stop) return; + + [_cachedInfoDictionariesByPluginSubpaths enumerateKeysAndObjectsUsingBlock:^(NSString* pluginSubpath, NSDictionary* pluginInfoDictionary, BOOL* stop_1) + { + if([pluginInfoDictionary isKindOfClass:NSDictionary.class]) + { + [pluginInfoDictionary enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSObject* value, BOOL * _Nonnull stop_2) { + enumerateBlock(key, value, &b_stop); + if(b_stop) + { + *stop_1 = YES; + *stop_2 = YES; + } + }]; + } + }]; +} + +- (void)enumerateAllEntitlements:(void (^)(NSString* key, NSObject* value, BOOL* stop))enumerateBlock +{ + __block BOOL b_stop = NO; + + [_cachedEntitlementsByBinarySubpaths enumerateKeysAndObjectsUsingBlock:^(NSString* binarySubpath, NSDictionary* binaryInfoDictionary, BOOL* stop_1) + { + if([binaryInfoDictionary isKindOfClass:NSDictionary.class]) + { + [binaryInfoDictionary enumerateKeysAndObjectsUsingBlock:^(NSString* key, NSObject* value, BOOL * _Nonnull stop_2) { + enumerateBlock(key, value, &b_stop); + if(b_stop) + { + *stop_1 = YES; + *stop_2 = YES; + } + }]; + } + }]; +} + +- (void)enumerateAvailableIcons:(void (^)(CGSize iconSize, NSUInteger iconScale, NSString* iconPath, BOOL* stop))enumerateBlock +{ + if(_cachedInfoDictionary) + { + NSString* iconName = nil; + NSDictionary* cfBundleIcons = _cachedInfoDictionary[@"CFBundleIcons"]; + if(!cfBundleIcons) + { + cfBundleIcons = _cachedInfoDictionary[@"CFBundleIcons~ipad"]; + } + if(cfBundleIcons && [cfBundleIcons isKindOfClass:NSDictionary.class]) + { + NSDictionary* cfBundlePrimaryIcon = cfBundleIcons[@"CFBundlePrimaryIcon"]; + + if(cfBundlePrimaryIcon && [cfBundlePrimaryIcon isKindOfClass:NSDictionary.class]) + { + NSString* potentialIconName = cfBundlePrimaryIcon[@"CFBundleIconName"]; + if(potentialIconName && [potentialIconName isKindOfClass:NSString.class]) + { + iconName = potentialIconName; + } + else + { + NSArray* cfBundleIconFiles = cfBundlePrimaryIcon[@"CFBundleIconFiles"]; + if(cfBundleIconFiles && [cfBundleIconFiles isKindOfClass:NSArray.class]) + { + NSString* oneIconFile = cfBundleIconFiles.firstObject; + NSString* otherIconFile = cfBundleIconFiles.lastObject; + iconName = [oneIconFile commonPrefixWithString:otherIconFile options:NSLiteralSearch]; + } + } + } + } + + if(!iconName) return; + + void (^wrapperBlock)(NSString* iconPath, BOOL* stop) = ^(NSString* iconPath, BOOL* stop) + { + NSString* currentIconName = iconPath.lastPathComponent; + NSString* iconSuffix = [currentIconName substringFromIndex:[iconName length]]; + NSArray* seperatedIconSuffix = [iconSuffix componentsSeparatedByString:@"@"]; + + NSString* currentIconResolution = seperatedIconSuffix.firstObject; + NSString* currentIconScale; + if(seperatedIconSuffix.count > 1) + { + currentIconScale = seperatedIconSuffix.lastObject; + } + + NSNumberFormatter* f = [[NSNumberFormatter alloc] init]; + f.numberStyle = NSNumberFormatterDecimalStyle; + + NSArray* separatedIconSize = [currentIconResolution componentsSeparatedByString:@"x"]; + NSNumber* widthNum = [f numberFromString:separatedIconSize.firstObject]; + NSNumber* heightNum = [f numberFromString:separatedIconSize.lastObject]; + + CGSize iconSize = CGSizeMake(widthNum.unsignedIntegerValue, heightNum.unsignedIntegerValue); + + NSUInteger scale = 1; + if(currentIconScale) + { + NSNumber* scaleNum = [f numberFromString:currentIconScale]; + scale = scaleNum.unsignedIntegerValue; + } + + enumerateBlock(iconSize, scale, iconPath, stop); + }; + + if(_isArchive) + { + NSString* iconPrefix = [NSString stringWithFormat:@"Payload/%@/%@", _cachedAppBundleName, iconName]; + [self enumerateArchive:^(struct archive_entry* entry, BOOL* stop) + { + NSString* currentSubpath = [NSString stringWithUTF8String:archive_entry_pathname(entry)]; + if([currentSubpath hasPrefix:iconPrefix]) + { + wrapperBlock(currentSubpath, stop); + } + }]; + } + else + { + NSArray* files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:_path error:nil]; + for(NSString* fileName in files) + { + if([fileName hasPrefix:iconName]) + { + NSString* iconPath = [_path stringByAppendingPathComponent:fileName]; + + BOOL stop = NO; + wrapperBlock(iconPath, &stop); + if(stop) return; + } + } + + } + } +} + +- (UIImage*)iconForSize:(CGSize)size +{ + if(size.width != size.height) + { + //not supported + return nil; + } + + // Flow: Check if icon with the exact size exists + // If not, take the next best one and scale it down + + //UIImage* imageToReturn; + __block NSString* foundIconPath; + + // Attempt 1: Check for icon with exact size + [self enumerateAvailableIcons:^(CGSize iconSize, NSUInteger iconScale, NSString* iconPath, BOOL* stop) + { + if(CGSizeEqualToSize(iconSize, size) && UIScreen.mainScreen.scale == iconScale) + { + foundIconPath = iconPath; + //imageToReturn = imageWithSize([UIImage imageWithContentsOfFile:iconPath], size); + *stop = YES; + } + }]; + + if(!foundIconPath) + { + // Attempt 2: Check for icon with bigger size + __block CGSize closestIconSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX); + + [self enumerateAvailableIcons:^(CGSize iconSize, NSUInteger iconScale, NSString* iconPath, BOOL* stop) + { + if(iconSize.width > size.width && iconSize.width < closestIconSize.width) + { + closestIconSize = iconSize; + } + }]; + + if(closestIconSize.width == CGFLOAT_MAX) + { + // Attempt 3: Take biggest icon and scale it up + closestIconSize = CGSizeMake(0,0); + [self enumerateAvailableIcons:^(CGSize iconSize, NSUInteger iconScale, NSString* iconPath, BOOL* stop) + { + if(iconSize.width > closestIconSize.width) + { + closestIconSize = iconSize; + } + }]; + } + + if(closestIconSize.width == 0) return nil; + + [self enumerateAvailableIcons:^(CGSize iconSize, NSUInteger iconScale, NSString* iconPath, BOOL* stop) + { + if(CGSizeEqualToSize(iconSize, closestIconSize)) + { + closestIconSize = iconSize; + foundIconPath = iconPath; + *stop = YES; + } + }]; + } + + if(!foundIconPath) return nil; + + if(_isArchive) + { + __block NSData* iconData; + + struct archive_entry* iconEntry = [self archiveEntryForSubpath:foundIconPath]; + if(iconEntry) + { + size_t size = archive_entry_size(iconEntry); + void* buf = malloc(size); + size_t read = archive_read_data(_archive, buf, size); + + if(read == size) + { + iconData = [NSData dataWithBytes:buf length:size]; + } + + free(buf); + } + + if(iconData) + { + return imageWithSize([UIImage imageWithData:iconData], size); + } + } + else + { + return imageWithSize([UIImage imageWithContentsOfFile:foundIconPath], size); + } + return nil; +} + +- (NSString*)displayName +{ + NSString* displayName = _cachedInfoDictionary[@"CFBundleDisplayName"]; + if(!displayName || ![displayName isKindOfClass:NSString.class]) + { + displayName = _cachedInfoDictionary[@"CFBundleName"]; + if(!displayName || ![displayName isKindOfClass:NSString.class]) + { + displayName = _cachedInfoDictionary[@"CFBundleExecutable"]; + if(!displayName || ![displayName isKindOfClass:NSString.class]) + { + if(_isArchive) + { + displayName = [_cachedAppBundleName stringByDeletingPathExtension]; + } + else + { + displayName = [[_path lastPathComponent] stringByDeletingPathExtension]; + } + } + } + } + return displayName; +} + +- (NSString*)bundleIdentifier +{ + return _cachedInfoDictionary[@"CFBundleIdentifier"]; +} + +- (NSString*)versionString +{ + NSString* version = _cachedInfoDictionary[@"CFBundleShortVersionString"]; + if(!version) + { + version = _cachedInfoDictionary[@"CFBundleVersion"]; + } + return version; +} + +- (NSString*)sizeString +{ + return [NSByteCountFormatter stringFromByteCount:_cachedSize countStyle:NSByteCountFormatterCountStyleFile]; +} + +- (NSString*)bundlePath +{ + if(!_isArchive) + { + return _path; + } + return nil; +} + +- (NSString*)registrationState +{ + return _cachedRegistrationState; +} + +- (NSAttributedString*)detailedInfoTitle +{ + NSString* displayName = [self displayName]; + + NSMutableDictionary* titleAttributes = @{ + NSFontAttributeName : [UIFont boldSystemFontOfSize:16] + }.mutableCopy; + NSMutableAttributedString* description = [NSMutableAttributedString new]; + + if(_cachedPreviewIcon) + { + titleAttributes[NSBaselineOffsetAttributeName] = @9.0; + + NSTextAttachment* previewAttachment = [[NSTextAttachment alloc] init]; + previewAttachment.image = _cachedPreviewIcon; + + [description appendAttributedString:[NSAttributedString attributedStringWithAttachment:previewAttachment]]; + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@" " attributes:titleAttributes]]; + } + + [description appendAttributedString:[[NSAttributedString alloc] initWithString:displayName attributes:titleAttributes]]; + + return description.copy; +} + +- (NSAttributedString*)detailedInfoDescription +{ + NSString* bundleId = [self bundleIdentifier]; + NSString* version = [self versionString]; + NSString* sizeString = [self sizeString]; + + // Check if any bundle contains a root binary + __block BOOL containsRootBinary = NO; + [self enumerateAllInfoDictionaries:^(NSString *key, NSObject *value, BOOL *stop) { + if([key isEqualToString:@"TSRootBinaries"]) + { + NSArray* valueArr = (NSArray*)value; + if([valueArr isKindOfClass:NSArray.class]) + { + containsRootBinary = valueArr.count; + if(containsRootBinary) *stop = YES; + } + } + }]; + + // Check if any bundles main binary runs unsandboxed + __block BOOL isUnsandboxed = NO; + [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) { + if([key isEqualToString:@"com.apple.private.security.container-required"]) + { + NSNumber* valueNum = (NSNumber*)value; + if(valueNum && [valueNum isKindOfClass:NSNumber.class]) + { + isUnsandboxed = !valueNum.boolValue; + if(isUnsandboxed) *stop = YES; + } + } else if([key isEqualToString:@"com.apple.private.security.no-container"]) + { + NSNumber* valueNum = (NSNumber*)value; + if(valueNum && [valueNum isKindOfClass:NSNumber.class]) + { + isUnsandboxed = valueNum.boolValue; + if(isUnsandboxed) *stop = YES; + } + } else if([key isEqualToString:@"com.apple.private.security.no-sandbox"]) + { + NSNumber* valueNum = (NSNumber*)value; + if(valueNum && [valueNum isKindOfClass:NSNumber.class]) + { + isUnsandboxed = valueNum.boolValue; + if(isUnsandboxed) *stop = YES; + } + } + }]; + + // Check if any bundles main binary can spawn an external binary + __block BOOL canSpawnBinaries = NO; + [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) + { + if([key isEqualToString:@"platform-application"]) + { + NSNumber* valueNum = (NSNumber*)value; + if(valueNum && [valueNum isKindOfClass:NSNumber.class]) + { + canSpawnBinaries = valueNum.boolValue; + if(canSpawnBinaries) *stop = YES; + } + } + }]; + + // Check if any bundles main binary can spawn an external binary as root + __block BOOL hasPersonaMngmt = NO; + [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) + { + if([key isEqualToString:@"com.apple.private.persona-mgmt"]) + { + NSNumber* valueNum = (NSNumber*)value; + if(valueNum && [valueNum isKindOfClass:NSNumber.class]) + { + hasPersonaMngmt = valueNum.boolValue; + if(hasPersonaMngmt) *stop = YES; + } + } + }]; + + // Accessible containers + // com.apple.developer.icloud-container-identifiers + // com.apple.security.application-groups + // Unrestricted if special entitlement + + __block BOOL unrestrictedContainerAccess = NO; + [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) + { + if([key isEqualToString:@"com.apple.private.security.storage.AppDataContainers"]) + { + NSNumber* valueNum = (NSNumber*)value; + if(valueNum && [valueNum isKindOfClass:NSNumber.class]) + { + unrestrictedContainerAccess = valueNum.boolValue; + if(hasPersonaMngmt) *stop = YES; + } + } + }]; + + __block NSMutableArray* accessibleContainers = [NSMutableArray new]; + if(!unrestrictedContainerAccess) + { + [self enumerateAllInfoDictionaries:^(NSString *key, NSObject *value, BOOL *stop) { + if([key isEqualToString:@"CFBundleIdentifier"]) + { + NSString* valueStr = (NSString*)value; + if([valueStr isKindOfClass:NSString.class]) + { + [accessibleContainers addObject:valueStr]; + } + } + }]; + + [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) + { + if([key isEqualToString:@"com.apple.developer.icloud-container-identifiers"] || [key isEqualToString:@"com.apple.security.application-groups"] || [key isEqualToString:@"com.apple.security.system-groups"]) + { + NSArray* valueArr = (NSArray*)value; + if([valueArr isKindOfClass:NSArray.class]) + { + for(NSString* containerID in valueArr) + { + if([containerID isKindOfClass:NSString.class]) + { + if(![accessibleContainers containsObject:containerID]) + { + [accessibleContainers addObject:containerID]; + } + } + } + } + } + }]; + } + + // Accessible Keychain Groups + // keychain-access-groups + // Unrestricted if single * (maybe?) + __block BOOL unrestrictedKeychainAccess = NO; + __block NSMutableArray* accessibleKeychainGroups = [NSMutableArray new]; + [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) { + if([key isEqualToString:@"keychain-access-groups"]) + { + NSArray* valueArr = (NSArray*)value; + if([valueArr isKindOfClass:NSArray.class]) + { + for(NSString* keychainID in valueArr) + { + if([keychainID isKindOfClass:NSString.class]) + { + if([keychainID isEqualToString:@"*"]) + { + unrestrictedKeychainAccess = YES; + } + else if(![accessibleKeychainGroups containsObject:keychainID]) + { + [accessibleKeychainGroups addObject:keychainID]; + } + } + } + } + } + }]; + + __block NSMutableArray* URLSchemes = [NSMutableArray new]; + [self enumerateAllInfoDictionaries:^(NSString *key, NSObject *value, BOOL *stop) { + if([key isEqualToString:@"CFBundleURLTypes"]) + { + NSArray* valueArr = (NSArray*)value; + if([valueArr isKindOfClass:NSArray.class]) + { + for(NSDictionary* URLTypeDict in valueArr) + { + if([URLTypeDict isKindOfClass:NSDictionary.class]) + { + NSArray* cURLSchemes = URLTypeDict[@"CFBundleURLSchemes"]; + if(cURLSchemes && [cURLSchemes isKindOfClass:NSArray.class]) + { + for(NSString* URLScheme in cURLSchemes) + { + if(![URLSchemes containsObject:URLScheme]) + { + [URLSchemes addObject:URLScheme]; + } + } + } + } + } + } + } + }]; + + NSMutableParagraphStyle* leftAlignment = [[NSMutableParagraphStyle alloc] init]; + leftAlignment.alignment = NSTextAlignmentLeft; + + UIColor* dangerColor = [UIColor colorWithDynamicProvider:^UIColor*(UITraitCollection *traitCollection) + { + if(traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) + { + return [UIColor orangeColor]; + } + else + { + return [UIColor redColor]; + } + }]; + + UIColor* warningColor = [UIColor colorWithDynamicProvider:^UIColor*(UITraitCollection *traitCollection) + { + if(traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) + { + return [UIColor yellowColor]; + } + else + { + return [UIColor orangeColor]; + } + }]; + + NSMutableAttributedString* description = [NSMutableAttributedString new]; + + NSDictionary* headerAttributes = @{ + NSFontAttributeName : [UIFont boldSystemFontOfSize:14], + NSParagraphStyleAttributeName : leftAlignment + }; + + NSDictionary* bodyAttributes = @{ + NSFontAttributeName : [UIFont systemFontOfSize:11], + NSParagraphStyleAttributeName : leftAlignment + }; + + NSDictionary* bodyWarningAttributes = @{ + NSFontAttributeName : [UIFont systemFontOfSize:11], + NSParagraphStyleAttributeName : leftAlignment, + NSForegroundColorAttributeName : warningColor + }; + + NSDictionary* bodyDangerAttributes = @{ + NSFontAttributeName : [UIFont systemFontOfSize:11], + NSParagraphStyleAttributeName : leftAlignment, + NSForegroundColorAttributeName : dangerColor + }; + + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"Metadata" attributes:headerAttributes]]; + + [description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\nBundle Identifier: %@", bundleId] attributes:bodyAttributes]]; + [description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\nVersion: %@", version] attributes:bodyAttributes]]; + [description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\nSize: %@", sizeString] attributes:bodyAttributes]]; + + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nSandboxing" attributes:headerAttributes]]; + if(isUnsandboxed) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app runs unsandboxed and can access most of the file system." attributes:bodyWarningAttributes]]; + } + else + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app runs sandboxed and can only access the containers listed below." attributes:bodyAttributes]]; + } + + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nCapabilities" attributes:headerAttributes]]; + if(containsRootBinary && canSpawnBinaries && hasPersonaMngmt) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can spawn it's own embedded binaries with root privileges." attributes:bodyDangerAttributes]]; + } + else if(canSpawnBinaries && hasPersonaMngmt) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can spawn arbitary binaries as root, but does not contain any such binaries by itself." attributes:bodyWarningAttributes]]; + } + else if(canSpawnBinaries) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can spawn arbitary binaries as the mobile user." attributes:bodyWarningAttributes]]; + } + else + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can not spawn other binaries." attributes:bodyAttributes]]; + } + + if(unrestrictedContainerAccess || accessibleContainers.count) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nAccessible Containers" attributes:headerAttributes]]; + if(unrestrictedContainerAccess) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nUnrestricted, the app can access all data containers on the system." attributes:bodyDangerAttributes]]; + } + else + { + for(NSString* containerID in accessibleContainers) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\n%@", containerID] attributes:bodyAttributes]]; + } + } + } + + if(unrestrictedKeychainAccess || accessibleKeychainGroups.count) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nAccessible Keychain Groups" attributes:headerAttributes]]; + if(unrestrictedKeychainAccess) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nUnrestricted, the app can access the entire keychain." attributes:bodyDangerAttributes]]; + } + else + { + for(NSString* keychainID in accessibleKeychainGroups) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\n%@", keychainID] attributes:bodyAttributes]]; + } + } + } + + if(URLSchemes.count) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nURL Schemes" attributes:headerAttributes]]; + + for(NSString* URLScheme in URLSchemes) + { + [description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\n%@", URLScheme] attributes:bodyAttributes]]; + } + } + + return description; +} + +- (void)log +{ + NSLog(@"entitlements:"); + [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) { + NSLog(@"%@ -> %@", key, value); + }]; + + NSLog(@"info dictionaries:"); + [self enumerateAllInfoDictionaries:^(NSString *key, NSObject *value, BOOL *stop) { + NSLog(@"%@ -> %@", key, value); + }]; +} + + +@end diff --git a/TrollStore/TSAppTableViewController.h b/TrollStore/TSAppTableViewController.h index fe826bf..16d3e65 100644 --- a/TrollStore/TSAppTableViewController.h +++ b/TrollStore/TSAppTableViewController.h @@ -1,10 +1,13 @@ #import +#import "TSAppInfo.h" -@interface TSAppTableViewController : UITableViewController +@interface TSAppTableViewController : UITableViewController { UIImage* _placeholderIcon; - NSArray* _cachedAppPaths; + NSArray* _cachedAppInfos; NSMutableDictionary* _cachedIcons; + UISearchController* _searchController; + NSString* _searchKey; } @end \ No newline at end of file diff --git a/TrollStore/TSAppTableViewController.m b/TrollStore/TSAppTableViewController.m index 6268a61..46ff141 100644 --- a/TrollStore/TSAppTableViewController.m +++ b/TrollStore/TSAppTableViewController.m @@ -1,6 +1,10 @@ #import "TSAppTableViewController.h" #import "TSApplicationsManager.h" +#import +#import "TSInstallationController.h" +#import "TSUtil.h" +@import UniformTypeIdentifiers; #define ICON_FORMAT_IPAD 8 #define ICON_FORMAT_IPHONE 10 @@ -34,16 +38,37 @@ UIImage* imageWithSize(UIImage* image, CGSize size) @implementation TSAppTableViewController -- (void)loadCachedAppPaths +- (void)loadAppInfos { NSArray* appPaths = [[TSApplicationsManager sharedInstance] installedAppPaths]; + NSMutableArray* appInfos = [NSMutableArray new]; - _cachedAppPaths = [appPaths sortedArrayUsingComparator:^NSComparisonResult(NSString* appPathA, NSString* appPathB) { - NSString* displayNameA = [[TSApplicationsManager sharedInstance] displayNameForAppPath:appPathA]; - NSString* displayNameB = [[TSApplicationsManager sharedInstance] displayNameForAppPath:appPathB]; + for(NSString* appPath in appPaths) + { + TSAppInfo* appInfo = [[TSAppInfo alloc] initWithAppBundlePath:appPath]; + [appInfo sync_loadBasicInfo]; + [appInfos addObject:appInfo]; + } - return [displayNameA localizedStandardCompare:displayNameB]; + if(_searchKey && ![_searchKey isEqualToString:@""]) + { + [appInfos enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(TSAppInfo* appInfo, NSUInteger idx, BOOL* stop) + { + NSString* appName = [appInfo displayName]; + BOOL nameMatch = [appName rangeOfString:_searchKey options:NSCaseInsensitiveSearch range:NSMakeRange(0, [appName length]) locale:[NSLocale currentLocale]].location != NSNotFound; + if(!nameMatch) + { + [appInfos removeObjectAtIndex:idx]; + } + }]; + } + + [appInfos sortUsingComparator:^(TSAppInfo* appInfoA, TSAppInfo* appInfoB) + { + return [[appInfoA displayName] localizedStandardCompare:[appInfoB displayName]]; }]; + + _cachedAppInfos = appInfos.copy; } - (instancetype)init @@ -51,7 +76,7 @@ UIImage* imageWithSize(UIImage* image, CGSize size) self = [super init]; if(self) { - [self loadCachedAppPaths]; + [self loadAppInfos]; _placeholderIcon = [UIImage _applicationIconImageForBundleIdentifier:@"com.apple.WebSheet" format:iconFormatToUse() scale:[UIScreen mainScreen].scale]; _cachedIcons = [NSMutableDictionary new]; } @@ -60,7 +85,7 @@ UIImage* imageWithSize(UIImage* image, CGSize size) - (void)reloadTable { - [self loadCachedAppPaths]; + [self loadAppInfos]; dispatch_async(dispatch_get_main_queue(), ^ { [self.tableView reloadData]; @@ -70,42 +95,187 @@ UIImage* imageWithSize(UIImage* image, CGSize size) - (void)loadView { [super loadView]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(reloadTable) - name:@"ApplicationsChanged" - object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadTable) name:@"ApplicationsChanged" object:nil]; } - (void)viewDidLoad { [super viewDidLoad]; self.tableView.allowsMultipleSelectionDuringEditing = NO; + + [self _setUpNavigationBar]; + [self _setUpSearchBar]; } -- (void)showError:(NSError*)error +- (void)_setUpNavigationBar { - UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Error %ld", error.code] message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]; - [errorAlert addAction:closeAction]; - [self presentViewController:errorAlert animated:YES completion:nil]; + UIAction* installFromFileAction = [UIAction actionWithTitle:@"Install IPA File" image:[UIImage systemImageNamed:@"doc.badge.plus"] identifier:@"InstallIPAFile" handler:^(__kindof UIAction *action) + { + UTType* ipaType = [UTType typeWithFilenameExtension:@"ipa" conformingToType:UTTypeData]; + UTType* tipaType = [UTType typeWithFilenameExtension:@"tipa" conformingToType:UTTypeData]; + + UIDocumentPickerViewController* documentPickerVC = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[ipaType, tipaType]]; + //UIDocumentPickerViewController* documentPickerVC = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"com.apple.itunes.ipa", @"com.opa334.trollstore.tipa"] inMode:UIDocumentPickerModeOpen]; + documentPickerVC.allowsMultipleSelection = NO; + documentPickerVC.delegate = self; + + [TSPresentationDelegate presentViewController:documentPickerVC animated:YES completion:nil]; + }]; + + UIAction* installFromURLAction = [UIAction actionWithTitle:@"Install from URL" image:[UIImage systemImageNamed:@"link.badge.plus"] identifier:@"InstallFromURL" handler:^(__kindof UIAction *action) + { + UIAlertController* installURLController = [UIAlertController alertControllerWithTitle:@"Install from URL" message:@"" preferredStyle:UIAlertControllerStyleAlert]; + + [installURLController addTextFieldWithConfigurationHandler:^(UITextField *textField) { + textField.placeholder = @"URL"; + }]; + + UIAlertAction* installAction = [UIAlertAction actionWithTitle:@"Install" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) + { + NSString* URLString = installURLController.textFields.firstObject.text; + NSURL* remoteURL = [NSURL URLWithString:URLString]; + + [TSInstallationController handleAppInstallFromRemoteURL:remoteURL completion:nil]; + }]; + [installURLController addAction:installAction]; + + UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]; + [installURLController addAction:cancelAction]; + + [TSPresentationDelegate presentViewController:installURLController animated:YES completion:nil]; + }]; + + UIMenu* installMenu = [UIMenu menuWithChildren:@[installFromFileAction, installFromURLAction]]; + + UIBarButtonItem* installBarButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage systemImageNamed:@"plus"] menu:installMenu]; + + self.navigationItem.rightBarButtonItems = @[installBarButtonItem]; } -- (void)openAppPressedForRowAtIndexPath:(NSIndexPath *)indexPath +- (void)_setUpSearchBar +{ + _searchController = [[UISearchController alloc] initWithSearchResultsController:nil]; + _searchController.searchResultsUpdater = self; + _searchController.obscuresBackgroundDuringPresentation = NO; + self.navigationItem.searchController = _searchController; + self.navigationItem.hidesSearchBarWhenScrolling = YES; +} + +- (void)updateSearchResultsForSearchController:(UISearchController *)searchController +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + _searchKey = searchController.searchBar.text; + [self reloadTable]; + }); +} + +- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls +{ + NSString* pathToIPA = urls.firstObject.path; + [TSInstallationController presentInstallationAlertForFile:pathToIPA completion:nil]; +} + +- (void)openAppPressedForRowAtIndexPath:(NSIndexPath*)indexPath { TSApplicationsManager* appsManager = [TSApplicationsManager sharedInstance]; - NSString* appPath = _cachedAppPaths[indexPath.row]; - NSString* appId = [appsManager appIdForAppPath:appPath]; + TSAppInfo* appInfo = _cachedAppInfos[indexPath.row]; + NSString* appId = [appInfo bundleIdentifier]; BOOL didOpen = [appsManager openApplicationWithBundleID:appId]; // if we failed to open the app, show an alert - if (!didOpen) { - NSString *failMessage = [NSString stringWithFormat: @"Failed to open %@", appId]; - UIAlertController* didFailController = [UIAlertController alertControllerWithTitle:failMessage message: nil preferredStyle:UIAlertControllerStyleAlert]; + if(!didOpen) + { + NSString* failMessage = @""; + if([[appInfo registrationState] isEqualToString:@"User"]) + { + failMessage = @"This app was not able to launch because it has a \"User\" registration state, register it as \"System\" and try again."; + } + + NSString* failTitle = [NSString stringWithFormat:@"Failed to open %@", appId]; + UIAlertController* didFailController = [UIAlertController alertControllerWithTitle:failTitle message:failMessage preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]; - [didFailController addAction: cancelAction]; - [self presentViewController:didFailController animated:YES completion:nil]; + [didFailController addAction:cancelAction]; + [TSPresentationDelegate presentViewController:didFailController animated:YES completion:nil]; + } +} + +- (void)showDetailsPressedForRowAtIndexPath:(NSIndexPath*)indexPath +{ + TSAppInfo* appInfo = _cachedAppInfos[indexPath.row]; + + [appInfo loadInfoWithCompletion:^(NSError* error) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + if(!error) + { + UIAlertController* detailsAlert = [UIAlertController alertControllerWithTitle:@"" message:@"" preferredStyle:UIAlertControllerStyleAlert]; + detailsAlert.attributedTitle = [appInfo detailedInfoTitle]; + detailsAlert.attributedMessage = [appInfo detailedInfoDescription]; + + UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]; + [detailsAlert addAction:closeAction]; + + [TSPresentationDelegate presentViewController:detailsAlert animated:YES completion:nil]; + } + else + { + UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Parse Error %ld", error.code] message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]; + [errorAlert addAction:closeAction]; + + [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; + } + }); + }]; +} + +- (void)changeAppRegistrationForRowAtIndexPath:(NSIndexPath*)indexPath toState:(NSString*)newState +{ + TSAppInfo* appInfo = _cachedAppInfos[indexPath.row]; + + if([newState isEqualToString:@"User"]) + { + NSString* title = [NSString stringWithFormat:@"Switching '%@' to \"User\" Registration", [appInfo displayName]]; + UIAlertController* confirmationAlert = [UIAlertController alertControllerWithTitle:title message:@"Switching this app to a \"User\" registration will make it unlaunchable after the next respring because the bugs exploited in TrollStore only affect apps registered as \"System\".\nThe purpose of this option is to make the app temporarily show up in settings, so you can adjust the settings and then switch it back to a \"System\" registration (TrollStore installed apps do not show up in settings otherwise). Additionally, the \"User\" registration state is also useful to temporarily fix iTunes file sharing, which also doesn't work for TrollStore installed apps otherwise.\nWhen you're done making the changes you need and want the app to become launchable again, you will need to switch it back to \"System\" state in TrollStore." preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* switchToUserAction = [UIAlertAction actionWithTitle:@"Switch to \"User\"" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) + { + [[TSApplicationsManager sharedInstance] changeAppRegistration:[appInfo bundlePath] toState:newState]; + [appInfo sync_loadBasicInfo]; + }]; + + [confirmationAlert addAction:switchToUserAction]; + + UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]; + + [confirmationAlert addAction:cancelAction]; + + [TSPresentationDelegate presentViewController:confirmationAlert animated:YES completion:nil]; + } + else + { + [[TSApplicationsManager sharedInstance] changeAppRegistration:[appInfo bundlePath] toState:newState]; + [appInfo sync_loadBasicInfo]; + + NSString* title = [NSString stringWithFormat:@"Switched '%@' to \"System\" Registration", [appInfo displayName]]; + + UIAlertController* infoAlert = [UIAlertController alertControllerWithTitle:title message:@"The app has been switched to the \"System\" registration state and will become launchable again after a respring." preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction* respringAction = [UIAlertAction actionWithTitle:@"Respring" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) + { + respring(); + }]; + + [infoAlert addAction:respringAction]; + + UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]; + + [infoAlert addAction:closeAction]; + + [TSPresentationDelegate presentViewController:infoAlert animated:YES completion:nil]; } } @@ -113,9 +283,11 @@ UIImage* imageWithSize(UIImage* image, CGSize size) { TSApplicationsManager* appsManager = [TSApplicationsManager sharedInstance]; - NSString* appPath = _cachedAppPaths[indexPath.row]; - NSString* appId = [appsManager appIdForAppPath:appPath]; - NSString* appName = [appsManager displayNameForAppPath:appPath]; + TSAppInfo* appInfo = _cachedAppInfos[indexPath.row]; + + NSString* appPath = [appInfo bundlePath]; + NSString* appId = [appInfo bundleIdentifier]; + NSString* appName = [appInfo displayName]; UIAlertController* confirmAlert = [UIAlertController alertControllerWithTitle:@"Confirm Uninstallation" message:[NSString stringWithFormat:@"Uninstalling the app '%@' will delete the app and all data associated to it.", appName] preferredStyle:UIAlertControllerStyleAlert]; @@ -135,7 +307,7 @@ UIImage* imageWithSize(UIImage* image, CGSize size) UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]; [confirmAlert addAction:cancelAction]; - [self presentViewController:confirmAlert animated:YES completion:nil]; + [TSPresentationDelegate presentViewController:confirmAlert animated:YES completion:nil]; } - (void)deselectRow @@ -150,7 +322,7 @@ UIImage* imageWithSize(UIImage* image, CGSize size) } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return _cachedAppPaths.count; + return _cachedAppInfos.count; } - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection @@ -164,12 +336,12 @@ UIImage* imageWithSize(UIImage* image, CGSize size) cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"ApplicationCell"]; } - NSString* appPath = _cachedAppPaths[indexPath.row]; - NSString* appId = [[TSApplicationsManager sharedInstance] appIdForAppPath:appPath]; - NSString* appVersion = [[TSApplicationsManager sharedInstance] versionStringForAppPath:appPath]; + TSAppInfo* appInfo = _cachedAppInfos[indexPath.row]; + NSString* appId = [appInfo bundleIdentifier]; + NSString* appVersion = [appInfo versionString]; // Configure the cell... - cell.textLabel.text = [[TSApplicationsManager sharedInstance] displayNameForAppPath:appPath]; + cell.textLabel.text = [appInfo displayName]; cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ • %@", appVersion, appId]; cell.imageView.layer.borderWidth = 1; cell.imageView.layer.borderColor = [UIColor.labelColor colorWithAlphaComponent:0.1].CGColor; @@ -228,32 +400,47 @@ UIImage* imageWithSize(UIImage* image, CGSize size) - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - TSApplicationsManager* appsManager = [TSApplicationsManager sharedInstance]; + TSAppInfo* appInfo = _cachedAppInfos[indexPath.row]; - NSString* appPath = _cachedAppPaths[indexPath.row]; - NSString* appId = [appsManager appIdForAppPath:appPath]; - NSString* appName = [appsManager displayNameForAppPath:appPath]; - - UIAlertController* appSelectAlert = [UIAlertController alertControllerWithTitle:appName message:appId?:@"" preferredStyle:UIAlertControllerStyleActionSheet]; - - /*UIAlertAction* detachAction = [UIAlertAction actionWithTitle:@"Detach from TrollStore" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) - { - int detachRet = [appsManager detachFromApp:appId]; - if(detachRet != 0) - { - [self showError:[appsManager errorForCode:detachRet]]; - } - [self deselectRow]; - }]; - [appSelectAlert addAction:detachAction];*/ + NSString* appId = [appInfo bundleIdentifier]; + NSString* appName = [appInfo displayName]; + UIAlertController* appSelectAlert = [UIAlertController alertControllerWithTitle:appName?:@"" message:appId?:@"" preferredStyle:UIAlertControllerStyleActionSheet]; UIAlertAction* openAction = [UIAlertAction actionWithTitle:@"Open" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) { [self openAppPressedForRowAtIndexPath:indexPath]; [self deselectRow]; }]; - [appSelectAlert addAction: openAction]; + [appSelectAlert addAction:openAction]; + + UIAlertAction* showDetailsAction = [UIAlertAction actionWithTitle:@"Show Details" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) + { + [self showDetailsPressedForRowAtIndexPath:indexPath]; + [self deselectRow]; + }]; + [appSelectAlert addAction:showDetailsAction]; + + NSString* switchState; + NSString* registrationState = [appInfo registrationState]; + UIAlertActionStyle switchActionStyle = 0; + if([registrationState isEqualToString:@"System"]) + { + switchState = @"User"; + switchActionStyle = UIAlertActionStyleDestructive; + } + else if([registrationState isEqualToString:@"User"]) + { + switchState = @"System"; + switchActionStyle = UIAlertActionStyleDefault; + } + + UIAlertAction* switchRegistrationAction = [UIAlertAction actionWithTitle:[NSString stringWithFormat:@"Switch to \"%@\" Registration", switchState] style:switchActionStyle handler:^(UIAlertAction* action) + { + [self changeAppRegistrationForRowAtIndexPath:indexPath toState:switchState]; + [self deselectRow]; + }]; + [appSelectAlert addAction:switchRegistrationAction]; UIAlertAction* uninstallAction = [UIAlertAction actionWithTitle:@"Uninstall App" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) { @@ -271,7 +458,7 @@ UIImage* imageWithSize(UIImage* image, CGSize size) appSelectAlert.popoverPresentationController.sourceView = tableView; appSelectAlert.popoverPresentationController.sourceRect = [tableView rectForRowAtIndexPath:indexPath]; - [self presentViewController:appSelectAlert animated:YES completion:nil]; + [TSPresentationDelegate presentViewController:appSelectAlert animated:YES completion:nil]; } @end \ No newline at end of file diff --git a/TrollStore/TSApplicationsManager.h b/TrollStore/TSApplicationsManager.h index c3c9723..2cb588b 100644 --- a/TrollStore/TSApplicationsManager.h +++ b/TrollStore/TSApplicationsManager.h @@ -9,10 +9,6 @@ + (instancetype)sharedInstance; - (NSArray*)installedAppPaths; -- (NSDictionary*)infoDictionaryForAppPath:(NSString*)appPath; -- (NSString*)appIdForAppPath:(NSString*)appPath; -- (NSString*)displayNameForAppPath:(NSString*)appPath; -- (NSString*)versionStringForAppPath:(NSString*)appPath; - (NSError*)errorForCode:(int)code; - (int)installIpa:(NSString*)pathToIpa force:(BOOL)force log:(NSString**)logOut; @@ -20,6 +16,6 @@ - (int)uninstallApp:(NSString*)appId; - (int)uninstallAppByPath:(NSString*)path; - (BOOL)openApplicationWithBundleID:(NSString *)appID; -//- (int)detachFromApp:(NSString*)appId; +- (int)changeAppRegistration:(NSString*)appPath toState:(NSString*)newState; @end \ No newline at end of file diff --git a/TrollStore/TSApplicationsManager.m b/TrollStore/TSApplicationsManager.m index eebc197..880d5eb 100644 --- a/TrollStore/TSApplicationsManager.m +++ b/TrollStore/TSApplicationsManager.m @@ -1,8 +1,6 @@ #import "TSApplicationsManager.h" #import -#define TrollStoreErrorDomain @"TrollStoreErrorDomain" - @implementation TSApplicationsManager + (instancetype)sharedInstance @@ -20,55 +18,6 @@ return trollStoreInstalledAppBundlePaths(); } -- (NSDictionary*)infoDictionaryForAppPath:(NSString*)appPath -{ - NSString* infoPlistPath = [appPath stringByAppendingPathComponent:@"Info.plist"]; - NSError* error; - NSDictionary* infoDict = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:infoPlistPath] error:&error]; - if(error) - { - NSLog(@"error getting info dict: %@", error); - } - return infoDict; -} - -- (NSString*)appIdForAppPath:(NSString*)appPath -{ - return [self infoDictionaryForAppPath:appPath][@"CFBundleIdentifier"]; -} - -- (NSString*)displayNameForAppPath:(NSString*)appPath -{ - NSDictionary* infoDict = [self infoDictionaryForAppPath:appPath]; - NSString* displayName = infoDict[@"CFBundleDisplayName"]; - if(![displayName isKindOfClass:[NSString class]]) displayName = nil; - if(!displayName || [displayName isEqualToString:@""]) - { - displayName = infoDict[@"CFBundleName"]; - if(![displayName isKindOfClass:[NSString class]]) displayName = nil; - if(!displayName || [displayName isEqualToString:@""]) - { - displayName = infoDict[@"CFBundleExecutable"]; - if(![displayName isKindOfClass:[NSString class]]) displayName = [appPath lastPathComponent]; - } - } - - return displayName; -} - -- (NSString*)versionStringForAppPath:(NSString*)appPath -{ - NSDictionary* infoDict = [self infoDictionaryForAppPath:appPath]; - NSString* versionString = infoDict[@"CFBundleShortVersionString"]; - - if(!versionString) - { - versionString = infoDict[@"CFBundleVersion"]; - } - - return versionString; -} - - (NSError*)errorForCode:(int)code { NSString* errorDescription = @"Unknown Error"; @@ -163,12 +112,10 @@ return [[LSApplicationWorkspace defaultWorkspace] openApplicationWithBundleID:appId]; } -/*- (int)detachFromApp:(NSString*)appId +- (int)changeAppRegistration:(NSString*)appPath toState:(NSString*)newState { - if(!appId) return -200; - int ret = spawnRoot(rootHelperPath(), @[@"detach", appId], nil, nil); - [[NSNotificationCenter defaultCenter] postNotificationName:@"ApplicationsChanged" object:nil]; - return ret; -}*/ + if(!appPath || !newState) return -200; + return spawnRoot(rootHelperPath(), @[@"modify-registration", appPath, newState], nil, nil); +} @end \ No newline at end of file diff --git a/TrollStore/TSInstallationController.h b/TrollStore/TSInstallationController.h new file mode 100644 index 0000000..122b6aa --- /dev/null +++ b/TrollStore/TSInstallationController.h @@ -0,0 +1,12 @@ +@import Foundation; + +@interface TSInstallationController : NSObject + ++ (void)presentInstallationAlertForFile:(NSString*)pathToIPA completion:(void (^)(BOOL, NSError*))completion; + ++ (void)handleAppInstallFromFile:(NSString*)pathToIPA forceInstall:(BOOL)force completion:(void (^)(BOOL, NSError*))completion; ++ (void)handleAppInstallFromFile:(NSString*)pathToIPA completion:(void (^)(BOOL, NSError*))completion; + ++ (void)handleAppInstallFromRemoteURL:(NSURL*)remoteURL completion:(void (^)(BOOL, NSError*))completion; + +@end \ No newline at end of file diff --git a/TrollStore/TSInstallationController.m b/TrollStore/TSInstallationController.m new file mode 100644 index 0000000..0c5d248 --- /dev/null +++ b/TrollStore/TSInstallationController.m @@ -0,0 +1,168 @@ +#import "TSInstallationController.h" + +#import "TSApplicationsManager.h" +#import "TSAppInfo.h" +#import +#import + +@implementation TSInstallationController + ++ (void)handleAppInstallFromFile:(NSString*)pathToIPA forceInstall:(BOOL)force completion:(void (^)(BOOL, NSError*))completionBlock +{ + dispatch_async(dispatch_get_main_queue(), ^ + { + [TSPresentationDelegate startActivity:@"Installing"]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ + { + // Install IPA + //NSString* log; + int ret = [[TSApplicationsManager sharedInstance] installIpa:pathToIPA force:force log:nil]; + + NSError* error; + if(ret != 0) + { + error = [[TSApplicationsManager sharedInstance] errorForCode:ret]; + } + + NSLog(@"installed app! ret:%d, error: %@", ret, error); + + dispatch_async(dispatch_get_main_queue(), ^ + { + [TSPresentationDelegate stopActivityWithCompletion:^ + { + if(ret != 0) + { + 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); + } + }]; + [errorAlert addAction:closeAction]; + + if(ret == 171) + { + 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 Log" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) + { + UIPasteboard* pasteboard = [UIPasteboard generalPasteboard]; + pasteboard.string = log; + }]; + [errorAlert addAction:copyLogAction];*/ + } + + [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; + } + + if(ret != 171) + { + if(completionBlock) completionBlock((BOOL)error, error); + } + }]; + }); + }); + }); +} + ++ (void)presentInstallationAlertForFile:(NSString*)pathToIPA completion:(void (^)(BOOL, NSError*))completionBlock +{ + TSAppInfo* appInfo = [[TSAppInfo alloc] initWithIPAPath:pathToIPA]; + [appInfo loadInfoWithCompletion:^(NSError* error) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + if(!error) + { + UIAlertController* installAlert = [UIAlertController alertControllerWithTitle:@"" message:@"" preferredStyle:UIAlertControllerStyleAlert]; + installAlert.attributedTitle = [appInfo detailedInfoTitle]; + installAlert.attributedMessage = [appInfo detailedInfoDescription]; + UIAlertAction* installAction = [UIAlertAction actionWithTitle:@"Install" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) + { + [self handleAppInstallFromFile:pathToIPA completion:completionBlock]; + }]; + [installAlert addAction:installAction]; + + UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction* action) + { + completionBlock(NO, nil); + }]; + [installAlert addAction:cancelAction]; + + [TSPresentationDelegate presentViewController:installAlert animated:YES completion:nil]; + } + else + { + UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Parse Error %ld", error.code] message:error.localizedDescription preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]; + [errorAlert addAction:closeAction]; + + [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; + } + }); + }]; +} + ++ (void)handleAppInstallFromFile:(NSString*)pathToIPA completion:(void (^)(BOOL, NSError*))completionBlock +{ + [self handleAppInstallFromFile:pathToIPA forceInstall:NO completion:completionBlock]; +} + ++ (void)handleAppInstallFromRemoteURL:(NSURL*)remoteURL completion:(void (^)(BOOL, NSError*))completionBlock +{ + NSURLRequest* downloadRequest = [NSURLRequest requestWithURL:remoteURL]; + + dispatch_async(dispatch_get_main_queue(), ^ + { + NSURLSessionDownloadTask* downloadTask = [NSURLSession.sharedSession downloadTaskWithRequest:downloadRequest completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) + { + dispatch_async(dispatch_get_main_queue(), ^ + { + [TSPresentationDelegate stopActivityWithCompletion:^ + { + if(error) + { + UIAlertController* errorAlert = [UIAlertController alertControllerWithTitle:@"Error" message:[NSString stringWithFormat:@"Error downloading app: %@", error] preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction* closeAction = [UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]; + [errorAlert addAction:closeAction]; + + [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:^ + { + if(completionBlock) completionBlock(NO, error); + }]; + } + else + { + NSString* tmpIpaPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tmp.ipa"]; + [[NSFileManager defaultManager] removeItemAtPath:tmpIpaPath error:nil]; + [[NSFileManager defaultManager] moveItemAtPath:location.path toPath:tmpIpaPath error:nil]; + [self presentInstallationAlertForFile:tmpIpaPath completion:^(BOOL success, NSError* error) + { + [[NSFileManager defaultManager] removeItemAtPath:tmpIpaPath error:nil]; + if(completionBlock) completionBlock(success, error); + }]; + } + }]; + }); + }]; + + [TSPresentationDelegate startActivity:@"Downloading" withCancelHandler:^ + { + [downloadTask cancel]; + }]; + + [downloadTask resume]; + }); +} + +//+ (void)showInstallAppAlertForFile:(NSString*)pathToIPA + +@end \ No newline at end of file diff --git a/TrollStore/TSRootViewController.m b/TrollStore/TSRootViewController.m index d377865..959767b 100644 --- a/TrollStore/TSRootViewController.m +++ b/TrollStore/TSRootViewController.m @@ -1,6 +1,7 @@ #import "TSRootViewController.h" #import "TSAppTableViewController.h" #import "TSSettingsListController.h" +#import @implementation TSRootViewController @@ -23,4 +24,11 @@ self.viewControllers = @[appNavigationController, settingsNavigationController]; } +- (void)viewDidLoad +{ + [super viewDidLoad]; + + TSPresentationDelegate.presentationViewController = self; +} + @end diff --git a/TrollStore/TSSceneDelegate.m b/TrollStore/TSSceneDelegate.m index d7c55ae..ebf6a26 100644 --- a/TrollStore/TSSceneDelegate.m +++ b/TrollStore/TSSceneDelegate.m @@ -1,125 +1,76 @@ #import "TSSceneDelegate.h" #import "TSRootViewController.h" #import "TSUtil.h" -#import "TSApplicationsManager.h" +#import "TSInstallationController.h" +#import @implementation TSSceneDelegate -- (void)doIPAInstall:(NSString*)ipaPath scene:(UIWindowScene*)scene force:(BOOL)force completion:(void (^)(void))completion -{ - UIWindow* keyWindow = nil; - for(UIWindow* window in scene.windows) - { - if(window.isKeyWindow) - { - keyWindow = window; - break; - } - } - - UIAlertController* infoAlert = [UIAlertController alertControllerWithTitle:@"Installing" message:@"" preferredStyle:UIAlertControllerStyleAlert]; - UIActivityIndicatorView* activityIndicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(5,5,50,50)]; - activityIndicator.hidesWhenStopped = YES; - activityIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium; - [activityIndicator startAnimating]; - [infoAlert.view addSubview:activityIndicator]; - - [keyWindow.rootViewController presentViewController:infoAlert animated:YES completion:nil]; - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ - { - // Install IPA - //NSString* log; - int ret = [[TSApplicationsManager sharedInstance] installIpa:ipaPath force:force log:nil]; - - NSError* error; - if(ret != 0) - { - error = [[TSApplicationsManager sharedInstance] errorForCode:ret]; - } - - NSLog(@"installed app! ret:%d, error: %@", ret, error); - - dispatch_async(dispatch_get_main_queue(), ^ - { - [infoAlert dismissViewControllerAnimated:YES completion:^ - { - if(ret != 0) - { - 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) - { - completion(); - } - }]; - [errorAlert addAction:closeAction]; - - if(ret == 171) - { - UIAlertAction* forceInstallAction = [UIAlertAction actionWithTitle:@"Force Installation" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) - { - [self doIPAInstall:ipaPath scene:scene force:YES completion:completion]; - }]; - [errorAlert addAction:forceInstallAction]; - } - else - { - /*UIAlertAction* copyLogAction = [UIAlertAction actionWithTitle:@"Copy Log" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) - { - UIPasteboard* pasteboard = [UIPasteboard generalPasteboard]; - pasteboard.string = log; - }]; - [errorAlert addAction:copyLogAction];*/ - } - - [keyWindow.rootViewController presentViewController:errorAlert animated:YES completion:nil]; - } - - if(ret != 171) - { - completion(); - } - }]; - }); - }); -} - -- (void)handleURLContexts:(NSSet *)URLContexts scene:(UIWindowScene*)scene +- (void)handleURLContexts:(NSSet*)URLContexts scene:(UIWindowScene*)scene { for(UIOpenURLContext* context in URLContexts) { NSLog(@"openURLContexts %@", context.URL); NSURL* url = context.URL; - if (url != nil && [url isFileURL]) { - [url startAccessingSecurityScopedResource]; - void (^doneBlock)(BOOL) = ^(BOOL shouldExit) - { - [url stopAccessingSecurityScopedResource]; - [[NSFileManager defaultManager] removeItemAtURL:url error:nil]; - if(shouldExit) + if(url) + { + NSLog(@"ts_test url: %@", url); + NSLog(@"ts_test url.scheme: %@", url.scheme); + if([url isFileURL]) + { + [url startAccessingSecurityScopedResource]; + void (^doneBlock)(BOOL) = ^(BOOL shouldExit) { - NSLog(@"Respring + Exit"); - respring(); - exit(0); + [url stopAccessingSecurityScopedResource]; + [[NSFileManager defaultManager] removeItemAtURL:url error:nil]; + + if(shouldExit) + { + NSLog(@"Respring + Exit"); + respring(); + exit(0); + } + }; + + if ([url.pathExtension.lowercaseString isEqualToString:@"ipa"] || [url.pathExtension.lowercaseString isEqualToString:@"tipa"]) + { + [TSInstallationController presentInstallationAlertForFile:url.path completion:^(BOOL success, NSError* error){ + doneBlock(NO); + }]; + } + else if([url.pathExtension.lowercaseString isEqualToString:@"tar"]) + { + // Update TrollStore itself + NSLog(@"Updating TrollStore..."); + int ret = spawnRoot(rootHelperPath(), @[@"install-trollstore", url.path], nil, nil); + doneBlock(ret == 0); + NSLog(@"Updated TrollStore!"); } - }; - - if ([url.pathExtension.lowercaseString isEqualToString:@"ipa"] || [url.pathExtension.lowercaseString isEqualToString:@"tipa"]) - { - [self doIPAInstall:url.path scene:(UIWindowScene*)scene force:NO completion:^{ - doneBlock(NO); - }]; } - else if([url.pathExtension.lowercaseString isEqualToString:@"tar"]) + else if([url.scheme isEqualToString:@"apple-magnifier"]) { - // Update TrollStore itself - NSLog(@"Updating TrollStore..."); - int ret = spawnRoot(rootHelperPath(), @[@"install-trollstore", url.path], nil, nil); - doneBlock(ret == 0); - NSLog(@"Updated TrollStore!"); + NSURLComponents* components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + if([components.host isEqualToString:@"install"]) + { + NSString* URLStringToInstall; + + for(NSURLQueryItem* queryItem in components.queryItems) + { + NSLog(@"ts_test queryItem %@ = %@", queryItem.name, queryItem.value); + if([queryItem.name isEqualToString:@"url"]) + { + URLStringToInstall = queryItem.value; + break; + } + } + + if(URLStringToInstall && [URLStringToInstall isKindOfClass:NSString.class]) + { + NSURL* URLToInstall = [NSURL URLWithString:URLStringToInstall]; + [TSInstallationController handleAppInstallFromRemoteURL:URLToInstall completion:nil]; + } + } } } } diff --git a/TrollStore/TSSettingsListController.m b/TrollStore/TSSettingsListController.m index 6bc00ad..9f3e6d2 100644 --- a/TrollStore/TSSettingsListController.m +++ b/TrollStore/TSSettingsListController.m @@ -1,6 +1,7 @@ #import "TSSettingsListController.h" #import #import +#import @implementation TSSettingsListController @@ -239,7 +240,7 @@ NSURL* ldidURL = [NSURL URLWithString:@"https://github.com/opa334/ldid/releases/download/v2.1.5-procursus5/ldid"]; NSURLRequest* ldidRequest = [NSURLRequest requestWithURL:ldidURL]; - [self startActivity:@"Installing ldid"]; + [TSPresentationDelegate startActivity:@"Installing ldid"]; NSURLSessionDownloadTask* downloadTask = [NSURLSession.sharedSession downloadTaskWithRequest:ldidRequest completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { @@ -251,9 +252,9 @@ dispatch_async(dispatch_get_main_queue(), ^ { - [self stopActivityWithCompletion:^ + [TSPresentationDelegate stopActivityWithCompletion:^ { - [self presentViewController:errorAlert animated:YES completion:nil]; + [TSPresentationDelegate presentViewController:errorAlert animated:YES completion:nil]; }]; }); } @@ -262,7 +263,7 @@ spawnRoot(rootHelperPath(), @[@"install-ldid", location.path], nil, nil); dispatch_async(dispatch_get_main_queue(), ^ { - [self stopActivityWithCompletion:nil]; + [TSPresentationDelegate stopActivityWithCompletion:nil]; [self reloadSpecifiers]; }); } @@ -309,7 +310,7 @@ UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]; [selectAppAlert addAction:cancelAction]; - [self presentViewController:selectAppAlert animated:YES completion:nil]; + [TSPresentationDelegate presentViewController:selectAppAlert animated:YES completion:nil]; } - (void)doTheDashPressed diff --git a/TrollStore/control b/TrollStore/control index d0661d1..528b400 100644 --- a/TrollStore/control +++ b/TrollStore/control @@ -1,6 +1,6 @@ Package: com.opa334.trollstore Name: TrollStore -Version: 1.2.2 +Version: 1.3 Architecture: iphoneos-arm Description: An awesome application! Maintainer: opa334 diff --git a/TrollStore/entitlements.plist b/TrollStore/entitlements.plist index f76e005..fcd0f94 100644 --- a/TrollStore/entitlements.plist +++ b/TrollStore/entitlements.plist @@ -37,5 +37,7 @@ data-allowed-write preferences-write + com.apple.springboard.opensensitiveurl + \ No newline at end of file