@@ -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 " ]
@@ -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
269402public 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+ }
0 commit comments