1fe5cfb17STomasz Sapeta import ABI48_0_0ExpoModulesCore
2fe5cfb17STomasz Sapeta import LocalAuthentication
3fe5cfb17STomasz Sapeta
4fe5cfb17STomasz Sapeta public class LocalAuthenticationModule: Module {
definitionnull5fe5cfb17STomasz Sapeta public func definition() -> ModuleDefinition {
6fe5cfb17STomasz Sapeta Name("ExpoLocalAuthentication")
7fe5cfb17STomasz Sapeta
8fe5cfb17STomasz Sapeta AsyncFunction("hasHardwareAsync") { () -> Bool in
9fe5cfb17STomasz Sapeta let context = LAContext()
10fe5cfb17STomasz Sapeta var error: NSError?
11fe5cfb17STomasz Sapeta let isSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error)
12fe5cfb17STomasz Sapeta let isAvailable: Bool = isSupported || error?.code != LAError.biometryNotAvailable.rawValue
13fe5cfb17STomasz Sapeta
14fe5cfb17STomasz Sapeta return isAvailable
15fe5cfb17STomasz Sapeta }
16fe5cfb17STomasz Sapeta
17fe5cfb17STomasz Sapeta AsyncFunction("isEnrolledAsync") { () -> Bool in
18fe5cfb17STomasz Sapeta let context = LAContext()
19fe5cfb17STomasz Sapeta var error: NSError?
20fe5cfb17STomasz Sapeta let isSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error)
21fe5cfb17STomasz Sapeta let isEnrolled: Bool = isSupported && error == nil
22fe5cfb17STomasz Sapeta
23fe5cfb17STomasz Sapeta return isEnrolled
24fe5cfb17STomasz Sapeta }
25fe5cfb17STomasz Sapeta
26fe5cfb17STomasz Sapeta AsyncFunction("supportedAuthenticationTypesAsync") { () -> [Int] in
27fe5cfb17STomasz Sapeta var supportedAuthenticationTypes: [Int] = []
28fe5cfb17STomasz Sapeta
29fe5cfb17STomasz Sapeta if isTouchIdDevice() {
30fe5cfb17STomasz Sapeta supportedAuthenticationTypes.append(AuthenticationType.fingerprint.rawValue)
31fe5cfb17STomasz Sapeta }
32fe5cfb17STomasz Sapeta
33fe5cfb17STomasz Sapeta if isFaceIdDevice() {
34fe5cfb17STomasz Sapeta supportedAuthenticationTypes.append(AuthenticationType.facialRecognition.rawValue)
35fe5cfb17STomasz Sapeta }
36fe5cfb17STomasz Sapeta
37fe5cfb17STomasz Sapeta return supportedAuthenticationTypes
38fe5cfb17STomasz Sapeta }
39fe5cfb17STomasz Sapeta
40fe5cfb17STomasz Sapeta AsyncFunction("getEnrolledLevelAsync") { () -> Int in
41fe5cfb17STomasz Sapeta let context = LAContext()
42fe5cfb17STomasz Sapeta var error: NSError?
43fe5cfb17STomasz Sapeta
44fe5cfb17STomasz Sapeta var level: Int = SecurityLevel.none.rawValue
45fe5cfb17STomasz Sapeta
46fe5cfb17STomasz Sapeta let isAuthenticationSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: &error)
47fe5cfb17STomasz Sapeta if isAuthenticationSupported && error == nil {
48fe5cfb17STomasz Sapeta level = SecurityLevel.secret.rawValue
49fe5cfb17STomasz Sapeta }
50fe5cfb17STomasz Sapeta
51fe5cfb17STomasz Sapeta let isBiometricsSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error)
52fe5cfb17STomasz Sapeta
53fe5cfb17STomasz Sapeta if isBiometricsSupported && error == nil {
54fe5cfb17STomasz Sapeta level = SecurityLevel.biometric.rawValue
55fe5cfb17STomasz Sapeta }
56fe5cfb17STomasz Sapeta
57fe5cfb17STomasz Sapeta return level
58fe5cfb17STomasz Sapeta }
59fe5cfb17STomasz Sapeta
60fe5cfb17STomasz Sapeta AsyncFunction("authenticateAsync") { (options: LocalAuthenticationOptions, promise: Promise) -> Void in
61*ab35a1c5SKudo Chien var warningMessage: String?
62fe5cfb17STomasz Sapeta var reason = options.promptMessage
63fe5cfb17STomasz Sapeta var cancelLabel = options.cancelLabel
64fe5cfb17STomasz Sapeta var fallbackLabel = options.fallbackLabel
65fe5cfb17STomasz Sapeta var disableDeviceFallback = options.disableDeviceFallback
66fe5cfb17STomasz Sapeta
67fe5cfb17STomasz Sapeta if isFaceIdDevice() {
68fe5cfb17STomasz Sapeta let usageDescription = Bundle.main.object(forInfoDictionaryKey: "NSFaceIDUsageDescription")
69fe5cfb17STomasz Sapeta
70*ab35a1c5SKudo Chien if usageDescription == nil {
71fe5cfb17STomasz Sapeta warningMessage = "FaceID is available but has not been configured. To enable FaceID, provide `NSFaceIDUsageDescription`."
72fe5cfb17STomasz Sapeta }
73fe5cfb17STomasz Sapeta }
74fe5cfb17STomasz Sapeta
75fe5cfb17STomasz Sapeta let context = LAContext()
76fe5cfb17STomasz Sapeta
77fe5cfb17STomasz Sapeta if fallbackLabel != nil {
78fe5cfb17STomasz Sapeta context.localizedFallbackTitle = fallbackLabel
79fe5cfb17STomasz Sapeta }
80fe5cfb17STomasz Sapeta
81fe5cfb17STomasz Sapeta if cancelLabel != nil {
82fe5cfb17STomasz Sapeta context.localizedCancelTitle = cancelLabel
83fe5cfb17STomasz Sapeta }
84fe5cfb17STomasz Sapeta
85fe5cfb17STomasz Sapeta context.interactionNotAllowed = false
86fe5cfb17STomasz Sapeta
87*ab35a1c5SKudo Chien let policyForAuth = disableDeviceFallback ? LAPolicy.deviceOwnerAuthenticationWithBiometrics : LAPolicy.deviceOwnerAuthentication
88fe5cfb17STomasz Sapeta
89fe5cfb17STomasz Sapeta if disableDeviceFallback {
90fe5cfb17STomasz Sapeta if warningMessage != nil {
91fe5cfb17STomasz Sapeta // If the warning message is set (NSFaceIDUsageDescription is not configured) then we can't use
92fe5cfb17STomasz Sapeta // authentication with biometrics — it would crash, so let's just resolve with no success.
93fe5cfb17STomasz Sapeta // We could reject, but we already resolve even if there are any errors, so sadly we would need to introduce a breaking change.
94fe5cfb17STomasz Sapeta return promise.resolve([
95fe5cfb17STomasz Sapeta "success": false,
96fe5cfb17STomasz Sapeta "error": "missing_usage_description",
97fe5cfb17STomasz Sapeta "warning": warningMessage
98fe5cfb17STomasz Sapeta ])
99fe5cfb17STomasz Sapeta }
100fe5cfb17STomasz Sapeta }
101fe5cfb17STomasz Sapeta
102fe5cfb17STomasz Sapeta context.evaluatePolicy(policyForAuth, localizedReason: reason ?? "") { success, error in
103fe5cfb17STomasz Sapeta var err: String?
104fe5cfb17STomasz Sapeta
105fe5cfb17STomasz Sapeta if let error = error as? NSError {
106fe5cfb17STomasz Sapeta err = convertErrorCode(error: error)
107fe5cfb17STomasz Sapeta }
108fe5cfb17STomasz Sapeta
109fe5cfb17STomasz Sapeta return promise.resolve([
110fe5cfb17STomasz Sapeta "success": success,
111fe5cfb17STomasz Sapeta "error": err,
112fe5cfb17STomasz Sapeta "warning": warningMessage
113fe5cfb17STomasz Sapeta ])
114fe5cfb17STomasz Sapeta }
115fe5cfb17STomasz Sapeta }
116fe5cfb17STomasz Sapeta }
117fe5cfb17STomasz Sapeta }
118fe5cfb17STomasz Sapeta
isFaceIdDevicenull119fe5cfb17STomasz Sapeta func isFaceIdDevice() -> Bool {
120fe5cfb17STomasz Sapeta let context = LAContext()
121fe5cfb17STomasz Sapeta context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil)
122fe5cfb17STomasz Sapeta
123fe5cfb17STomasz Sapeta return context.biometryType == LABiometryType.faceID
124fe5cfb17STomasz Sapeta }
125fe5cfb17STomasz Sapeta
isTouchIdDevicenull126fe5cfb17STomasz Sapeta func isTouchIdDevice() -> Bool {
127fe5cfb17STomasz Sapeta let context = LAContext()
128fe5cfb17STomasz Sapeta context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil)
129fe5cfb17STomasz Sapeta
130fe5cfb17STomasz Sapeta return context.biometryType == LABiometryType.touchID
131fe5cfb17STomasz Sapeta }
132fe5cfb17STomasz Sapeta
convertErrorCodenull133fe5cfb17STomasz Sapeta func convertErrorCode(error: NSError) -> String {
134fe5cfb17STomasz Sapeta switch error.code {
135fe5cfb17STomasz Sapeta case LAError.systemCancel.rawValue:
136fe5cfb17STomasz Sapeta return "system_cancel"
137fe5cfb17STomasz Sapeta case LAError.appCancel.rawValue:
138fe5cfb17STomasz Sapeta return "app_cancel"
139fe5cfb17STomasz Sapeta case LAError.biometryLockout.rawValue:
140fe5cfb17STomasz Sapeta return "lockout"
141fe5cfb17STomasz Sapeta case LAError.userFallback.rawValue:
142fe5cfb17STomasz Sapeta return "user_fallback"
143fe5cfb17STomasz Sapeta case LAError.userCancel.rawValue:
144fe5cfb17STomasz Sapeta return "user_cancel"
145fe5cfb17STomasz Sapeta case LAError.biometryNotAvailable.rawValue:
146fe5cfb17STomasz Sapeta return "not_available"
147fe5cfb17STomasz Sapeta case LAError.invalidContext.rawValue:
148fe5cfb17STomasz Sapeta return "invalid_context"
149fe5cfb17STomasz Sapeta case LAError.biometryNotEnrolled.rawValue:
150fe5cfb17STomasz Sapeta return "not_enrolled"
151fe5cfb17STomasz Sapeta case LAError.passcodeNotSet.rawValue:
152fe5cfb17STomasz Sapeta return "passcode_not_set"
153fe5cfb17STomasz Sapeta case LAError.authenticationFailed.rawValue:
154fe5cfb17STomasz Sapeta return "authentication_failed"
155fe5cfb17STomasz Sapeta default:
156fe5cfb17STomasz Sapeta return "unknown: \(error.code), \(error.localizedDescription)"
157fe5cfb17STomasz Sapeta }
158fe5cfb17STomasz Sapeta }
159fe5cfb17STomasz Sapeta
160fe5cfb17STomasz Sapeta enum AuthenticationType: Int {
161fe5cfb17STomasz Sapeta case fingerprint = 1
162fe5cfb17STomasz Sapeta case facialRecognition = 2
163fe5cfb17STomasz Sapeta }
164fe5cfb17STomasz Sapeta
165fe5cfb17STomasz Sapeta enum SecurityLevel: Int {
166fe5cfb17STomasz Sapeta case none = 0
167fe5cfb17STomasz Sapeta case secret = 1
168fe5cfb17STomasz Sapeta case biometric = 2
169fe5cfb17STomasz Sapeta }
170