diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index ea087e79..e707e7f3 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -173,6 +173,7 @@ BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */; }; BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */; }; BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC84A4C2421A19100853474 /* SourcesViewController.swift */; }; + BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCCB519245E3401001853EA /* VerifyAppOperation.swift */; }; BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; }; BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; }; BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; }; @@ -508,6 +509,7 @@ BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppsCollectionHeaderView.swift; sourceTree = ""; }; BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstalledAppsCollectionHeaderView.xib; sourceTree = ""; }; BFC84A4C2421A19100853474 /* SourcesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesViewController.swift; sourceTree = ""; }; + BFCCB519245E3401001853EA /* VerifyAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerifyAppOperation.swift; sourceTree = ""; }; BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; }; BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; BFD247742284B9A500981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -1242,6 +1244,7 @@ BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */, BF56D2AB23DF8E170006506D /* FetchAppIDsOperation.swift */, BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */, + BFCCB519245E3401001853EA /* VerifyAppOperation.swift */, ); path = Operations; sourceTree = ""; @@ -1756,6 +1759,7 @@ BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */, BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */, BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */, + BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */, BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */, BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */, BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */, diff --git a/AltStore/Extensions/NSError+LocalizedFailure.swift b/AltStore/Extensions/NSError+LocalizedFailure.swift index 7a3a17eb..f7cf10a2 100644 --- a/AltStore/Extensions/NSError+LocalizedFailure.swift +++ b/AltStore/Extensions/NSError+LocalizedFailure.swift @@ -25,3 +25,18 @@ extension NSError return error } } + +protocol ALTLocalizedError: LocalizedError, CustomNSError +{ + var errorFailure: String? { get } +} + +extension ALTLocalizedError +{ + var errorUserInfo: [String : Any] { + let userInfo = [NSLocalizedDescriptionKey: self.errorDescription, + NSLocalizedFailureReasonErrorKey: self.failureReason, + NSLocalizedFailureErrorKey: self.errorFailure].compactMapValues { $0 } + return userInfo + } +} diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 75b12d59..a9f38cd5 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -396,6 +396,11 @@ private extension AppManager self.set(progress, for: operation) } + if let viewController = presentingViewController + { + group.context.presentingViewController = viewController + } + /* Authenticate (if necessary) */ var authenticationOperation: AuthenticationOperation? if group.context.session == nil @@ -503,6 +508,16 @@ private extension AppManager } progress.addChild(downloadOperation.progress, withPendingUnitCount: 25) + /* Verify App */ + let verifyOperation = VerifyAppOperation(context: context) + verifyOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): context.error = error + case .success: break + } + } + verifyOperation.addDependency(downloadOperation) /* Refresh Anisette Data */ let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context) @@ -513,7 +528,7 @@ private extension AppManager case .success(let anisetteData): group.context.session?.anisetteData = anisetteData } } - refreshAnisetteDataOperation.addDependency(downloadOperation) + refreshAnisetteDataOperation.addDependency(verifyOperation) /* Fetch Provisioning Profiles */ @@ -579,7 +594,7 @@ private extension AppManager progress.addChild(installOperation.progress, withPendingUnitCount: 30) installOperation.addDependency(sendAppOperation) - let operations = [downloadOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation] + let operations = [downloadOperation, verifyOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation] group.add(operations) self.run(operations, context: group.context) diff --git a/AltStore/Operations/OperationContexts.swift b/AltStore/Operations/OperationContexts.swift index 406ddee7..1b666785 100644 --- a/AltStore/Operations/OperationContexts.swift +++ b/AltStore/Operations/OperationContexts.swift @@ -17,6 +17,8 @@ class OperationContext var server: Server? var error: Error? + var presentingViewController: UIViewController? + let operations: NSHashTable init(server: Server? = nil, error: Error? = nil, operations: [Foundation.Operation] = []) diff --git a/AltStore/Operations/VerifyAppOperation.swift b/AltStore/Operations/VerifyAppOperation.swift new file mode 100644 index 00000000..f17ab4d9 --- /dev/null +++ b/AltStore/Operations/VerifyAppOperation.swift @@ -0,0 +1,131 @@ +// +// VerifyAppOperation.swift +// AltStore +// +// Created by Riley Testut on 5/2/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import Foundation + +import AltSign +import AltKit + +import Roxas + +enum VerificationError: ALTLocalizedError +{ + case privateEntitlements(ALTApplication, [String: Any]) + + var app: ALTApplication { + switch self { + case .privateEntitlements(let app, _): return app + } + } + + var errorFailure: String? { + return String(format: NSLocalizedString("“%@” could not be installed.", comment: ""), app.name) + } + + var failureReason: String? { + switch self + { + case .privateEntitlements(let app, _): + return String(format: NSLocalizedString("“%@” requires private permissions.", comment: ""), app.name) + } + } +} + +@objc(VerifyAppOperation) +class VerifyAppOperation: ResultOperation +{ + let context: AppOperationContext + var verificationHandler: ((VerificationError) -> Bool)? + + init(context: AppOperationContext) + { + self.context = context + + super.init() + } + + override func main() + { + super.main() + + do + { + if let error = self.context.error + { + throw error + } + + guard let app = self.context.app else { throw OperationError.invalidParameters } + + if let commentStart = app.entitlementsString.range(of: ""), let commentEnd = app.entitlementsString.range(of: "") + { + // Psychic Paper private entitlements. + + let entitlementsStart = app.entitlementsString.index(after: commentStart.upperBound) + let rawEntitlements = String(app.entitlementsString[entitlementsStart ..< commentEnd.lowerBound]) + + let plistTemplate = """ + + + + + %@ + + + """ + let entitlementsPlist = String(format: plistTemplate, rawEntitlements) + let entitlements = try PropertyListSerialization.propertyList(from: entitlementsPlist.data(using: .utf8)!, options: [], format: nil) as! [String: Any] + + let error = VerificationError.privateEntitlements(app, entitlements) + self.process(error) { (result) in + self.finish(result.mapError { $0 as Error }) + } + + return + } + + self.finish(.success(())) + } + catch + { + self.finish(.failure(error)) + } + } +} + +private extension VerifyAppOperation +{ + func process(_ error: VerificationError, completion: @escaping (Result) -> Void) + { + guard let presentingViewController = self.context.presentingViewController else { return completion(.failure(error)) } + + DispatchQueue.main.async { + switch error + { + case .privateEntitlements(_, let entitlements): + let permissions = entitlements.keys.sorted().joined(separator: "\n") + let message = String(format: NSLocalizedString(""" + You must allow access to these private permissions before continuing: + + %@ + + Private permissions allow apps to do more than normally allowed by iOS, including potentially accessing sensitive private data. Make sure to only install apps from sources you trust. + """, comment: ""), permissions) + + let alertController = UIAlertController(title: error.failureReason ?? error.localizedDescription, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Allow Access", comment: ""), style: .destructive) { (action) in + completion(.success(())) + }) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Deny Access", comment: ""), style: .default, handler: { (action) in + completion(.failure(error)) + })) + presentingViewController.present(alertController, animated: true, completion: nil) + } + } + } +}