diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 88342f5b..0a368977 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -347,9 +347,16 @@ BFF7C90F257844C900E55F36 /* AltXPC.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = BFF7C904257844C900E55F36 /* AltXPC.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; BFF7C920257844FA00E55F36 /* ALTPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5C5FCE237DF69100EDD0C6 /* ALTPluginService.m */; }; BFF7C9342578492100E55F36 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */; }; + D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8B62727841800A9B5DD /* libAppleArchive.tbd */; }; + D533E8BC2727BBEE00A9B5DD /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */; }; + D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BD2727BBF800A9B5DD /* libcurl.a */; }; + D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D57DF637271E32F000677701 /* PatchApp.storyboard */; }; + D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = D57DF63E271E51E400677701 /* ALTAppPatcher.m */; }; D57F2C9126E0070200B9FA39 /* EnableJITOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */; }; D57F2C9426E01BC700B9FA39 /* UIDevice+Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */; }; D58D5F2E26DFE68E00E55E38 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = D58D5F2D26DFE68E00E55E38 /* LaunchAtLogin */; }; + D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; }; + D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -797,8 +804,17 @@ BFF7EC4C25081E9300BDE521 /* AltStore 8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 8.xcdatamodel"; sourceTree = ""; }; BFFCFA45248835530077BFCE /* AltDaemon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltDaemon.entitlements; sourceTree = ""; }; C9EEAA842DA87A88A870053B /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D533E8B62727841800A9B5DD /* libAppleArchive.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libAppleArchive.tbd; path = usr/lib/libAppleArchive.tbd; sourceTree = SDKROOT; }; + D533E8B82727B61400A9B5DD /* fragmentzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fragmentzip.h; sourceTree = ""; }; + D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libfragmentzip.a; path = Dependencies/fragmentzip/libfragmentzip.a; sourceTree = SOURCE_ROOT; }; + D533E8BD2727BBF800A9B5DD /* libcurl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcurl.a; path = Dependencies/libcurl/libcurl.a; sourceTree = SOURCE_ROOT; }; + D57DF637271E32F000677701 /* PatchApp.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PatchApp.storyboard; sourceTree = ""; }; + D57DF63D271E51E400677701 /* ALTAppPatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPatcher.h; sourceTree = ""; }; + D57DF63E271E51E400677701 /* ALTAppPatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPatcher.m; sourceTree = ""; }; D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableJITOperation.swift; sourceTree = ""; }; D57F2C9326E01BC700B9FA39 /* UIDevice+Vibration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIDevice+Vibration.swift"; sourceTree = ""; }; + D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = ""; }; + D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = ""; }; EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = ""; }; FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltServer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -869,8 +885,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */, + D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */, BF1614F1250822F100767AEA /* Roxas.framework in Frameworks */, BF088D332501A4FF008082D9 /* OpenSSL.xcframework in Frameworks */, + D533E8BC2727BBEE00A9B5DD /* libfragmentzip.a in Frameworks */, BF66EE852501AE50007EE018 /* AltStoreCore.framework in Frameworks */, 2A77E3D272F3D92436FAC272 /* Pods_AltStore.framework in Frameworks */, ); @@ -1341,6 +1360,21 @@ name = "Supporting Files"; sourceTree = ""; }; + BF7B44062725A4B8005288A4 /* Patch App */ = { + isa = PBXGroup; + children = ( + D593F1932717749A006E82DE /* PatchAppOperation.swift */, + D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */, + D57DF637271E32F000677701 /* PatchApp.storyboard */, + D57DF63D271E51E400677701 /* ALTAppPatcher.h */, + D57DF63E271E51E400677701 /* ALTAppPatcher.m */, + D533E8B82727B61400A9B5DD /* fragmentzip.h */, + D533E8BD2727BBF800A9B5DD /* libcurl.a */, + D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */, + ); + path = "Patch App"; + sourceTree = ""; + }; BF98916C250AABF3002ACF50 /* AltWidget */ = { isa = PBXGroup; children = ( @@ -1493,6 +1527,7 @@ BFD247852284BB3300981D42 /* Frameworks */ = { isa = PBXGroup; children = ( + D533E8B62727841800A9B5DD /* libAppleArchive.tbd */, BF088D322501A4FF008082D9 /* OpenSSL.xcframework */, BF580497246A3D19008AE704 /* UIKit.framework */, BF4588872298DD3F00BD7491 /* libxml2.tbd */, @@ -1626,6 +1661,7 @@ BFDBBD7F246CB84F004ED2F3 /* RemoveAppBackupOperation.swift */, BFF00D312501BDA100746320 /* BackgroundRefreshAppsOperation.swift */, D57F2C9026E0070200B9FA39 /* EnableJITOperation.swift */, + BF7B44062725A4B8005288A4 /* Patch App */, ); path = Operations; sourceTree = ""; @@ -2101,6 +2137,7 @@ BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */, BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */, BFE60738231ADF49002B0E8E /* Settings.storyboard in Resources */, + D57DF638271E32F000677701 /* PatchApp.storyboard in Resources */, BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */, BF44EEF3246B3A17002A52F2 /* AltBackup.ipa in Resources */, BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */, @@ -2528,6 +2565,7 @@ BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */, BF41B808233433C100C593A3 /* LoadingState.swift in Sources */, BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */, + D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */, BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */, BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */, BFB39B5C252BC10E00D1BE50 /* Managed.swift in Sources */, @@ -2541,6 +2579,7 @@ BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */, BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */, BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */, + D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */, BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */, BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */, BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */, @@ -2558,6 +2597,7 @@ BF4B78FE24B3D1DB008AB4AC /* SceneDelegate.swift in Sources */, BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */, BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */, + D57DF63F271E51E400677701 /* ALTAppPatcher.m in Sources */, BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */, BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */, BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */, @@ -3286,6 +3326,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 6XVY5G3U44; + ENABLE_BITCODE = NO; INFOPLIST_FILE = AltStore/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.2; LD_RUNPATH_SEARCH_PATHS = ( @@ -3293,6 +3334,11 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.5b; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Dependencies/fragmentzip", + "$(PROJECT_DIR)/Dependencies/libcurl", + ); PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3314,6 +3360,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 6XVY5G3U44; + ENABLE_BITCODE = NO; INFOPLIST_FILE = AltStore/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.2; LD_RUNPATH_SEARCH_PATHS = ( @@ -3321,6 +3368,11 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.5b; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Dependencies/fragmentzip", + "$(PROJECT_DIR)/Dependencies/libcurl", + ); PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/AltStore/AltStore-Bridging-Header.h b/AltStore/AltStore-Bridging-Header.h index c8dd6ac5..f75b06b6 100644 --- a/AltStore/AltStore-Bridging-Header.h +++ b/AltStore/AltStore-Bridging-Header.h @@ -3,3 +3,6 @@ // #import "NSAttributedString+Markdown.h" +#import "ALTAppPatcher.h" + +#include "fragmentzip.h" diff --git a/AltStore/App Detail/AppViewController.swift b/AltStore/App Detail/AppViewController.swift index 2ecd89cf..5a764f88 100644 --- a/AltStore/App Detail/AppViewController.swift +++ b/AltStore/App Detail/AppViewController.swift @@ -498,7 +498,7 @@ extension AppViewController { guard self.app.installedApp == nil else { return } - let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in + let group = AppManager.shared.install(self.app, presentingViewController: self) { (result) in do { _ = try result.get() @@ -522,8 +522,8 @@ extension AppViewController } } - self.bannerView.button.progress = progress - self.navigationBarDownloadButton.progress = progress + self.bannerView.button.progress = group.progress + self.navigationBarDownloadButton.progress = group.progress } func open(_ installedApp: InstalledApp) diff --git a/AltStore/Authentication/RefreshAltStoreViewController.swift b/AltStore/Authentication/RefreshAltStoreViewController.swift index 46dac9a2..e75c22bf 100644 --- a/AltStore/Authentication/RefreshAltStoreViewController.swift +++ b/AltStore/Authentication/RefreshAltStoreViewController.swift @@ -49,7 +49,7 @@ private extension RefreshAltStoreViewController } // Install, _not_ refresh, to ensure we are installing with a non-revoked certificate. - let progress = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in + let group = AppManager.shared.install(altStore, presentingViewController: self, context: self.context) { (result) in switch result { case .success: self.completionHandler?(.success(())) @@ -71,7 +71,7 @@ private extension RefreshAltStoreViewController } } - sender.progress = progress + sender.progress = group.progress } refresh() diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index d7d95276..406d6fc5 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -40,6 +40,7 @@ + @@ -608,6 +609,14 @@ World + + + + + + + + @@ -1036,7 +1045,7 @@ World - + diff --git a/AltStore/Extensions/UIDevice+Jailbreak.swift b/AltStore/Extensions/UIDevice+Jailbreak.swift index 77b73a88..78a6542e 100644 --- a/AltStore/Extensions/UIDevice+Jailbreak.swift +++ b/AltStore/Extensions/UIDevice+Jailbreak.swift @@ -7,6 +7,7 @@ // import UIKit +import ARKit extension UIDevice { @@ -27,4 +28,23 @@ extension UIDevice return false } } + + @available(iOS 14, *) + var supportsFugu14: Bool { + #if targetEnvironment(simulator) + return true + #else + // Fugu14 is supported on devices with an A12 processor or better. + // ARKit 3 is only supported by devices with an A12 processor or better, according to the documentation. + return ARBodyTrackingConfiguration.isSupported + #endif + } + + @available(iOS 14, *) + var isUntetheredJailbreakRequired: Bool { + let ios14_4 = OperatingSystemVersion(majorVersion: 14, minorVersion: 4, patchVersion: 0) + + let isUntetheredJailbreakRequired = ProcessInfo.processInfo.isOperatingSystemAtLeast(ios14_4) + return isUntetheredJailbreakRequired + } } diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 28e3e751..072d8651 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -36,6 +36,11 @@ class AppManagerPublisher: ObservableObject fileprivate(set) var refreshProgress = [String: Progress]() } +private func ==(lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool +{ + return (lhs.majorVersion == rhs.majorVersion && lhs.minorVersion == rhs.minorVersion && lhs.patchVersion == rhs.patchVersion) +} + class AppManager { static let shared = AppManager() @@ -143,6 +148,12 @@ extension AppManager // This UTI is not declared by any apps, which means this app has been deleted by the user. // This app is also not a legacy sideloaded app, so we can assume it's fine to delete it. context.delete(app) + + if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: app.bundleIdentifier) + { + patchedApps.remove(at: index) + UserDefaults.standard.patchedApps = patchedApps + } } } @@ -384,13 +395,13 @@ extension AppManager } @discardableResult - func install(_ app: T, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result) -> Void) -> Progress + func install(_ app: T, presentingViewController: UIViewController?, context: AuthenticatedOperationContext = AuthenticatedOperationContext(), completionHandler: @escaping (Result) -> Void) -> RefreshGroup { let group = RefreshGroup(context: context) group.completionHandler = { (results) in do { - guard let result = results.values.first else { throw OperationError.unknown } + guard let result = results.values.first else { throw context.error ?? OperationError.unknown } completionHandler(result) } catch @@ -402,7 +413,7 @@ extension AppManager let operation = AppOperation.install(app) self.perform([operation], presentingViewController: presentingViewController, group: group) - return group.progress + return group } @discardableResult @@ -630,6 +641,67 @@ extension AppManager self.run([enableJITOperation], context: context, requiresSerialQueue: true) } + @available(iOS 14.0, *) + func patch(resignedApp: ALTApplication, presentingViewController: UIViewController, context authContext: AuthenticatedOperationContext, completionHandler: @escaping (Result) -> Void) -> PatchAppOperation + { + class Context: InstallAppOperationContext, PatchAppContext + { + } + + guard let originalBundleID = resignedApp.bundle.infoDictionary?[Bundle.Info.altBundleID] as? String else { + let context = Context(bundleIdentifier: resignedApp.bundleIdentifier, authenticatedContext: authContext) + completionHandler(.failure(OperationError.invalidApp)) + + return PatchAppOperation(context: context) + } + + let context = Context(bundleIdentifier: originalBundleID, authenticatedContext: authContext) + context.resignedApp = resignedApp + + let patchAppOperation = PatchAppOperation(context: context) + let sendAppOperation = SendAppOperation(context: context) + let installOperation = InstallAppOperation(context: context) + + let installationProgress = Progress.discreteProgress(totalUnitCount: 100) + installationProgress.addChild(sendAppOperation.progress, withPendingUnitCount: 40) + installationProgress.addChild(installOperation.progress, withPendingUnitCount: 60) + + /* Patch */ + patchAppOperation.resultHandler = { [weak patchAppOperation] (result) in + switch result + { + case .failure(let error): context.error = error + case .success: + // Kinda hacky that we're calling patchAppOperation's progressHandler manually, but YOLO. + patchAppOperation?.progressHandler?(installationProgress, NSLocalizedString("Patching placeholder app...", comment: "")) + } + } + + /* Send */ + sendAppOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): context.error = error + case .success(let installationConnection): context.installationConnection = installationConnection + } + } + sendAppOperation.addDependency(patchAppOperation) + + + /* Install */ + installOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let installedApp): completionHandler(.success(installedApp)) + } + } + installOperation.addDependency(sendAppOperation) + + self.run([patchAppOperation, sendAppOperation, installOperation], context: context.authenticatedContext) + return patchAppOperation + } + func installationProgress(for app: AppProtocol) -> Progress? { let progress = self.installationProgress[app.bundleIdentifier] @@ -950,6 +1022,78 @@ private extension AppManager deactivateAppsOperation.addDependency(verifyOperation) + /* Patch App */ + let patchAppOperation = RSTAsyncBlockOperation { operation in + do + { + // Only attempt to patch app if we're installing a new app, not refreshing existing app. + // Post reboot, we install the correct jailbreak app by refreshing the patched app, + // so this check avoids infinite recursion. + guard case .install = appOperation else { + operation.finish() + return + } + + guard let presentingViewController = context.presentingViewController, #available(iOS 14, *) else { return operation.finish() } + + if let error = context.error + { + throw error + } + + guard let app = context.app else { throw OperationError.invalidParameters } + + guard let isUntetherRequired = app.bundle.infoDictionary?[Bundle.Info.untetherRequired] as? Bool, + let minimumiOSVersionString = app.bundle.infoDictionary?[Bundle.Info.untetherMinimumiOSVersion] as? String, + let maximumiOSVersionString = app.bundle.infoDictionary?[Bundle.Info.untetherMaximumiOSVersion] as? String, + case let minimumiOSVersion = OperatingSystemVersion(string: minimumiOSVersionString), + case let maximumiOSVersion = OperatingSystemVersion(string: maximumiOSVersionString) + else { return operation.finish() } + + let iOSVersion = ProcessInfo.processInfo.operatingSystemVersion + let iOSVersionSupported = ProcessInfo.processInfo.isOperatingSystemAtLeast(minimumiOSVersion) && + (!ProcessInfo.processInfo.isOperatingSystemAtLeast(maximumiOSVersion) || maximumiOSVersion == iOSVersion) + + guard isUntetherRequired, iOSVersionSupported, UIDevice.current.supportsFugu14 else { return operation.finish() } + + guard let patchAppLink = app.bundle.infoDictionary?[Bundle.Info.untetherURL] as? String, + let patchAppURL = URL(string: patchAppLink) + else { throw OperationError.invalidApp } + + let patchApp = AnyApp(name: app.name, bundleIdentifier: app.bundleIdentifier, url: patchAppURL) + + DispatchQueue.main.async { + let storyboard = UIStoryboard(name: "PatchApp", bundle: nil) + let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController + + let patchViewController = navigationController.topViewController as! PatchViewController + patchViewController.patchApp = patchApp + patchViewController.completionHandler = { [weak presentingViewController] (result) in + switch result + { + case .failure(OperationError.cancelled): break // Ignore + case .failure(let error): group.context.error = error + case .success: group.context.error = OperationError.cancelled + } + + operation.finish() + + DispatchQueue.main.async { + presentingViewController?.dismiss(animated: true, completion: nil) + } + } + presentingViewController.present(navigationController, animated: true, completion: nil) + } + } + catch + { + group.context.error = error + operation.finish() + } + } + patchAppOperation.addDependency(deactivateAppsOperation) + + /* Refresh Anisette Data */ let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context) refreshAnisetteDataOperation.resultHandler = { (result) in @@ -959,7 +1103,7 @@ private extension AppManager case .success(let anisetteData): group.context.session?.anisetteData = anisetteData } } - refreshAnisetteDataOperation.addDependency(deactivateAppsOperation) + refreshAnisetteDataOperation.addDependency(patchAppOperation) /* Fetch Provisioning Profiles */ @@ -1028,7 +1172,7 @@ private extension AppManager progress.addChild(installOperation.progress, withPendingUnitCount: 30) installOperation.addDependency(sendAppOperation) - let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation] + let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, patchAppOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation] group.add(operations) self.run(operations, context: group.context) diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 184c406c..f030208c 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -861,7 +861,7 @@ private extension MyAppsViewController guard let application = context.application else { throw OperationError.invalidParameters } - let progress = AppManager.shared.install(application, presentingViewController: self) { (result) in + let group = AppManager.shared.install(application, presentingViewController: self) { (result) in switch result { case .success(let installedApp): context.installedApp = installedApp @@ -869,7 +869,7 @@ private extension MyAppsViewController } operation.finish() } - installProgress.addChild(progress, withPendingUnitCount: 100) + installProgress.addChild(group.progress, withPendingUnitCount: 100) } catch { diff --git a/AltStore/Operations/InstallAppOperation.swift b/AltStore/Operations/InstallAppOperation.swift index 78fe9cbe..520c5c2a 100644 --- a/AltStore/Operations/InstallAppOperation.swift +++ b/AltStore/Operations/InstallAppOperation.swift @@ -107,11 +107,11 @@ class InstallAppOperation: ResultOperation installedApp.appExtensions = installedExtensions + self.context.beginInstallationHandler?(installedApp) + // Temporary directory and resigned .ipa no longer needed, so delete them now to ensure AltStore doesn't quit before we get the chance to. self.cleanUp() - self.context.beginInstallationHandler?(installedApp) - var activeProfiles: Set? if let sideloadedAppsLimit = UserDefaults.standard.activeAppsLimit { diff --git a/AltStore/Operations/Patch App/ALTAppPatcher.h b/AltStore/Operations/Patch App/ALTAppPatcher.h new file mode 100644 index 00000000..38bf8843 --- /dev/null +++ b/AltStore/Operations/Patch App/ALTAppPatcher.h @@ -0,0 +1,19 @@ +// +// ALTAppPatcher.h +// AltStore +// +// Created by Riley Testut on 10/18/21. +// Copyright © 2021 Riley Testut. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ALTAppPatcher : NSObject + +- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *_Nullable *)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AltStore/Operations/Patch App/ALTAppPatcher.m b/AltStore/Operations/Patch App/ALTAppPatcher.m new file mode 100644 index 00000000..5cfbbdbe --- /dev/null +++ b/AltStore/Operations/Patch App/ALTAppPatcher.m @@ -0,0 +1,143 @@ +// +// ALTAppPatcher.m +// AltStore +// +// Created by Riley Testut on 10/18/21. +// Copied with minor modifications from sample code provided by Linus Henze. +// + +#import "ALTAppPatcher.h" + +#include +#include +#include + +@import Roxas; + +#define CPU_SUBTYPE_PAC 0x80000000 +#define FAT_MAGIC 0xcafebabe + +#define ROUND_TO_PAGE(val) (((val % 0x4000) == 0) ? val : (val + (0x4000 - (val & 0x3FFF)))) + +typedef struct { + uint32_t magic; + uint32_t cpuType; + uint32_t cpuSubType; + // Incomplete, we don't need anything else +} MachOHeader; + +typedef struct { + uint32_t cpuType; + uint32_t cpuSubType; + uint32_t fileOffset; + uint32_t size; + uint32_t alignment; +} FatArch; + +typedef struct { + uint32_t magic; + uint32_t archCount; + FatArch archs[0]; +} FatHeader; + +// Given two MachO files, return a FAT file with the following properties: +// 1. installd will still see the original MachO and validate it's code signature +// 2. The kernel will only see the injected MachO instead +// +// Only arm64e for now +void *injectApp(void *originalApp, size_t originalAppSize, void *appToInject, size_t appToInjectSize, size_t *outputSize) { + *outputSize = 0; + + // First validate the App to inject: It must be an arm64e application + if (appToInjectSize < sizeof(MachOHeader)) { + return NULL; + } + + MachOHeader *injectedHeader = (MachOHeader*) appToInject; + if (injectedHeader->cpuType != CPU_TYPE_ARM64) { + return NULL; + } + + if (injectedHeader->cpuSubType != (CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC)) { + return NULL; + } + + // Ok, the App to inject is ok + // Now build a fat header + size_t originalAppSizeRounded = ROUND_TO_PAGE(originalAppSize); + size_t appToInjectSizeRounded = ROUND_TO_PAGE(appToInjectSize); + size_t totalSize = 0x4000 /* Fat Header + Alignment */ + originalAppSizeRounded + appToInjectSizeRounded; + + void *fatBuf = malloc(totalSize); + if (fatBuf == NULL) { + return NULL; + } + + bzero(fatBuf, totalSize); + + FatHeader *fatHeader = (FatHeader*) fatBuf; + fatHeader->magic = htonl(FAT_MAGIC); + fatHeader->archCount = htonl(2); + + // Write first arch (original app) + fatHeader->archs[0].cpuType = htonl(CPU_TYPE_ARM64); + fatHeader->archs[0].cpuSubType = htonl(CPU_SUBTYPE_ARM64E); /* Note that this is not a valid cpu subtype */ + fatHeader->archs[0].fileOffset = htonl(0x4000); + fatHeader->archs[0].size = htonl(originalAppSize); + fatHeader->archs[0].alignment = htonl(0xE); + + // Write second arch (injected app) + fatHeader->archs[1].cpuType = htonl(CPU_TYPE_ARM64); + fatHeader->archs[1].cpuSubType = htonl(CPU_SUBTYPE_ARM64E | CPU_SUBTYPE_PAC); + fatHeader->archs[1].fileOffset = htonl(0x4000 + originalAppSizeRounded); + fatHeader->archs[1].size = htonl(appToInjectSize); + fatHeader->archs[1].alignment = htonl(0xE); + + // Ok, now write the MachOs + memcpy(fatBuf + 0x4000, originalApp, originalAppSize); + memcpy(fatBuf + 0x4000 + originalAppSizeRounded, appToInject, appToInjectSize); + + // We're done! + *outputSize = totalSize; + return fatBuf; +} + +@implementation ALTAppPatcher + +- (BOOL)patchAppBinaryAtURL:(NSURL *)appFileURL withBinaryAtURL:(NSURL *)patchFileURL error:(NSError *__autoreleasing *)error +{ + NSMutableData *originalApp = [NSMutableData dataWithContentsOfURL:appFileURL options:0 error:error]; + if (originalApp == nil) + { + return NO; + } + + NSMutableData *injectedApp = [NSMutableData dataWithContentsOfURL:patchFileURL options:0 error:error]; + if (injectedApp == nil) + { + return NO; + } + + size_t outputSize = 0; + void *output = injectApp(originalApp.mutableBytes, originalApp.length, injectedApp.mutableBytes, injectedApp.length, &outputSize); + if (output == NULL) + { + if (error) + { + // If injectApp fails, it means the patch app is in the wrong format. + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{NSURLErrorKey: patchFileURL}]; + } + + return NO; + } + + NSData *outputData = [NSData dataWithBytesNoCopy:output length:outputSize freeWhenDone:YES]; + if (![outputData writeToURL:appFileURL options:NSDataWritingAtomic error:error]) + { + return NO; + } + + return YES; +} + +@end diff --git a/AltStore/Operations/Patch App/PatchApp.storyboard b/AltStore/Operations/Patch App/PatchApp.storyboard new file mode 100644 index 00000000..6b6bc322 --- /dev/null +++ b/AltStore/Operations/Patch App/PatchApp.storyboard @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AltStore/Operations/Patch App/PatchAppOperation.swift b/AltStore/Operations/Patch App/PatchAppOperation.swift new file mode 100644 index 00000000..f4272ad3 --- /dev/null +++ b/AltStore/Operations/Patch App/PatchAppOperation.swift @@ -0,0 +1,223 @@ +// +// PatchAppOperation.swift +// AltStore +// +// Created by Riley Testut on 10/13/21. +// Copyright © 2021 Riley Testut. All rights reserved. +// + +import UIKit +import Combine +import AppleArchive +import System + +import AltStoreCore +import AltSign +import Roxas + +@available(iOS 14, *) +protocol PatchAppContext +{ + var bundleIdentifier: String { get } + var temporaryDirectory: URL { get } + + var resignedApp: ALTApplication? { get } + var error: Error? { get } +} + +enum PatchAppError: LocalizedError +{ + case unsupportedOperatingSystemVersion(OperatingSystemVersion) + + var errorDescription: String? { + switch self + { + case .unsupportedOperatingSystemVersion(let osVersion): + var osVersionString = "\(osVersion.majorVersion).\(osVersion.minorVersion)" + if osVersion.patchVersion != 0 + { + osVersionString += ".\(osVersion.patchVersion)" + } + + let errorDescription = String(format: NSLocalizedString("The OTA download URL for iOS %@ could not be determined.", comment: ""), osVersionString) + return errorDescription + } + } +} + +private struct OTAUpdate +{ + var url: URL + var archivePath: String +} + +@available(iOS 14, *) +class PatchAppOperation: ResultOperation +{ + let context: PatchAppContext + + var progressHandler: ((Progress, String) -> Void)? + + private let appPatcher = ALTAppPatcher() + private lazy var patchDirectory: URL = self.context.temporaryDirectory.appendingPathComponent("Patch", isDirectory: true) + + private var cancellable: AnyCancellable? + + init(context: PatchAppContext) + { + self.context = context + + super.init() + + self.progress.totalUnitCount = 100 + } + + override func main() + { + super.main() + + if let error = self.context.error + { + self.finish(.failure(error)) + return + } + + guard let resignedApp = self.context.resignedApp else { return self.finish(.failure(OperationError.invalidParameters)) } + + self.progressHandler?(self.progress, NSLocalizedString("Downloading iOS firmware...", comment: "")) + + self.cancellable = self.fetchOTAUpdate() + .flatMap { self.downloadArchive(from: $0) } + .flatMap { self.extractSpotlightFromArchive(at: $0) } + .flatMap { self.patch(resignedApp, withBinaryAt: $0) } + .tryMap { try FileManager.default.zipAppBundle(at: $0) } + .tryMap { (fileURL) in + let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.fileURL) + + let destinationURL = InstalledApp.refreshedIPAURL(for: app) + try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true) + } + .receive(on: RunLoop.main) + .sink { completion in + switch completion + { + case .failure(let error): self.finish(.failure(error)) + case .finished: self.finish(.success(())) + } + } receiveValue: { _ in } + } + + override func cancel() + { + super.cancel() + + self.cancellable?.cancel() + self.cancellable = nil + } +} + +private let ALTFragmentZipCallback: @convention(c) (UInt32) -> Void = { (percentageComplete) in + guard let progress = Progress.current() else { return } + + if percentageComplete == 100 && progress.completedUnitCount == 0 + { + // Ignore first percentageComplete, which is always 100. + return + } + + progress.completedUnitCount = Int64(percentageComplete) +} + +@available(iOS 14, *) +private extension PatchAppOperation +{ + func fetchOTAUpdate() -> AnyPublisher + { + Just(()).tryMap { + let osVersion = ProcessInfo.processInfo.operatingSystemVersion + switch (osVersion.majorVersion, osVersion.minorVersion) + { + #if DEBUG + case (14, 3): fallthrough + #endif + + case (14, 4): + return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021WinterFCS/patches/001-98606/43AF99A1-F286-43B1-A101-F9F856EA395A/com_apple_MobileAsset_SoftwareUpdate/c4985c32c344beb7b49c61919b4e39d1fd336c90.zip")!, + archivePath: "AssetData/payloadv2/payload.042") + + case (14, 5): + return OTAUpdate(url: URL(string: "https://updates.cdn-apple.com/2021SpringFCS/patches/061-84483/AB525139-066E-46F8-8E85-DCE802C03BA8/com_apple_MobileAsset_SoftwareUpdate/788573ae93113881db04269acedeecabbaa643e3.zip")!, + archivePath: "AssetData/payloadv2/payload.043") + + default: throw PatchAppError.unsupportedOperatingSystemVersion(osVersion) + } + } + .eraseToAnyPublisher() + } + + func downloadArchive(from update: OTAUpdate) -> AnyPublisher + { + Just(()).tryMap { + try FileManager.default.createDirectory(at: self.patchDirectory, withIntermediateDirectories: true, attributes: nil) + + let archiveURL = self.patchDirectory.appendingPathComponent("ota.archive") + archiveURL.withUnsafeFileSystemRepresentation { archivePath in + let fz = fragmentzip_open((update.url.absoluteString as NSString).utf8String!) + defer { fragmentzip_close(fz) } + + self.progress.becomeCurrent(withPendingUnitCount: 100) + fragmentzip_download_file(fz, update.archivePath, archivePath!, ALTFragmentZipCallback) + self.progress.resignCurrent() + } + + print("Downloaded OTA archive.") + return archiveURL + } + .mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not download OTA archive.", comment: "")) } + .eraseToAnyPublisher() + } + + func extractSpotlightFromArchive(at archiveURL: URL) -> AnyPublisher + { + Just(()).tryMap { + let spotlightPath = "Applications/Spotlight.app/Spotlight" + let spotlightFileURL = self.patchDirectory.appendingPathComponent(spotlightPath) + + guard let readFileStream = ArchiveByteStream.fileStream(path: FilePath(archiveURL.path), mode: .readOnly, options: [], permissions: FilePermissions(rawValue: 0o644)), + let decompressStream = ArchiveByteStream.decompressionStream(readingFrom: readFileStream), + let decodeStream = ArchiveStream.decodeStream(readingFrom: decompressStream), + let readStream = ArchiveStream.extractStream(extractingTo: FilePath(self.patchDirectory.path)) + else { throw CocoaError(.fileReadCorruptFile, userInfo: [NSURLErrorKey: archiveURL]) } + + _ = try ArchiveStream.process(readingFrom: decodeStream, writingTo: readStream) { message, filePath, data in + guard filePath == FilePath(spotlightPath) else { return .skip } + return .ok + } + + print("Extracted Spotlight from OTA archive.") + return spotlightFileURL + } + .mapError { ($0 as NSError).withLocalizedFailure(NSLocalizedString("Could not extract Spotlight from OTA archive.", comment: "")) } + .eraseToAnyPublisher() + } + + func patch(_ app: ALTApplication, withBinaryAt patchFileURL: URL) -> AnyPublisher + { + Just(()).tryMap { + // executableURL may be nil, so use infoDictionary instead to determine executable name. + // guard let appName = app.bundle.executableURL?.lastPathComponent else { throw OperationError.invalidApp } + guard let appName = app.bundle.infoDictionary?[kCFBundleExecutableKey as String] as? String else { throw OperationError.invalidApp } + + let temporaryAppURL = self.patchDirectory.appendingPathComponent("Patched.app", isDirectory: true) + try FileManager.default.copyItem(at: app.fileURL, to: temporaryAppURL) + + let appBinaryURL = temporaryAppURL.appendingPathComponent(appName, isDirectory: false) + try self.appPatcher.patchAppBinary(at: appBinaryURL, withBinaryAt: patchFileURL) + + print("Patched \(app.name).") + return temporaryAppURL + } + .mapError { ($0 as NSError).withLocalizedFailure(String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), app.name)) } + .eraseToAnyPublisher() + } +} diff --git a/AltStore/Operations/Patch App/PatchViewController.swift b/AltStore/Operations/Patch App/PatchViewController.swift new file mode 100644 index 00000000..3b7b0cfb --- /dev/null +++ b/AltStore/Operations/Patch App/PatchViewController.swift @@ -0,0 +1,499 @@ +// +// PatchViewController.swift +// AltStore +// +// Created by Riley Testut on 10/20/21. +// Copyright © 2021 Riley Testut. All rights reserved. +// + +import UIKit +import Combine + +import AltStoreCore +import AltSign +import Roxas + +@available(iOS 14.0, *) +extension PatchViewController +{ + enum Step + { + case confirm + case install + case openApp + case patchApp + case reboot + case refresh + case finish + } +} + +@available(iOS 14.0, *) +class PatchViewController: UIViewController +{ + var patchApp: AnyApp? + var installedApp: InstalledApp? + + var completionHandler: ((Result) -> Void)? + + private let context = AuthenticatedOperationContext() + + private var currentStep: Step = .confirm { + didSet { + DispatchQueue.main.async { + self.update() + } + } + } + + private var buttonHandler: (() -> Void)? + private var resignedApp: ALTApplication? + + private lazy var temporaryDirectory: URL = FileManager.default.uniqueTemporaryURL() + + private var didEnterBackgroundObservation: NSObjectProtocol? + private weak var cancellableProgress: Progress? + + @IBOutlet private var placeholderView: RSTPlaceholderView! + @IBOutlet private var taskDescriptionLabel: UILabel! + @IBOutlet private var pillButton: PillButton! + @IBOutlet private var cancelBarButtonItem: UIBarButtonItem! + @IBOutlet private var cancelButton: UIButton! + + override func viewDidLoad() + { + super.viewDidLoad() + + self.isModalInPresentation = true + + self.placeholderView.stackView.spacing = 20 + self.placeholderView.textLabel.textColor = .white + + self.placeholderView.detailTextLabel.textAlignment = .left + self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6) + + self.buttonHandler = { [weak self] in + self?.startProcess() + } + + do + { + try FileManager.default.createDirectory(at: self.temporaryDirectory, withIntermediateDirectories: true, attributes: nil) + } + catch + { + print("Failed to create temporary directory:", error) + } + + self.update() + } + + override func viewWillAppear(_ animated: Bool) + { + super.viewWillAppear(animated) + + if self.installedApp != nil + { + self.refreshApp() + } + } +} + +@available(iOS 14.0, *) +private extension PatchViewController +{ + func update() + { + self.cancelButton.alpha = 0.0 + + switch self.currentStep + { + case .confirm: + guard let app = self.patchApp else { break } + + if UIDevice.current.isUntetheredJailbreakRequired + { + self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Requires Untethering", comment: "") + self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak is untethered, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but AltStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name) + } + else + { + self.placeholderView.textLabel.text = NSLocalizedString("Jailbreak Supports Untethering", comment: "") + self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("This jailbreak has an untethered version, which means %@ will never expire — even after 7 days or rebooting the device.\n\nInstalling an untethered jailbreak requires a few extra steps, but AltStore will walk you through the process.\n\nWould you like to continue? ", comment: ""), app.name) + } + + self.pillButton.setTitle(NSLocalizedString("Install Untethered Jailbreak", comment: ""), for: .normal) + + self.cancelButton.alpha = 1.0 + + case .install: + guard let app = self.patchApp else { break } + + self.placeholderView.textLabel.text = String(format: NSLocalizedString("Installing %@ placeholder…", comment: ""), app.name) + self.placeholderView.detailTextLabel.text = NSLocalizedString("A placeholder app needs to be installed in order to prepare your device for untethering.\n\nThis may take a few moments.", comment: "") + + case .openApp: + self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "") + self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "") + + self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal) + + case .patchApp: + guard let app = self.patchApp else { break } + + self.placeholderView.textLabel.text = String(format: NSLocalizedString("Patching %@ placeholder…", comment: ""), app.name) + self.placeholderView.detailTextLabel.text = NSLocalizedString("This will take a few moments. Please do not turn off the screen or leave the app until patching is complete.", comment: "") + + self.pillButton.setTitle(NSLocalizedString("Patch Placeholder", comment: ""), for: .normal) + + case .reboot: + self.placeholderView.textLabel.text = NSLocalizedString("Continue in App", comment: "") + self.placeholderView.detailTextLabel.text = NSLocalizedString("Please open the placeholder app and follow the instructions to continue jailbreaking your device.", comment: "") + + self.pillButton.setTitle(NSLocalizedString("Open Placeholder", comment: ""), for: .normal) + + case .refresh: + guard let installedApp = self.installedApp else { break } + + self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish installing %@?", comment: ""), installedApp.name) + self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("In order to finish jailbreaking this device, you need to install %@ then follow the instructions in the app.", comment: ""), installedApp.name) + + self.pillButton.setTitle(String(format: NSLocalizedString("Install %@", comment: ""), installedApp.name), for: .normal) + + case .finish: + guard let installedApp = self.installedApp else { break } + + self.placeholderView.textLabel.text = String(format: NSLocalizedString("Finish in %@", comment: ""), installedApp.name) + self.placeholderView.detailTextLabel.text = String(format: NSLocalizedString("Follow the instructions in %@ to finish jailbreaking this device.", comment: ""), installedApp.name) + + self.pillButton.setTitle(String(format: NSLocalizedString("Open %@", comment: ""), installedApp.name), for: .normal) + } + } + + func present(_ error: Error, title: String) + { + DispatchQueue.main.async { + let nsError = error as NSError + + let alertController = UIAlertController(title: nsError.localizedFailure ?? title, message: error.localizedDescription, preferredStyle: .alert) + alertController.addAction(.ok) + self.present(alertController, animated: true, completion: nil) + + self.setProgress(nil, description: nil) + } + } + + func setProgress(_ progress: Progress?, description: String?) + { + DispatchQueue.main.async { + self.pillButton.progress = progress + self.taskDescriptionLabel.text = description ?? " " // Use non-empty string to prevent label resizing itself. + } + } + + func finish(with result: Result) + { + do + { + try FileManager.default.removeItem(at: self.temporaryDirectory) + } + catch + { + print("Failed to remove temporary directory:", error) + } + + if let observation = self.didEnterBackgroundObservation + { + NotificationCenter.default.removeObserver(observation) + } + + self.completionHandler?(result) + self.completionHandler = nil + } +} + +@available(iOS 14.0, *) +private extension PatchViewController +{ + @IBAction func performButtonAction() + { + self.buttonHandler?() + } + + @IBAction func cancel() + { + self.finish(with: .success(())) + + self.cancellableProgress?.cancel() + } + + @IBAction func installRegularJailbreak() + { + guard let app = self.patchApp else { return } + + let title: String + let message: String + + if UIDevice.current.isUntetheredJailbreakRequired + { + title = NSLocalizedString("Untethering Required", comment: "") + message = String(format: NSLocalizedString("%@ can not jailbreak this device unless you untether it first. Are you sure you want to install without untethering?", comment: ""), app.name) + } + else + { + title = NSLocalizedString("Untethering Recommended", comment: "") + message = String(format: NSLocalizedString("Untethering this jailbreak will prevent %@ from expiring, even after 7 days or rebooting the device. Are you sure you want to install without untethering?", comment: ""), app.name) + } + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Install Without Untethering", comment: ""), style: .default) { _ in + self.finish(with: .failure(OperationError.cancelled)) + }) + alertController.addAction(.cancel) + self.present(alertController, animated: true, completion: nil) + } +} + +@available(iOS 14.0, *) +private extension PatchViewController +{ + func startProcess() + { + guard let patchApp = self.patchApp else { return } + + self.currentStep = .install + + if let progress = AppManager.shared.installationProgress(for: patchApp) + { + // Cancel pending jailbreak app installation so we can start a new one. + progress.cancel() + } + + let appURL = InstalledApp.fileURL(for: patchApp) + let cachedAppURL = self.temporaryDirectory.appendingPathComponent("Cached.app") + + do + { + // Make copy of original app, so we can replace the cached patch app with it later. + try FileManager.default.copyItem(at: appURL, to: cachedAppURL, shouldReplace: true) + } + catch + { + self.present(error, title: NSLocalizedString("Could not back up jailbreak app.", comment: "")) + return + } + + var unzippingError: Error? + let refreshGroup = AppManager.shared.install(patchApp, presentingViewController: self, context: self.context) { result in + do + { + _ = try result.get() + + if let unzippingError = unzippingError + { + throw unzippingError + } + + // Replace cached patch app with original app so we can resume installing it post-reboot. + try FileManager.default.copyItem(at: cachedAppURL, to: appURL, shouldReplace: true) + + self.openApp() + } + catch + { + self.present(error, title: String(format: NSLocalizedString("Could not install %@ placeholder.", comment: ""), patchApp.name)) + } + } + refreshGroup.beginInstallationHandler = { (installedApp) in + do + { + // Replace patch app name with correct name. + installedApp.name = patchApp.name + + let ipaURL = installedApp.refreshedIPAURL + let resignedAppURL = try FileManager.default.unzipAppBundle(at: ipaURL, toDirectory: self.temporaryDirectory) + + self.resignedApp = ALTApplication(fileURL: resignedAppURL) + } + catch + { + print("Error unzipping app bundle:", error) + unzippingError = error + } + } + self.setProgress(refreshGroup.progress, description: nil) + + self.cancellableProgress = refreshGroup.progress + } + + func openApp() + { + guard let patchApp = self.patchApp else { return } + + self.setProgress(nil, description: nil) + self.currentStep = .openApp + + // This observation is willEnterForeground because patching starts immediately upon return. + self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { (notification) in + self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) } + self.patchApplication() + } + + self.buttonHandler = { [weak self] in + guard let self = self else { return } + + #if !targetEnvironment(simulator) + + let openURL = InstalledApp.openAppURL(for: patchApp) + UIApplication.shared.open(openURL) { success in + guard !success else { return } + self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name)) + } + + #endif + } + } + + func patchApplication() + { + guard let resignedApp = self.resignedApp else { return } + + self.currentStep = .patchApp + + self.buttonHandler = { [weak self] in + self?.patchApplication() + } + + let patchAppOperation = AppManager.shared.patch(resignedApp: resignedApp, presentingViewController: self, context: self.context) { result in + switch result + { + case .failure(let error): self.present(error, title: String(format: NSLocalizedString("Could not patch %@ placeholder.", comment: ""), resignedApp.name)) + case .success: self.rebootDevice() + } + } + patchAppOperation.progressHandler = { (progress, description) in + self.setProgress(progress, description: description) + } + self.cancellableProgress = patchAppOperation.progress + } + + func rebootDevice() + { + guard let patchApp = self.patchApp else { return } + + self.setProgress(nil, description: nil) + self.currentStep = .reboot + + self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in + self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) } + + var patchedApps = UserDefaults.standard.patchedApps ?? [] + if !patchedApps.contains(patchApp.bundleIdentifier) + { + patchedApps.append(patchApp.bundleIdentifier) + UserDefaults.standard.patchedApps = patchedApps + } + + self.finish(with: .success(())) + } + + self.buttonHandler = { [weak self] in + guard let self = self else { return } + + #if !targetEnvironment(simulator) + + let openURL = InstalledApp.openAppURL(for: patchApp) + UIApplication.shared.open(openURL) { success in + guard !success else { return } + self.present(OperationError.openAppFailed(name: patchApp.name), title: String(format: NSLocalizedString("Could not open %@ placeholder.", comment: ""), patchApp.name)) + } + + #endif + } + } + + func refreshApp() + { + guard let installedApp = self.installedApp else { return } + + self.currentStep = .refresh + + self.buttonHandler = { [weak self] in + guard let self = self else { return } + DatabaseManager.shared.persistentContainer.performBackgroundTask { context in + let tempApp = context.object(with: installedApp.objectID) as! InstalledApp + tempApp.needsResign = true + + let errorTitle = String(format: NSLocalizedString("Could not install %@.", comment: ""), tempApp.name) + + do + { + try context.save() + + installedApp.managedObjectContext?.perform { + // Refreshing ensures we don't attempt to patch the app again, + // since that is only checked when installing a new app. + let refreshGroup = AppManager.shared.refresh([installedApp], presentingViewController: self, group: nil) + refreshGroup.completionHandler = { [weak refreshGroup, weak self] (results) in + guard let self = self else { return } + + do + { + guard let (bundleIdentifier, result) = results.first else { throw refreshGroup?.context.error ?? OperationError.unknown } + _ = try result.get() + + if var patchedApps = UserDefaults.standard.patchedApps, let index = patchedApps.firstIndex(of: bundleIdentifier) + { + patchedApps.remove(at: index) + UserDefaults.standard.patchedApps = patchedApps + } + + self.finish() + } + catch + { + self.present(error, title: errorTitle) + } + } + self.setProgress(refreshGroup.progress, description: String(format: NSLocalizedString("Installing %@...", comment: ""), installedApp.name)) + } + } + catch + { + self.present(error, title: errorTitle) + } + } + } + } + + func finish() + { + guard let installedApp = self.installedApp else { return } + + self.setProgress(nil, description: nil) + self.currentStep = .finish + + self.didEnterBackgroundObservation = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: .main) { (notification) in + self.didEnterBackgroundObservation.map { NotificationCenter.default.removeObserver($0) } + self.finish(with: .success(())) + } + + installedApp.managedObjectContext?.perform { + let appName = installedApp.name + let openURL = installedApp.openAppURL + + self.buttonHandler = { [weak self] in + guard let self = self else { return } + + #if !targetEnvironment(simulator) + + UIApplication.shared.open(openURL) { success in + guard !success else { return } + self.present(OperationError.openAppFailed(name: appName), title: String(format: NSLocalizedString("Could not open %@.", comment: ""), appName)) + } + + #endif + } + } + } +} diff --git a/AltStore/Operations/Patch App/fragmentzip.h b/AltStore/Operations/Patch App/fragmentzip.h new file mode 100644 index 00000000..6fe1f2eb --- /dev/null +++ b/AltStore/Operations/Patch App/fragmentzip.h @@ -0,0 +1,18 @@ +// +// fragmentzip.h +// AltStore +// +// Created by Riley Testut on 10/25/21. +// Copyright © 2021 Riley Testut. All rights reserved. +// + +#ifndef fragmentzip_h +#define fragmentzip_h + +typedef void fragmentzip_t; +typedef void (*fragmentzip_process_callback_t)(unsigned int progress); +fragmentzip_t *fragmentzip_open(const char *url); +int fragmentzip_download_file(fragmentzip_t *info, const char *remotepath, const char *savepath, fragmentzip_process_callback_t callback); +void fragmentzip_close(fragmentzip_t *info); + +#endif /* fragmentzip_h */ diff --git a/AltStore/Operations/SendAppOperation.swift b/AltStore/Operations/SendAppOperation.swift index 4dc52fa5..8a378d67 100644 --- a/AltStore/Operations/SendAppOperation.swift +++ b/AltStore/Operations/SendAppOperation.swift @@ -14,13 +14,13 @@ import AltStoreCore @objc(SendAppOperation) class SendAppOperation: ResultOperation { - let context: AppOperationContext + let context: InstallAppOperationContext private let dispatchQueue = DispatchQueue(label: "com.altstore.SendAppOperation") private var serverConnection: ServerConnection? - init(context: AppOperationContext) + init(context: InstallAppOperationContext) { self.context = context @@ -39,9 +39,10 @@ class SendAppOperation: ResultOperation return } - guard let app = self.context.app, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) } + guard let resignedApp = self.context.resignedApp, let server = self.context.server else { return self.finish(.failure(OperationError.invalidParameters)) } // self.context.resignedApp.fileURL points to the app bundle, but we want the .ipa. + let app = AnyApp(name: resignedApp.name, bundleIdentifier: self.context.bundleIdentifier, url: resignedApp.url) let fileURL = InstalledApp.refreshedIPAURL(for: app) // Connect to server. diff --git a/AltStore/TabBarController.swift b/AltStore/TabBarController.swift index 5765e4b9..f1b9daa9 100644 --- a/AltStore/TabBarController.swift +++ b/AltStore/TabBarController.swift @@ -7,6 +7,7 @@ // import UIKit +import AltStoreCore extension TabBarController { @@ -45,18 +46,44 @@ class TabBarController: UITabBarController self.initialSegue = nil self.performSegue(withIdentifier: identifier, sender: sender) } + else if let patchedApps = UserDefaults.standard.patchedApps, !patchedApps.isEmpty + { + // Check if we need to finish installing untethered jailbreak. + let activeApps = InstalledApp.fetchActiveApps(in: DatabaseManager.shared.viewContext) + guard let patchedApp = activeApps.first(where: { patchedApps.contains($0.bundleIdentifier) }) else { return } + + self.performSegue(withIdentifier: "finishJailbreak", sender: patchedApp) + } } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - guard segue.identifier == "presentSources", - let notification = sender as? Notification, - let sourceURL = notification.userInfo?[AppDelegate.addSourceDeepLinkURLKey] as? URL - else { return } + guard let identifier = segue.identifier else { return } - let navigationController = segue.destination as! UINavigationController - let sourcesViewController = navigationController.viewControllers.first as! SourcesViewController - sourcesViewController.deepLinkSourceURL = sourceURL + switch identifier + { + case "presentSources": + guard let notification = sender as? Notification, + let sourceURL = notification.userInfo?[AppDelegate.addSourceDeepLinkURLKey] as? URL + else { return } + + let navigationController = segue.destination as! UINavigationController + let sourcesViewController = navigationController.viewControllers.first as! SourcesViewController + sourcesViewController.deepLinkSourceURL = sourceURL + + case "finishJailbreak": + guard let installedApp = sender as? InstalledApp, #available(iOS 14, *) else { return } + + let navigationController = segue.destination as! UINavigationController + + let patchViewController = navigationController.viewControllers.first as! PatchViewController + patchViewController.installedApp = installedApp + patchViewController.completionHandler = { [weak self] _ in + self?.dismiss(animated: true, completion: nil) + } + + default: break + } } override func performSegue(withIdentifier identifier: String, sender: Any?) diff --git a/AltStoreCore/Extensions/UserDefaults+AltStore.swift b/AltStoreCore/Extensions/UserDefaults+AltStore.swift index db828fc6..bcd66a9f 100644 --- a/AltStoreCore/Extensions/UserDefaults+AltStore.swift +++ b/AltStoreCore/Extensions/UserDefaults+AltStore.swift @@ -35,6 +35,8 @@ public extension UserDefaults @NSManaged var localServerSupportsRefreshing: Bool + @NSManaged var patchedApps: [String]? + var activeAppsLimit: Int? { get { return self._activeAppsLimit?.intValue diff --git a/AltStoreCore/Protocols/AppProtocol.swift b/AltStoreCore/Protocols/AppProtocol.swift index 7c56b985..94fa6f99 100644 --- a/AltStoreCore/Protocols/AppProtocol.swift +++ b/AltStoreCore/Protocols/AppProtocol.swift @@ -16,6 +16,20 @@ public protocol AppProtocol var url: URL { get } } +public struct AnyApp: AppProtocol +{ + public var name: String + public var bundleIdentifier: String + public var url: URL + + public init(name: String, bundleIdentifier: String, url: URL) + { + self.name = name + self.bundleIdentifier = bundleIdentifier + self.url = url + } +} + extension ALTApplication: AppProtocol { public var url: URL { diff --git a/Dependencies/AltSign b/Dependencies/AltSign index 2483f45c..ac85b055 160000 --- a/Dependencies/AltSign +++ b/Dependencies/AltSign @@ -1 +1 @@ -Subproject commit 2483f45ce6f7a93cec97164e6d9e9f13012447b7 +Subproject commit ac85b05592ab88a0a1c429f52930e395a9caa310 diff --git a/Dependencies/fragmentzip/libfragmentzip.a b/Dependencies/fragmentzip/libfragmentzip.a new file mode 100644 index 00000000..36383620 Binary files /dev/null and b/Dependencies/fragmentzip/libfragmentzip.a differ diff --git a/Dependencies/libcurl/libcurl.a b/Dependencies/libcurl/libcurl.a new file mode 100644 index 00000000..0766ced7 Binary files /dev/null and b/Dependencies/libcurl/libcurl.a differ diff --git a/Shared/Extensions/Bundle+AltStore.swift b/Shared/Extensions/Bundle+AltStore.swift index a56b45fc..65d2b087 100644 --- a/Shared/Extensions/Bundle+AltStore.swift +++ b/Shared/Extensions/Bundle+AltStore.swift @@ -20,6 +20,11 @@ public extension Bundle public static let urlTypes = "CFBundleURLTypes" public static let exportedUTIs = "UTExportedTypeDeclarations" + + public static let untetherURL = "ALTFugu14UntetherURL" + public static let untetherRequired = "ALTFugu14UntetherRequired" + public static let untetherMinimumiOSVersion = "ALTFugu14UntetherMinimumVersion" + public static let untetherMaximumiOSVersion = "ALTFugu14UntetherMaximumVersion" } }