Skip to content

Commit d0fe4ec

Browse files
Merge pull request #12 from JoaoPauloCMarra/fix/keychain-locked-migration-crash
v0.4.2/v0.4.3
2 parents 6cf9649 + e90e404 commit d0fe4ec

38 files changed

Lines changed: 3958 additions & 2367 deletions

.github/workflows/ci.yml

Lines changed: 23 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,54 @@
11
name: CI
22

33
on:
4-
push:
5-
branches: [main]
64
pull_request:
75
branches: [main]
86

7+
env:
8+
TZ: UTC
9+
BUN_VERSION: "1.3.10"
10+
911
jobs:
1012
quality:
11-
name: Quality Checks
13+
name: Lint, Typecheck & Unit Tests
1214
runs-on: ubuntu-latest
1315

1416
steps:
15-
- name: Checkout code
16-
uses: actions/checkout@v4
17+
- uses: actions/checkout@v6
1718

18-
- name: Setup Bun
19-
uses: oven-sh/setup-bun@v2
19+
- uses: oven-sh/setup-bun@v2
2020
with:
21-
bun-version: 1.3.9
21+
bun-version: ${{ env.BUN_VERSION }}
2222

23-
- name: Cache dependencies
24-
uses: actions/cache@v4
23+
- uses: actions/cache@v5
2524
with:
2625
path: |
2726
~/.bun/install/cache
2827
node_modules
2928
packages/*/node_modules
29+
apps/*/node_modules
3030
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
31-
restore-keys: |
32-
${{ runner.os }}-bun-
31+
restore-keys: ${{ runner.os }}-bun-
32+
33+
- run: bun install --frozen-lockfile
3334

34-
- name: Install dependencies
35-
run: bun install
35+
- name: Lint
36+
run: bun run lint
3637

37-
- name: Run type checking
38+
- name: Format check
39+
run: bun run format:check
40+
41+
- name: Typecheck
3842
run: bun run typecheck
3943

40-
- name: Run type-level API tests
44+
- name: Type-level API tests
4145
run: bun run test:types
4246

43-
- name: Run JavaScript tests
44-
run: bun run test
45-
46-
- name: Run JavaScript tests with coverage
47+
- name: Unit tests with coverage
4748
run: bun run test:coverage
4849

49-
- name: Run benchmark regression checks
50+
- name: Benchmark regression checks
5051
run: bun run benchmark
5152

52-
- name: Setup CMake (for C++ tests)
53-
run: |
54-
cmake --version
55-
gcc --version || clang --version || echo "C++ compiler available"
56-
57-
- name: Run C++ tests
53+
- name: C++ tests
5854
run: bun run test:cpp
59-
60-
- name: Build packages
61-
run: bun run build
62-
63-
- name: Verify build artifacts
64-
run: |
65-
# Check that lib directories were created
66-
[ -d "packages/react-native-nitro-storage/lib" ] || exit 1
67-
# Check that TypeScript definitions exist
68-
[ -f "packages/react-native-nitro-storage/lib/typescript/index.d.ts" ] || exit 1
69-
# Check that all build targets exist
70-
[ -d "packages/react-native-nitro-storage/lib/commonjs" ] || exit 1
71-
[ -d "packages/react-native-nitro-storage/lib/module" ] || exit 1
72-
echo "✅ Build artifacts verified"
73-
74-
- name: Test package packaging
75-
run: |
76-
cd packages/react-native-nitro-storage
77-
bun run check:pack
78-
79-
build-example-android:
80-
name: Android Example Build
81-
runs-on: ubuntu-latest
82-
needs: quality
83-
84-
steps:
85-
- name: Checkout code
86-
uses: actions/checkout@v4
87-
88-
- name: Setup Bun
89-
uses: oven-sh/setup-bun@v2
90-
with:
91-
bun-version: 1.3.9
92-
93-
- name: Setup Java
94-
uses: actions/setup-java@v4
95-
with:
96-
distribution: temurin
97-
java-version: "17"
98-
99-
- name: Install dependencies
100-
run: bun install
101-
102-
- name: Expo prebuild (Android)
103-
run: bun run example:prebuild:clean -- -- --platform android --non-interactive
104-
env:
105-
EXPO_NO_TELEMETRY: 1
106-
107-
- name: Gradle build check
108-
working-directory: apps/example/android
109-
run: ./gradlew assembleDebug --no-daemon -q
110-

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@ All notable changes to this project are documented in this file.
44

