Skip to content

Instantly share code, notes, and snippets.

@matux
Created April 27, 2020 00:52
Show Gist options
  • Select an option

  • Save matux/b3ee48cee89921997bb2b5c25427b785 to your computer and use it in GitHub Desktop.

Select an option

Save matux/b3ee48cee89921997bb2b5c25427b785 to your computer and use it in GitHub Desktop.

Revisions

  1. matux created this gist Apr 27, 2020.
    439 changes: 439 additions & 0 deletions API.swift
    Original 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)
    }
    }