@@ -5,6 +5,7 @@ import Rainbow
55import SRP
66import Crypto
77import CommonCrypto
8+ import SwiftFido2
89
910public class Client {
1011 private static let authTypes = [ " sa " , " hsa " , " non-sa " , " hsa2 " ]
@@ -236,7 +237,7 @@ public class Client {
236237 case . twoFactor:
237238 return self . handleTwoFactor ( serviceKey: serviceKey, sessionID: sessionID, scnt: scnt, authOptions: authOptions)
238239 case . hardwareKey:
239- throw Error . accountUsesHardwareKey
240+ return self . handleHardwareKey ( serviceKey : serviceKey , sessionID : sessionID , scnt : scnt , authOptions : authOptions )
240241 case . unknown:
241242 Current . logging. log ( " Received a response from Apple that indicates this account has two-step or two-factor authentication enabled, but xcodes is unsure how to handle this response: " . red)
242243 String ( data: data, encoding: . utf8) . map { Current . logging. log ( $0) }
@@ -285,6 +286,100 @@ public class Client {
285286 }
286287 }
287288
289+ func handleHardwareKey( serviceKey: String , sessionID: String , scnt: String , authOptions: AuthOptionsResponse ) -> Promise < Void > {
290+ Current . logging. log ( " Hardware security key authentication is required for this account. \n " )
291+
292+ guard let fsaChallenge = authOptions. fsaChallenge else {
293+ return Promise ( error: Error . accountUsesHardwareKey)
294+ }
295+
296+ // Build the assertion request
297+ let origin = " https://idmsa.apple.com "
298+ let clientDataJSON = """
299+ { " type " : " webauthn.get " , " challenge " : " \( fsaChallenge. challenge) " , " origin " : " \( origin) " , " crossOrigin " :false}
300+ """
301+ let clientDataJSONBytes = Data ( clientDataJSON. utf8)
302+ let clientDataHash = Data ( CryptoKit . SHA256. hash ( data: clientDataJSONBytes) )
303+
304+ let credentialIds = fsaChallenge. allowedCredentials
305+ . split ( separator: " , " )
306+ . compactMap { Data ( base64Encoded: base64urlToBase64 ( String ( $0) ) ) }
307+ . map { CredentialDescriptor ( id: $0) }
308+
309+ let request = AssertionRequest (
310+ rpId: " apple.com " ,
311+ clientDataHash: clientDataHash,
312+ allowCredentials: credentialIds
313+ )
314+
315+ // Discover and open FIDO device
316+ Current . logging. log ( " Looking for your security key... " )
317+
318+ return Promise { seal in
319+ Task {
320+ do {
321+ let client = FidoClient ( )
322+
323+ let device : FidoDevice
324+ do {
325+ device = try await client. waitForDevice ( timeoutSeconds: 30 )
326+ } catch {
327+ Current . logging. log ( " No security key detected. Please plug in your key and try again. " . red)
328+ seal. reject ( Error . accountUsesHardwareKey)
329+ return
330+ }
331+ Current . logging. log ( " Found \( device. name) " )
332+
333+ Current . logging. log ( " Touch your security key... " )
334+ let assertion = try await client. getAssertion ( device, request: request)
335+
336+ // Build the response Apple expects
337+ let challengeResponse = SecurityKeyResponse (
338+ challenge: fsaChallenge. challenge,
339+ clientData: clientDataJSONBytes. base64EncodedString ( ) ,
340+ signatureData: assertion. signature. base64EncodedString ( ) ,
341+ authenticatorData: assertion. authData. base64EncodedString ( ) ,
342+ userHandle: assertion. userHandle. flatMap { String ( data: $0, encoding: . utf8) } ?? " " ,
343+ credentialID: assertion. credentialId. base64EncodedString ( ) ,
344+ rpId: " apple.com "
345+ )
346+
347+ let responseData = try JSONEncoder ( ) . encode ( challengeResponse)
348+
349+ Current . logging. log ( " Security key response received, submitting to Apple... " )
350+
351+ // Submit to Apple
352+ let submitRequest = URLRequest . submitSecurityKeyAssertion (
353+ serviceKey: serviceKey,
354+ sessionID: sessionID,
355+ scnt: scnt,
356+ response: responseData
357+ )
358+
359+ Current . network. dataTask ( with: submitRequest)
360+ . then { ( data, response) -> Promise < Void > in
361+ self . updateSession ( serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
362+ }
363+ . done { seal. fulfill ( ( ) ) }
364+ . catch { seal. reject ( $0) }
365+
366+ } catch {
367+ seal. reject ( error)
368+ }
369+ }
370+ }
371+ }
372+
373+ private func base64urlToBase64( _ input: String ) -> String {
374+ var base64 = input
375+ . replacingOccurrences ( of: " - " , with: " + " )
376+ . replacingOccurrences ( of: " _ " , with: " / " )
377+ while base64. count % 4 != 0 {
378+ base64. append ( " = " )
379+ }
380+ return base64
381+ }
382+
288383 func updateSession( serviceKey: String , sessionID: String , scnt: String ) -> Promise < Void > {
289384 return Current . network. dataTask ( with: URLRequest . trust ( serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) )
290385 . then { ( data, response) -> Promise < Void > in
@@ -412,6 +507,16 @@ public class Client {
412507 }
413508}
414509
510+ struct SecurityKeyResponse : Encodable {
511+ let challenge : String
512+ let clientData : String
513+ let signatureData : String
514+ let authenticatorData : String
515+ let userHandle : String
516+ let credentialID : String
517+ let rpId : String
518+ }
519+
415520public extension Promise where T == ( data: Data , response: URLResponse ) {
416521 func validateSecurityCodeResponse( ) -> Promise < T > {
417522 validate ( )
0 commit comments