@onekeyfe/react-native-check-biometric-auth-changed 3.0.179 → 6.7.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -3
- package/docs/SPEC.md +87 -0
- package/ios/BiometricAuthStateStore.swift +57 -0
- package/ios/ReactNativeCheckBiometricAuthChanged.swift +31 -59
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -29,14 +29,16 @@ if (changed) {
|
|
|
29
29
|
|
|
30
30
|
- iOS reads LocalAuthentication's evaluated policy domain state and compares it
|
|
31
31
|
with the previous value stored in Keychain. A check with available state
|
|
32
|
-
|
|
32
|
+
successfully saves that state as the baseline before resolving the current call.
|
|
33
33
|
- The first available state establishes a baseline unless legacy stored state
|
|
34
|
-
exists.
|
|
34
|
+
exists. Missing current domain state or failed Keychain reads/writes reject.
|
|
35
|
+
Only `errSecItemNotFound` means an absent stored baseline; legacy state is
|
|
36
|
+
deleted only after the new baseline has been saved successfully.
|
|
35
37
|
- Android currently returns `false` from a stub implementation. It does not
|
|
36
38
|
detect enrollment changes.
|
|
37
39
|
|
|
38
40
|
A `false` result is not proof that biometrics are available or that the user has
|
|
39
|
-
authenticated: it can also mean
|
|
41
|
+
authenticated: on iOS it can also mean a first baseline was established. This API does not
|
|
40
42
|
replace an authentication challenge, and has no Web implementation.
|
|
41
43
|
|
|
42
44
|
The wrapper is in [src/index.tsx](src/index.tsx); its native interface is
|
|
@@ -44,6 +46,40 @@ The wrapper is in [src/index.tsx](src/index.tsx); its native interface is
|
|
|
44
46
|
Review the platform sources when integrating this signal into an authentication
|
|
45
47
|
workflow.
|
|
46
48
|
|
|
49
|
+
## Interpreting repeated checks
|
|
50
|
+
|
|
51
|
+
`checkBiometricAuthChanged()` returns only a boolean: there is no enrollment
|
|
52
|
+
count, authentication credential or event subscription in this API. On iOS the
|
|
53
|
+
check persists an available comparison baseline before resolving. A process-wide
|
|
54
|
+
synchronous transaction lock serializes checks on the native background queue;
|
|
55
|
+
callers still own authentication and UI policy.
|
|
56
|
+
|
|
57
|
+
A changed result is consumed by that baseline update. Persist a checkpoint before
|
|
58
|
+
checking, and retain it if the application cannot finish disabling biometrics.
|
|
59
|
+
After an interrupted check, require password/passcode recovery rather than
|
|
60
|
+
trusting a fresh `false`; see the transaction requirements in the contract.
|
|
61
|
+
|
|
62
|
+
Handle native-operation rejection separately from a resolved `false`. Neither
|
|
63
|
+
an Android stub result nor an iOS rejection establishes that enrollment
|
|
64
|
+
has remained unchanged. Applications must handle rejection as unavailable, rather
|
|
65
|
+
than substituting `false`. See [the contract](docs/SPEC.md) for error and migration
|
|
66
|
+
boundaries. These iOS changes require a new SDK release and an application rebuild. The package exports the Nitro interface as a type for
|
|
67
|
+
adapters; the public JavaScript call remains `checkBiometricAuthChanged()`.
|
|
68
|
+
|
|
47
69
|
## License
|
|
48
70
|
|
|
49
71
|
MIT.
|
|
72
|
+
|
|
73
|
+
## Focused state tests
|
|
74
|
+
|
|
75
|
+
From this package directory in a repository source checkout on macOS with the
|
|
76
|
+
Xcode command-line tools, run the actual state transaction core
|
|
77
|
+
with deterministic provider failures (without accessing a real Keychain):
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
xcrun swiftc ios/BiometricAuthStateStore.swift tests/BiometricAuthStateStoreTests.swift -o /tmp/biometric-state-tests
|
|
81
|
+
/tmp/biometric-state-tests
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The runner checks eleven scenarios, including concurrent transactions. It does not
|
|
85
|
+
replace device enrollment, locked-Keychain or native bridge acceptance tests.
|
package/docs/SPEC.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Biometric authentication state contract
|
|
2
|
+
|
|
3
|
+
Status: Implemented. Eleven deterministic Swift core scenarios and the iOS Simulator
|
|
4
|
+
module build pass; real device Keychain/LocalAuthentication acceptance is pending.
|
|
5
|
+
|
|
6
|
+
## Purpose and ownership
|
|
7
|
+
|
|
8
|
+
`checkBiometricAuthChanged(): Promise<boolean>` compares the current iOS
|
|
9
|
+
LocalAuthentication domain state with a saved comparison baseline. The application
|
|
10
|
+
owns authentication challenges and the security policy applied to the result.
|
|
11
|
+
The package neither authenticates a user nor authorizes a password operation.
|
|
12
|
+
The Keychain baseline is shared within the native process, including independent
|
|
13
|
+
JavaScript runtimes. It is not a JavaScript-runtime-local store.
|
|
14
|
+
|
|
15
|
+
## API and platform behavior
|
|
16
|
+
|
|
17
|
+
- iOS resolves `true` when a previous Keychain or legacy baseline differs from
|
|
18
|
+
the current domain state, and `false` when it matches or when a first baseline
|
|
19
|
+
is successfully established. Every successful comparison persists the current
|
|
20
|
+
state as the next baseline before resolving.
|
|
21
|
+
- `false` is not proof of authentication or biometric availability. Callers MUST
|
|
22
|
+
distinguish promise rejection from a resolved `false` and MUST NOT continue a
|
|
23
|
+
biometric-only path when the state check is unavailable.
|
|
24
|
+
- Android retains its existing stub: it resolves `false`, does not persist a
|
|
25
|
+
baseline and does not detect enrollment changes. Web is unsupported.
|
|
26
|
+
- No new JavaScript return type, event or configuration API is introduced.
|
|
27
|
+
|
|
28
|
+
## Transaction, migration and concurrency
|
|
29
|
+
|
|
30
|
+
A synchronous process-wide lock serializes the complete iOS operation: obtain
|
|
31
|
+
current state, read the saved state, compare and persist. Work runs through Nitro `Promise.parallel` on its default global dispatch queue,
|
|
32
|
+
not on the UI thread or an inherited Swift task executor. Lock release is required on every exit.
|
|
33
|
+
The opaque domain state is neither returned to JavaScript nor logged.
|
|
34
|
+
|
|
35
|
+
The comparison is consumptive: after a changed result advances the baseline,
|
|
36
|
+
the same state returns `false` on a subsequent check. Application-side handling
|
|
37
|
+
is a separate transaction. Before invoking this API, a security-sensitive caller
|
|
38
|
+
must durably checkpoint that a check is in progress, serialize its check and
|
|
39
|
+
settings update, and clear the checkpoint only after an unchanged result or
|
|
40
|
+
successful handling of a changed result. A failed or interrupted operation must
|
|
41
|
+
remain blocked across UI remounts and process restarts until password/passcode
|
|
42
|
+
verification resolves the uncertainty. JavaScript component state alone is
|
|
43
|
+
insufficient; independent runtimes share the native baseline, not their heaps.
|
|
44
|
+
This caller requirement does not change the SDK boolean API or baseline behavior.
|
|
45
|
+
|
|
46
|
+
Only `errSecItemNotFound` means the Keychain baseline is absent. In that case,
|
|
47
|
+
legacy `UserDefaults["biometricAuthState"]` participates in the comparison.
|
|
48
|
+
The legacy value is removed only after current-state persistence succeeds;
|
|
49
|
+
read/write failure MUST leave it intact. A first-use result can resolve `false`
|
|
50
|
+
only after persistence succeeds. Updating the Keychain item may fall back to add
|
|
51
|
+
only on `errSecItemNotFound`; other update errors MUST NOT attempt an add.
|
|
52
|
+
|
|
53
|
+
## Failure and safety
|
|
54
|
+
|
|
55
|
+
A missing current domain state, a Keychain read failure, successful read with
|
|
56
|
+
non-Data result, update failure or add failure rejects the promise. These cases
|
|
57
|
+
MUST NOT silently become an absent baseline or a resolved `false`. Keychain
|
|
58
|
+
failures include operation and OSStatus in the error message, without state data;
|
|
59
|
+
errors use the `BiometricAuthChanged` NSError domain. Unavailable current state
|
|
60
|
+
uses code 1; malformed successful reads use `errSecDecode`; Keychain operations
|
|
61
|
+
retain their OSStatus code. Native error transport need not expose NSError fields
|
|
62
|
+
as stable JavaScript properties.
|
|
63
|
+
|
|
64
|
+
Changing this failure contract does not add enrollment notification, cancellation,
|
|
65
|
+
late-loading recovery, or a new biometric prompt. Consumers can keep password or
|
|
66
|
+
passcode authentication available when the state check fails. Already-published
|
|
67
|
+
3.0.165 artifacts remain unchanged; the new native contract takes effect only
|
|
68
|
+
when the SDK is published, upgraded and rebuilt by the application.
|
|
69
|
+
|
|
70
|
+
## Conformance and acceptance
|
|
71
|
+
|
|
72
|
+
The iOS wrapper owns LocalAuthentication and Security calls. A small state core
|
|
73
|
+
uses injected current-state, Keychain and legacy operations so deterministic Swift
|
|
74
|
+
executable tests exercise the actual transaction and error handling without
|
|
75
|
+
altering a user's Keychain. Checks cover read errors/type mismatch, nil state,
|
|
76
|
+
update/add failures, successful absent baseline, changed/unchanged comparisons,
|
|
77
|
+
legacy preservation/removal and serialized concurrent checks.
|
|
78
|
+
|
|
79
|
+
A real package build and tarball must contain the core and this spec, preserve the
|
|
80
|
+
Nitro boolean signature, and resolve the existing native module name. App tests
|
|
81
|
+
must prove that rejection blocks automatic, manual and foreground biometric
|
|
82
|
+
attempts while password entry remains available, without falsely disabling the
|
|
83
|
+
persistent biometric switch or showing an enrollment-change warning.
|
|
84
|
+
|
|
85
|
+
Actual iOS device Keychain/LocalAuthentication behavior, protected-data states,
|
|
86
|
+
biometric enrollment changes and App authentication UI remain runtime acceptance
|
|
87
|
+
requirements; deterministic injected-operation tests do not establish those.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
enum BiometricAuthStateStore {
|
|
5
|
+
private static let transactionLock = NSLock()
|
|
6
|
+
|
|
7
|
+
static func check(
|
|
8
|
+
currentState: () -> Data?,
|
|
9
|
+
read: () -> (OSStatus, AnyObject?),
|
|
10
|
+
update: (Data) -> OSStatus,
|
|
11
|
+
add: (Data) -> OSStatus,
|
|
12
|
+
legacyState: () -> Data?,
|
|
13
|
+
removeLegacyState: () -> Void
|
|
14
|
+
) throws -> Bool {
|
|
15
|
+
transactionLock.lock()
|
|
16
|
+
defer { transactionLock.unlock() }
|
|
17
|
+
|
|
18
|
+
guard let current = currentState() else {
|
|
19
|
+
throw failure("Current biometric domain state is unavailable", status: 1)
|
|
20
|
+
}
|
|
21
|
+
let (readStatus, result) = read()
|
|
22
|
+
let previous: Data?
|
|
23
|
+
let migrating: Bool
|
|
24
|
+
switch readStatus {
|
|
25
|
+
case errSecSuccess:
|
|
26
|
+
guard let data = result as? Data else {
|
|
27
|
+
throw failure("Keychain read returned invalid data", status: errSecDecode)
|
|
28
|
+
}
|
|
29
|
+
previous = data
|
|
30
|
+
migrating = false
|
|
31
|
+
case errSecItemNotFound:
|
|
32
|
+
previous = legacyState()
|
|
33
|
+
migrating = previous != nil
|
|
34
|
+
default:
|
|
35
|
+
throw failure("Keychain read failed", status: readStatus)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let updateStatus = update(current)
|
|
39
|
+
if updateStatus == errSecItemNotFound {
|
|
40
|
+
let addStatus = add(current)
|
|
41
|
+
guard addStatus == errSecSuccess else {
|
|
42
|
+
throw failure("Keychain add failed", status: addStatus)
|
|
43
|
+
}
|
|
44
|
+
} else if updateStatus != errSecSuccess {
|
|
45
|
+
throw failure("Keychain update failed", status: updateStatus)
|
|
46
|
+
}
|
|
47
|
+
if migrating { removeLegacyState() }
|
|
48
|
+
return previous.map { $0 != current } ?? false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private static func failure(_ message: String, status: OSStatus) -> NSError {
|
|
52
|
+
NSError(
|
|
53
|
+
domain: "BiometricAuthChanged", code: Int(status),
|
|
54
|
+
userInfo: [NSLocalizedDescriptionKey: "\(message) (OSStatus \(status))"]
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,74 +1,46 @@
|
|
|
1
|
-
|
|
2
1
|
import NitroModules
|
|
3
2
|
import LocalAuthentication
|
|
4
3
|
import Security
|
|
5
|
-
import ReactNativeNativeLogger
|
|
6
4
|
|
|
7
5
|
class ReactNativeCheckBiometricAuthChanged: HybridReactNativeCheckBiometricAuthChangedSpec {
|
|
8
|
-
|
|
9
6
|
private static let keychainKey = "com.onekey.biometricAuthState"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
let query: [String: Any] = [
|
|
7
|
+
private static var identityQuery: [String: Any] {
|
|
8
|
+
[
|
|
13
9
|
kSecClass as String: kSecClassGenericPassword,
|
|
14
10
|
kSecAttrAccount as String: keychainKey,
|
|
15
|
-
kSecReturnData as String: true,
|
|
16
|
-
kSecMatchLimit as String: kSecMatchLimitOne
|
|
17
11
|
]
|
|
18
|
-
var result: AnyObject?
|
|
19
|
-
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
20
|
-
if status == errSecSuccess {
|
|
21
|
-
return result as? Data
|
|
22
|
-
}
|
|
23
|
-
return nil
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private static func saveDomainState(_ data: Data?) {
|
|
27
|
-
guard let data = data else { return }
|
|
28
|
-
let query: [String: Any] = [
|
|
29
|
-
kSecClass as String: kSecClassGenericPassword,
|
|
30
|
-
kSecAttrAccount as String: keychainKey,
|
|
31
|
-
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
|
32
|
-
]
|
|
33
|
-
let attrs: [String: Any] = [
|
|
34
|
-
kSecValueData as String: data
|
|
35
|
-
]
|
|
36
|
-
let status = SecItemUpdate(query as CFDictionary, attrs as CFDictionary)
|
|
37
|
-
if status == errSecItemNotFound {
|
|
38
|
-
var addQuery = query
|
|
39
|
-
addQuery[kSecValueData as String] = data
|
|
40
|
-
SecItemAdd(addQuery as CFDictionary, nil)
|
|
41
|
-
}
|
|
42
12
|
}
|
|
43
13
|
|
|
44
14
|
func checkChanged() throws -> Promise<Bool> {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
15
|
+
Promise.parallel {
|
|
16
|
+
let defaults = UserDefaults.standard
|
|
17
|
+
return try BiometricAuthStateStore.check(
|
|
18
|
+
currentState: {
|
|
19
|
+
let context = LAContext()
|
|
20
|
+
_ = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)
|
|
21
|
+
return context.evaluatedPolicyDomainState
|
|
22
|
+
},
|
|
23
|
+
read: {
|
|
24
|
+
var query = Self.identityQuery
|
|
25
|
+
query[kSecReturnData as String] = true
|
|
26
|
+
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
27
|
+
var result: AnyObject?
|
|
28
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
29
|
+
return (status, result)
|
|
30
|
+
},
|
|
31
|
+
update: { data in
|
|
32
|
+
let attributes: [String: Any] = [kSecValueData as String: data]
|
|
33
|
+
return SecItemUpdate(Self.identityQuery as CFDictionary, attributes as CFDictionary)
|
|
34
|
+
},
|
|
35
|
+
add: { data in
|
|
36
|
+
var query = Self.identityQuery
|
|
37
|
+
query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
|
38
|
+
query[kSecValueData as String] = data
|
|
39
|
+
return SecItemAdd(query as CFDictionary, nil)
|
|
40
|
+
},
|
|
41
|
+
legacyState: { defaults.data(forKey: "biometricAuthState") },
|
|
42
|
+
removeLegacyState: { defaults.removeObject(forKey: "biometricAuthState") }
|
|
43
|
+
)
|
|
72
44
|
}
|
|
73
45
|
}
|
|
74
46
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-check-biometric-auth-changed",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.7.0-1",
|
|
4
4
|
"description": "Detect stored biometric authentication state changes on iOS",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"ios",
|
|
20
20
|
"cpp",
|
|
21
21
|
"nitrogen",
|
|
22
|
+
"docs",
|
|
22
23
|
"nitro.json",
|
|
23
24
|
"*.podspec",
|
|
24
25
|
"react-native.config.js",
|