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