Skip to content

Commit 6a619e7

Browse files
authored
Merge pull request #393 from XcodesOrg/matt/supportSRPLogin
Add support for SRP Apple login
2 parents 74516ad + 72540e9 commit 6a619e7

File tree

5 files changed

+219
-2
lines changed

5 files changed

+219
-2
lines changed

Package.resolved

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ let package = Package(
2323
.package(url: "https://github.com/xcodereleases/data", revision: "fcf527b187817f67c05223676341f3ab69d4214d"),
2424
.package(url: "https://github.com/onevcat/Rainbow.git", .upToNextMinor(from: "3.2.0")),
2525
.package(url: "https://github.com/jpsim/Yams", .upToNextMinor(from: "5.0.1")),
26+
.package(url: "https://github.com/xcodesOrg/swift-srp", branch: "main")
2627
],
2728
targets: [
2829
.executableTarget(
@@ -50,7 +51,7 @@ let package = Package(
5051
"Version",
5152
.product(name: "XCModel", package: "data"),
5253
"Rainbow",
53-
"Yams",
54+
"Yams"
5455
]),
5556
.testTarget(
5657
name: "XcodesKitTests",
@@ -68,6 +69,7 @@ let package = Package(
6869
"PromiseKit",
6970
.product(name: "PMKFoundation", package: "Foundation"),
7071
"Rainbow",
72+
.product(name: "SRP", package: "swift-srp")
7173
]),
7274
.testTarget(
7375
name: "AppleAPITests",

Sources/AppleAPI/Client.swift

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import Foundation
22
import PromiseKit
33
import PMKFoundation
44
import Rainbow
5+
import SRP
6+
import Crypto
7+
import CommonCrypto
58

69
public class Client {
710
private static let authTypes = ["sa", "hsa", "non-sa", "hsa2"]
@@ -20,6 +23,8 @@ public class Client {
2023
case invalidHashcash
2124
case missingSecurityCodeInfo
2225
case accountUsesHardwareKey
26+
case srpInvalidPublicKey
27+
case srpError(String)
2328

2429
public var errorDescription: String? {
2530
switch self {
@@ -56,6 +61,97 @@ public class Client {
5661
}
5762
}
5863

64+
/// SRPLogin - Secure Remote Password
65+
/// https://tools.ietf.org/html/rfc2945
66+
/// Forked from https://github.com/adam-fowler/swift-srp that provides the algorithm
67+
public func srpLogin(accountName: String, password: String) -> Promise<Void> {
68+
var serviceKey: String!
69+
let client = SRPClient(configuration: SRPConfiguration<SHA256>(.N2048))
70+
let clientKeys = client.generateKeys()
71+
let a = clientKeys.public
72+
73+
// Get the Service Key needed from olympus session needed in headers
74+
return firstly { () -> Promise<(data: Data, response: URLResponse)> in
75+
Current.network.dataTask(with: URLRequest.itcServiceKey)
76+
}
77+
.then { (data, _) -> Promise<(serviceKey: String, hashcash: String)> in
78+
struct ServiceKeyResponse: Decodable {
79+
let authServiceKey: String?
80+
}
81+
82+
let response = try JSONDecoder().decode(ServiceKeyResponse.self, from: data)
83+
serviceKey = response.authServiceKey
84+
85+
/// Load a hashcash of the account name
86+
return self.loadHashcash(accountName: accountName, serviceKey: serviceKey).map { (serviceKey, $0) }
87+
}
88+
.then { (serviceKey, hashcash) -> Promise<(serviceKey: String, hashcash: String, data: Data)> in
89+
/// Call the SRP /init endpoint to start the login
90+
return Current.network.dataTask(with: URLRequest.SRPInit(serviceKey: serviceKey, a: Data(a.bytes).base64EncodedString(), accountName: accountName)).map { (serviceKey, hashcash, $0.data)}
91+
}
92+
.then { (serviceKey, hashcash, data) -> Promise<(data: Data, response: URLResponse)> in
93+
let srpInit = try JSONDecoder().decode(ServerSRPInitResponse.self, from: data)
94+
95+
guard let decodedB = Data(base64Encoded: srpInit.b) else {
96+
throw Error.srpInvalidPublicKey
97+
}
98+
guard let decodedSalt = Data(base64Encoded: srpInit.salt) else {
99+
throw Error.srpInvalidPublicKey
100+
}
101+
102+
let iterations = srpInit.iteration
103+
104+
do {
105+
guard let encryptedPassword = self.pbkdf2(password: password, saltData: decodedSalt, keyByteCount: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), rounds: iterations) else {
106+
throw Error.srpInvalidPublicKey
107+
}
108+
109+
let sharedSecret = try client.calculateSharedSecret(password: encryptedPassword, salt: [UInt8](decodedSalt), clientKeys: clientKeys, serverPublicKey: .init([UInt8](decodedB)))
110+
111+
let m1 = client.calculateClientProof(username: accountName, salt: [UInt8](decodedSalt), clientPublicKey: a, serverPublicKey: .init([UInt8](decodedB)), sharedSecret: .init(sharedSecret.bytes))
112+
let m2 = client.calculateServerProof(clientPublicKey: a, clientProof: m1, sharedSecret: .init([UInt8](sharedSecret.bytes)))
113+
114+
/// call the /complete endpoint passing in the hashcash, servicekey, and the calculated proof.
115+
return Current.network.dataTask(with: URLRequest.SRPComplete(serviceKey: serviceKey, hashcash: hashcash, accountName: accountName, c: srpInit.c, m1: Data(m1).base64EncodedString(), m2: Data(m2).base64EncodedString()))
116+
} catch {
117+
throw Error.srpError(error.localizedDescription)
118+
}
119+
}
120+
.then { (data, response) -> Promise<Void> in
121+
struct SignInResponse: Decodable {
122+
let authType: String?
123+
let serviceErrors: [ServiceError]?
124+
125+
struct ServiceError: Decodable, CustomStringConvertible {
126+
let code: String
127+
let message: String
128+
129+
var description: String {
130+
return "\(code): \(message)"
131+
}
132+
}
133+
}
134+
135+
let httpResponse = response as! HTTPURLResponse
136+
let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data)
137+
138+
switch httpResponse.statusCode {
139+
case 200:
140+
return Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
141+
case 401:
142+
throw Error.invalidUsernameOrPassword(username: accountName)
143+
case 409:
144+
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
145+
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
146+
throw Error.appleIDAndPrivacyAcknowledgementRequired
147+
default:
148+
throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
149+
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", "))
150+
}
151+
}
152+
}
153+
154+
@available(*, deprecated, message: "Please use srpLogin")
59155
public func login(accountName: String, password: String) -> Promise<Void> {
60156
var serviceKey: String!
61157

@@ -264,6 +360,43 @@ public class Client {
264360
return .value(hashcash)
265361
}
266362
}
363+
364+
private func sha256(data : Data) -> Data {
365+
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
366+
data.withUnsafeBytes {
367+
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
368+
}
369+
return Data(hash)
370+
}
371+
372+
private func pbkdf2(password: String, saltData: Data, keyByteCount: Int, prf: CCPseudoRandomAlgorithm, rounds: Int) -> Data? {
373+
guard let passwordData = password.data(using: .utf8) else { return nil }
374+
let hashedPasswordData = sha256(data: passwordData)
375+
376+
var derivedKeyData = Data(repeating: 0, count: keyByteCount)
377+
let derivedCount = derivedKeyData.count
378+
let derivationStatus: Int32 = derivedKeyData.withUnsafeMutableBytes { derivedKeyBytes in
379+
let keyBuffer: UnsafeMutablePointer<UInt8> =
380+
derivedKeyBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
381+
return saltData.withUnsafeBytes { saltBytes -> Int32 in
382+
let saltBuffer: UnsafePointer<UInt8> = saltBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
383+
return hashedPasswordData.withUnsafeBytes { hashedPasswordBytes -> Int32 in
384+
let passwordBuffer: UnsafePointer<UInt8> = hashedPasswordBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)
385+
return CCKeyDerivationPBKDF(
386+
CCPBKDFAlgorithm(kCCPBKDF2),
387+
passwordBuffer,
388+
hashedPasswordData.count,
389+
saltBuffer,
390+
saltData.count,
391+
prf,
392+
UInt32(rounds),
393+
keyBuffer,
394+
derivedCount)
395+
}
396+
}
397+
}
398+
return derivationStatus == kCCSuccess ? derivedKeyData : nil
399+
}
267400
}
268401

269402
public extension Promise where T == (data: Data, response: URLResponse) {
@@ -363,3 +496,10 @@ enum SecurityCode {
363496
}
364497
}
365498
}
499+
500+
public struct ServerSRPInitResponse: Decodable {
501+
let iteration: Int
502+
let salt: String
503+
let b: String
504+
let c: String
505+
}

Sources/AppleAPI/URLRequest+Apple.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ extension URL {
99
static func submitSecurityCode(_ code: SecurityCode) -> URL { URL(string: "https://idmsa.apple.com/appleauth/auth/verify/\(code.urlPathComponent)/securitycode")! }
1010
static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")!
1111
static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")!
12+
13+
static let srpInit = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/init")!
14+
static let srpComplete = URL(string: "https://idmsa.apple.com/appleauth/auth/signin/complete?isRememberMeEnabled=false")!
1215
}
1316

1417
extension URLRequest {
@@ -129,4 +132,49 @@ extension URLRequest {
129132

130133
return request
131134
}
135+
136+
static func SRPInit(serviceKey: String, a: String, accountName: String) -> URLRequest {
137+
struct ServerSRPInitRequest: Encodable {
138+
public let a: String
139+
public let accountName: String
140+
public let protocols: [SRPProtocol]
141+
}
142+
143+
var request = URLRequest(url: .srpInit)
144+
request.httpMethod = "POST"
145+
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
146+
request.allHTTPHeaderFields?["Accept"] = "application/json"
147+
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
148+
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
149+
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
150+
151+
request.httpBody = try? JSONEncoder().encode(ServerSRPInitRequest(a: a, accountName: accountName, protocols: [.s2k, .s2k_fo]))
152+
return request
153+
}
154+
155+
static func SRPComplete(serviceKey: String, hashcash: String, accountName: String, c: String, m1: String, m2: String) -> URLRequest {
156+
struct ServerSRPCompleteRequest: Encodable {
157+
let accountName: String
158+
let c: String
159+
let m1: String
160+
let m2: String
161+
let rememberMe: Bool
162+
}
163+
164+
var request = URLRequest(url: .srpComplete)
165+
request.httpMethod = "POST"
166+
request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
167+
request.allHTTPHeaderFields?["Accept"] = "application/json"
168+
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
169+
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
170+
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
171+
request.allHTTPHeaderFields?["X-Apple-HC"] = hashcash
172+
173+
request.httpBody = try? JSONEncoder().encode(ServerSRPCompleteRequest(accountName: accountName, c: c, m1: m1, m2: m2, rememberMe: false))
174+
return request
175+
}
176+
}
177+
178+
public enum SRPProtocol: String, Codable {
179+
case s2k, s2k_fo
132180
}

Sources/XcodesKit/Environment.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ public struct Network {
300300

301301
public var validateSession: () -> Promise<Void> = client.validateSession
302302

303-
public var login: (String, String) -> Promise<Void> = { client.login(accountName: $0, password: $1) }
303+
public var login: (String, String) -> Promise<Void> = { client.srpLogin(accountName: $0, password: $1) }
304304
public func login(accountName: String, password: String) -> Promise<Void> {
305305
login(accountName, password)
306306
}

0 commit comments

Comments
 (0)