@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 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
- stores that state as the baseline for subsequent calls.
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. A missing current domain state returns `false`.
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 no comparison was possible. This API does not
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
- private static func loadDomainState() -> Data? {
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
- return Promise.async {
46
- var changed = false
47
- let context = LAContext()
48
- _ = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)
49
- let domainState = context.evaluatedPolicyDomainState
50
- if domainState == nil {
51
- OneKeyLog.warn("Biometric", "evaluatedPolicyDomainState is nil, cannot determine biometric changes")
52
- return false
53
- }
54
- let oldDomainState = Self.loadDomainState()
55
- if let oldDomainState = oldDomainState {
56
- changed = oldDomainState != domainState
57
- } else {
58
- // Migrate from UserDefaults if exists
59
- let defaults = UserDefaults.standard
60
- if let legacyState = defaults.data(forKey: "biometricAuthState") {
61
- changed = legacyState != domainState
62
- defaults.removeObject(forKey: "biometricAuthState")
63
- } else {
64
- OneKeyLog.info("Biometric", "No previous biometric state stored, saving initial state")
65
- }
66
- }
67
- Self.saveDomainState(domainState)
68
- if changed {
69
- OneKeyLog.info("Biometric", "Biometric auth change detected")
70
- }
71
- return changed
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.0.179",
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",