55
The format follows Keep a Changelog and the project adheres to SemVer.
66

7+
## 0.4.2/0.4.3 - 2026-03-05
8+
9+
### Fixed
10+
11+
- Fix crash on Android devices without biometric hardware — all biometric storage paths now catch initialization failures gracefully (non-biometric operations unaffected).
12+
- Fix Android keystore corruption recovery incorrectly wiping data on a locked keystore — only `AEADBadTagException` now triggers wipe; all other init failures throw without touching stored data.
13+
- Synchronize `AndroidStorageAdapter.invalidateSecureKeysCache()` under instance lock to close a race between concurrent reads and writes.
14+
- Synchronize `setSecureBatch`/`deleteSecureBatch` under instance lock to prevent cache rebuild racing a mid-batch write.
15+
- Propagate `SharedPreferences.commit()` failures out of `applySecureEditor` instead of swallowing them.
16+
- Fix `IOSStorageAdapterCpp::clearDisk()` using `dictionaryRepresentation` (includes OS-injected keys) — switched to `persistentDomainForName:` scoped strictly to the app suite.
17+
- Fix `clearSecure()`/`clearSecureBiometric()` clearing the in-memory key cache before confirming `SecItemDelete` succeeded — cache is now only updated after the deletion is confirmed.
18+
- Fix potential unexpected biometric auth prompt in `getSecure()` — added `kSecUseAuthenticationUI = kSecUseAuthenticationUIFail` consistent with `hasSecure()`.
19+
- Fix `setKeychainAccessGroup()` race where a concurrent `getAllKeysSecure()` could observe a stale cache between group update and cache invalidation — both are now updated atomically under both mutexes.
20+
- Fix CFErrorRef leak in `SecAccessControlCreateWithFlags` error path.
21+
- Fix `setSecureBiometricWithLevel()` incorrectly reporting "value restored" when backup restoration itself threw — now propagates the composite error.
22+
- Mark `secureKeyCacheHydrated_` as `std::atomic<bool>` to satisfy the C++ memory model.
23+
- Fix `HybridStorage::addOnChange()` unsubscribe lambda capturing `this` raw pointer — switched to `std::weak_ptr` capture to prevent use-after-free if `HybridStorage` is destroyed before the JS unsubscribe callback fires.
24+
- Validate access control level in `setSecureAccessControl()` (must be 0–4) and biometric level in `setSecureBiometricWithLevel()` (must be 0–2) — invalid values now throw instead of being silently passed to the native adapter.
25+
- Fix `clearSecureBiometric()` calling `onScopeClear` which unnecessarily evicted all secure keys from the index — now only marks the index stale for lazy re-hydration.
26+
- Fix `fromJavaStringArray()` silently dropping null JNI array elements — null entries are now preserved as empty strings to maintain positional alignment.
27+
- Extend `isKeychainLockedError()` to detect Android `KeyPermanentlyInvalidatedException` and `InvalidKeyException` in addition to existing iOS/Android patterns.
28+
- Fix web `getAll()` performing O(n) individual reads — switched to `WebStorage.getBatch()`.
29+
- Fix web `subscribe()` accumulating `window.addEventListener("storage", …)` calls — now reference-counted and removed when the last subscriber unsubscribes.
30+
- Fix web `import()` for Secure scope skipping `flushSecureWrites()` and `setSecureAccessControl()` before writing.
31+
- Expand ProGuard/R8 keep rules with explicit method-signature patterns so JNI-callable methods survive aggressive R8 shrinking in release builds.
32+
733
## 0.4.1 - 2026-03-04
834

