// // AuthenticationOperation.swift // AltStore // // Created by Riley Testut on 6/5/19. // Copyright © 2019 Riley Testut. All rights reserved. // import Foundation import Roxas import Network import AltStoreCore import AltSign import minimuxer private extension UIColor { static let altInvertedPrimary = UIColor(named: "SettingsHighlighted")! } typealias AuthenticationError = AuthenticationErrorCode.Error enum AuthenticationErrorCode: Int, ALTErrorEnum, CaseIterable { case noTeam case noCertificate case teamSelectorError case missingPrivateKey case missingCertificate var errorFailureReason: String { switch self { case .noTeam: return NSLocalizedString("Your Apple ID has no developer teams?", comment: "") case .noCertificate: return NSLocalizedString("The developer certificate could not be found.", comment: "") case .teamSelectorError: return NSLocalizedString("Error presenting team selector view.", comment: "") case .missingPrivateKey: return NSLocalizedString("The certificate's private key could not be found.", comment: "") case .missingCertificate: return NSLocalizedString("The certificate could not be found.", comment: "") } } } @objc(AuthenticationOperation) final class AuthenticationOperation: ResultOperation<(ALTTeam, ALTCertificate, ALTAppleAPISession)> { let context: AuthenticatedOperationContext private weak var presentingViewController: UIViewController? private lazy var navigationController: UINavigationController = { let navigationController = self.storyboard.instantiateViewController(withIdentifier: "navigationController") as! UINavigationController navigationController.isModalInPresentation = true return navigationController }() private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil) private var appleIDEmailAddress: String? private var appleIDPassword: String? private var shouldShowInstructions = false private let operationQueue = OperationQueue() private var submitCodeAction: UIAlertAction? init(context: AuthenticatedOperationContext, presentingViewController: UIViewController?) { self.context = context self.presentingViewController = presentingViewController super.init() self.context.authenticationOperation = self self.operationQueue.name = "com.altstore.AuthenticationOperation" self.progress.totalUnitCount = 4 } override func main() { super.main() if let error = self.context.error { self.finish(.failure(error)) return } // Sign In self.signIn() { (result) in guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } switch result { case .failure(let error): self.finish(.failure(error)) case .success((let account, let session)): self.context.session = session self.progress.completedUnitCount += 1 // Fetch Team self.fetchTeam(for: account, session: session) { (result) in guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } switch result { case .failure(let error): self.finish(.failure(error)) case .success(let team): self.context.team = team self.progress.completedUnitCount += 1 // Fetch Certificate self.fetchCertificate(for: team, session: session) { (result) in guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } switch result { case .failure(let error): self.finish(.failure(error)) case .success(let certificate): self.context.certificate = certificate self.progress.completedUnitCount += 1 // Register Device self.registerCurrentDevice(for: team, session: session) { (result) in guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } switch result { case .failure(let error): self.finish(.failure(error)) case .success: self.progress.completedUnitCount += 1 // Save account/team to disk. self.save(team) { (result) in guard !self.isCancelled else { return self.finish(.failure(OperationError.cancelled)) } switch result { case .failure(let error): self.finish(.failure(error)) case .success: // Must cache App IDs _after_ saving account/team to disk. self.cacheAppIDs(team: team, session: session) { (result) in let result = result.map { _ in (team, certificate, session) } self.finish(result) } } } } } } } } } } } } func save(_ altTeam: ALTTeam, completionHandler: @escaping (Result) -> Void) { let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() context.performAndWait { do { let account: Account let team: Team if let tempAccount = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context) { account = tempAccount } else { account = Account(altTeam.account, context: context) } if let tempTeam = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context) { team = tempTeam } else { team = Team(altTeam, account: account, context: context) } account.update(account: altTeam.account) if let providedEmailAddress = self.appleIDEmailAddress { // Save the user's provided email address instead of the one associated with their account (which may be outdated). account.appleID = providedEmailAddress } team.update(team: altTeam) try context.save() completionHandler(.success(())) } catch { completionHandler(.failure(error)) } } } override func finish(_ result: Result<(ALTTeam, ALTCertificate, ALTAppleAPISession), Error>) { guard !self.isFinished else { return } switch result { case .failure(let error): Logger.sideload.error("Failed to authenticate account. \(error.localizedDescription, privacy: .public)") case .success((let team, _, _)): Logger.sideload.notice("Authenticated account for team \(team.identifier, privacy: .public).") } let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() context.perform { do { let (altTeam, altCertificate, session) = try result.get() guard let account = Account.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Account.identifier), altTeam.account.identifier), in: context), let team = Team.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Team.identifier), altTeam.identifier), in: context) else { throw AuthenticationError(.noTeam) } // Account account.isActiveAccount = true let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier) let otherAccounts = try context.fetch(otherAccountsFetchRequest) for account in otherAccounts { account.isActiveAccount = false } // Team team.isActiveTeam = true let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier) let otherTeams = try context.fetch(otherTeamsFetchRequest) for team in otherTeams { team.isActiveTeam = false } let activeAppsMinimumVersion = OperatingSystemVersion(majorVersion: 13, minorVersion: 3, patchVersion: 1) if team.type == .free, !UserDefaults.standard.isAppLimitDisabled, ProcessInfo().sparseRestorePatched { UserDefaults.standard.activeAppsLimit = ALTActiveAppsLimit } else if UserDefaults.standard.isAppLimitDisabled, !ProcessInfo().sparseRestorePatched { UserDefaults.standard.activeAppsLimit = 10 } else { UserDefaults.standard.activeAppsLimit = nil } // Save try context.save() // Update keychain Keychain.shared.appleIDEmailAddress = self.appleIDEmailAddress ?? altTeam.account.appleID // Prefer the user's provided email address over the one associated with their account (which may be outdated). Keychain.shared.appleIDPassword = self.appleIDPassword Keychain.shared.signingCertificate = altCertificate.p12Data() Keychain.shared.signingCertificatePassword = altCertificate.machineIdentifier self.showInstructionsIfNecessary() { (didShowInstructions) in let signer = ALTSigner(team: altTeam, certificate: altCertificate) // Refresh screen must go last since a successful refresh will cause the app to quit. self.showRefreshScreenIfNecessary(signer: signer, session: session) { (didShowRefreshAlert) in super.finish(result) DispatchQueue.main.async { self.navigationController.dismiss(animated: true, completion: nil) } } } } catch { super.finish(result) DispatchQueue.main.async { self.navigationController.dismiss(animated: true, completion: nil) } } } } } private extension AuthenticationOperation { func present(_ viewController: UIViewController) -> Bool { guard let presentingViewController = self.presentingViewController else { return false } self.navigationController.view.tintColor = .altInvertedPrimary if self.navigationController.viewControllers.isEmpty { guard presentingViewController.presentedViewController == nil else { return false } self.navigationController.setViewControllers([viewController], animated: false) presentingViewController.present(self.navigationController, animated: true, completion: nil) } else { viewController.navigationItem.leftBarButtonItem = nil self.navigationController.pushViewController(viewController, animated: true) } return true } } private extension AuthenticationOperation { func signIn(completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void) { func authenticate() { DispatchQueue.main.async { let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController authenticationViewController.authenticationHandler = { (appleID, password, completionHandler) in self.authenticate(appleID: appleID, password: password) { (result) in completionHandler(result) } } authenticationViewController.completionHandler = { (result) in if let (account, session, password) = result { // We presented the Auth UI and the user signed in. // In this case, we'll assume we should show the instructions again. self.shouldShowInstructions = true self.appleIDPassword = password completionHandler(.success((account, session))) } else { completionHandler(.failure(OperationError.cancelled)) } } if !self.present(authenticationViewController) { completionHandler(.failure(OperationError.notAuthenticated)) } } } if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword { Logger.sideload.notice("Authenticating Apple ID...") self.authenticate(appleID: appleID, password: password) { (result) in switch result { case .success((let account, let session)): self.appleIDPassword = password completionHandler(.success((account, session))) case .failure(ALTAppleAPIError.incorrectCredentials), .failure(ALTAppleAPIError.appSpecificPasswordRequired): authenticate() case .failure(let error): completionHandler(.failure(error)) } } } else { authenticate() } } func authenticate(appleID: String, password: String, completionHandler: @escaping (Result<(ALTAccount, ALTAppleAPISession), Swift.Error>) -> Void) { self.appleIDEmailAddress = appleID let fetchAnisetteDataOperation = FetchAnisetteDataOperation(context: self.context) fetchAnisetteDataOperation.resultHandler = { (result) in switch result { case .failure(let error): completionHandler(.failure(error)) case .success(let anisetteData): let verificationHandler: ((@escaping (String?) -> Void) -> Void)? if let presentingViewController = self.presentingViewController { verificationHandler = { (completionHandler) in DispatchQueue.main.async { let alertController = UIAlertController(title: NSLocalizedString("Please enter the 6-digit verification code that was sent to your Apple devices.", comment: ""), message: nil, preferredStyle: .alert) alertController.addTextField { (textField) in textField.autocorrectionType = .no textField.autocapitalizationType = .none textField.keyboardType = .numberPad NotificationCenter.default.addObserver(self, selector: #selector(AuthenticationOperation.textFieldTextDidChange(_:)), name: UITextField.textDidChangeNotification, object: textField) } let submitAction = UIAlertAction(title: NSLocalizedString("Continue", comment: ""), style: .default) { (action) in let textField = alertController.textFields?.first let code = textField?.text ?? "" completionHandler(code) } submitAction.isEnabled = false alertController.addAction(submitAction) self.submitCodeAction = submitAction alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("Cancel"), style: .cancel) { (action) in completionHandler(nil) }) if self.navigationController.presentingViewController != nil { self.navigationController.present(alertController, animated: true, completion: nil) } else { presentingViewController.present(alertController, animated: true, completion: nil) } } } } else { // No view controller to present security code alert, so don't provide verificationHandler. verificationHandler = nil } ALTAppleAPI.shared.authenticate(appleID: appleID, password: password, anisetteData: anisetteData, verificationHandler: verificationHandler) { (account, session, error) in if let account = account, let session = session { completionHandler(.success((account, session))) } else { completionHandler(.failure(error ?? OperationError.unknown())) } } } } self.operationQueue.addOperation(fetchAnisetteDataOperation) } func fetchTeam(for account: ALTAccount, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { func selectTeam(from teams: [ALTTeam]) { if teams.count <= 1 { if let team = teams.first { return completionHandler(.success(team)) } else { return completionHandler(.failure(AuthenticationError(.noTeam))) } } else { DispatchQueue.main.async { let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController selectTeamViewController.teams = teams selectTeamViewController.completionHandler = completionHandler if !self.present(selectTeamViewController) { return completionHandler(.failure(AuthenticationError(.noTeam))) } } } } ALTAppleAPI.shared.fetchTeams(for: account, session: session) { (teams, error) in switch Result(teams, error) { case .failure(let error): completionHandler(.failure(error)) case .success(let teams): DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in if let activeTeam = DatabaseManager.shared.activeTeam(in: context), let altTeam = teams.first(where: { $0.identifier == activeTeam.identifier }) { completionHandler(.success(altTeam)) } else { selectTeam(from: teams) } } } } } func fetchCertificate(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { func requestCertificate() { let machineName: String = "SideStore - \(team.account.firstName)'s \(UIDevice.current.name)" ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team, session: session) { (certificate, error) in do { let certificate = try Result(certificate, error).get() guard let privateKey = certificate.privateKey else { throw AuthenticationError(.missingPrivateKey) } ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in do { let certificates = try Result(certificates, error).get() guard let certificate = certificates.first(where: { $0.serialNumber == certificate.serialNumber }) else { throw AuthenticationError(.missingCertificate) } certificate.privateKey = privateKey completionHandler(.success(certificate)) } catch { completionHandler(.failure(error)) } } } catch { completionHandler(.failure(error)) } } } func replaceCertificate(from certificates: [ALTCertificate]) { let ourCertificates = certificates.filter { a in a.machineName?.starts(with: "SideStore") == true || a.machineName?.starts(with: "AltStore") == true } if ourCertificates.isEmpty { return requestCertificate() } // We don't have private keys for any of the certificates, // so we need to revoke one and create a new one. var certsText = "" for certificate in ourCertificates { if let name = certificate.machineName { certsText.append("\(name)\n") } } let alertController = UIAlertController(title: NSLocalizedString("Would you like to revoke your previous certificates?\n\(certsText)", comment: ""), message: nil, preferredStyle: .alert) let noAction = UIAlertAction(title: NSLocalizedString("No", comment: ""), style: .default) { (action) in requestCertificate() } let yesAction = UIAlertAction(title: NSLocalizedString("Yes", comment: ""), style: .default) { (action) in for certificate in ourCertificates { ALTAppleAPI.shared.revoke(certificate, for: team, session: session) { (success, error) in if let error = error, !success { completionHandler(.failure(error)) } } } requestCertificate() } alertController.addAction(noAction) alertController.addAction(yesAction) DispatchQueue.main.async { if self.navigationController.presentingViewController != nil { self.navigationController.present(alertController, animated: true, completion: nil) } else { self.presentingViewController?.present(alertController, animated: true, completion: nil) } } } ALTAppleAPI.shared.fetchCertificates(for: team, session: session) { (certificates, error) in do { 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 { replaceCertificate(from: certificates) } } catch { completionHandler(.failure(error)) } } } func registerCurrentDevice(for team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { guard let udid = fetch_udid()?.toString() else { return completionHandler(.failure(OperationError.unknownUDID)) } ALTAppleAPI.shared.fetchDevices(for: team, types: [.iphone, .ipad], session: session) { (devices, error) in do { let devices = try Result(devices, error).get() if let device = devices.first(where: { $0.identifier == udid }) { completionHandler(.success(device)) } else { ALTAppleAPI.shared.registerDevice(name: UIDevice.current.name, identifier: udid, type: .iphone, team: team, session: session) { (device, error) in completionHandler(Result(device, error)) } } } catch { completionHandler(.failure(error)) } } } func cacheAppIDs(team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { let fetchAppIDsOperation = FetchAppIDsOperation(context: self.context) fetchAppIDsOperation.resultHandler = { (result) in do { let (_, context) = try result.get() try context.save() completionHandler(.success(())) } catch { completionHandler(.failure(error)) } } self.operationQueue.addOperation(fetchAppIDsOperation) } func showInstructionsIfNecessary(completionHandler: @escaping (Bool) -> Void) { guard self.shouldShowInstructions else { return completionHandler(false) } DispatchQueue.main.async { let instructionsViewController = self.storyboard.instantiateViewController(withIdentifier: "instructionsViewController") as! InstructionsViewController instructionsViewController.showsBottomButton = true instructionsViewController.completionHandler = { completionHandler(true) } if !self.present(instructionsViewController) { completionHandler(false) } } } func showRefreshScreenIfNecessary(signer: ALTSigner, session: ALTAppleAPISession, completionHandler: @escaping (Bool) -> Void) { 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) } #if DEBUG completionHandler(false) #else DispatchQueue.main.async { let context = AuthenticatedOperationContext(context: self.context) context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish. let refreshViewController = self.storyboard.instantiateViewController(withIdentifier: "refreshAltStoreViewController") as! RefreshAltStoreViewController refreshViewController.context = context refreshViewController.completionHandler = { _ in completionHandler(true) } if !self.present(refreshViewController) { completionHandler(false) } } #endif } } extension AuthenticationOperation { @objc func textFieldTextDidChange(_ notification: Notification) { guard let textField = notification.object as? UITextField else { return } self.submitCodeAction?.isEnabled = (textField.text ?? "").count == 6 } }