From e785fc47ee57931a9de690011e056a2b9a09d6b7 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 28 Oct 2019 13:16:55 -0700 Subject: [PATCH] Fixes issue where AltStore revokes its own certificate Uses embedded certificate from AltServer if possible, but then falls back to asking user to refresh AltStore manually if the certificate used to install AltStore is revoked. --- AltStore.xcodeproj/project.pbxproj | 4 + .../Authentication/Authentication.storyboard | 67 +++++++++- .../RefreshAltStoreViewController.swift | 87 +++++++++++++ AltStore/Components/Keychain.swift | 119 ++++++++---------- AltStore/Managing Apps/AppManager.swift | 36 +++--- .../Operations/AuthenticationOperation.swift | 83 +++++++++--- AltStore/Operations/OperationGroup.swift | 7 +- AltStore/Operations/ResignAppOperation.swift | 15 +++ 8 files changed, 316 insertions(+), 102 deletions(-) create mode 100644 AltStore/Authentication/RefreshAltStoreViewController.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index b6897373..b964c44b 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */ = {isa = PBXBuildFile; fileRef = BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */; }; BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; }; BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */; }; BF74989B23621C0700CED65F /* ForwardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */; }; BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */; }; BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5322BC044E002A40FE /* AppOperationContext.swift */; }; @@ -396,6 +397,7 @@ BF54E81F2315EF0D000AE0D8 /* ALTPatreonBenefitType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTPatreonBenefitType.h; sourceTree = ""; }; BF54E8202315EF0D000AE0D8 /* ALTPatreonBenefitType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTPatreonBenefitType.m; sourceTree = ""; }; BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAltStoreViewController.swift; sourceTree = ""; }; BF74989A23621C0700CED65F /* ForwardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForwardingNavigationController.swift; sourceTree = ""; }; BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallAppOperation.swift; sourceTree = ""; }; BF770E5322BC044E002A40FE /* AppOperationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOperationContext.swift; sourceTree = ""; }; @@ -1068,6 +1070,7 @@ BFE6325922A83BEB00F30809 /* Authentication.storyboard */, BFF0B6932321CB85007A79E1 /* AuthenticationViewController.swift */, BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */, + BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */, ); path = Authentication; sourceTree = ""; @@ -1487,6 +1490,7 @@ BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */, BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */, BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */, + BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */, BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */, BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */, BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */, diff --git a/AltStore/Authentication/Authentication.storyboard b/AltStore/Authentication/Authentication.storyboard index 2ada4c02..cbaa43f0 100644 --- a/AltStore/Authentication/Authentication.storyboard +++ b/AltStore/Authentication/Authentication.storyboard @@ -2,7 +2,6 @@ - @@ -443,13 +442,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/AltStore/Authentication/RefreshAltStoreViewController.swift b/AltStore/Authentication/RefreshAltStoreViewController.swift new file mode 100644 index 00000000..708a9d9d --- /dev/null +++ b/AltStore/Authentication/RefreshAltStoreViewController.swift @@ -0,0 +1,87 @@ +// +// RefreshAltStoreViewController.swift +// AltStore +// +// Created by Riley Testut on 10/26/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit +import AltSign + +import Roxas + +class RefreshAltStoreViewController: UIViewController +{ + var signer: ALTSigner! + + var completionHandler: ((Result) -> Void)? + + @IBOutlet private var placeholderView: RSTPlaceholderView! + + override func viewDidLoad() + { + super.viewDidLoad() + + self.placeholderView.textLabel.isHidden = true + + self.placeholderView.detailTextLabel.textAlignment = .left + self.placeholderView.detailTextLabel.textColor = UIColor.white.withAlphaComponent(0.6) + self.placeholderView.detailTextLabel.text = NSLocalizedString("AltStore was unable to use an existing signing certificate, so it had to create a new one. This will cause any apps installed with an existing certificate to expire — including AltStore.\n\nTo prevent AltStore from expiring early, please refresh the app now. AltStore will quit once refreshing is complete.", comment: "") + } +} + +private extension RefreshAltStoreViewController +{ + @IBAction func refreshAltStore(_ sender: PillButton) + { + guard let altStore = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext) else { return } + + func refresh() + { + sender.isIndicatingActivity = true + + if let progress = AppManager.shared.refreshProgress(for: altStore) ?? AppManager.shared.installationProgress(for: altStore) + { + // Cancel pending AltStore refresh so we can start a new one. + progress.cancel() + } + + let group = OperationGroup() + group.signer = self.signer // Prevent us from trying to authenticate a second time. + group.completionHandler = { (result) in + if let error = result.error ?? result.value?.values.compactMap({ $0.error }).first + { + DispatchQueue.main.async { + sender.progress = nil + sender.isIndicatingActivity = false + + let alertController = UIAlertController(title: NSLocalizedString("Failed to Refresh AltStore", comment: ""), message: error.localizedDescription, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Try Again", comment: ""), style: .default, handler: { (action) in + refresh() + })) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh Later", comment: ""), style: .cancel, handler: { (action) in + self.completionHandler?(.failure(error)) + })) + + self.present(alertController, animated: true, completion: nil) + } + } + else + { + self.completionHandler?(.success(())) + } + } + + _ = AppManager.shared.refresh([altStore], presentingViewController: self, group: group) + sender.progress = group.progress + } + + refresh() + } + + @IBAction func cancel(_ sender: UIButton) + { + self.completionHandler?(.failure(OperationError.cancelled)) + } +} diff --git a/AltStore/Components/Keychain.swift b/AltStore/Components/Keychain.swift index df82eb60..da44ae2d 100644 --- a/AltStore/Components/Keychain.swift +++ b/AltStore/Components/Keychain.swift @@ -11,11 +11,65 @@ import KeychainAccess import AltSign +@propertyWrapper +struct KeychainItem +{ + let key: String + + var wrappedValue: Value? { + get { + switch Value.self + { + case is Data.Type: return try? Keychain.shared.keychain.getData(self.key) as? Value + case is String.Type: return try? Keychain.shared.keychain.getString(self.key) as? Value + default: return nil + } + } + set { + switch Value.self + { + case is Data.Type: Keychain.shared.keychain[data: self.key] = newValue as? Data + case is String.Type: Keychain.shared.keychain[self.key] = newValue as? String + default: break + } + } + } + + init(key: String) + { + self.key = key + } +} + class Keychain { static let shared = Keychain() - private let keychain = KeychainAccess.Keychain(service: "com.rileytestut.AltStore").accessibility(.afterFirstUnlock).synchronizable(true) + fileprivate let keychain = KeychainAccess.Keychain(service: "com.rileytestut.AltStore").accessibility(.afterFirstUnlock).synchronizable(true) + + @KeychainItem(key: "appleIDEmailAddress") + var appleIDEmailAddress: String? + + @KeychainItem(key: "appleIDPassword") + var appleIDPassword: String? + + @KeychainItem(key: "signingCertificatePrivateKey") + var signingCertificatePrivateKey: Data? + + @KeychainItem(key: "signingCertificateSerialNumber") + var signingCertificateSerialNumber: String? + + @KeychainItem(key: "signingCertificate") + var signingCertificate: Data? + + @KeychainItem(key: "signingCertificatePassword") + var signingCertificatePassword: String? + + @KeychainItem(key: "patreonAccessToken") + var patreonAccessToken: String? + + @KeychainItem(key: "patreonRefreshToken") + var patreonRefreshToken: String? private init() { @@ -29,66 +83,3 @@ class Keychain self.signingCertificateSerialNumber = nil } } - -extension Keychain -{ - var appleIDEmailAddress: String? { - get { - let emailAddress = try? self.keychain.get("appleIDEmailAddress") - return emailAddress - } - set { - self.keychain["appleIDEmailAddress"] = newValue - } - } - - var appleIDPassword: String? { - get { - let password = try? self.keychain.get("appleIDPassword") - return password - } - set { - self.keychain["appleIDPassword"] = newValue - } - } - - var signingCertificatePrivateKey: Data? { - get { - let privateKey = try? self.keychain.getData("signingCertificatePrivateKey") - return privateKey - } - set { - self.keychain[data: "signingCertificatePrivateKey"] = newValue - } - } - - var signingCertificateSerialNumber: String? { - get { - let serialNumber = try? self.keychain.get("signingCertificateSerialNumber") - return serialNumber - } - set { - self.keychain["signingCertificateSerialNumber"] = newValue - } - } - - var patreonAccessToken: String? { - get { - let accessToken = try? self.keychain.get("patreonAccessToken") - return accessToken - } - set { - self.keychain["patreonAccessToken"] = newValue - } - } - - var patreonRefreshToken: String? { - get { - let refreshToken = try? self.keychain.get("patreonRefreshToken") - return refreshToken - } - set { - self.keychain["patreonRefreshToken"] = newValue - } - } -} diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 993ff6d8..a87519c8 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -149,7 +149,7 @@ extension AppManager func refresh(_ installedApps: [InstalledApp], presentingViewController: UIViewController?, group: OperationGroup? = nil) -> OperationGroup { - let apps = installedApps.filter { self.refreshProgress(for: $0) == nil } + let apps = installedApps.filter { self.refreshProgress(for: $0) == nil || self.refreshProgress(for: $0)?.isCancelled == true } let group = self.install(apps, forceDownload: false, presentingViewController: presentingViewController, group: group) @@ -183,18 +183,6 @@ private extension AppManager let group = group ?? OperationGroup() var operations = [Operation]() - - /* Authenticate */ - let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController) - authenticationOperation.resultHandler = { (result) in - switch result - { - case .failure(let error): group.error = error - case .success(let signer): group.signer = signer - } - } - operations.append(authenticationOperation) - /* Find Server */ let findServerOperation = FindServerOperation(group: group) findServerOperation.resultHandler = { (result) in @@ -204,9 +192,23 @@ private extension AppManager case .success(let server): group.server = server } } - findServerOperation.addDependency(authenticationOperation) operations.append(findServerOperation) + if group.signer == nil + { + /* Authenticate */ + let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController) + authenticationOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): group.error = error + case .success(let signer): group.signer = signer + } + } + operations.append(authenticationOperation) + + findServerOperation.addDependency(authenticationOperation) + } for app in apps { @@ -344,7 +346,11 @@ private extension AppManager guard !context.isFinished else { return } context.isFinished = true - self.refreshProgress[context.bundleIdentifier] = nil + if let progress = self.refreshProgress[context.bundleIdentifier], progress == context.group.progress(forAppWithBundleIdentifier: context.bundleIdentifier) + { + // Only remove progress if it hasn't been replaced by another one. + self.refreshProgress[context.bundleIdentifier] = nil + } if let error = context.error { diff --git a/AltStore/Operations/AuthenticationOperation.swift b/AltStore/Operations/AuthenticationOperation.swift index db8271dc..932826ad 100644 --- a/AltStore/Operations/AuthenticationOperation.swift +++ b/AltStore/Operations/AuthenticationOperation.swift @@ -36,7 +36,10 @@ class AuthenticationOperation: ResultOperation private lazy var navigationController: UINavigationController = { let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController - navigationController.presentationController?.delegate = self + if #available(iOS 13.0, *) + { + navigationController.isModalInPresentation = true + } return navigationController }() @@ -150,18 +153,25 @@ class AuthenticationOperation: ResultOperation Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved. Keychain.shared.appleIDPassword = self.appleIDPassword - Keychain.shared.signingCertificateSerialNumber = signer.certificate.serialNumber - Keychain.shared.signingCertificatePrivateKey = signer.certificate.privateKey + Keychain.shared.signingCertificate = signer.certificate.p12Data() + Keychain.shared.signingCertificatePassword = signer.certificate.machineIdentifier - super.finish(.success(signer)) + // Refresh screen must go last since a successful refresh will cause the app to quit. + self.showRefreshScreenIfNecessary() { (didShowRefreshAlert) in + super.finish(.success(signer)) + + DispatchQueue.main.async { + self.navigationController.dismiss(animated: true, completion: nil) + } + } } catch { super.finish(.failure(error)) - } - - DispatchQueue.main.async { - self.navigationController.dismiss(animated: true, completion: nil) + + DispatchQueue.main.async { + self.navigationController.dismiss(animated: true, completion: nil) + } } } } @@ -352,19 +362,45 @@ private extension AuthenticationOperation let certificates = try Result(certificates, error).get() if + let data = Keychain.shared.signingCertificate, + let localCertificate = ALTCertificate(p12Data: data, password: nil), + let certificate = certificates.first(where: { $0.serialNumber == localCertificate.serialNumber }) + { + // We have a certificate stored in the keychain and it hasn't been revoked. + localCertificate.machineIdentifier = certificate.machineIdentifier + completionHandler(.success(localCertificate)) + } + else if let serialNumber = Keychain.shared.signingCertificateSerialNumber, let privateKey = Keychain.shared.signingCertificatePrivateKey, let certificate = certificates.first(where: { $0.serialNumber == serialNumber }) { + // LEGACY + // We have the private key for one of the certificates, so add it to certificate and use it. certificate.privateKey = privateKey completionHandler(.success(certificate)) } + else if + let serialNumber = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.certificateID) as? String, + let certificate = certificates.first(where: { $0.serialNumber == serialNumber }), + let machineIdentifier = certificate.machineIdentifier, + FileManager.default.fileExists(atPath: Bundle.main.certificateURL.path), + let data = try? Data(contentsOf: Bundle.main.certificateURL), + let localCertificate = ALTCertificate(p12Data: data, password: machineIdentifier) + { + // We have an embedded certificate that hasn't been revoked. + localCertificate.machineIdentifier = machineIdentifier + completionHandler(.success(localCertificate)) + } else if certificates.isEmpty { + // No certificates, so request a new one. requestCertificate() } else { + // We don't have private keys for any of the certificates, + // so we need to revoke one and create a new one. replaceCertificate(from: certificates) } } @@ -392,19 +428,26 @@ private extension AuthenticationOperation } } } -} - -extension AuthenticationOperation: UIAdaptivePresentationControllerDelegate -{ - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) + + func showRefreshScreenIfNecessary(completionHandler: @escaping (Bool) -> Void) { - if let signer = self.signer - { - self.finish(.success(signer)) - } - else - { - self.finish(.failure(OperationError.cancelled)) + guard let signer = self.signer else { return completionHandler(false) } + guard let application = ALTApplication(fileURL: Bundle.main.bundleURL), let provisioningProfile = application.provisioningProfile else { return completionHandler(false) } + + // If we're not using the same certificate used to install AltStore, warn user that they need to refresh. + guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) } + + DispatchQueue.main.async { + let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController + refreshViewController.signer = signer + refreshViewController.completionHandler = { _ in + completionHandler(true) + } + + if !self.present(refreshViewController) + { + completionHandler(false) + } } } } diff --git a/AltStore/Operations/OperationGroup.swift b/AltStore/Operations/OperationGroup.swift index 83b765f3..a6891823 100644 --- a/AltStore/Operations/OperationGroup.swift +++ b/AltStore/Operations/OperationGroup.swift @@ -73,7 +73,12 @@ class OperationGroup func progress(for app: AppProtocol) -> Progress? { - let progress = self.progressByBundleIdentifier[app.bundleIdentifier] + return self.progress(forAppWithBundleIdentifier: app.bundleIdentifier) + } + + func progress(forAppWithBundleIdentifier bundleIdentifier: String) -> Progress? + { + let progress = self.progressByBundleIdentifier[bundleIdentifier] return progress } } diff --git a/AltStore/Operations/ResignAppOperation.swift b/AltStore/Operations/ResignAppOperation.swift index ec776819..77ecc50e 100644 --- a/AltStore/Operations/ResignAppOperation.swift +++ b/AltStore/Operations/ResignAppOperation.swift @@ -400,6 +400,21 @@ private extension ResignAppOperation guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { throw OperationError.unknownUDID } additionalValues[Bundle.Info.deviceID] = udid additionalValues[Bundle.Info.serverID] = UserDefaults.standard.preferredServerID + + if + let data = Keychain.shared.signingCertificate, + let signingCertificate = ALTCertificate(p12Data: data, password: nil), + let encryptingPassword = Keychain.shared.signingCertificatePassword + { + additionalValues[Bundle.Info.certificateID] = signingCertificate.serialNumber + + let encryptedData = signingCertificate.encryptedP12Data(withPassword: encryptingPassword) + try encryptedData?.write(to: appBundle.certificateURL, options: .atomic) + } + else + { + // The embedded certificate + certificate identifier are already in app bundle, no need to update them. + } } // Prepare app