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