#import "TSAppInfo.h" #import "TSCommonTCCServiceNames.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 { _cachedSize = 0; 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]; } _archive = archive_read_new(); archive_read_support_format_all(_archive); archive_read_support_filter_all(_archive); int r = archive_read_open_filename(_archive, _path.fileSystemRepresentation, 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*))completionBlock { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if(completionBlock) completionBlock([self sync_loadBasicInfo]); }); } - (void)loadInfoWithCompletion:(void (^)(NSError*))completionBlock { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if(completionBlock) completionBlock([self sync_loadInfo]); }); } - (void)enumerateAllInfoDictionaries:(void (^)(NSString* key, NSObject* value, BOOL* stop))enumerateBlock { if(!enumerateBlock) return; __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 { if(!enumerateBlock) return; __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(!enumerateBlock) return; 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 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 isPlatformApplication = NO; [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) { if([key isEqualToString:@"platform-application"]) { NSNumber* valueNum = (NSNumber*)value; if(valueNum && [valueNum isKindOfClass:NSNumber.class]) { isPlatformApplication = valueNum.boolValue; if(isPlatformApplication) *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]; //array by design, should be ordered if(!unrestrictedContainerAccess) { __block NSString *dataContainer = nil; // If com.apple.private.security.container-required Entitlement is a string, prefer it to CFBundleIdentifier [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) { if([key isEqualToString:@"com.apple.private.security.container-required"]) { NSString* valueString = (NSString*)value; if(valueString && [valueString isKindOfClass:NSString.class]) { dataContainer = valueString; } } }]; // Else take CFBundleIdentifier if (!dataContainer) { [self enumerateAllInfoDictionaries:^(NSString *key, NSObject *value, BOOL *stop) { if([key isEqualToString:@"CFBundleIdentifier"]) { NSString* valueStr = (NSString*)value; if([valueStr isKindOfClass:NSString.class]) { dataContainer = valueStr; } } }]; } if (dataContainer) { [accessibleContainers addObject:dataContainer]; } [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 NSMutableSet* accessibleKeychainGroups = [NSMutableSet 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 { [accessibleKeychainGroups addObject:keychainID]; } } } } } }]; __block NSMutableSet* URLSchemes = [NSMutableSet 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) { [URLSchemes addObject:URLScheme]; } } } } } } }]; __block NSMutableSet* allowedTccServices = [NSMutableSet new]; [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) { if([key isEqualToString:@"com.apple.private.tcc.allow"]) { NSArray* valueArr = (NSArray*)value; if([valueArr isKindOfClass:NSArray.class]) { for(NSString* serviceID in valueArr) { if([serviceID isKindOfClass:NSString.class]) { NSString* displayName = commonTCCServices[serviceID]; if(displayName == nil) { [allowedTccServices addObject:[serviceID stringByReplacingOccurrencesOfString:@"kTCCService" withString:@""]]; } else { [allowedTccServices addObject:displayName]; } } } } } else if ([key isEqualToString:@"com.apple.locationd.preauthorized"]) { NSNumber* valueNum = (NSNumber*)value; if([valueNum isKindOfClass:NSNumber.class]) { if([valueNum boolValue]) { [allowedTccServices addObject:@"Location"]; } } } }]; __block NSMutableSet* allowedMGKeys = [NSMutableSet new]; [self enumerateAllEntitlements:^(NSString *key, NSObject *value, BOOL *stop) { if([key isEqualToString:@"com.apple.private.MobileGestalt.AllowedProtectedKeys"]) { NSArray* valueArr = (NSArray*)value; if([valueArr isKindOfClass:NSArray.class]) { for(NSString* protectedKey in valueArr) { if([protectedKey isKindOfClass:NSString.class]) { [allowedMGKeys addObject:protectedKey]; } } } } }]; 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(isPlatformApplication && isUnsandboxed && hasPersonaMngmt) { [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can spawn its own embedded binaries with root privileges." attributes:bodyDangerAttributes]]; } else if(isPlatformApplication && isUnsandboxed) { [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can spawn arbitrary binaries as the mobile user." attributes:bodyWarningAttributes]]; } else { [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can not spawn other binaries." attributes:bodyAttributes]]; } if(allowedTccServices.count) { [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nPrivacy" attributes:headerAttributes]]; [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can access the following services without asking for permission:\n" attributes:bodyWarningAttributes]]; [description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSListFormatter localizedStringByJoiningStrings:[allowedTccServices allObjects]] attributes:bodyAttributes]]; } if (allowedMGKeys.count) { [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\nDevice Info" attributes:headerAttributes]]; [description appendAttributedString:[[NSAttributedString alloc] initWithString:@"\nThe app can access protected information about this device:\n" attributes:bodyWarningAttributes]]; [description appendAttributedString:[[NSAttributedString alloc] initWithString:[NSListFormatter localizedStringByJoiningStrings:[allowedMGKeys allObjects]] 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