935
### Added

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Every feature in this package is documented with at least one runnable example i
4747
- Transactions — see Transactions and Atomic Balance Transfer
4848
- Migrations (`registerMigration`, `migrateToLatest`) — see Migrations
4949
- MMKV migration (`migrateFromMMKV`) — see MMKV Migration and Migrating From MMKV
50+
- Raw string API (`getString`, `setString`, `deleteString`) — see Raw String API
51+
- Keychain locked detection (`isKeychainLockedError`) — see `isKeychainLockedError(err)`
5052
- Auth storage factory (`createSecureAuthStorage`) — see Auth Token Management
5153

5254
## Requirements
@@ -280,6 +282,9 @@ import { storage, StorageScope } from "react-native-nitro-storage";
280282
| `storage.setSecureWritesAsync(enabled)` | Toggle async secure writes on Android (`false` by default) |
281283
| `storage.flushSecureWrites()` | Force flush of queued secure writes when coalescing is enabled |
282284
| `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
285+
| `storage.getString(key, scope)` | Read a raw string value directly (bypasses serialization) |
286+
| `storage.setString(key, value, scope)` | Write a raw string value directly (bypasses serialization) |
287+
| `storage.deleteString(key, scope)` | Delete a raw string value by key |
283288
| `storage.import(data, scope)` | Bulk-load a `Record<string, string>` of raw key/value pairs into a scope |
284289
| `storage.setMetricsObserver(observer?)` | Subscribe to per-operation timing events |
285290
| `storage.getMetricsSnapshot()` | Get aggregate counters/latency stats keyed by operation |
@@ -509,6 +514,40 @@ const migrated = migrateFromMMKV(mmkv, myStorageItem, true);
509514

510515
---
511516

517+
### Raw String API
518+
519+
For cases where you want to bypass `createStorageItem` serialization entirely and work with raw key/value strings:
520+
521+
```ts
522+
import { storage, StorageScope } from "react-native-nitro-storage";
523+
524+
storage.setString("raw-key", "raw-value", StorageScope.Disk);
525+
const value = storage.getString("raw-key", StorageScope.Disk); // "raw-value" | undefined
526+
storage.deleteString("raw-key", StorageScope.Disk);
527+
```
528+
529+
These are synchronous and go directly to the native backend without any serialize/deserialize step.
530+
531+
---
532+
533+
### `isKeychainLockedError(err)`
534+
535+
Utility to detect iOS Keychain locked errors and Android key invalidation errors in secure storage operations. Returns `true` if the error was caused by a locked keychain (device locked, first unlock not yet performed, etc.) or an Android `KeyPermanentlyInvalidatedException` / `InvalidKeyException`. Always returns `false` on web.
536+
537+
```ts
538+
import { isKeychainLockedError } from "react-native-nitro-storage";
539+
540+
try {
541+
secureItem.get();
542+
} catch (err) {
543+
if (isKeychainLockedError(err)) {
544+
// device is locked — retry after unlock
545+
}
546+
}
547+
```
548+
549+
---
550+
512551
### Enums
513552

514553
#### `AccessControl`

apps/example/.maestro/main.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
appId: com.nitrostorage.example
2+
name: "Nitro Storage - Smoke Test"
3+
---
4+
- launchApp:
5+
clearState: true
6+
7+
# Scroll to the Smoke Test runner at the bottom
8+
- scrollUntilVisible:
9+
element:
10+
id: "smoke-run-all"
11+
direction: DOWN
12+
timeout: 20000
13+
speed: 80
14+
15+
# Tap "Run All" to execute every smoke test
16+
- tapOn:
17+
id: "smoke-run-all"
18+
19+
# Wait for the test run to complete (button text reverts to "Run All")
20+
- extendedWaitUntil:
21+
visible:
22+
text: "Run All"
23+
id: "smoke-run-all"
24+
timeout: 30000
25+
26+
# Assert no test failed
27+
- assertNotVisible: "failed"

apps/example/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"userInterfaceStyle": "automatic",
1010
"ios": {
1111
"supportsTablet": true,
12-
"bundleIdentifier": "com.nitrostorage.example"
12+
"bundleIdentifier": "com.nitrostorage.example",
1313
},
1414
"android": {
1515
"package": "com.nitrostorage.example"

0 commit comments

Comments
 (0)