From fd74b494a2124292755c51374c84df09a6775b8d Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Mon, 28 Oct 2024 23:18:48 -0500 Subject: [PATCH] fix: add support for SRP Apple login --- Package.resolved | 27 +++++ Package.swift | 4 +- Sources/AppleAPI/Client.swift | 133 ++++++++++++++++++++++++ Sources/AppleAPI/URLRequest+Apple.swift | 48 +++++++++ Sources/XcodesKit/Environment.swift | 2 +- 5 files changed, 212 insertions(+), 2 deletions(-) diff --git a/Package.resolved b/Package.resolved index 01d83c2..23eeeb6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "big-num", + "kind" : "remoteSourceControl", + "location" : "https://github.com/adam-fowler/big-num", + "state" : { + "revision" : "5c5511ad06aeb2b97d0868f7394e14a624bfb1c7", + "version" : "2.0.2" + } + }, { "identity" : "data", "kind" : "remoteSourceControl", @@ -71,6 +80,24 @@ "version" : "1.1.4" } }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto", + "state" : { + "revision" : "ddb07e896a2a8af79512543b1c7eb9797f8898a5", + "version" : "1.1.7" + } + }, + { + "identity" : "swift-srp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/xcodesorg/swift-srp", + "state" : { + "branch" : "main", + "revision" : "543aa0122a0257b992f6c7d62d18a26e3dffb8fe" + } + }, { "identity" : "swiftsoup", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 0f6c442..5d336ae 100644 --- a/Package.swift +++ b/Package.swift @@ -23,6 +23,7 @@ let package = Package( .package(url: "https://github.com/xcodereleases/data", revision: "fcf527b187817f67c05223676341f3ab69d4214d"), .package(url: "https://github.com/onevcat/Rainbow.git", .upToNextMinor(from: "3.2.0")), .package(url: "https://github.com/jpsim/Yams", .upToNextMinor(from: "5.0.1")), + .package(url: "https://github.com/xcodesOrg/swift-srp", branch: "main") ], targets: [ .executableTarget( @@ -50,7 +51,7 @@ let package = Package( "Version", .product(name: "XCModel", package: "data"), "Rainbow", - "Yams", + "Yams" ]), .testTarget( name: "XcodesKitTests", @@ -68,6 +69,7 @@ let package = Package( "PromiseKit", .product(name: "PMKFoundation", package: "Foundation"), "Rainbow", + .product(name: "SRP", package: "swift-srp") ]), .testTarget( name: "AppleAPITests", diff --git a/Sources/AppleAPI/Client.swift b/Sources/AppleAPI/Client.swift index e145f27..78f4d96 100644 --- a/Sources/AppleAPI/Client.swift +++ b/Sources/AppleAPI/Client.swift @@ -2,6 +2,9 @@ import Foundation import PromiseKit import PMKFoundation import Rainbow +import SRP +import Crypto +import CommonCrypto public class Client { private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"] @@ -20,6 +23,8 @@ public class Client { case invalidHashcash case missingSecurityCodeInfo case accountUsesHardwareKey + case srpInvalidPublicKey + case srpError(String) public var errorDescription: String? { switch self { @@ -56,6 +61,90 @@ public class Client { } } + public func srpLogin(accountName: String, password: String) -> Promise { + var serviceKey: String! + let client = SRPClient(configuration: SRPConfiguration(.N2048)) + let clientKeys = client.generateKeys() + let a = clientKeys.public + + return firstly { () -> Promise<(data: Data, response: URLResponse)> in + Current.network.dataTask(with: URLRequest.itcServiceKey) + } + .then { (data, _) -> Promise<(serviceKey: String, hashcash: String)> in + struct ServiceKeyResponse: Decodable { + let authServiceKey: String? + } + + let response = try JSONDecoder().decode(ServiceKeyResponse.self, from: data) + serviceKey = response.authServiceKey + + return self.loadHashcash(accountName: accountName, serviceKey: serviceKey).map { (serviceKey, $0) } + } + .then { (serviceKey, hashcash) -> Promise<(serviceKey: String, hashcash: String, data: Data)> in + return Current.network.dataTask(with: URLRequest.SRPInit(serviceKey: serviceKey, a: Data(a.bytes).base64EncodedString(), accountName: accountName)).map { (serviceKey, hashcash, $0.data)} + } + .then { (serviceKey, hashcash, data) -> Promise<(data: Data, response: URLResponse)> in + let srpInit = try JSONDecoder().decode(ServerSRPInitResponse.self, from: data) + + guard let decodedB = Data(base64Encoded: srpInit.b) else { + throw Error.srpInvalidPublicKey + } + guard let decodedSalt = Data(base64Encoded: srpInit.salt) else { + throw Error.srpInvalidPublicKey + } + + let iterations = srpInit.iteration + + do { + guard let encryptedPassword = self.pbkdf2(password: password, saltData: decodedSalt, keyByteCount: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), rounds: iterations) else { + throw Error.srpInvalidPublicKey + } + + let sharedSecret = try client.calculateSharedSecret(password: encryptedPassword, salt: [UInt8](decodedSalt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB))) + + let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](decodedSalt), clientPublicKey: a, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes)) + let m2 = client.calculateServerProof(clientPublicKey: a, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes))) + + return Current.network.dataTask(with: URLRequest.SRPComplete(serviceKey: serviceKey, hashcash: hashcash, accountName: accountName, c: srpInit.c, m1: Data(m1).base64EncodedString(), m2: Data(m2).base64EncodedString())) + } catch { + throw Error.srpError(error.localizedDescription) + } + } + .then { (data, response) -> Promise in + struct SignInResponse: Decodable { + let authType: String? + let serviceErrors: [ServiceError]? + + struct ServiceError: Decodable, CustomStringConvertible { + let code: String + let message: String + + var description: String { + return "\(code): \(message)" + } + } + } + + let httpResponse = response as! HTTPURLResponse + let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data) + + switch httpResponse.statusCode { + case 200: + return Current.network.dataTask(with: URLRequest.olympusSession).asVoid() + case 401: + throw Error.invalidUsernameOrPassword(username: accountName) + case 409: + return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey) + case 412 where Client.authTypes.contains(responseBody.authType ?? ""): + throw Error.appleIDAndPrivacyAcknowledgementRequired + default: + throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode, + message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", ")) + } + } + } + + @available(*, deprecated, message: "Please use srpLogin") public func login(accountName: String, password: String) -> Promise { var serviceKey: String! @@ -264,6 +353,43 @@ public class Client { return .value(hashcash) } } + + private func sha256(data : Data) -> Data { + var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) + } + return Data(hash) + } + + private func pbkdf2(password: String, saltData: Data, keyByteCount: Int, prf: CCPseudoRandomAlgorithm, rounds: Int) -> Data? { + guard let passwordData = password.data(using: .utf8) else { return nil } + let hashedPasswordData = sha256(data: passwordData) + + var derivedKeyData = Data(repeating: 0, count: keyByteCount) + let derivedCount = derivedKeyData.count + let derivationStatus: Int32 = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in + let keyBuffer: UnsafeMutablePointer = + derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + return saltData.withUnsafeBytes { saltBytes -> Int32 in + let saltBuffer: UnsafePointer = saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + return hashedPasswordData.withUnsafeBytes { hashedPasswordBytes -> Int32 in + let passwordBuffer: UnsafePointer = hashedPasswordBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) + return CCKeyDerivationPBKDF( + CCPBKDFAlgorithm(kCCPBKDF2), + passwordBuffer, + hashedPasswordData.count, + saltBuffer, + saltData.count, + prf, + UInt32(rounds), + keyBuffer, + derivedCount) + } + } + } + return derivationStatus == kCCSuccess ? derivedKeyData : nil + } } public extension Promise where T == (data: Data, response: URLResponse) { @@ -363,3 +489,10 @@ enum SecurityCode { } } } + +public struct ServerSRPInitResponse: Decodable { + let iteration: Int + let salt: String + let b: String + let c: String +} diff --git a/Sources/AppleAPI/URLRequest+Apple.swift b/Sources/AppleAPI/URLRequest+Apple.swift index 8dee5f2..47704ef 100644 --- a/Sources/AppleAPI/URLRequest+Apple.swift +++ b/Sources/AppleAPI/URLRequest+Apple.swift @@ -9,6 +9,9 @@ extension URL { static func submitSecurityCode(_ code: SecurityCode) -> URL { URL(string: "https://idmsa.apple.com/appleauth/auth/verify/\(code.urlPathComponent)/securitycode")! } static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")! static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")! + + static let srpInit = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/init")! + static let srpComplete = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/complete?isRememberMeEnabled=false")! } extension URLRequest { @@ -129,4 +132,49 @@ extension URLRequest { return request } + + static func SRPInit(serviceKey: String, a: String, accountName: String) -> URLRequest { + struct ServerSRPInitRequest: Encodable { + public let a: String + public let accountName: String + public let protocols: [SRPProtocol] + } + + var request = URLRequest(url: .srpInit) + request.httpMethod = "POST" + request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:] + request.allHTTPHeaderFields?["Accept"] = "application/json" + request.allHTTPHeaderFields?["Content-Type"] = "application/json" + request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest" + request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey + + request.httpBody = try? JSONEncoder().encode(ServerSRPInitRequest(a: a, accountName: accountName, protocols: [.s2k, .s2k_fo])) + return request + } + + static func SRPComplete(serviceKey: String, hashcash: String, accountName: String, c: String, m1: String, m2: String) -> URLRequest { + struct ServerSRPCompleteRequest: Encodable { + let accountName: String + let c: String + let m1: String + let m2: String + let rememberMe: Bool + } + + var request = URLRequest(url: .srpComplete) + request.httpMethod = "POST" + request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:] + request.allHTTPHeaderFields?["Accept"] = "application/json" + request.allHTTPHeaderFields?["Content-Type"] = "application/json" + request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest" + request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey + request.allHTTPHeaderFields?["X-Apple-HC"] = hashcash + + request.httpBody = try? JSONEncoder().encode(ServerSRPCompleteRequest(accountName: accountName, c: c, m1: m1, m2: m2, rememberMe: false)) + return request + } +} + +public enum SRPProtocol: String, Codable { + case s2k, s2k_fo } diff --git a/Sources/XcodesKit/Environment.swift b/Sources/XcodesKit/Environment.swift index cec674c..393e19c 100644 --- a/Sources/XcodesKit/Environment.swift +++ b/Sources/XcodesKit/Environment.swift @@ -300,7 +300,7 @@ public struct Network { public var validateSession: () -> Promise = client.validateSession - public var login: (String, String) -> Promise = { client.login(accountName: $0, password: $1) } + public var login: (String, String) -> Promise = { client.srpLogin(accountName: $0, password: $1) } public func login(accountName: String, password: String) -> Promise { login(accountName, password) }