Skip to content

Instantly share code, notes, and snippets.

@matux
Created April 27, 2020 00:52
Show Gist options
  • Save matux/b3ee48cee89921997bb2b5c25427b785 to your computer and use it in GitHub Desktop.
Save matux/b3ee48cee89921997bb2b5c25427b785 to your computer and use it in GitHub Desktop.
// 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)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment