@@ -2,6 +2,9 @@ import Foundation
22import PromiseKit
33import PMKFoundation
44import Rainbow
5+ import SRP
6+ import Crypto
7+ import CommonCrypto
58
69public class Client {
710 private static let authTypes = [ " sa " , " hsa " , " non-sa " , " hsa2 " ]
@@ -15,18 +18,23 @@ public class Client {
1518 case incorrectSecurityCode
1619 case unexpectedSignInResponse( statusCode: Int , message: String ? )
1720 case appleIDAndPrivacyAcknowledgementRequired
21+ case serviceTemporarilyUnavailable
1822 case noTrustedPhoneNumbers
1923 case notAuthenticated
2024 case invalidHashcash
2125 case missingSecurityCodeInfo
2226 case accountUsesHardwareKey
27+ case srpInvalidPublicKey
28+ case srpError( String )
2329
2430 public var errorDescription : String ? {
2531 switch self {
2632 case . invalidUsernameOrPassword( let username) :
2733 return " Invalid username and password combination. Attempted to sign in with username \( username) . "
2834 case . appleIDAndPrivacyAcknowledgementRequired:
2935 return " You must sign in to https://appstoreconnect.apple.com and acknowledge the Apple ID & Privacy agreement. "
36+ case . serviceTemporarilyUnavailable:
37+ return " The service is temporarily unavailable. Please try again later. "
3038 case . invalidPhoneNumberIndex( let min, let max, let given) :
3139 return " Not a valid phone number index. Expecting a whole number between \( min) - \( max) , but was given \( given ?? " nothing " ) . "
3240 case . noTrustedPhoneNumbers:
@@ -56,6 +64,102 @@ public class Client {
5664 }
5765 }
5866
67+ /// SRPLogin - Secure Remote Password
68+ /// https://tools.ietf.org/html/rfc2945
69+ /// Forked from https://github.com/adam-fowler/swift-srp that provides the algorithm
70+ public func srpLogin( accountName: String , password: String ) -> Promise < Void > {
71+ var serviceKey : String !
72+ let client = SRPClient ( configuration: SRPConfiguration < SHA256 > ( . N2048) )
73+ let clientKeys = client. generateKeys ( )
74+ let a = clientKeys. public
75+
76+ // Get the Service Key needed from olympus session needed in headers
77+ return firstly { ( ) -> Promise < ( data: Data , response: URLResponse ) > in
78+ Current . network. dataTask ( with: URLRequest . itcServiceKey)
79+ }
80+ . then { ( data, _) -> Promise < ( serviceKey: String , hashcash: String ) > in
81+ struct ServiceKeyResponse : Decodable {
82+ let authServiceKey : String ?
83+ }
84+
85+ let response = try JSONDecoder ( ) . decode ( ServiceKeyResponse . self, from: data)
86+ serviceKey = response. authServiceKey
87+
88+ /// Load a hashcash of the account name
89+ return self . loadHashcash ( accountName: accountName, serviceKey: serviceKey) . map { ( serviceKey, $0) }
90+ }
91+ . then { ( serviceKey, hashcash) -> Promise < ( serviceKey: String , hashcash: String , data: Data ) > in
92+ /// Call the SRP /init endpoint to start the login
93+ return Current . network. dataTask ( with: URLRequest . SRPInit ( serviceKey: serviceKey, a: Data ( a. bytes) . base64EncodedString ( ) , accountName: accountName) ) . map { ( serviceKey, hashcash, $0. data) }
94+ }
95+ . then { ( serviceKey, hashcash, data) -> Promise < ( data: Data , response: URLResponse ) > in
96+ let srpInit = try JSONDecoder ( ) . decode ( ServerSRPInitResponse . self, from: data)
97+
98+ guard let decodedB = Data ( base64Encoded: srpInit. b) else {
99+ throw Error . srpInvalidPublicKey
100+ }
101+ guard let decodedSalt = Data ( base64Encoded: srpInit. salt) else {
102+ throw Error . srpInvalidPublicKey
103+ }
104+
105+ let iterations = srpInit. iteration
106+
107+ do {
108+ guard let encryptedPassword = self . pbkdf2 ( password: password, saltData: decodedSalt, keyByteCount: 32 , prf: CCPseudoRandomAlgorithm ( kCCPRFHmacAlgSHA256) , rounds: iterations) else {
109+ throw Error . srpInvalidPublicKey
110+ }
111+
112+ let sharedSecret = try client. calculateSharedSecret ( password: encryptedPassword, salt: [ UInt8] ( decodedSalt) , clientKeys: clientKeys, serverPublicKey: . init( [ UInt8] ( decodedB) ) )
113+
114+ let m1 = client. calculateClientProof ( username: accountName, salt: [ UInt8] ( decodedSalt) , clientPublicKey: a, serverPublicKey: . init( [ UInt8] ( decodedB) ) , sharedSecret: . init( sharedSecret. bytes) )
115+ let m2 = client. calculateServerProof ( clientPublicKey: a, clientProof: m1, sharedSecret: . init( [ UInt8] ( sharedSecret. bytes) ) )
116+
117+ /// call the /complete endpoint passing in the hashcash, servicekey, and the calculated proof.
118+ return Current . network. dataTask ( with: URLRequest . SRPComplete ( serviceKey: serviceKey, hashcash: hashcash, accountName: accountName, c: srpInit. c, m1: Data ( m1) . base64EncodedString ( ) , m2: Data ( m2) . base64EncodedString ( ) ) )
119+ } catch {
120+ throw Error . srpError ( error. localizedDescription)
121+ }
122+ }
123+ . then { ( data, response) -> Promise < Void > in
124+ struct SignInResponse : Decodable {
125+ let authType : String ?
126+ let serviceErrors : [ ServiceError ] ?
127+
128+ struct ServiceError : Decodable , CustomStringConvertible {
129+ let code : String
130+ let message : String
131+
132+ var description : String {
133+ return " \( code) : \( message) "
134+ }
135+ }
136+ }
137+
138+ let httpResponse = response as! HTTPURLResponse
139+ do {
140+ let responseBody = try JSONDecoder ( ) . decode ( SignInResponse . self, from: data)
141+ switch httpResponse. statusCode {
142+ case 200 :
143+ return Current . network. dataTask ( with: URLRequest . olympusSession) . asVoid ( )
144+ case 401 :
145+ throw Error . invalidUsernameOrPassword ( username: accountName)
146+ case 409 :
147+ return self . handleTwoStepOrFactor ( data: data, response: response, serviceKey: serviceKey)
148+ case 412 where Client . authTypes. contains ( responseBody. authType ?? " " ) :
149+ throw Error . appleIDAndPrivacyAcknowledgementRequired
150+ default :
151+ throw Error . unexpectedSignInResponse ( statusCode: httpResponse. statusCode,
152+ message: responseBody. serviceErrors? . map { $0. description } . joined ( separator: " , " ) )
153+ }
154+ } catch DecodingError . dataCorrupted where httpResponse. statusCode == 503 {
155+ throw Error . serviceTemporarilyUnavailable
156+ } catch {
157+ throw error
158+ }
159+ }
160+ }
161+
162+ @available ( * , deprecated, message: " Please use srpLogin " )
59163 public func login( accountName: String , password: String ) -> Promise < Void > {
60164 var serviceKey : String !
61165
@@ -92,20 +196,25 @@ public class Client {
92196 }
93197
94198 let httpResponse = response as! HTTPURLResponse
95- let responseBody = try JSONDecoder ( ) . decode ( SignInResponse . self, from: data)
96-
97- switch httpResponse. statusCode {
98- case 200 :
99- return Current . network. dataTask ( with: URLRequest . olympusSession) . asVoid ( )
100- case 401 :
101- throw Error . invalidUsernameOrPassword ( username: accountName)
102- case 409 :
103- return self . handleTwoStepOrFactor ( data: data, response: response, serviceKey: serviceKey)
104- case 412 where Client . authTypes. contains ( responseBody. authType ?? " " ) :
105- throw Error . appleIDAndPrivacyAcknowledgementRequired
106- default :
107- throw Error . unexpectedSignInResponse ( statusCode: httpResponse. statusCode,
108- message: responseBody. serviceErrors? . map { $0. description } . joined ( separator: " , " ) )
199+ do {
200+ let responseBody = try JSONDecoder ( ) . decode ( SignInResponse . self, from: data)
201+ switch httpResponse. statusCode {
202+ case 200 :
203+ return Current . network. dataTask ( with: URLRequest . olympusSession) . asVoid ( )
204+ case 401 :
205+ throw Error . invalidUsernameOrPassword ( username: accountName)
206+ case 409 :
207+ return self . handleTwoStepOrFactor ( data: data, response: response, serviceKey: serviceKey)
208+ case 412 where Client . authTypes. contains ( responseBody. authType ?? " " ) :
209+ throw Error . appleIDAndPrivacyAcknowledgementRequired
210+ default :
211+ throw Error . unexpectedSignInResponse ( statusCode: httpResponse. statusCode,
212+ message: responseBody. serviceErrors? . map { $0. description } . joined ( separator: " , " ) )
213+ }
214+ } catch DecodingError . dataCorrupted where httpResponse. statusCode == 503 {
215+ throw Error . serviceTemporarilyUnavailable
216+ } catch {
217+ throw error
109218 }
110219 }
111220 }
@@ -264,6 +373,43 @@ public class Client {
264373 return . value( hashcash)
265374 }
266375 }
376+
377+ private func sha256( data : Data ) -> Data {
378+ var hash = [ UInt8] ( repeating: 0 , count: Int ( CC_SHA256_DIGEST_LENGTH) )
379+ data. withUnsafeBytes {
380+ _ = CC_SHA256 ( $0. baseAddress, CC_LONG ( data. count) , & hash)
381+ }
382+ return Data ( hash)
383+ }
384+
385+ private func pbkdf2( password: String , saltData: Data , keyByteCount: Int , prf: CCPseudoRandomAlgorithm , rounds: Int ) -> Data ? {
386+ guard let passwordData = password. data ( using: . utf8) else { return nil }
387+ let hashedPasswordData = sha256 ( data: passwordData)
388+
389+ var derivedKeyData = Data ( repeating: 0 , count: keyByteCount)
390+ let derivedCount = derivedKeyData. count
391+ let derivationStatus : Int32 = derivedKeyData. withUnsafeMutableBytes { derivedKeyBytes in
392+ let keyBuffer : UnsafeMutablePointer < UInt8 > =
393+ derivedKeyBytes. baseAddress!. assumingMemoryBound ( to: UInt8 . self)
394+ return saltData. withUnsafeBytes { saltBytes -> Int32 in
395+ let saltBuffer : UnsafePointer < UInt8 > = saltBytes. baseAddress!. assumingMemoryBound ( to: UInt8 . self)
396+ return hashedPasswordData. withUnsafeBytes { hashedPasswordBytes -> Int32 in
397+ let passwordBuffer : UnsafePointer < UInt8 > = hashedPasswordBytes. baseAddress!. assumingMemoryBound ( to: UInt8 . self)
398+ return CCKeyDerivationPBKDF (
399+ CCPBKDFAlgorithm ( kCCPBKDF2) ,
400+ passwordBuffer,
401+ hashedPasswordData. count,
402+ saltBuffer,
403+ saltData. count,
404+ prf,
405+ UInt32 ( rounds) ,
406+ keyBuffer,
407+ derivedCount)
408+ }
409+ }
410+ }
411+ return derivationStatus == kCCSuccess ? derivedKeyData : nil
412+ }
267413}
268414
269415public extension Promise where T == ( data: Data , response: URLResponse ) {
@@ -363,3 +509,10 @@ enum SecurityCode {
363509 }
364510 }
365511}
512+
513+ public struct ServerSRPInitResponse : Decodable {
514+ let iteration : Int
515+ let salt : String
516+ let b : String
517+ let c : String
518+ }
0 commit comments