diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 526bfcf0..2bb9ed9a 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -144,6 +144,13 @@ BFD52C2022A1A9EC000B7ED1 /* node.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1D22A1A9EC000B7ED1 /* node.c */; }; BFD52C2122A1A9EC000B7ED1 /* node_list.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1E22A1A9EC000B7ED1 /* node_list.c */; }; BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; }; + BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; }; + BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */; }; + BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */; }; + BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; }; + BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; }; + BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */; }; + BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; }; BFFC044E22A204F40066B31F /* App.ipa in Resources */ = {isa = PBXBuildFile; fileRef = BFFC044D22A204F30066B31F /* App.ipa */; }; DBAC68F8EC03F4A41D62EDE1 /* Pods_AltStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1039C07E517311FC499A0B64 /* Pods_AltStore.framework */; }; /* End PBXBuildFile section */ @@ -354,6 +361,13 @@ BFD52C1D22A1A9EC000B7ED1 /* node.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node.c; path = Dependencies/libplist/libcnary/node.c; sourceTree = SOURCE_ROOT; }; BFD52C1E22A1A9EC000B7ED1 /* node_list.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node_list.c; path = Dependencies/libplist/libcnary/node_list.c; sourceTree = SOURCE_ROOT; }; BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; }; + BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = ""; }; + BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = ""; }; + BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTeamViewController.swift; sourceTree = ""; }; + BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = ""; }; + BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; + BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaceCertificateViewController.swift; sourceTree = ""; }; + BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = ""; }; BFFC044D22A204F30066B31F /* App.ipa */ = {isa = PBXFileReference; lastKnownFileType = file; path = App.ipa; sourceTree = ""; }; EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -657,6 +671,7 @@ children = ( BFD2476D2284B9A500981D42 /* AppDelegate.swift */, BFD247732284B9A500981D42 /* Main.storyboard */, + BFE6325822A83BA800F30809 /* Authentication */, BFD2478A2284C49000981D42 /* Apps */, BFBBE2E2229320A2002097FA /* My Apps */, BFB1169E22933DDC00BB457C /* Updates */, @@ -730,8 +745,10 @@ children = ( BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */, BFB11691229322E400BB457C /* DatabaseManager.swift */, + BFE6326722A858F300F30809 /* Account.swift */, BFBBE2DE22931F73002097FA /* App.swift */, BFBBE2E022931F81002097FA /* InstalledApp.swift */, + BFE6326522A857C100F30809 /* Team.swift */, ); path = Model; sourceTree = ""; @@ -754,6 +771,18 @@ path = Connections; sourceTree = ""; }; + BFE6325822A83BA800F30809 /* Authentication */ = { + isa = PBXGroup; + children = ( + BFE6325922A83BEB00F30809 /* Authentication.storyboard */, + BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */, + BFE6325D22A8497000F30809 /* SelectTeamViewController.swift */, + BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */, + BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */, + ); + path = Authentication; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -956,6 +985,7 @@ BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */, BFD247772284B9A700981D42 /* Assets.xcassets in Resources */, BFD247752284B9A500981D42 /* Main.storyboard in Resources */, + BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1096,23 +1126,29 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */, + BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */, BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */, BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */, BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */, BFBBE2DF22931F73002097FA /* App.swift in Sources */, BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */, BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, + BFE6326822A858F300F30809 /* Account.swift in Sources */, + BFE6326622A857C200F30809 /* Team.swift in Sources */, BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */, BFB116A022933DEB00BB457C /* UpdatesViewController.swift in Sources */, BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */, + BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */, BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */, BFD247932284D4B700981D42 /* AppTableViewCell.swift in Sources */, BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, BFD52BD622A08A85000B7ED1 /* Server.swift in Sources */, + BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */, BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */, BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */, ); diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index 31f89ba5..c065c3d4 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -39,6 +39,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Keychain.shared.appleIDEmailAddress = nil Keychain.shared.appleIDPassword = nil Keychain.shared.signingCertificatePrivateKey = nil + Keychain.shared.signingCertificateIdentifier = nil UserDefaults.standard.firstLaunch = Date() } diff --git a/AltStore/Apps/AppDetailViewController.swift b/AltStore/Apps/AppDetailViewController.swift index a83f7efc..5596979d 100644 --- a/AltStore/Apps/AppDetailViewController.swift +++ b/AltStore/Apps/AppDetailViewController.swift @@ -86,16 +86,19 @@ private extension AppDetailViewController self.descriptionLabel.text = self.app.localizedDescription - if self.app.installedApp == nil + if !self.downloadButton.isIndicatingActivity { - let text = String(format: NSLocalizedString("Download %@", comment: ""), self.app.name) - self.downloadButton.setTitle(text, for: .normal) - self.downloadButton.isEnabled = true - } - else - { - self.downloadButton.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal) - self.downloadButton.isEnabled = false + if self.app.installedApp == nil + { + let text = String(format: NSLocalizedString("Download %@", comment: ""), self.app.name) + self.downloadButton.setTitle(text, for: .normal) + self.downloadButton.isEnabled = true + } + else + { + self.downloadButton.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal) + self.downloadButton.isEnabled = false + } } } @@ -135,6 +138,10 @@ private extension AppDetailViewController toastView.show(in: self.navigationController!.view, duration: 2) } } + catch AppManager.AppError.authentication(AuthenticationOperation.Error.cancelled) + { + // Ignore + } catch { DispatchQueue.main.async { @@ -145,8 +152,8 @@ private extension AppDetailViewController } DispatchQueue.main.async { - self.update() sender.isIndicatingActivity = false + self.update() } } } diff --git a/AltStore/Apps/AppManager.swift b/AltStore/Apps/AppManager.swift index 0a920341..fb6e8c70 100644 --- a/AltStore/Apps/AppManager.swift +++ b/AltStore/Apps/AppManager.swift @@ -60,9 +60,11 @@ class AppManager static let shared = AppManager() private let session = URLSession(configuration: .default) + private let operationQueue = OperationQueue() private init() { + self.operationQueue.name = "com.rileytestut.AltStore.AppManager" } } @@ -97,6 +99,15 @@ extension AppManager print("Error while fetching installed apps") } } + + func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTTeam, ALTCertificate), Error>) -> Void) + { + let authenticationOperation = AuthenticationOperation(presentingViewController: presentingViewController) + authenticationOperation.resultHandler = { (result) in + completionHandler(result) + } + self.operationQueue.addOperation(authenticationOperation) + } } extension AppManager @@ -131,14 +142,14 @@ extension AppManager switch result { case .failure(let error): finish(.failure(.authentication(error))) - case .success(let team): + case .success(let team, let certificate): - // Fetch signing resources - self.fetchSigningResources(for: app, team: team, presentingViewController: presentingViewController) { (result) in + // Fetch provisioning profile + self.prepareProvisioningProfile(for: app, team: team) { (result) in switch result { case .failure(let error): finish(.failure(.fetchingSigningResources(error))) - case .success(let certificate, let profile): + case .success(let profile): // Prepare app DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in @@ -233,44 +244,37 @@ extension AppManager switch result { case .failure(let error): finish(.failure(.authentication(error))) - case .success(let team): + case .success(let team, let certificate): - // Fetch Certificate - self.fetchCertificate(for: team, presentingViewController: nil) { (result) in - switch result - { - case .failure(let error): finish(.failure(.fetchingSigningResources(error))) - case .success(let certificate): - let signer = ALTSigner(team: team, certificate: certificate) + // Sign + let signer = ALTSigner(team: team, certificate: certificate) + + let dispatchGroup = DispatchGroup() + var results = [String: Result]() + + let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() + + for app in installedApps + { + dispatchGroup.enter() + + app.managedObjectContext?.perform { + let bundleIdentifier = app.bundleIdentifier + print("Refreshing App:", bundleIdentifier) - let dispatchGroup = DispatchGroup() - var results = [String: Result]() - - let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() - - for app in installedApps - { - dispatchGroup.enter() - - app.managedObjectContext?.perform { - let bundleIdentifier = app.bundleIdentifier - print("Refreshing App:", bundleIdentifier) - - self.refresh(app, signer: signer, context: context) { (result) in - print("Refreshed App: \(bundleIdentifier).", result) - results[bundleIdentifier] = result - dispatchGroup.leave() - } - } - } - - dispatchGroup.notify(queue: .global()) { - context.perform { - finish(.success(results)) - } + self.refresh(app, signer: signer, context: context) { (result) in + print("Refreshed App: \(bundleIdentifier).", result) + results[bundleIdentifier] = result + dispatchGroup.leave() } } } + + dispatchGroup.notify(queue: .global()) { + context.perform { + finish(.success(results)) + } + } } } } @@ -299,63 +303,6 @@ private extension AppManager downloadTask.resume() } - func authenticate(presentingViewController: UIViewController?, completionHandler: @escaping (Result) -> Void) - { - func authenticate(emailAddress: String, password: String) - { - ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in - do - { - let account = try Result(account, error).get() - - Keychain.shared.appleIDEmailAddress = emailAddress - Keychain.shared.appleIDPassword = password - - self.fetchTeam(for: account, presentingViewController: presentingViewController, completionHandler: completionHandler) - } - catch - { - completionHandler(.failure(error)) - } - } - } - - if let emailAddress = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword - { - authenticate(emailAddress: emailAddress, password: password) - } - else if let presentingViewController = presentingViewController - { - DispatchQueue.main.async { - let alertController = UIAlertController(title: "Enter Apple ID + Password", message: "", preferredStyle: .alert) - alertController.addTextField { (textField) in - textField.placeholder = "Apple ID" - textField.textContentType = .emailAddress - } - alertController.addTextField { (textField) in - textField.placeholder = "Password" - textField.textContentType = .password - } - alertController.addAction(.cancel) - alertController.addAction(UIAlertAction(title: "Sign In", style: .default) { [unowned alertController] (action) in - guard - let emailAddress = alertController.textFields![0].text, - let password = alertController.textFields![1].text, - !emailAddress.isEmpty, !password.isEmpty - else { return completionHandler(.failure(ALTAppleAPIError(.incorrectCredentials))) } - - authenticate(emailAddress: emailAddress, password: password) - }) - - presentingViewController.present(alertController, animated: true, completion: nil) - } - } - else - { - completionHandler(.failure(AppError.notAuthenticated)) - } - } - func prepareProvisioningProfile(for app: App, team: ALTTeam, completionHandler: @escaping (Result) -> Void) { guard let udid = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.deviceID) as? String else { return completionHandler(.failure(AppError.missingUDID)) } @@ -397,32 +344,6 @@ private extension AppManager } } - func fetchSigningResources(for app: App, team: ALTTeam, presentingViewController: UIViewController?, completionHandler: @escaping (Result<(ALTCertificate, ALTProvisioningProfile), Error>) -> Void) - { - self.fetchCertificate(for: team, presentingViewController: presentingViewController) { (result) in - do - { - let certificate = try result.get() - - self.prepareProvisioningProfile(for: app, team: team) { (result) in - do - { - let provisioningProfile = try result.get() - completionHandler(.success((certificate, provisioningProfile))) - } - catch - { - completionHandler(.failure(error)) - } - } - } - catch - { - completionHandler(.failure(error)) - } - } - } - func prepare(_ installedApp: InstalledApp, provisioningProfile: ALTProvisioningProfile, signer: ALTSigner, completionHandler: @escaping (Result) -> Void) { do @@ -484,132 +405,6 @@ private extension AppManager private extension AppManager { - func fetchTeam(for account: ALTAccount, presentingViewController: UIViewController?, completionHandler: @escaping (Result) -> Void) - { - ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in - do - { - let teams = try Result(teams, error).get() - guard teams.count > 0 else { throw ALTAppleAPIError(.noTeams) } - - if let team = teams.first, teams.count == 1 - { - completionHandler(.success(team)) - } - else - { - DispatchQueue.main.async { - let alertController = UIAlertController(title: "Select Team", message: "", preferredStyle: .actionSheet) - alertController.addAction(.cancel) - - for team in teams - { - alertController.addAction(UIAlertAction(title: team.name, style: .default) { (action) in - completionHandler(.success(team)) - }) - } - - presentingViewController?.present(alertController, animated: true, completion: nil) - } - } - } - catch - { - completionHandler(.failure(error)) - } - } - } - - func fetchCertificate(for team: ALTTeam, presentingViewController: UIViewController?, completionHandler: @escaping (Result) -> Void) - { - ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in - do - { - let certificates = try Result(certificates, error).get() - - if - let identifier = UserDefaults.standard.signingCertificateIdentifier, - let privateKey = Keychain.shared.signingCertificatePrivateKey, - let certificate = certificates.first(where: { $0.identifier == identifier }) - { - certificate.privateKey = privateKey - completionHandler(.success(certificate)) - } - else if certificates.count < 1 - { - let machineName = "AltStore - " + UIDevice.current.name - ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team) { (certificate, error) in - do - { - let certificate = try Result(certificate, error).get() - guard let privateKey = certificate.privateKey else { throw AppError.missingPrivateKey } - - ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in - do - { - let certificates = try Result(certificates, error).get() - - guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else { - throw AppError.missingCertificate - } - - certificate.privateKey = privateKey - - UserDefaults.standard.signingCertificateIdentifier = certificate.identifier - Keychain.shared.signingCertificatePrivateKey = privateKey - - completionHandler(.success(certificate)) - } - catch - { - completionHandler(.failure(error)) - } - } - } - catch - { - completionHandler(.failure(error)) - } - } - } - else if let presentingViewController = presentingViewController - { - DispatchQueue.main.async { - let alertController = UIAlertController(title: "Too Many Certificates", message: "Please select the certificate you would like to revoke.", preferredStyle: .actionSheet) - alertController.addAction(.cancel) - - for certificate in certificates - { - alertController.addAction(UIAlertAction(title: certificate.name, style: .default) { (action) in - ALTAppleAPI.shared.revoke(certificate, for: team) { (success, error) in - do - { - try Result(success, error).get() - self.fetchCertificate(for: team, presentingViewController: presentingViewController, completionHandler: completionHandler) - } - catch - { - completionHandler(.failure(error)) - } - } - }) - } - - presentingViewController.present(alertController, animated: true, completion: nil) - } - } - else - { - completionHandler(.failure(AppError.multipleCertificates)) - } - } - catch - { - completionHandler(.failure(error)) - } - } - } - func register(_ device: ALTDevice, team: ALTTeam, completionHandler: @escaping (Result) -> Void) { ALTAppleAPI.shared.fetchDevices(for: team) { (devices, error) in diff --git a/AltStore/Authentication/Authentication.storyboard b/AltStore/Authentication/Authentication.storyboard new file mode 100644 index 00000000..a5bc8197 --- /dev/null +++ b/AltStore/Authentication/Authentication.storyboard @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + Your email address and password are used only to sign in with Apple and is never stored. + +If you have two-factor authentication enabled, make sure to use an app-specific password. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AltStore/Authentication/AuthenticationOperation.swift b/AltStore/Authentication/AuthenticationOperation.swift new file mode 100644 index 00000000..70f4192b --- /dev/null +++ b/AltStore/Authentication/AuthenticationOperation.swift @@ -0,0 +1,382 @@ +// +// AuthenticationOperation.swift +// AltStore +// +// Created by Riley Testut on 6/5/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation +import Roxas + +import AltSign + +extension AuthenticationOperation +{ + enum Error: LocalizedError + { + case cancelled + + case notAuthenticated + case noTeam + case noCertificate + + case missingPrivateKey + case missingCertificate + + var errorDescription: String? { + switch self { + case .cancelled: return NSLocalizedString("The operation was cancelled.", comment: "") + case .notAuthenticated: return NSLocalizedString("You are not signed in.", comment: "") + case .noTeam: return NSLocalizedString("Developer team could not be found.", comment: "") + case .noCertificate: return NSLocalizedString("Developer certificate could not be found.", 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: "") + } + } + } +} + +class AuthenticationOperation: RSTOperation +{ + var resultHandler: ((Result<(ALTTeam, ALTCertificate), Swift.Error>) -> Void)? + + private weak var presentingViewController: UIViewController? + + private lazy var navigationController = UINavigationController() + private lazy var storyboard = UIStoryboard(name: "Authentication", bundle: nil) + + private var appleIDPassword: String? + + override var isAsynchronous: Bool { + return true + } + + init(presentingViewController: UIViewController?) + { + self.presentingViewController = presentingViewController + + super.init() + } + + override func main() + { + super.main() + + let backgroundTaskID = RSTBeginBackgroundTask("com.rileytestut.AltStore.Authenticate") + + func finish(_ result: Result<(ALTTeam, ALTCertificate), Swift.Error>) + { + print("Finished authenticating with result:", result) + + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + do + { + let (altTeam, altCertificate) = try result.get() + let altAccount = altTeam.account + + // Account + let account = Account(altAccount, context: context) + + let otherAccountsFetchRequest = Account.fetchRequest() as NSFetchRequest + otherAccountsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Account.identifier), account.identifier) + + let otherAccounts = try context.fetch(otherAccountsFetchRequest) + otherAccounts.forEach(context.delete(_:)) + + // Team + let team = Team(altTeam, account: account, context: context) + + let otherTeamsFetchRequest = Team.fetchRequest() as NSFetchRequest + otherTeamsFetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(Team.identifier), team.identifier) + + let otherTeams = try context.fetch(otherTeamsFetchRequest) + otherTeams.forEach(context.delete(_:)) + + // Save + try context.save() + + // Update keychain + Keychain.shared.appleIDEmailAddress = altAccount.appleID // "account" may have nil appleID since we just saved. + Keychain.shared.appleIDPassword = self.appleIDPassword + + Keychain.shared.signingCertificateIdentifier = altCertificate.identifier + Keychain.shared.signingCertificatePrivateKey = altCertificate.privateKey + + self.resultHandler?(.success((altTeam, altCertificate))) + } + catch + { + self.resultHandler?(.failure(error)) + } + + self.finish() + + DispatchQueue.main.async { + self.navigationController.dismiss(animated: true, completion: nil) + } + + RSTEndBackgroundTask(backgroundTaskID) + } + } + + // Sign In + self.signIn { (result) in + switch result + { + case .failure(let error): finish(.failure(error)) + case .success(let account): + + // Fetch Team + self.fetchTeam(for: account) { (result) in + switch result + { + case .failure(let error): finish(.failure(error)) + case .success(let team): + + // Fetch Certificate + self.fetchCertificate(for: team) { (result) in + switch result + { + case .failure(let error): finish(.failure(error)) + case .success(let certificate): finish(.success((team, certificate))) + } + } + } + } + } + } + } +} + +private extension AuthenticationOperation +{ + func present(_ viewController: UIViewController) -> Bool + { + guard let presentingViewController = self.presentingViewController else { return false } + + self.navigationController.view.tintColor = .altPurple + + if self.navigationController.viewControllers.isEmpty + { + 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) -> Void) + { + func authenticate() + { + DispatchQueue.main.async { + let authenticationViewController = self.storyboard.instantiateViewController(withIdentifier: "authenticationViewController") as! AuthenticationViewController + authenticationViewController.authenticationHandler = { (result) in + if let (account, password) = result + { + self.appleIDPassword = password + + completionHandler(.success(account)) + } + else + { + completionHandler(.failure(Error.cancelled)) + } + } + + if !self.present(authenticationViewController) + { + completionHandler(.failure(Error.notAuthenticated)) + } + } + } + + if let appleID = Keychain.shared.appleIDEmailAddress, let password = Keychain.shared.appleIDPassword + { + ALTAppleAPI.shared.authenticate(appleID: appleID, password: password) { (account, error) in + do + { + self.appleIDPassword = password + + let account = try Result(account, error).get() + completionHandler(.success(account)) + } + catch ALTAppleAPIError.incorrectCredentials + { + authenticate() + } + catch ALTAppleAPIError.appSpecificPasswordRequired + { + authenticate() + } + catch + { + completionHandler(.failure(error)) + } + } + } + else + { + authenticate() + } + } + + func fetchTeam(for account: ALTAccount, completionHandler: @escaping (Result) -> Void) + { + func selectTeam(from teams: [ALTTeam]) + { + DispatchQueue.main.async { + let selectTeamViewController = self.storyboard.instantiateViewController(withIdentifier: "selectTeamViewController") as! SelectTeamViewController + selectTeamViewController.teams = teams + selectTeamViewController.selectionHandler = { (team) in + if let team = team + { + completionHandler(.success(team)) + } + else + { + completionHandler(.failure(Error.cancelled)) + } + } + + if !self.present(selectTeamViewController) + { + completionHandler(.failure(Error.noTeam)) + } + } + } + + ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in + switch Result(teams, error) + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let teams): + + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + do + { + let fetchRequest = Team.fetchRequest() as NSFetchRequest + fetchRequest.fetchLimit = 1 + fetchRequest.returnsObjectsAsFaults = false + + let fetchedTeams = try context.fetch(fetchRequest) + + if let fetchedTeam = fetchedTeams.first, let altTeam = teams.first(where: { $0.identifier == fetchedTeam.identifier }) + { + completionHandler(.success(altTeam)) + } + else + { + selectTeam(from: teams) + } + } + catch + { + print("Error fetching Teams.", error) + + selectTeam(from: teams) + } + } + } + } + } + + func fetchCertificate(for team: ALTTeam, completionHandler: @escaping (Result) -> Void) + { + func requestCertificate() + { + let machineName = "AltStore - " + UIDevice.current.name + ALTAppleAPI.shared.addCertificate(machineName: machineName, to: team) { (certificate, error) in + do + { + let certificate = try Result(certificate, error).get() + guard let privateKey = certificate.privateKey else { throw Error.missingPrivateKey } + + ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in + do + { + let certificates = try Result(certificates, error).get() + + guard let certificate = certificates.first(where: { $0.identifier == certificate.identifier }) else { + throw Error.missingCertificate + } + + certificate.privateKey = privateKey + completionHandler(.success(certificate)) + } + catch + { + completionHandler(.failure(error)) + } + } + } + catch + { + completionHandler(.failure(error)) + } + } + } + + func replaceCertificate(from certificates: [ALTCertificate]) + { + DispatchQueue.main.async { + let replaceCertificateViewController = self.storyboard.instantiateViewController(withIdentifier: "replaceCertificateViewController") as! ReplaceCertificateViewController + replaceCertificateViewController.team = team + replaceCertificateViewController.certificates = certificates + replaceCertificateViewController.replacementHandler = { (certificate) in + if certificate != nil + { + requestCertificate() + } + else + { + completionHandler(.failure(Error.cancelled)) + } + } + + if !self.present(replaceCertificateViewController) + { + completionHandler(.failure(Error.noCertificate)) + } + } + } + + ALTAppleAPI.shared.fetchCertificates(for: team) { (certificates, error) in + do + { + let certificates = try Result(certificates, error).get() + + if + let identifier = Keychain.shared.signingCertificateIdentifier, + let privateKey = Keychain.shared.signingCertificatePrivateKey, + let certificate = certificates.first(where: { $0.identifier == identifier }) + { + certificate.privateKey = privateKey + completionHandler(.success(certificate)) + } + else if certificates.isEmpty + { + requestCertificate() + } + else + { + replaceCertificate(from: certificates) + } + } + catch + { + completionHandler(.failure(error)) + } + } + } + +} diff --git a/AltStore/Authentication/AuthenticationViewController.swift b/AltStore/Authentication/AuthenticationViewController.swift new file mode 100644 index 00000000..039a8b80 --- /dev/null +++ b/AltStore/Authentication/AuthenticationViewController.swift @@ -0,0 +1,151 @@ +// +// AuthenticationViewController.swift +// AltStore +// +// Created by Riley Testut on 6/5/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +import AltSign +import Roxas + +class AuthenticationViewController: UITableViewController +{ + var authenticationHandler: (((ALTAccount, String)?) -> Void)? + + private var _didLayoutSubviews = false + + @IBOutlet private var emailAddressTextField: UITextField! + @IBOutlet private var passwordTextField: UITextField! + + override func viewDidLoad() + { + super.viewDidLoad() + + self.update() + } + + override func viewDidLayoutSubviews() + { + super.viewDidLayoutSubviews() + + if !_didLayoutSubviews + { + self.emailAddressTextField.becomeFirstResponder() + } + + _didLayoutSubviews = true + } + + override func viewDidDisappear(_ animated: Bool) + { + super.viewDidDisappear(animated) + + self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false + } +} + +private extension AuthenticationViewController +{ + func update() + { + if let _ = self.validate() + { + self.navigationItem.rightBarButtonItem?.isEnabled = true + } + else + { + self.navigationItem.rightBarButtonItem?.isEnabled = false + } + } + + func validate() -> (String, String)? + { + guard + let emailAddress = self.emailAddressTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !emailAddress.isEmpty, + let password = self.passwordTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !password.isEmpty + else { return nil } + + return (emailAddress, password) + } + + func authenticate(emailAddress: String, password: String, completionHandler: @escaping (Result<(ALTAccount, [ALTTeam]), Error>) -> Void) + { + ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in + switch Result(account, error) + { + case .failure(let error): completionHandler(.failure(error)) + case .success(let account): + + ALTAppleAPI.shared.fetchTeams(for: account) { (teams, error) in + let result = Result(teams, error).map { (account, $0) } + completionHandler(result) + } + } + } + } +} + +private extension AuthenticationViewController +{ + @IBAction func authenticate() + { + guard let (emailAddress, password) = self.validate() else { return } + + self.emailAddressTextField.resignFirstResponder() + self.passwordTextField.resignFirstResponder() + + self.navigationItem.rightBarButtonItem?.isIndicatingActivity = true + + ALTAppleAPI.shared.authenticate(appleID: emailAddress, password: password) { (account, error) in + do + { + let account = try Result(account, error).get() + self.authenticationHandler?((account, password)) + } + catch + { + DispatchQueue.main.async { + let toastView = RSTToastView(text: NSLocalizedString("Failed to Log In", comment: ""), detailText: error.localizedDescription) + toastView.tintColor = .altPurple + toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + + self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false + } + } + } + } + + @IBAction func cancel() + { + self.authenticationHandler?(nil) + } +} + +extension AuthenticationViewController: UITextFieldDelegate +{ + func textFieldShouldReturn(_ textField: UITextField) -> Bool + { + switch textField + { + case self.emailAddressTextField: self.passwordTextField.becomeFirstResponder() + case self.passwordTextField: self.authenticate() + default: break + } + + self.update() + + return false + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool + { + DispatchQueue.main.async { + self.update() + } + + return true + } +} diff --git a/AltStore/Authentication/ReplaceCertificateViewController.swift b/AltStore/Authentication/ReplaceCertificateViewController.swift new file mode 100644 index 00000000..d140291d --- /dev/null +++ b/AltStore/Authentication/ReplaceCertificateViewController.swift @@ -0,0 +1,156 @@ +// +// ReplaceCertificateViewController.swift +// AltStore +// +// Created by Riley Testut on 6/5/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +import AltSign +import Roxas + +extension ReplaceCertificateViewController +{ + private enum Error: LocalizedError + { + case missingPrivateKey + case missingCertificate + + var errorDescription: String? { + switch self + { + 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: "") + } + } + } +} + +class ReplaceCertificateViewController: UITableViewController +{ + var replacementHandler: ((ALTCertificate?) -> Void)? + + var team: ALTTeam! + + var certificates: [ALTCertificate] { + get { + return self.dataSource.items + } + set { + self.dataSource.items = newValue + } + } + + private var selectedCertificate: ALTCertificate? { + didSet { + self.update() + } + } + + private lazy var dataSource = self.makeDataSource() + + override func viewDidLoad() + { + super.viewDidLoad() + + self.tableView.dataSource = self.dataSource + + self.update() + } +} + +private extension ReplaceCertificateViewController +{ + func makeDataSource() -> RSTArrayTableViewDataSource + { + let dataSource = RSTArrayTableViewDataSource(items: []) + dataSource.proxy = self + dataSource.cellConfigurationHandler = { [weak self] (cell, certificate, indexPath) in + cell.textLabel?.text = certificate.name + cell.accessoryType = (self?.selectedCertificate == certificate) ? .checkmark : .none + } + + let placeholderView = RSTPlaceholderView(frame: .zero) + placeholderView.textLabel.text = NSLocalizedString("No Certificates", comment: "") + placeholderView.detailTextLabel.text = NSLocalizedString("There are no certificates associated with this team.", comment: "") + dataSource.placeholderView = placeholderView + + return dataSource + } + + func update() + { + self.navigationItem.rightBarButtonItem?.isEnabled = (self.selectedCertificate != nil) + + if self.isViewLoaded + { + self.tableView.reloadData() + } + } +} + +private extension ReplaceCertificateViewController +{ + @IBAction func replaceCertificate(_ sender: UIBarButtonItem) + { + guard let certificate = self.selectedCertificate else { return } + + func replace() + { + sender.isIndicatingActivity = true + + ALTAppleAPI.shared.revoke(certificate, for: self.team) { (success, error) in + let result = Result(success, error).map { certificate } + + do + { + let certificate = try result.get() + self.replacementHandler?(certificate) + } + catch + { + DispatchQueue.main.async { + let toastView = RSTToastView(text: NSLocalizedString("Error Replacing Certificate", comment: ""), detailText: error.localizedDescription) + toastView.tintColor = .altPurple + toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + + sender.isIndicatingActivity = false + } + } + } + } + + let localizedTitle = String(format: NSLocalizedString("Are you sure you want to replace %@?", comment: ""), certificate.name) + let localizedMessage = NSLocalizedString("Any AltStore apps currently installed with this certificate will need to be refreshed.", comment: "") + let localizedReplaceActionTitle = String(format: NSLocalizedString("Replace %@", comment: ""), certificate.name) + + let alertController = UIAlertController(title: localizedTitle, message: localizedMessage, preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction(title: localizedReplaceActionTitle, style: .destructive) { (action) in + replace() + }) + alertController.addAction(.cancel) + + self.present(alertController, animated: true, completion: nil) + } + + @IBAction func cancel() + { + self.replacementHandler?(nil) + } +} + +extension ReplaceCertificateViewController +{ + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? + { + return NSLocalizedString("You have reached the maximum number of development certificates. Please select a certificate to replace.", comment: "") + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) + { + let certificate = self.dataSource.item(at: indexPath) + self.selectedCertificate = certificate + } +} diff --git a/AltStore/Authentication/SelectTeamViewController.swift b/AltStore/Authentication/SelectTeamViewController.swift new file mode 100644 index 00000000..6d52ad4b --- /dev/null +++ b/AltStore/Authentication/SelectTeamViewController.swift @@ -0,0 +1,150 @@ +// +// SelectTeamViewController.swift +// AltStore +// +// Created by Riley Testut on 6/5/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +import AltSign +import Roxas + +class SelectTeamViewController: UITableViewController +{ + var selectionHandler: ((ALTTeam?) -> Void)? + + var teams: [ALTTeam] { + get { + return self.dataSource.items + } + set { + self.dataSource.items = newValue + } + } + + private var selectedTeam: ALTTeam? { + didSet { + self.update() + } + } + + private lazy var dataSource = self.makeDataSource() + + override func viewDidLoad() + { + super.viewDidLoad() + + self.tableView.dataSource = self.dataSource + + self.update() + } + + override func viewWillDisappear(_ animated: Bool) + { + super.viewDidDisappear(animated) + + self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false + } +} + +private extension SelectTeamViewController +{ + func makeDataSource() -> RSTArrayTableViewDataSource + { + let dataSource = RSTArrayTableViewDataSource(items: []) + dataSource.proxy = self + dataSource.cellConfigurationHandler = { [weak self] (cell, team, indexPath) in + cell.textLabel?.text = team.name + + switch team.type + { + case .unknown: cell.detailTextLabel?.text = NSLocalizedString("Unknown", comment: "") + case .free: cell.detailTextLabel?.text = NSLocalizedString("Free Developer Account", comment: "") + case .individual: cell.detailTextLabel?.text = NSLocalizedString("Individual", comment: "") + case .organization: cell.detailTextLabel?.text = NSLocalizedString("Organization", comment: "") + @unknown default: cell.detailTextLabel?.text = nil + } + + cell.accessoryType = (self?.selectedTeam == team) ? .checkmark : .none + } + + let placeholderView = RSTPlaceholderView(frame: .zero) + placeholderView.textLabel.text = NSLocalizedString("No Teams", comment: "") + placeholderView.detailTextLabel.text = NSLocalizedString("You are not a member of any development teams.", comment: "") + dataSource.placeholderView = placeholderView + + return dataSource + } + + func update() + { + self.navigationItem.rightBarButtonItem?.isEnabled = (self.selectedTeam != nil) + + if self.isViewLoaded + { + self.tableView.reloadData() + } + } + + func fetchCertificates(for team: ALTTeam, completionHandler: @escaping (Result<[ALTCertificate], Error>) -> Void) + { + ALTAppleAPI.shared.fetchCertificates(for: team) { (certificate, error) in + let result = Result(certificate, error) + completionHandler(result) + } + } +} + +private extension SelectTeamViewController +{ + @IBAction func chooseTeam(_ sender: UIBarButtonItem) + { + guard let team = self.selectedTeam else { return } + + func choose() + { + sender.isIndicatingActivity = true + + self.selectionHandler?(team) + } + + if team.type == .organization + { + let localizedActionTitle = String(format: NSLocalizedString("Use %@?", comment: ""), team.name) + + let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to use an Organization team?", comment: ""), + message: NSLocalizedString("Doing so may affect other members of this team.", comment: ""), preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction(title: localizedActionTitle, style: .destructive, handler: { (action) in + choose() + })) + alertController.addAction(.cancel) + + self.present(alertController, animated: true, completion: nil) + } + else + { + choose() + } + } + + @IBAction func cancel() + { + self.selectionHandler?(nil) + } +} + +extension SelectTeamViewController +{ + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? + { + return NSLocalizedString("Select the team you would like to use to install apps.", comment: "") + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) + { + let team = self.dataSource.item(at: indexPath) + self.selectedTeam = team + } +} diff --git a/AltStore/Components/Keychain.swift b/AltStore/Components/Keychain.swift index 3b05b1f4..ee700327 100644 --- a/AltStore/Components/Keychain.swift +++ b/AltStore/Components/Keychain.swift @@ -53,4 +53,14 @@ extension Keychain self.keychain[data: "signingCertificatePrivateKey"] = newValue } } + + var signingCertificateIdentifier: String? { + get { + let identifier = try? self.keychain.get("signingCertificateIdentifier") + return identifier + } + set { + self.keychain["signingCertificateIdentifier"] = newValue + } + } } diff --git a/AltStore/Extensions/UserDefaults+AltStore.swift b/AltStore/Extensions/UserDefaults+AltStore.swift index 4e845d0e..f563bfc0 100644 --- a/AltStore/Extensions/UserDefaults+AltStore.swift +++ b/AltStore/Extensions/UserDefaults+AltStore.swift @@ -11,5 +11,4 @@ import Foundation extension UserDefaults { @NSManaged var firstLaunch: Date? - @NSManaged var signingCertificateIdentifier: String? } diff --git a/AltStore/Model/Account.swift b/AltStore/Model/Account.swift new file mode 100644 index 00000000..5945f2dd --- /dev/null +++ b/AltStore/Model/Account.swift @@ -0,0 +1,59 @@ +// +// Account.swift +// AltStore +// +// Created by Riley Testut on 6/5/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation +import CoreData + +import AltSign + +@objc(Account) +class Account: NSManagedObject +{ + var localizedName: String { + var components = PersonNameComponents() + components.givenName = self.firstName + components.familyName = self.lastName + + let name = PersonNameComponentsFormatter.localizedString(from: components, style: .default) + return name + } + + /* Properties */ + @NSManaged var appleID: String + @NSManaged var identifier: String + + @NSManaged var firstName: String + @NSManaged var lastName: String + + /* Relationships */ + @NSManaged var teams: Set + + private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) + { + super.init(entity: entity, insertInto: context) + } + + init(_ account: ALTAccount, context: NSManagedObjectContext) + { + super.init(entity: Account.entity(), insertInto: context) + + self.appleID = account.appleID + self.identifier = account.identifier + + self.firstName = account.firstName + self.lastName = account.lastName + } +} + +extension Account +{ + @nonobjc class func fetchRequest() -> NSFetchRequest + { + return NSFetchRequest(entityName: "Account") + } +} diff --git a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents index 6eb45007..50511af0 100644 --- a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents +++ b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents @@ -1,5 +1,17 @@ + + + + + + + + + + + + @@ -25,8 +37,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AltStore/Model/Team.swift b/AltStore/Model/Team.swift new file mode 100644 index 00000000..4b808e30 --- /dev/null +++ b/AltStore/Model/Team.swift @@ -0,0 +1,52 @@ +// +// Team.swift +// AltStore +// +// Created by Riley Testut on 5/31/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation +import CoreData + +import AltSign + +@objc(Team) +class Team: NSManagedObject +{ + /* Properties */ + @NSManaged var name: String + @NSManaged var identifier: String + @NSManaged var type: ALTTeamType + + /* Relationships */ + @NSManaged private(set) var account: Account! + + var altTeam: ALTTeam? + + private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) + { + super.init(entity: entity, insertInto: context) + } + + init(_ team: ALTTeam, account: Account, context: NSManagedObjectContext) + { + super.init(entity: Team.entity(), insertInto: context) + + self.altTeam = team + + self.name = team.name + self.identifier = team.identifier + self.type = team.type + + self.account = account + } +} + +extension Team +{ + @nonobjc class func fetchRequest() -> NSFetchRequest + { + return NSFetchRequest(entityName: "Team") + } +} diff --git a/Dependencies/AltSign b/Dependencies/AltSign index 453d54d5..b8d7cf5e 160000 --- a/Dependencies/AltSign +++ b/Dependencies/AltSign @@ -1 +1 @@ -Subproject commit 453d54d5522e4f775112de065f79541f2b7a2481 +Subproject commit b8d7cf5e93897758219713cefb9bd4734bfc8ccc