Created
April 27, 2020 00:52
-
-
Save matux/b3ee48cee89921997bb2b5c25427b785 to your computer and use it in GitHub Desktop.
Revisions
-
matux created this gist
Apr 27, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,439 @@ // 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<String, ᵦAppId> ; enum ᵦAppId {} /// The user's account id generated upon signup. typealias AccountId = Newtype<String, ᵦAccountId> ; enum ᵦSessionId {} /// The user's current session id, generated upon login. typealias SessionId = Newtype<String, ᵦSessionId> ; 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<Inferred>(safe field: Field) -> Inferred? { mutating set { self[*field] = newValue } get { self[*field].flatMap(T.cast) } } subscript<Inferred>(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<Dictionary<String, Any>, 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)) } } /// <daondoc> Perform UAF deregister operation </daondoc> private static func deregister( andThen result: @escaping (APIResult<()>) -> () ) -> (String) -> () { partial(Env.daon.deregister, __, result • APIResult.init) } }