From bb3b0396721c3830c4bb8710411d533796321633 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Wed, 11 Nov 2020 17:25:16 -0800 Subject: [PATCH] [AltServer] Supports sideloading .ipa files directly to iOS devices --- AltServer/AppDelegate.swift | 48 ++++-- AltServer/Base.lproj/Main.storyboard | 17 ++ .../ALTDeviceManager+Installation.swift | 156 +++++++++++------- AltStore.xcodeproj/project.pbxproj | 6 + .../ALTApplication+AltStoreApp.swift | 19 +++ 5 files changed, 172 insertions(+), 74 deletions(-) create mode 100644 Shared/Extensions/ALTApplication+AltStoreApp.swift diff --git a/AltServer/AppDelegate.swift b/AltServer/AppDelegate.swift index 6fc4c464..2393c730 100644 --- a/AltServer/AppDelegate.swift +++ b/AltServer/AppDelegate.swift @@ -13,6 +13,12 @@ import AltSign import LaunchAtLogin +#if STAGING +private let altstoreAppURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altstore.ipa")! +#else +private let altstoreAppURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")! +#endif + @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { @@ -26,6 +32,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet private var appMenu: NSMenu! @IBOutlet private var connectedDevicesMenu: NSMenu! + @IBOutlet private var sideloadIPAConnectedDevicesMenu: NSMenu! @IBOutlet private var launchAtLoginMenuItem: NSMenuItem! @IBOutlet private var installMailPluginMenuItem: NSMenuItem! @@ -48,6 +55,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.appMenu.delegate = self self.connectedDevicesMenu.delegate = self + self.sideloadIPAConnectedDevicesMenu.delegate = self UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (success, error) in guard success else { return } @@ -81,8 +89,30 @@ private extension AppDelegate { @objc func installAltStore(_ item: NSMenuItem) { - guard case let index = self.connectedDevicesMenu.index(of: item), index != -1 else { return } + guard let index = item.menu?.index(of: item), index != -1 else { return } + let device = self.connectedDevices[index] + self.installApplication(at: altstoreAppURL, to: device) + } + + @objc func sideloadIPA(_ item: NSMenuItem) + { + guard let index = item.menu?.index(of: item), index != -1 else { return } + + let device = self.connectedDevices[index] + + let openPanel = NSOpenPanel() + openPanel.canChooseDirectories = false + openPanel.allowsMultipleSelection = false + openPanel.allowedFileTypes = ["ipa"] + openPanel.begin { (response) in + guard let fileURL = openPanel.url, response == .OK else { return } + self.installApplication(at: fileURL, to: device) + } + } + + func installApplication(at url: URL, to device: ALTDevice) + { let alert = NSAlert() alert.messageText = NSLocalizedString("Please enter your Apple ID and password.", comment: "") alert.informativeText = NSLocalizedString("Your Apple ID and password are not saved and are only sent to Apple for authentication.", comment: "") @@ -125,18 +155,16 @@ private extension AppDelegate let username = appleIDTextField.stringValue let password = passwordTextField.stringValue - - let device = self.connectedDevices[index] - + func install() { - ALTDeviceManager.shared.installAltStore(to: device, appleID: username, password: password) { (result) in + ALTDeviceManager.shared.installApplication(at: url, to: device, appleID: username, password: password) { (result) in switch result { - case .success: + case .success(let application): let content = UNMutableNotificationContent() content.title = NSLocalizedString("Installation Succeeded", comment: "") - content.body = String(format: NSLocalizedString("AltStore was successfully installed on %@.", comment: ""), device.name) + content.body = String(format: NSLocalizedString("%@ was successfully installed on %@.", comment: ""), application.name, device.name) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) @@ -283,14 +311,14 @@ extension AppDelegate: NSMenuDelegate func numberOfItems(in menu: NSMenu) -> Int { - guard menu == self.connectedDevicesMenu else { return -1 } + guard menu == self.connectedDevicesMenu || menu == self.sideloadIPAConnectedDevicesMenu else { return -1 } return self.connectedDevices.isEmpty ? 1 : self.connectedDevices.count } func menu(_ menu: NSMenu, update item: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool { - guard menu == self.connectedDevicesMenu else { return false } + guard menu == self.connectedDevicesMenu || menu == self.sideloadIPAConnectedDevicesMenu else { return false } if self.connectedDevices.isEmpty { @@ -305,7 +333,7 @@ extension AppDelegate: NSMenuDelegate item.title = device.name item.isEnabled = true item.target = self - item.action = #selector(AppDelegate.installAltStore) + item.action = (menu == self.connectedDevicesMenu) ? #selector(AppDelegate.installAltStore(_:)) : #selector(AppDelegate.sideloadIPA(_:)) item.tag = index } diff --git a/AltServer/Base.lproj/Main.storyboard b/AltServer/Base.lproj/Main.storyboard index 33f11093..9d6926ab 100644 --- a/AltServer/Base.lproj/Main.storyboard +++ b/AltServer/Base.lproj/Main.storyboard @@ -64,6 +64,7 @@ + @@ -97,6 +98,22 @@ + + + + + + + + + + + + + + + + diff --git a/AltServer/Devices/ALTDeviceManager+Installation.swift b/AltServer/Devices/ALTDeviceManager+Installation.swift index 63339ac0..9048232d 100644 --- a/AltServer/Devices/ALTDeviceManager+Installation.swift +++ b/AltServer/Devices/ALTDeviceManager+Installation.swift @@ -10,12 +10,6 @@ import Cocoa import UserNotifications import ObjectiveC -#if STAGING -private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/altstore.ipa")! -#else -private let appURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")! -#endif - private let appGroupsLock = NSLock() enum InstallError: LocalizedError @@ -38,21 +32,14 @@ enum InstallError: LocalizedError extension ALTDeviceManager { - func installAltStore(to device: ALTDevice, appleID: String, password: String, completion: @escaping (Result) -> Void) + func installApplication(at url: URL, to device: ALTDevice, appleID: String, password: String, completion: @escaping (Result) -> Void) { let destinationDirectoryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - func finish(_ error: Error?, title: String = "") + func finish(_ result: Result, title: String = "") { DispatchQueue.main.async { - if let error = error - { - completion(.failure(error)) - } - else - { - completion(.success(())) - } + completion(result) } try? FileManager.default.removeItem(at: destinationDirectoryURL) @@ -83,14 +70,13 @@ extension ALTDeviceManager { let certificate = try result.get() - let content = UNMutableNotificationContent() - content.title = String(format: NSLocalizedString("Installing AltStore to %@...", comment: ""), device.name) - content.body = NSLocalizedString("This may take a few seconds.", comment: "") - - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) - UNUserNotificationCenter.current().add(request) - - self.downloadApp { (result) in + if !url.isFileURL + { + // Show alert before downloading remote .ipa. + self.showInstallationAlert(appName: NSLocalizedString("AltStore", comment: ""), deviceName: device.name) + } + + self.downloadApp(from: url) { (result) in do { let fileURL = try result.get() @@ -98,18 +84,14 @@ extension ALTDeviceManager try FileManager.default.createDirectory(at: destinationDirectoryURL, withIntermediateDirectories: true, attributes: nil) let appBundleURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: destinationDirectoryURL) - - do - { - try FileManager.default.removeItem(at: fileURL) - } - catch - { - print("Failed to remove downloaded .ipa.", error) - } - guard let application = ALTApplication(fileURL: appBundleURL) else { throw ALTError(.invalidApp) } + if url.isFileURL + { + // Show alert after "downloading" local .ipa. + self.showInstallationAlert(appName: application.name, deviceName: device.name) + } + // Refresh anisette data to prevent session timeouts. AnisetteDataManager.shared.requestAnisetteData { (result) in do @@ -123,65 +105,73 @@ extension ALTDeviceManager let profiles = try result.get() self.install(application, to: device, team: team, certificate: certificate, profiles: profiles) { (result) in - finish(result.error, title: "Failed to Install AltStore") + finish(result.map { application }, title: "Failed to Install AltStore") } } catch { - finish(error, title: "Failed to Fetch Provisioning Profiles") + finish(.failure(error), title: "Failed to Fetch Provisioning Profiles") } } } catch { - finish(error, title: "Failed to Refresh Anisette Data") + finish(.failure(error), title: "Failed to Refresh Anisette Data") } } } catch { - finish(error, title: "Failed to Download AltStore") + finish(.failure(error), title: "Failed to Download AltStore") } } } catch { - finish(error, title: "Failed to Fetch Certificate") + finish(.failure(error), title: "Failed to Fetch Certificate") } } } catch { - finish(error, title: "Failed to Register Device") + finish(.failure(error), title: "Failed to Register Device") } } } catch { - finish(error, title: "Failed to Fetch Team") + finish(.failure(error), title: "Failed to Fetch Team") } } } catch { - finish(error, title: "Failed to Authenticate") + finish(.failure(error), title: "Failed to Authenticate") } } } catch { - finish(error, title: "Failed to Fetch Anisette Data") + finish(.failure(error), title: "Failed to Fetch Anisette Data") } } } - - func downloadApp(completionHandler: @escaping (Result) -> Void) +} + +private extension ALTDeviceManager +{ + func downloadApp(from url: URL, completionHandler: @escaping (Result) -> Void) { - let downloadTask = URLSession.shared.downloadTask(with: appURL) { (fileURL, response, error) in + guard !url.isFileURL else { return completionHandler(.success(url)) } + + let downloadTask = URLSession.shared.downloadTask(with: url) { (fileURL, response, error) in do { let (fileURL, _) = try Result((fileURL, response), error).get() completionHandler(.success(fileURL)) + + do { try FileManager.default.removeItem(at: fileURL) } + catch { print("Failed to remove downloaded .ipa.", error) } } catch { @@ -259,11 +249,11 @@ extension ALTDeviceManager { DispatchQueue.main.sync { let alert = NSAlert() - alert.messageText = NSLocalizedString("Installing AltStore will revoke your iOS development certificate.", comment: "") + alert.messageText = NSLocalizedString("Installing this app will revoke your iOS development certificate.", comment: "") alert.informativeText = NSLocalizedString(""" This will not affect apps you've submitted to the App Store, but may cause apps you've installed to your devices with Xcode to stop working until you reinstall them. -To prevent this from happening, feel free to try again with another Apple ID to install AltStore. +To prevent this from happening, feel free to try again with another Apple ID. """, comment: "") alert.addButton(withTitle: NSLocalizedString("Continue", comment: "")) @@ -410,7 +400,7 @@ To prevent this from happening, feel free to try again with another Apple ID to func prepareAllProvisioningProfiles(for application: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completion: @escaping (Result<[String: ALTProvisioningProfile], Error>) -> Void) { - self.prepareProvisioningProfile(for: application, team: team, session: session) { (result) in + self.prepareProvisioningProfile(for: application, parentApp: nil, team: team, session: session) { (result) in do { let profile = try result.get() @@ -424,7 +414,7 @@ To prevent this from happening, feel free to try again with another Apple ID to { dispatchGroup.enter() - self.prepareProvisioningProfile(for: appExtension, team: team, session: session) { (result) in + self.prepareProvisioningProfile(for: appExtension, parentApp: application, team: team, session: session) { (result) in switch result { case .failure(let e): error = e @@ -453,9 +443,35 @@ To prevent this from happening, feel free to try again with another Apple ID to } } - func prepareProvisioningProfile(for application: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + func prepareProvisioningProfile(for application: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { - self.registerAppID(name: application.name, identifier: application.bundleIdentifier, team: team, session: session) { (result) in + let parentBundleID = parentApp?.bundleIdentifier ?? application.bundleIdentifier + let updatedParentBundleID: String + + if application.isAltStoreApp + { + // Use legacy bundle ID format for AltStore (and its extensions). + updatedParentBundleID = "com.\(team.identifier).\(parentBundleID)" + } + else + { + updatedParentBundleID = parentBundleID + "." + team.identifier // Append just team identifier to make it harder to track. + } + + let bundleID = application.bundleIdentifier.replacingOccurrences(of: parentBundleID, with: updatedParentBundleID) + + let preferredName: String + + if let parentApp = parentApp + { + preferredName = parentApp.name + " " + application.name + } + else + { + preferredName = application.name + } + + self.registerAppID(name: preferredName, bundleID: bundleID, team: team, session: session) { (result) in do { let appID = try result.get() @@ -493,10 +509,8 @@ To prevent this from happening, feel free to try again with another Apple ID to } } - func registerAppID(name appName: String, identifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) + func registerAppID(name appName: String, bundleID: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result) -> Void) { - let bundleID = "com.\(team.identifier).\(identifier)" - ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in do { @@ -723,17 +737,21 @@ To prevent this from happening, feel free to try again with another Apple ID to allURLSchemes.append(altstoreURLScheme) var additionalValues: [String: Any] = [Bundle.Info.urlTypes: allURLSchemes] - additionalValues[Bundle.Info.deviceID] = device.identifier - additionalValues[Bundle.Info.serverID] = UserDefaults.standard.serverID - if - let machineIdentifier = certificate.machineIdentifier, - let encryptedData = certificate.encryptedP12Data(withPassword: machineIdentifier) + if application.isAltStoreApp { - additionalValues[Bundle.Info.certificateID] = certificate.serialNumber + additionalValues[Bundle.Info.deviceID] = device.identifier + additionalValues[Bundle.Info.serverID] = UserDefaults.standard.serverID - let certificateURL = application.fileURL.appendingPathComponent("ALTCertificate.p12") - try encryptedData.write(to: certificateURL, options: .atomic) + if + let machineIdentifier = certificate.machineIdentifier, + let encryptedData = certificate.encryptedP12Data(withPassword: machineIdentifier) + { + additionalValues[Bundle.Info.certificateID] = certificate.serialNumber + + let certificateURL = application.fileURL.appendingPathComponent("ALTCertificate.p12") + try encryptedData.write(to: certificateURL, options: .atomic) + } } try prepare(appBundle, additionalInfoDictionaryValues: additionalValues) @@ -750,7 +768,7 @@ To prevent this from happening, feel free to try again with another Apple ID to { try Result(success, error).get() - let activeProfiles: Set? = (team.type == .free) ? Set(profiles.values.map(\.bundleIdentifier)) : nil + let activeProfiles: Set? = (team.type == .free && application.isAltStoreApp) ? Set(profiles.values.map(\.bundleIdentifier)) : nil ALTDeviceManager.shared.installApp(at: application.fileURL, toDeviceWithUDID: device.identifier, activeProvisioningProfiles: activeProfiles) { (success, error) in completionHandler(Result(success, error)) } @@ -769,6 +787,16 @@ To prevent this from happening, feel free to try again with another Apple ID to } } } + + func showInstallationAlert(appName: String, deviceName: String) + { + let content = UNMutableNotificationContent() + content.title = String(format: NSLocalizedString("Installing %@ to %@...", comment: ""), appName, deviceName) + content.body = NSLocalizedString("This may take a few seconds.", comment: "") + + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) + UNUserNotificationCenter.current().add(request) + } } private var securityCodeAlertKey = 0 diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 4db6af16..95f60421 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -333,6 +333,8 @@ BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B695232242D3007A79E1 /* LicensesViewController.swift */; }; BFF0B6982322CAB8007A79E1 /* InstructionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */; }; BFF0B69A2322D7D0007A79E1 /* UIScreen+CompactHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */; }; + BFF435D8255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF435D7255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift */; }; + BFF435D9255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF435D7255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift */; }; BFF615A82510042B00484D3B /* AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; }; BFF767C82489A74E0097E58C /* WirelessConnectionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF767C72489A74E0097E58C /* WirelessConnectionHandler.swift */; }; /* End PBXBuildFile section */ @@ -750,6 +752,7 @@ BFF0B695232242D3007A79E1 /* LicensesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicensesViewController.swift; sourceTree = ""; }; BFF0B6972322CAB8007A79E1 /* InstructionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstructionsViewController.swift; sourceTree = ""; }; BFF0B6992322D7D0007A79E1 /* UIScreen+CompactHeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScreen+CompactHeight.swift"; sourceTree = ""; }; + BFF435D7255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTApplication+AltStoreApp.swift"; sourceTree = ""; }; BFF767C72489A74E0097E58C /* WirelessConnectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WirelessConnectionHandler.swift; sourceTree = ""; }; BFF767CB2489AB5C0097E58C /* ALTServerError+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTServerError+Conveniences.swift"; sourceTree = ""; }; BFF767CD2489ABE90097E58C /* NetworkConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConnection.swift; sourceTree = ""; }; @@ -1596,6 +1599,7 @@ BF1E314122A05D4C00370A3C /* Bundle+AltStore.swift */, BFF767CB2489AB5C0097E58C /* ALTServerError+Conveniences.swift */, BF1FE357251A9FB000C3CE09 /* NSXPCConnection+MachServices.swift */, + BFF435D7255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift */, ); path = Extensions; sourceTree = ""; @@ -2177,6 +2181,7 @@ BFECAC8124FD950B0077C41F /* ALTServerError+Conveniences.swift in Sources */, BFECAC7F24FD950B0077C41F /* CodableServerError.swift in Sources */, BFECAC8624FD950B0077C41F /* Result+Conveniences.swift in Sources */, + BFF435D9255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift in Sources */, BF718BD523C928A300A89F2D /* ALTNotificationConnection.mm in Sources */, BF1E312B229F474900370A3C /* RequestHandler.swift in Sources */, BF718BD123C91BD300A89F2D /* ALTWiredConnection.mm in Sources */, @@ -2418,6 +2423,7 @@ BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */, BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */, BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */, + BFF435D8255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift in Sources */, BF4B78FE24B3D1DB008AB4AC /* SceneDelegate.swift in Sources */, BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */, BF663C4F2433ED8200DAA738 /* FileManager+DirectorySize.swift in Sources */, diff --git a/Shared/Extensions/ALTApplication+AltStoreApp.swift b/Shared/Extensions/ALTApplication+AltStoreApp.swift new file mode 100644 index 00000000..bd06f010 --- /dev/null +++ b/Shared/Extensions/ALTApplication+AltStoreApp.swift @@ -0,0 +1,19 @@ +// +// ALTApplication+AltStoreApp.swift +// AltStore +// +// Created by Riley Testut on 11/11/20. +// Copyright © 2020 Riley Testut. All rights reserved. +// + +import AltSign + +extension ALTApplication +{ + static let altstoreBundleID = "com.rileytestut.AltStore" + + var isAltStoreApp: Bool { + let isAltStoreApp = self.bundleIdentifier.contains(ALTApplication.altstoreBundleID) + return isAltStoreApp + } +}