Created
April 27, 2020 00:52
-
-
Save matux/b3ee48cee89921997bb2b5c25427b785 to your computer and use it in GitHub Desktop.
This file contains 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 characters
// 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