Skip to content

Commit fd74b49

Browse files
committed
fix: add support for SRP Apple login
1 parent 74516ad commit fd74b49

File tree

5 files changed

+212
-2
lines changed

5 files changed

+212
-2
lines changed

Package.resolved

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
{
22
"pins" : [
3+
{
4+
"identity" : "big-num",
5+
"kind" : "remoteSourceControl",
6+
"location" : "https://github.com/adam-fowler/big-num",
7+
"state" : {
8+
"revision" : "5c5511ad06aeb2b97d0868f7394e14a624bfb1c7",
9+
"version" : "2.0.2"
10+
}
11+
},
312
{
413
"identity" : "data",
514
"kind" : "remoteSourceControl",
@@ -71,6 +80,24 @@
7180
"version" : "1.1.4"
7281
}
7382
},
83+
{
84+
"identity" : "swift-crypto",
85+
"kind" : "remoteSourceControl",
86+
"location" : "https://github.com/apple/swift-crypto",
87+
"state" : {
88+
"revision" : "ddb07e896a2a8af79512543b1c7eb9797f8898a5",
89+
"version" : "1.1.7"
90+
}
91+
},
92+
{
93+
"identity" : "swift-srp",
94+
"kind" : "remoteSourceControl",
95+
"location" : "https://github.com/xcodesorg/swift-srp",
96+
"state" : {
97+
"branch" : "main",
98+
"revision" : "543aa0122a0257b992f6c7d62d18a26e3dffb8fe"
99+
}
100+
},
74101
{
75102
"identity" : "swiftsoup",
76103
"kind" : "remoteSourceControl",

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: 133 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,90 @@ public class Client {
5661
}
5762
}
5863

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

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

269395
public extension Promise where T == (data: Data, response: URLResponse) {
@@ -363,3 +489,10 @@ enum SecurityCode {
363489
}
364490
}
365491
}
492+
493+
public struct ServerSRPInitResponse: Decodable {
494+
let iteration: Int
495+
let salt: String
496+
let b: String
497+
let c: String
498+
}

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)