// Extensions to the Daon's API peripheral to the biometric verification // process. import class ObjectiveC.NSObject import Swift import Dispatch import DaonFIDOSDK import DaonAuthenticatorSDK // MARK: - Configuration extension Daon { /// Dictionary which holds predefined parameters related to Daon's /// initialization. typealias Configuration = Newtype<[String: String], ᵦConfig> ; enum ᵦConfig {} } extension Daon.Configuration { /// Default configuration stored in `Const.plist` for `DaonFIDO`. static var `default`: Self { .init((*Const) .filterByKey(where: Constant.daonCases.contains) .bimap(^\.rawValue, { const in switch const { case let b as Bool: return .init(b) case let i as Int: return .init(i) case let f as Double: return .init(f) case let s as String: return s case _: preconditionFailure("Unsupported type `\(type(of: const))`.") } })) } } // MARK: - Message extension Daon { /// The FIDO API communicates internally through `IXUAFMessageReader` objects, /// unfortunately, these are passed around crudely as `String` encoded maps /// of type `[String: String]`. /// /// Given almost all usable data FIDO sends our way are `Strings`, the /// `Message` _newtype_ introduces the class of `Strings` that _are_ known to /// be `Messages` improving a function's signature ability to self-document /// by preventing any String from being used in contexts that require a /// `String` that _is_ a `Message` at compile time. typealias Message = IXUAFMessageReader } extension Daon.Message { var appId: String? { application() } convenience init(_ message: String) { self.init(message: message) } } // MARK: - IDs extension Daon { /// The application id assigned by Daon/FIDO. typealias AppId = Newtype ; enum ᵦAppId {} /// The user's account id generated upon signup. typealias AccountId = Newtype ; enum ᵦSessionId {} /// The user's current session id, generated upon login. typealias SessionId = Newtype ; enum ᵦAccountId {} } extension Daon.AppId { /// Creates a new app id by extracting it from the given Daon message. /// /// - Parameter message: A Daon API response in `Message` format. @_transparent init(message: Daon.Message) { self.init(message.appId !! "Message has no app id") } } // MARK: - Delegate /// DaonFIDO's authenticator delegate used to filter preferred authenticators /// and instantiate biometric verification view controllers. @objcMembers final class AuthenticatorDelegate: NSObject, IXUAFDelegate { static var `default` = AuthenticatorDelegate() private override init() { } func operation( _: IXUAFOperation, willAllowAuthenticators authenticators: [[IXUAFAuthenticator]] ) -> [[IXUAFAuthenticator]]? { authenticators .map(filter(\.aaid == Const.aaids[Env.vars.biometricMode])) .filter(^\.hasElements) } func operation( _: IXUAFOperation, shouldUseViewControllerForUserVerification mode: Int, context: DASAuthenticatorContext ) -> UIViewController? { switch (Env.vars.authenticationMode, Env.vars.biometricMode) { case (_, .faceprint): return FaceViewController(context: context) case (.signup, .voiceprint): return VoiceSignupViewController(context: context) case (.login, .voiceprint): return VoiceLoginViewController(context: context) } } } // MARK: - JSON extension JSON { enum Field: String { case id, policy, policyInfo, domain, statusCode, sessionId, authenticators = "authenticatorInfoList", deregistrationRequest = "deregistrationRequest", description = "NSLocalizedDescription", failureReason = "NSLocalizedFailureReasonErrorKey", registrationConfirmation = "fidoRegistrationConfirmation", registrationId = "registrationRequestId", registrationRequest = "fidoRegistrationRequest", responseCode = "fidoResponseCode" } init?(data: Data) { guard data.hasElements else { return nil } self = (( try? JSONSerialization.jsonObject(with: data)) !! "Invalid JSON Data.") as? JSON !! "Map is not a `JSON` dictionary." } subscript(json field: Field) -> JSON { self[*field].flatMap(T.cast) !! "Dictionary in JSON is not JSON." } subscript(safe field: Field) -> Inferred? { mutating set { self[*field] = newValue } get { self[*field].flatMap(T.cast) } } subscript(field: Field) -> Inferred { mutating set { self[*field] = newValue } get { self[*field].flatMap(T.cast) !! "Can't get \(field)." } } } // MARK: - Context extension DASAuthenticatorContext { /// Notifies the context of a failure. /// /// This will check the attempts count for that authenticator and will /// return `true` if the authenticator is now locked. At this point, the /// context will take care of displaying an error and dismissing the current /// authenticator. /// /// If `false` is returned, then you should display an appropriate error /// yourself and allow the user to try again. /// /// - Note: As part of this call, `reportAttemptWithErrorCode:score:` will /// also be called. /// /// - Remark: The failed attempts methods allow you to notify the context /// that an error has occurred, and to check whether the authenticator is /// now locked. /// /// - Parameter code: The error code reported by the context. /// - Returns: Whether the authenticator is now locked or not. func reportError(code: Int) -> Bool { return incrementFailuresAndCheckForLockWithErrorCode(code, score: 0) } } // MARK: - Daon typealias Daon = DaonFIDO /// Extensions aimed at bringing a semblance of sanity to the universe. @objc extension Daon: SelfAware { typealias Error = DASAuthenticatorError typealias ClientCode = IXUAFErrorCode typealias ServerCode = IXUAFServerErrorCode /// Creates an instance of `DaonFIDO` fine-tuned for the purposes of the app. /// /// - Parameters: /// - configuration: A dictionary with settings to pass along to Daon. /// - delegate: An instance to describe the app's behavior upon /// receiving authentication events. @nonobjc convenience init( configuration: Configuration, delegate: AuthenticatorDelegate ) { self.init() async(on: .global(qos: .userInitiated)) { self.setLogging(enabled: true, level: .info) self.initialize(withParameters: *configuration) => APIError.init • IXUAFError.error • { $0.rawValue } => when(some: { preconditionFailure($0.description) }, none: { self.delegate = delegate; async(Daon.locate) }) } } } extension Daon { static func faceController( for delegate: DASFaceControllerDelegate, context: DASAuthenticatorContext? ) -> (UIView) -> DASFaceControllerProtocol { { view in DASFaceAuthenticatorFactory .createFaceController( withPreviewView: view, delegate: delegate, context: context) } } } // MARK: - API entry-point @objc extension Daon { /// Initiates the account creation flow using a dummy user provided by the /// currently active `Environment`. static func createAccount() { createAccount(success: {}, failure: {}) } /// Initiates the account creation flow using a dummy user provided by the /// currently active `Environment`. static func createAccount( success succeed: @escaping () -> (), failure fail: @escaping () -> () ) { Env.user = .randomize() =>> { user in Env.net.request(.user(user), result: select(\.[.registrationRequest], \.[.sessionId], or: .badRequest) >>> fmap { Env.vars.id = ( AppId(message: .init($0.0)), AccountId(user.email), SessionId($0.1)) } >>> when(success: succeed, failure: discard >>> fail)) } } /// Initiates Daon's biometric recognition UX for the given access and /// verification methods. /// /// - Parameters: /// - authenticationMode: Authentication purpose, either signup or login. /// - biometricMode: Biometric method of detection. /// - request: Access request model for FIDO. /// - result: A closure that responds to the result of the operation. static func request( authenticationMode: Int, biometricMode: Int, success succeed: @escaping () -> (), failure fail: @escaping () -> () ) { guard [.appId, .sessionId].allSatisfy(Env.vars.contains) else { return fail() } Env.daon.delegate = AuthenticatorDelegate.default Env.vars.authenticationMode = .init(authenticationMode) Env.vars.biometricMode = .init(biometricMode) Env.net.request(.signup, result: select(\.[.registrationId], \.[.registrationRequest], or: .badRequest) >>> tap { Env.vars.id.app = AppId(message: .init($0.1)) } >>> when(success: async(partial(presentBiometrics, __, __, succeed, fail)), failure: when(.noSession, createAccount, otherwise: fail))) // Env.net.request(.signup) { (result: Result, APIError>) in // switch result { // case .success(let json): // let registrationIdKey = "registrationRequestId" // let registrationRequestKey = "fidoRegistrationRequest" // let optionalRegistrationId = json[registrationIdKey] as? String // let optionalRegistrationRequest = json[registrationRequestKey] as? String // // guard // let registrationId = optionalRegistrationId, // let registrationRequest = optionalRegistrationRequest // else { // return fail() // } // // Env.vars.id.app = AppId(message: .init(registrationRequest)) // DispatchQueue.main.async { // presentBiometrics( // id: registrationId, // message: registrationRequest, // success: succeed, failure: fail) // } // // case .failure(.noSession): // createAccount() // // case .failure: // fail() // } // } } /// Presents the biometric UI. Must be called on a main thread. static func presentBiometrics( id: String, message: String, success succeed: @escaping () -> (), failure fail: @escaping () -> () ) { Env.daon.register(message: message, completion: map { APIResult($0.0, or: APIError($0.1) ?? .badRequest) } >>> when( success: partial(APIRequest.authenticator, id, __) >>> request(.registrationConfirmation, andThen: succeed), failure: fail • void)) } static func logout(completion complete: @escaping () -> ()) { Env.net.request(.sessionRemoval, result: resetAll >>> complete) } static func purge( success succeed: @escaping () -> (), failure fail: @escaping () -> () ) { guard Env.vars.contains(.sessionId) else { return reset() => succeed } Env.net.request(.authenticators, result: select(\.[.authenticators], or: .badRequest) >>> flatMap(`guard`(^\.hasElements) ??? .noAuthenticators) >>> either(deleteAuthenticators(andThen: either( resetAll >>> succeed, or: resetAll >>> fail)), or: resetAll >>> fail)) } } // MARK: - Private API @nonobjc extension Daon { private static let locate = IXUAFLocator.sharedInstance()?.locate ?? noop private static let resetAll = { (_: Any) in Env.user = .none Session.reset(Env.vars) reset() } /// Notify the UAF client about the result of a UAF registration or /// authentication operation. private static var notify = { Env.daon.notifyResult(message: $0, code: $1, completion: void <<< APIError.init >=> { trace($0.description) }) } private static let checkPolicy: (String) -> () = { Env.daon.checkRegistrations( policy: $0, username: *Env.vars.id.account, appId: *Env.vars.id.app, completion: .none) } private static let checkPolicies = { Env.net.request(.policies, result: select(\.[json: .policyInfo][.policy], or: .badRequest) >>> when(success: checkPolicy)) } static func request( _ field: JSON.Field, andThen complete: @escaping () -> () ) -> (APIRequest) -> () { { request in Env.net.request(request, result: select(\.[field], \.[.responseCode], or: .badRequest) >>> when( success: notify >>> checkPolicies >>> complete, failure: { notify(request.auth, $0.code) => complete })) } } private static func deleteAuthenticators( andThen complete: @escaping (APIResult<()>) -> () ) -> (_ authenticators: [JSON]) -> () { { delete(authenticators: $0, andThen: complete) } } private static func delete( authenticators: [JSON], andThen complete: @escaping (APIResult<()>) -> () ) { authenticators .compactMap(^\.[.id]) .contMap(with: { _ in complete(.success) }) { id, next in Env.net.request( .authRemoval(id: id), result: select(\.[.deregistrationRequest], or: .badRequest) >>> when( success: deregister(andThen: next • APIResult.init), failure: next • APIResult.init)) } } /// Perform UAF deregister operation private static func deregister( andThen result: @escaping (APIResult<()>) -> () ) -> (String) -> () { partial(Env.daon.deregister, __, result • APIResult.init) } }