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