From fd89f35246b1bf3c8f7cd183dfdf1fc2aba9bdc9 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 15 May 2023 15:13:13 -0500 Subject: [PATCH] Verifies app updates have same permissions as previously installed versions --- AltStore/Managing Apps/AppManager.swift | 32 ++++++++----- AltStore/Operations/VerificationError.swift | 9 ++++ AltStore/Operations/VerifyAppOperation.swift | 48 +++++++++++++++++--- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 9ef033c4..06c05169 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -988,12 +988,18 @@ private extension AppManager switch operation { - case .install(let app), .update(let app): - let installProgress = self._install(app, operation: operation, group: group) { (result) in + case .install(let app): + let installProgress = self._install(app, operation: operation, group: group, reviewPermissions: .all) { (result) in self.finish(operation, result: result, group: group, progress: progress) } progress?.addChild(installProgress, withPendingUnitCount: 80) + case .update(let app): + let updateProgress = self._install(app, operation: operation, group: group, reviewPermissions: .added) { (result) in + self.finish(operation, result: result, group: group, progress: progress) + } + progress?.addChild(updateProgress, withPendingUnitCount: 80) + case .activate(let app) where UserDefaults.standard.isLegacyDeactivationSupported: fallthrough case .refresh(let app): // Check if backup app is installed in place of real app. @@ -1258,11 +1264,6 @@ private extension AppManager { let app = try result.get() context.app = app - - if cacheApp - { - try FileManager.default.copyItem(at: app.fileURL, to: InstalledApp.fileURL(for: app), shouldReplace: true) - } } catch { @@ -1273,12 +1274,21 @@ private extension AppManager /* Verify App */ - let verifyOperation = VerifyAppOperation(context: context) + let verifyOperation = VerifyAppOperation(permissionsMode: permissionReviewMode, context: context) verifyOperation.resultHandler = { (result) in - switch result + do { - case .failure(let error): context.error = error - case .success: break + try result.get() + + // Wait until we've finished verifying app before caching it. + if let app = context.app, cacheApp + { + try FileManager.default.copyItem(at: app.fileURL, to: InstalledApp.fileURL(for: app), shouldReplace: true) + } + } + catch + { + context.error = error } } verifyOperation.addDependency(downloadOperation) diff --git a/AltStore/Operations/VerificationError.swift b/AltStore/Operations/VerificationError.swift index 8d393157..2a20dd95 100644 --- a/AltStore/Operations/VerificationError.swift +++ b/AltStore/Operations/VerificationError.swift @@ -25,6 +25,7 @@ extension VerificationError case mismatchedVersion = 4 case undeclaredPermissions = 6 + case addedPermissions = 7 } static func mismatchedBundleIdentifiers(sourceBundleID: String, app: ALTApplication) -> VerificationError { @@ -46,6 +47,10 @@ extension VerificationError static func undeclaredPermissions(_ permissions: [any ALTAppPermission], app: AppProtocol) -> VerificationError { VerificationError(code: .undeclaredPermissions, app: app, permissions: permissions) } + + static func addedPermissions(_ permissions: [any ALTAppPermission], app: AppProtocol) -> VerificationError { + VerificationError(code: .addedPermissions, app: app, permissions: permissions) + } } struct VerificationError: ALTLocalizedError @@ -142,6 +147,10 @@ struct VerificationError: ALTLocalizedError case .undeclaredPermissions: let appName = self.$app.name ?? NSLocalizedString("The app", comment: "") return String(format: NSLocalizedString("%@ requires additional permissions not specified by the source.", comment: ""), appName) + + case .addedPermissions: + let appName = self.$app.name ?? NSLocalizedString("The app", comment: "") + return String(format: NSLocalizedString("%@ requires more permissions than the version that is already installed.", comment: ""), appName) } } diff --git a/AltStore/Operations/VerifyAppOperation.swift b/AltStore/Operations/VerifyAppOperation.swift index 9288a1fb..c94ce864 100644 --- a/AltStore/Operations/VerifyAppOperation.swift +++ b/AltStore/Operations/VerifyAppOperation.swift @@ -104,14 +104,27 @@ private extension ALTEntitlement ] } +extension VerifyAppOperation +{ + enum PermissionReviewMode + { + case none + case all + case added + } +} + @objc(VerifyAppOperation) final class VerifyAppOperation: ResultOperation { + let permissionsMode: PermissionReviewMode let context: InstallAppOperationContext + var verificationHandler: ((VerificationError) -> Bool)? - init(context: InstallAppOperationContext) + init(permissionsMode: PermissionReviewMode, context: InstallAppOperationContext) { + self.permissionsMode = permissionsMode self.context = context super.init() @@ -155,11 +168,7 @@ final class VerifyAppOperation: ResultOperation try await self.verifyHash(of: app, at: ipaURL, matches: appVersion) try await self.verifyDownloadedVersion(of: app, matches: appVersion) - - if let storeApp = await self.context.$appVersion.app - { - try await self.verifyPermissions(of: app, match: storeApp) - } + try await self.verifyPermissions(of: app, match: appVersion) self.finish(.success(())) } @@ -199,6 +208,33 @@ private extension VerifyAppOperation guard version == app.version else { throw VerificationError.mismatchedVersion(app.version, expectedVersion: version, app: app) } } + func verifyPermissions(of app: ALTApplication, @AsyncManaged match appVersion: AppVersion) async throws + { + guard self.permissionsMode != .none else { return } + guard let storeApp = await $appVersion.app else { throw OperationError.invalidParameters } + + // Verify source permissions match first. + let allPermissions = try await self.verifyPermissions(of: app, match: storeApp) + + switch self.permissionsMode + { + case .none, .all: break + case .added: + let installedAppURL = InstalledApp.fileURL(for: app) + guard let previousApp = ALTApplication(fileURL: installedAppURL) else { throw OperationError.appNotFound(name: app.name) } + + var previousEntitlements = Set(previousApp.entitlements.keys) + for appExtension in previousApp.appExtensions + { + previousEntitlements.formUnion(appExtension.entitlements.keys) + } + + // Make sure all entitlements already exist in previousApp. + let addedEntitlements = Array(allPermissions.lazy.compactMap { $0 as? ALTEntitlement }.filter { !previousEntitlements.contains($0) }) + guard addedEntitlements.isEmpty else { throw VerificationError.addedPermissions(addedEntitlements, app: appVersion) } + } + } + @discardableResult func verifyPermissions(of app: ALTApplication, @AsyncManaged match storeApp: StoreApp) async throws -> [any ALTAppPermission] {