diff --git a/Supersign/Sources/Supersign/DeveloperServices/Certificates/DeveloperServicesFetchCertificateOperation.swift b/Supersign/Sources/Supersign/DeveloperServices/Certificates/DeveloperServicesFetchCertificateOperation.swift index 011db35..0abc6a1 100644 --- a/Supersign/Sources/Supersign/DeveloperServices/Certificates/DeveloperServicesFetchCertificateOperation.swift +++ b/Supersign/Sources/Supersign/DeveloperServices/Certificates/DeveloperServicesFetchCertificateOperation.swift @@ -45,7 +45,7 @@ public struct DeveloperServicesFetchCertificateOperation: DeveloperServicesOpera platform: context.platform, teamID: context.teamID, csr: csr, - machineName: "Supercharge: \(context.deviceName)", + machineName: "Supercharge: \(SigningContext.hostName)", machineID: context.client.deviceInfo.deviceID ) context.client.send(request) { result in diff --git a/Supersign/Sources/Supersign/DeveloperServices/DeveloperServicesClient.swift b/Supersign/Sources/Supersign/DeveloperServices/DeveloperServicesClient.swift index 793c1ca..0fb6007 100644 --- a/Supersign/Sources/Supersign/DeveloperServices/DeveloperServicesClient.swift +++ b/Supersign/Sources/Supersign/DeveloperServices/DeveloperServicesClient.swift @@ -61,14 +61,14 @@ public final class DeveloperServicesClient { public init( loginToken: DeveloperServicesLoginToken, deviceInfo: DeviceInfo, - httpFactory: HTTPClientFactory.Type = defaultHTTPClientFactory, + httpFactory: HTTPClientFactory = defaultHTTPClientFactory, customAnisetteDataProvider: AnisetteDataProvider? = nil ) { self.loginToken = loginToken self.deviceInfo = deviceInfo self.anisetteDataProvider = customAnisetteDataProvider ?? SupersetteDataProvider(deviceInfo: deviceInfo) - self.httpClient = httpFactory.shared.makeClient() + self.httpClient = httpFactory.makeClient() } private func send( diff --git a/Supersign/Sources/Supersign/GrandSlam/Anisette/Supersette/SupersetteDataProvider.swift b/Supersign/Sources/Supersign/GrandSlam/Anisette/Supersette/SupersetteDataProvider.swift index 51ad0c9..5413d80 100644 --- a/Supersign/Sources/Supersign/GrandSlam/Anisette/Supersette/SupersetteDataProvider.swift +++ b/Supersign/Sources/Supersign/GrandSlam/Anisette/Supersette/SupersetteDataProvider.swift @@ -23,9 +23,9 @@ public class SupersetteDataProvider: AnisetteDataProvider { public let deviceInfo: DeviceInfo private let httpClient: HTTPClientProtocol - public init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory.Type = defaultHTTPClientFactory) { + public init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory = defaultHTTPClientFactory) { self.deviceInfo = deviceInfo - self.httpClient = httpFactory.shared.makeClient() + self.httpClient = httpFactory.makeClient() } private struct AnisetteRequestBody: Encodable { diff --git a/Supersign/Sources/Supersign/GrandSlam/GrandSlamClient.swift b/Supersign/Sources/Supersign/GrandSlam/GrandSlamClient.swift index 2a784d1..29062d0 100644 --- a/Supersign/Sources/Supersign/GrandSlam/GrandSlamClient.swift +++ b/Supersign/Sources/Supersign/GrandSlam/GrandSlamClient.swift @@ -19,14 +19,14 @@ class GrandSlamClient { private let httpClient: HTTPClientProtocol init( deviceInfo: DeviceInfo, - httpFactory: HTTPClientFactory.Type = defaultHTTPClientFactory, + httpFactory: HTTPClientFactory = defaultHTTPClientFactory, customAnisetteDataProvider: AnisetteDataProvider? = nil ) { self.deviceInfo = deviceInfo self.anisetteDataProvider = customAnisetteDataProvider ?? SupersetteDataProvider(deviceInfo: deviceInfo) self.lookupManager = .init(deviceInfo: deviceInfo, httpFactory: httpFactory) - self.httpClient = httpFactory.shared.makeClient() + self.httpClient = httpFactory.makeClient() } private func send( diff --git a/Supersign/Sources/Supersign/GrandSlam/Lookup/GrandSlamLookupManager.swift b/Supersign/Sources/Supersign/GrandSlam/Lookup/GrandSlamLookupManager.swift index 13ae994..a545fa0 100644 --- a/Supersign/Sources/Supersign/GrandSlam/Lookup/GrandSlamLookupManager.swift +++ b/Supersign/Sources/Supersign/GrandSlam/Lookup/GrandSlamLookupManager.swift @@ -21,9 +21,9 @@ class GrandSlamLookupManager { let httpClient: HTTPClientProtocol let deviceInfo: DeviceInfo - init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory.Type = defaultHTTPClientFactory) { + init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory = defaultHTTPClientFactory) { self.deviceInfo = deviceInfo - self.httpClient = httpFactory.shared.makeClient() + self.httpClient = httpFactory.makeClient() } private func performLookup(completion: @escaping (Result) -> Void) { diff --git a/Supersign/Sources/Supersign/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift b/Supersign/Sources/Supersign/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift index 0baa914..7354eab 100644 --- a/Supersign/Sources/Supersign/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift +++ b/Supersign/Sources/Supersign/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift @@ -89,7 +89,7 @@ extension HTTPClient: HTTPClientProtocol { } } -public final class AsyncHTTPClientFactory: HTTPClientFactory { +final class AsyncHTTPClientFactory: HTTPClientFactory { private static let appleRootPEM = """ -----BEGIN CERTIFICATE----- MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET @@ -131,12 +131,12 @@ public final class AsyncHTTPClientFactory: HTTPClientFactory { ) client = HTTPClient(eventLoopGroupProvider: .createNew, configuration: config) } - public static let shared = AsyncHTTPClientFactory() + static let shared = AsyncHTTPClientFactory() - public func shutdown() { + func shutdown() { try! client.syncShutdown() } - public func makeClient() -> HTTPClientProtocol { client } + func makeClient() -> HTTPClientProtocol { client } } #endif diff --git a/Supersign/Sources/Supersign/HTTPClientProtocol/HTTPClientProtocol.swift b/Supersign/Sources/Supersign/HTTPClientProtocol/HTTPClientProtocol.swift index 659431e..461a950 100644 --- a/Supersign/Sources/Supersign/HTTPClientProtocol/HTTPClientProtocol.swift +++ b/Supersign/Sources/Supersign/HTTPClientProtocol/HTTPClientProtocol.swift @@ -66,15 +66,14 @@ public protocol HTTPClientProtocol { } public protocol HTTPClientFactory { - static var shared: Self { get } func shutdown() // client is invalidated after this func makeClient() -> HTTPClientProtocol } -public let defaultHTTPClientFactory: HTTPClientFactory.Type = { +public let defaultHTTPClientFactory: HTTPClientFactory = { #if os(Linux) - return AsyncHTTPClientFactory.self + return AsyncHTTPClientFactory.shared #else - return URLHTTPClientFactory.self + return URLHTTPClientFactory.shared #endif }() diff --git a/Supersign/Sources/Supersign/HTTPClientProtocol/URLSession+HTTP.swift b/Supersign/Sources/Supersign/HTTPClientProtocol/URLSession+HTTP.swift index 9dd26cf..e1b7e66 100644 --- a/Supersign/Sources/Supersign/HTTPClientProtocol/URLSession+HTTP.swift +++ b/Supersign/Sources/Supersign/HTTPClientProtocol/URLSession+HTTP.swift @@ -57,8 +57,8 @@ extension URLSession: HTTPClientProtocol { } } -public final class URLHTTPClientFactory: HTTPClientFactory { - public static let shared = URLHTTPClientFactory() +final class URLHTTPClientFactory: HTTPClientFactory { + static let shared = URLHTTPClientFactory() // no need for cert handling since the Apple Root CA is already // installed on any devices which support the Security APIs which @@ -66,7 +66,7 @@ public final class URLHTTPClientFactory: HTTPClientFactory { private let session = URLSession(configuration: .ephemeral) private init() {} - public func shutdown() {} - public func makeClient() -> HTTPClientProtocol { session } + func shutdown() {} + func makeClient() -> HTTPClientProtocol { session } } #endif diff --git a/Supersign/Sources/Supersign/Integration/SuperchargeInstaller.swift b/Supersign/Sources/Supersign/Integration/SuperchargeInstaller.swift index ad0f8ef..712234a 100644 --- a/Supersign/Sources/Supersign/Integration/SuperchargeInstaller.swift +++ b/Supersign/Sources/Supersign/Integration/SuperchargeInstaller.swift @@ -82,6 +82,7 @@ public final class SuperchargeInstaller { let udid: String let appleID: String let password: String + let signingInfoManager: SigningInfoManager public weak var delegate: SuperchargeInstallerDelegate? private var appInstaller: AppInstaller? @@ -144,45 +145,46 @@ public final class SuperchargeInstaller { udid: String, appleID: String, password: String, + signingInfoManager: SigningInfoManager, delegate: SuperchargeInstallerDelegate ) { self.udid = udid self.appleID = appleID self.password = password + self.signingInfoManager = signingInfoManager self.delegate = delegate } - private func performWithRecovery(block: () throws -> T) throws -> T { - var presentedMessage: SuperchargeInstaller.Message? - - func present(message: SuperchargeInstaller.Message) throws { - if presentedMessage != message { - delegate?.setPresentedMessage(message) - } - presentedMessage = message - // allow some time before looping - Thread.sleep(forTimeInterval: 0.1) - } + private func performWithRecovery(repeatAfter: TimeInterval = 0.1, block: () throws -> T) throws -> T { + var currMessage: SuperchargeInstaller.Message? defer { - if presentedMessage != nil { + if currMessage != nil { delegate?.setPresentedMessage(nil) } } while true { guard shouldContinue() else { throw Error.userCancelled } + let nextMessage: SuperchargeInstaller.Message do { return try block() } catch let error as LockdownClient.Error where error == .pairingDialogResponsePending { - try present(message: .pairDevice) + nextMessage = .pairDevice } catch let error as LockdownClient.Error where error == .passwordProtected { - try present(message: .unlockDevice) + nextMessage = .unlockDevice + } + if currMessage != nextMessage { + delegate?.setPresentedMessage(nextMessage) + currMessage = nextMessage + } + if repeatAfter > 0 { + Thread.sleep(forTimeInterval: repeatAfter) } } } - private func fetchPairingKeys() throws -> Data? { + private func prepareDevice() throws -> (deviceName: String, pairingKeys: Data)? { // TODO: Maybe use `Connection` here instead of creating the lockdown // client manually? @@ -199,6 +201,8 @@ public final class SuperchargeInstaller { ) } + let deviceName = try lockdownClient.deviceName() + try lockdownClient.setValue(udid, forDomain: "com.apple.mobile.wireless_lockdown", key: "WirelessBuddyID") try lockdownClient.setValue(true, forDomain: "com.apple.mobile.wireless_lockdown", key: "EnableWifiConnections") @@ -251,7 +255,7 @@ public final class SuperchargeInstaller { guard updateProgress(to: 1) else { return nil } - return data + return (deviceName, data) } private func install( @@ -314,30 +318,22 @@ public final class SuperchargeInstaller { ) { guard self.updateStage(to: "Preparing device") else { return } + let deviceName: String let pairingKeys: Data do { - guard let _pairingKeys = try fetchPairingKeys() + guard let prepareResult = try prepareDevice() else { return } // nil: user cancelled - pairingKeys = _pairingKeys + (deviceName, pairingKeys) = prepareResult } catch { return completion(.failure(error)) } - #warning("Persist manager on Windows/Linux") - let signingInfoManager: SigningInfoManager - #if os(macOS) - signingInfoManager = KeyValueSigningInfoManager( - storage: KeychainStorage(service: "com.kabiroberai.Supercharge-Installer.credentials") - ) - #else - signingInfoManager = MemoryBackedSigningInfoManager() - #endif - let context: SigningContext do { context = try SigningContext( udid: udid, + deviceName: deviceName, teamID: team.id, client: client, signingInfoManager: signingInfoManager, @@ -461,7 +457,11 @@ public final class SuperchargeInstaller { extension SuperchargeInstaller: TwoFactorAuthDelegate { public func fetchCode(completion: @escaping (String?) -> Void) { - delegate?.fetchCode { code in + guard let delegate = delegate else { + self.installQueue.async { completion(nil) } + return + } + delegate.fetchCode { code in self.installQueue.async { completion(code) } diff --git a/Supersign/Sources/Supersign/Signer/SigningContext.swift b/Supersign/Sources/Supersign/Signer/SigningContext.swift index 13b16f1..6694243 100644 --- a/Supersign/Sources/Supersign/Signer/SigningContext.swift +++ b/Supersign/Sources/Supersign/Signer/SigningContext.swift @@ -11,6 +11,7 @@ import Foundation public struct SigningContext { public let udid: String + public let deviceName: String public let teamID: DeveloperServicesTeam.ID public let client: DeveloperServicesClient public let signingInfoManager: SigningInfoManager @@ -19,6 +20,7 @@ public struct SigningContext { public init( udid: String, + deviceName: String, teamID: DeveloperServicesTeam.ID, client: DeveloperServicesClient, signingInfoManager: SigningInfoManager, @@ -26,6 +28,7 @@ public struct SigningContext { signerImpl: SignerImpl? = nil ) throws { self.udid = udid + self.deviceName = deviceName self.teamID = teamID self.client = client self.signingInfoManager = signingInfoManager @@ -39,7 +42,7 @@ public struct SigningContext { import UIKit #endif extension SigningContext { - public var deviceName: String { + public static var hostName: String { #if targetEnvironment(simulator) return "Simulator" #elseif canImport(UIKit) diff --git a/Supersign/Sources/Supersign/Utilities/KeyValueStorage.swift b/Supersign/Sources/Supersign/Utilities/KeyValueStorage.swift index e8f1228..4a19e4d 100644 --- a/Supersign/Sources/Supersign/Utilities/KeyValueStorage.swift +++ b/Supersign/Sources/Supersign/Utilities/KeyValueStorage.swift @@ -73,6 +73,9 @@ public struct KeychainStorage: KeyValueStorage { kSecClass: kSecClassGenericPassword, kSecAttrAccount: key ] + if #available(macOS 10.15, *) { + query[kSecUseDataProtectionKeychain] = true + } query[kSecAttrService] = service query.merge(parameters) { _, b in b } return query as CFDictionary diff --git a/Supersign/Sources/Supersign/Utilities/SigningInfo.swift b/Supersign/Sources/Supersign/Utilities/SigningInfo.swift index a8dde01..b266a31 100644 --- a/Supersign/Sources/Supersign/Utilities/SigningInfo.swift +++ b/Supersign/Sources/Supersign/Utilities/SigningInfo.swift @@ -44,14 +44,8 @@ public class MemoryBackedSigningInfoManager: SigningInfoManager { } public struct KeyValueSigningInfoManager: SigningInfoManager { - private enum KeyType: String { - case certificate - case privateKey - } - - private func key(_ teamID: DeveloperServicesTeam.ID, _ keyType: KeyType) -> String { - "\(teamID.rawValue).\(keyType.rawValue)" - } + private let encoder = PropertyListEncoder() + private let decoder = PropertyListDecoder() public let storage: KeyValueStorage public init(storage: KeyValueStorage) { @@ -59,22 +53,13 @@ public struct KeyValueSigningInfoManager: SigningInfoManager { } public func info(forTeamID teamID: DeveloperServicesTeam.ID) throws -> SigningInfo? { - guard let privKeyData = try storage.data(forKey: key(teamID, .privateKey)), - let certData = try storage.data(forKey: key(teamID, .certificate)) + guard let data = try storage.data(forKey: teamID.rawValue) else { return nil } - let cert = try Certificate(data: certData) - let privKey = PrivateKey(data: privKeyData) - return SigningInfo(privateKey: privKey, certificate: cert) + return try decoder.decode(SigningInfo.self, from: data) } public func setInfo(_ info: SigningInfo?, forTeamID teamID: DeveloperServicesTeam.ID) throws { - try storage.setData( - info?.certificate.data(), - forKey: key(teamID, .certificate) - ) - try storage.setData( - info?.privateKey.data, - forKey: key(teamID, .privateKey) - ) + let data = try encoder.encode(info) + try storage.setData(data, forKey: teamID.rawValue) } } diff --git a/Supersign/Sources/SupersignCLI/main.swift b/Supersign/Sources/SupersignCLI/main.swift index 7f8146d..fcb5ef8 100644 --- a/Supersign/Sources/SupersignCLI/main.swift +++ b/Supersign/Sources/SupersignCLI/main.swift @@ -1,244 +1,27 @@ import Foundation import Supersign -defer { defaultHTTPClientFactory.shared.shutdown() } +let moduleBundle: Bundle +#if swift(>=5.5) || os(macOS) +moduleBundle = Bundle.module +#else +moduleBundle = Bundle(url: Bundle.main.url(forResource: "Supersign_SupersignCLI", withExtension: "resources")!)! +#endif +let app = moduleBundle.url(forResource: "Supercharge", withExtension: "ipa")! -// nil = EOF -func prompt(_ message: String) -> String? { - if !message.isEmpty { - print(message, terminator: "") - } - return readLine() -} +#warning("Persist manager on Windows/Linux") -func getPassword(_ message: String) -> String? { - if !message.isEmpty { - print(message, terminator: "") - } +// for Windows, we could use dpapi.h or wincred.h. +// for Linux, maybe use libsecret? +// see https://github.com/atom/node-keytar - var origAttr = termios() - tcgetattr(STDIN_FILENO, &origAttr) +let signingInfoManager: SigningInfoManager +#if os(macOS) +signingInfoManager = KeyValueSigningInfoManager( + storage: KeychainStorage(service: "com.kabiroberai.Supercharge-Keychain.credentials") +) +#else +signingInfoManager = MemoryBackedSigningInfoManager() +#endif - var newAttr = origAttr - newAttr.c_lflag = newAttr.c_lflag & ~tcflag_t(ECHO) - tcsetattr(STDIN_FILENO, TCSANOW, &newAttr) - - let password = prompt("") - - tcsetattr(STDIN_FILENO, TCSANOW, &origAttr) - print() - - return password -} - -func chooseNumber(in range: Range) -> Int { - print("Choice (\(range.lowerBound)-\(range.upperBound - 1)): ", terminator: "") - guard let choice = readLine().flatMap(Int.init), range.contains(choice) - else { return chooseNumber(in: range) } - return choice -} - -func choose( - from elements: [T], - onNoElement: () throws -> T, - multiPrompt: @autoclosure () -> String, - formatter: (T) throws -> String -) rethrows -> T { - switch elements.count { - case 0: - return try onNoElement() - case 1: - return elements[0] - default: - print(multiPrompt()) - try elements.enumerated().forEach { index, element in - try print("\(index): \(formatter(element))") - } - let choice = chooseNumber(in: elements.indices) - return elements[choice] - } -} - -class SupersignCLIDelegate: SuperchargeInstallerDelegate { - let completion: () -> Void - init(completion: @escaping () -> Void) { - self.completion = completion - } - - func fetchCode(completion: @escaping (String?) -> Void) { - let code = prompt("Code: ") - completion(code) - } - - func fetchTeam(fromTeams teams: [DeveloperServicesTeam], completion: @escaping (DeveloperServicesTeam?) -> Void) { - let selected = choose( - from: teams, - onNoElement: { fatalError() }, - multiPrompt: "Select a team", - formatter: { - "\($0.name) (\($0.id))" - } - ) - completion(selected) - } - - func setPresentedMessage(_ message: SuperchargeInstaller.Message?) { - let text: String - switch message { - case .pairDevice: - text = "Please tap 'trust' on your device..." - case .unlockDevice: - text = "Please unlock your device..." - case nil: - text = "Continuing..." - } - print("\n\(text)", terminator: "") - fflush(stdout) - } - - var prevStage: String? - var prevProgress: String? - - func installerDidUpdate(toStage stage: String, progress: Double?) { - let progString: String? - if let progress = progress { - let progInt = Int(progress * 100) - if progInt < 10 { - progString = " \(progInt)%" - } else if progInt < 100 { - progString = " \(progInt)%" - } else { - progString = "\(progInt)%" - } - } else { - progString = nil - } - - defer { - prevStage = stage - prevProgress = progString - } - - if stage != prevStage { - if let progString = progString { - print("\n[\(stage)] \(progString)", terminator: "") - fflush(stdout) - } else { - print("\n[\(stage)] ...", terminator: "") - fflush(stdout) - } - } else if progString != prevProgress { - if let progString = progString { - print("\r[\(stage)] \(progString)", terminator: "") - fflush(stdout) - } else { - print("\r[\(stage)]", terminator: "") - fflush(stdout) - } - } - } - - func installerDidComplete(withResult result: Result<(), Error>) { - print("\n") - switch result { - case .success: - print("Complete!") - case .failure(let error): - print("Failed :(") - print("Error: \(error)") - } - completion() - } - - func decompress( - ipa: URL, - in directory: URL, - progress: @escaping (Double?) -> Void, - completion: @escaping (_ success: Bool) -> Void - ) { - progress(nil) - - let unzip = Process() - unzip.launchPath = "/usr/bin/unzip" - unzip.arguments = ["-q", ipa.path, "-d", directory.path] - unzip.launch() - unzip.waitUntilExit() - guard unzip.terminationStatus == 0 else { return completion(false) } - - completion(true) - } - - func compress(payloadDir: URL, progress: @escaping (Double?) -> Void, completion: @escaping (URL?) -> Void) { - progress(nil) - - let dest = payloadDir.deletingLastPathComponent().appendingPathComponent("app.ipa") - - let zip = Process() - zip.launchPath = "/usr/bin/zip" - zip.currentDirectoryPath = payloadDir.deletingLastPathComponent().path - zip.arguments = ["-yqru0", dest.path, "Payload"] - zip.launch() - zip.waitUntilExit() - guard zip.terminationStatus == 0 else { return completion(nil) } - - completion(dest) - } -} - -class ConnectionDelegate: ConnectionManagerDelegate { - var onConnect: (([ConnectionManager.Client]) -> Void)? - init(onConnect: @escaping ([ConnectionManager.Client]) -> Void) { - self.onConnect = onConnect - } - - func connectionManager(_ manager: ConnectionManager, clientsDidChangeFrom oldValue: [ConnectionManager.Client]) { - let usableClients = manager.clients - guard !usableClients.isEmpty else { return } - onConnect?(usableClients) - onConnect = nil - } -} - -func main() throws { - let moduleBundle: Bundle - #if swift(>=5.5) || os(macOS) - moduleBundle = Bundle.module - #else - moduleBundle = Bundle(url: Bundle.main.url(forResource: "Supersign_SupersignCLI", withExtension: "resources")!)! - #endif - let app = moduleBundle.url(forResource: "Supercharge", withExtension: "ipa")! - - guard let username = prompt("Apple ID: "), - let password = getPassword("Password: ") - else { return } - - print("Waiting for device to be connected...") - var clients: [ConnectionManager.Client]! - let semaphore = DispatchSemaphore(value: 0) - let connDelegate = ConnectionDelegate { currClients in - clients = currClients - semaphore.signal() - } - try withExtendedLifetime(ConnectionManager(usbOnly: true, delegate: connDelegate)) { - semaphore.wait() - } - - let client = choose( - from: clients, - onNoElement: { fatalError() }, - multiPrompt: "Choose device", - formatter: { "\($0.deviceName) (udid: \($0.udid))" } - ) - - print("Installing to device: \(client.deviceName) (udid: \(client.udid))") - - let installDelegate = SupersignCLIDelegate { - semaphore.signal() - } - let installer = SuperchargeInstaller(udid: client.udid, appleID: username, password: password, delegate: installDelegate) - installer.install(ipa: app) - semaphore.wait() - _ = installer -} - -try main() +try SupersignCLI(app: app, signingInfoManager: signingInfoManager).run()