Skip to content

Commit 5017b91

Browse files
authored
Merge pull request #586 from mCodex/feat/nitro35Support
Feat/nitro35 support
2 parents 635dfa9 + 3dc9dfc commit 5017b91

115 files changed

Lines changed: 9481 additions & 9425 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/android-build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343

4444
- uses: actions/setup-node@v4
4545
with:
46-
node-version: '22'
46+
node-version: '24'
4747
cache: yarn
4848

4949
- name: Install dependencies (yarn)
@@ -55,11 +55,11 @@ jobs:
5555
- name: Generate Nitro modules (codegen)
5656
run: yarn codegen
5757

58-
- name: Setup JDK 17
58+
- name: Setup JDK 21
5959
uses: actions/setup-java@v5
6060
with:
6161
distribution: zulu
62-
java-version: '17'
62+
java-version: '21'
6363
cache: gradle
6464

6565
- name: Cache Gradle

.github/workflows/ios-build.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
- uses: actions/checkout@v4
5454
- uses: actions/setup-node@v4
5555
with:
56-
node-version: '22'
56+
node-version: '24'
5757
cache: 'yarn'
5858
- name: Setup Xcode
5959
uses: maxim-lobanov/setup-xcode@v1
@@ -72,7 +72,7 @@ jobs:
7272
- name: Setup Ruby (bundle)
7373
uses: ruby/setup-ruby@v1
7474
with:
75-
ruby-version: '3.2'
75+
ruby-version: '3.4'
7676
bundler-cache: true
7777
working-directory: example/ios
7878

@@ -103,6 +103,6 @@ jobs:
103103
-scheme SensitiveInfoExample \
104104
-sdk iphonesimulator \
105105
-configuration Debug \
106-
-destination 'platform=iOS Simulator,name=iPhone 16' \
106+
-destination 'platform=iOS Simulator,name=iPhone 17' \
107107
build \
108108
CODE_SIGNING_ALLOWED=NO | xcpretty

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Setup Node.js
2828
uses: actions/setup-node@v4
2929
with:
30-
node-version: '22'
30+
node-version: '24'
3131
cache: 'yarn'
3232

3333
- name: Install npm dependencies (yarn)

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535

3636
- uses: actions/setup-node@v4
3737
with:
38-
node-version: '22'
38+
node-version: '24'
3939
cache: yarn
4040

4141
- name: Install dependencies

.prettierrc.js

Lines changed: 0 additions & 8 deletions
This file was deleted.

.release-it.json

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
{
2-
"git": {
3-
"commitMessage": "chore(release): v${version}",
4-
"tagName": "v${version}",
5-
"requireCleanWorkingDir": true,
6-
"push": true,
7-
"requireUpstream": false
8-
},
9-
"npm": {
10-
"publish": true,
11-
"verifyAccess": true,
12-
"tokenRef": "NPM_TOKEN"
13-
},
14-
"github": {
15-
"release": true,
16-
"releaseName": "v${version}",
17-
"tokenRef": "GITHUB_TOKEN"
18-
},
19-
"hooks": {
20-
"before:init": ["npm run typecheck", "npm run build", "npm run codegen"],
21-
"after:bump": "conventional-changelog -p conventionalcommits -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
22-
"after:release": "git push --follow-tags"
23-
}
2+
"git": {
3+
"commitMessage": "chore(release): v${version}",
4+
"tagName": "v${version}",
5+
"requireCleanWorkingDir": true,
6+
"push": true,
7+
"requireUpstream": false
8+
},
9+
"npm": {
10+
"publish": true,
11+
"verifyAccess": true,
12+
"tokenRef": "NPM_TOKEN"
13+
},
14+
"github": {
15+
"release": true,
16+
"releaseName": "v${version}",
17+
"tokenRef": "GITHUB_TOKEN"
18+
},
19+
"hooks": {
20+
"before:init": ["npm run typecheck", "npm run build", "npm run codegen"],
21+
"after:bump": "conventional-changelog -p conventionalcommits -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md",
22+
"after:release": "git push --follow-tags"
23+
}
2424
}

CHANGELOG.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,38 @@
1+
## Unreleased (6.0.0-rc.13)
2+
3+
### Features
4+
5+
* **rotation:** Add versioned key rotation via `rotateKeys()` and `getKeyVersion()` with lazy re-encryption on read. New `useKeyRotation` hook exposes the same flow declaratively.
6+
* **security hardening:** Defense-in-depth pass — non-breaking, applied transparently to new writes and via lazy upgrade on rotation:
7+
- HMAC-SHA256 integrity tag bound to every entry's metadata + ciphertext, surfaced on `StorageMetadata.integrityTag`. Tampering with SharedPreferences/Keychain attributes now raises `IntegrityViolationError` (`E_INTEGRITY_VIOLATION`) before any biometric prompt is shown.
8+
- AES-GCM AAD on Android binds ciphertext to `service|key|v<version>`, defeating cross-entry swap attacks.
9+
- `setUnlockedDeviceRequired(true)` on every Android Keystore key (API 28+), mirroring iOS's `kSecAttrAccessibleWhenUnlocked` semantics.
10+
- Plaintext byte buffers are zeroized after use on both platforms.
11+
- Constant-time HMAC comparison via `MessageDigest.isEqual` / manual `UInt8` XOR fold.
12+
- Backwards compatible: entries written by earlier versions decode without verification and are upgraded on the next write or rotation.
13+
* **errors:** New typed error classes (`SensitiveInfoError`, `NotFoundError`, `AuthenticationCanceledError`, `IntegrityViolationError`, `KeyInvalidatedError`, `RotationFailedError`) with `code` discriminants and `instanceof` predicates. Importable from the `react-native-sensitive-info/errors` subpath.
14+
* **tree-shaking:** `"sideEffects": false` everywhere; the package now publishes three focused subpath entries (`.`, `/hooks`, `/errors`). The default export has been removed — import only the helpers you use.
15+
* **nitro 0.35:** Regenerated against `nitrogen@0.35.5` and `react-native-nitro-modules@0.35.5`.
16+
* **tooling:** Migrated linting/formatting from ESLint + Prettier to **Biome 2**. Single config at `biome.json`, faster CI runs.
17+
18+
### Refactor (KISS · DRY · SRP)
19+
20+
* Introduced `useAsyncQuery` (read-only hooks) and `useMutation` (mutation hooks) primitives. `useHasSecret`, `useSecretItem`, `useSecureOperation`, `useKeyRotation`, and `useSecureStorage` now compose the same lifecycle/abort/error-handling pipeline — no duplicated state machines.
21+
* `useSecureStorage` shrunk from ~230 LOC to ~180 LOC and reuses the shared abort + auth-cancel semantics; behaviour is unchanged.
22+
* Test fixtures consolidated in `src/__tests__/__mocks__/fixtures.ts` (`buildTestItem`, `buildTestMetadata`).
23+
* Removed redundant re-exports from `src/internal/errors.ts`.
24+
25+
### Breaking changes
26+
27+
* The default export is gone. Use named imports: `import { setItem } from 'react-native-sensitive-info'`.
28+
* React hooks are no longer re-exported from the package root — import them from `react-native-sensitive-info/hooks`.
29+
30+
### Notes
31+
32+
* **iOS rotation** updates the Keychain metadata via `SecItemUpdate`, preserving the original access-control attributes while bumping `keyVersion`.
33+
* **Android rotation** mints a fresh per-entry Keystore alias (`SensitiveInfo_<hash>_v<version>`) during lazy or eager re-encryption and deletes the stale alias after a successful rewrite.
34+
* Version state lives in a non-secret registry (`SharedPreferences` on Android, `UserDefaults` on iOS). Delete the app's data to reset.
35+
136
## [6.0.0-rc.12](https://github.com/mcodex/react-native-sensitive-info/compare/v6.0.0-rc.11...v6.0.0-rc.12) (2025-12-16)
237

338
### Features

README.md

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ Modern secure storage for React Native, powered by Nitro Modules. Version 6 ship
3232
- [📚 API reference](#-api-reference)
3333
- [🔐 Access control & metadata](#-access-control--metadata)
3434
- [❗ Error handling](#-error-handling)
35+
- [🔁 Key rotation](#-key-rotation)
36+
- [🛡️ Security model](#-security-model)
37+
- [🌳 Tree-shaking](#-tree-shaking)
3538
- [🧪 Simulators and emulators](#-simulators-and-emulators)
3639
- [📈 Performance benchmarks](#-performance-benchmarks)
3740
- [🎮 Example application](#-example-application)
@@ -150,7 +153,7 @@ import { Text, View, ActivityIndicator } from 'react-native'
150153
import {
151154
useSecureStorage,
152155
useSecurityAvailability,
153-
} from 'react-native-sensitive-info'
156+
} from 'react-native-sensitive-info/hooks'
154157

155158
// Use hooks directly in any component - no provider needed!
156159
function YourComponent() {
@@ -193,6 +196,7 @@ function YourComponent() {
193196
| `useSecret()` | Single secret + mutations | `{ data, isLoading, error, saveSecret, deleteSecret, refetch }` |
194197
| `useHasSecret()` | Check if secret exists (lightweight) | `{ data (boolean), isLoading, error, refetch }` |
195198
| `useSecurityAvailability()` | Query device capabilities (cached) | `{ data, isLoading, error, refetch }` |
199+
| `useKeyRotation()` | Rotate the master key for a service | `{ lastResult, error, isRotating, rotate, readVersion }` |
196200

197201
### Best practices
198202

@@ -220,6 +224,18 @@ function YourComponent() {
220224

221225
For comprehensive examples and advanced patterns, see [`HOOKS.md`](./HOOKS.md).
222226

227+
### 🧱 Hook architecture (DRY · KISS · SRP)
228+
229+
Every hook in this package is a thin choreography layer over three internal primitives, so adding or auditing a hook stays a single-file change:
230+
231+
| Primitive | Responsibility |
232+
| --- | --- |
233+
| `useAsyncLifecycle` | Mount tracking + `AbortController` plumbing — _one job, no React state of its own_. |
234+
| `useAsync` / `useAsyncQuery` | The shared "stable options → strip `skip` → memoize → fetch" recipe used by every read-only hook (`useHasSecret`, `useSecretItem`, `useSecret`, `useSecureStorage`, `useSecurityAvailability`). |
235+
| `useMutation` | The imperative state machine (loading + error + auth-cancel handling) reused by every mutation-style hook (`useSecureOperation`, `useKeyRotation`, plus the `saveSecret`/`removeSecret`/`clearAll` helpers in `useSecureStorage`). |
236+
237+
Net effect: the data-fetching hooks are 25–35 lines each, mutations are ~10 lines, and the abort/cancel/error contract is identical across the surface — there is no place where a bug fix has to be repeated.
238+
223239
## ❗ Error handling
224240

225241
Every public hook returns failures as `HookError` instances. Besides `message`, each error carries:
@@ -232,7 +248,7 @@ Biometric or device-credential prompts cancelled by the user now surface as a fr
232248

233249
```tsx
234250
import { Text } from 'react-native'
235-
import { useSecureStorage } from 'react-native-sensitive-info'
251+
import { useSecureStorage } from 'react-native-sensitive-info/hooks'
236252

237253
function SecretsList() {
238254
const { items, error } = useSecureStorage({ service: 'auth', includeValues: true })
@@ -261,6 +277,93 @@ function SecretsList() {
261277
> [!TIP]
262278
> When using the imperative API, look for the `[E_AUTH_CANCELED]` marker in the thrown error message to detect cancellations.
263279
280+
## 🔁 Key rotation
281+
282+
The library supports **versioned master keys** with lazy re-encryption. Each stored entry is tagged with the `keyVersion` that produced its ciphertext. Calling `rotateKeys()` bumps the active version; subsequent reads transparently re-encrypt entries that were stored under older versions.
283+
284+
```tsx
285+
import { rotateKeys, getKeyVersion } from 'react-native-sensitive-info'
286+
287+
// Lazy rotation — new writes use v+1, reads upgrade older entries as they happen
288+
await rotateKeys({ service: 'auth' })
289+
290+
// Eager rotation — walks every entry in the service and re-encrypts in one go
291+
await rotateKeys({ service: 'auth', reEncryptEagerly: true })
292+
293+
// Inspect the currently active version for telemetry
294+
const version = await getKeyVersion({ service: 'auth' })
295+
```
296+
297+
Or with the hook:
298+
299+
```tsx
300+
import { useKeyRotation } from 'react-native-sensitive-info/hooks'
301+
302+
function RotationButton() {
303+
const { rotate, isRotating, lastResult, error } = useKeyRotation({
304+
service: 'auth',
305+
})
306+
307+
return (
308+
<Button
309+
title={isRotating ? 'Rotating…' : 'Rotate master key'}
310+
onPress={rotate}
311+
disabled={isRotating}
312+
/>
313+
)
314+
}
315+
```
316+
317+
## 🛡️ Security model
318+
319+
| Concern | Android | iOS / Apple platforms |
320+
| --- | --- | --- |
321+
| Master key | Android Keystore (`AES/GCM`, StrongBox when available) | Secure Enclave-gated (P-256) + AES-GCM |
322+
| Authentication | BiometricPrompt (Class 3 preferred), device credential fallback | LAContext / Face ID / Touch ID / Optic ID |
323+
| At-rest integrity | AES-GCM tag **+** HMAC-SHA256 metadata tag (Keystore-bound) | AES-GCM tag **+** HMAC-SHA256 metadata tag (Keychain-stored, after-first-unlock) |
324+
| Replay / swap defense | AES-GCM AAD bound to `service\|key\|v<version>` | Keychain `kSecAttrService` + `kSecAttrAccount` binding |
325+
| Device-state gating | `setUnlockedDeviceRequired(true)` on every key (API 28+) | `kSecAttrAccessibleWhenUnlocked*` defaults |
326+
| Plaintext lifetime | Buffers zeroized after encrypt/decrypt | `Data` buffers zeroized via `memset_s` |
327+
| Key rotation | Versioned Keystore aliases, lazy re-encryption | Versioned Keychain metadata, lazy re-wrap (preserves original access control) |
328+
| Error classification | Typed `SensitiveInfoError` subclasses via `/errors` subpath | Same |
329+
330+
> **Tamper detection:** every read recomputes the HMAC over the persisted `(service, key, version, accessControl, securityLevel, timestamp, ciphertext, iv)` tuple. A mismatch raises `IntegrityViolationError` (`E_INTEGRITY_VIOLATION`) **before** any biometric prompt fires, so spoofed entries can never trigger user authentication. Entries written by older library versions (no `integrityTag`) are accepted on first read and upgraded on the next write or rotation.
331+
332+
Typed errors can be imported from the `/errors` subpath for tree-shakeable error handling:
333+
334+
```tsx
335+
import {
336+
isNotFoundError,
337+
isAuthenticationCanceledError,
338+
isIntegrityViolationError,
339+
isKeyInvalidatedError,
340+
} from 'react-native-sensitive-info/errors'
341+
342+
try {
343+
await getItem('token', { service: 'auth' })
344+
} catch (error) {
345+
if (isAuthenticationCanceledError(error)) return
346+
if (isKeyInvalidatedError(error)) {
347+
// The hardware key was invalidated (e.g. biometrics re-enrolled).
348+
// Delete the affected entry and ask the user to re-enter.
349+
await deleteItem('token', { service: 'auth' })
350+
}
351+
throw error
352+
}
353+
```
354+
355+
## 🌳 Tree-shaking
356+
357+
Every entry point is side-effect-free (`"sideEffects": false`) and split into focused subpaths:
358+
359+
| Import | Contents |
360+
| --- | --- |
361+
| `react-native-sensitive-info` | `setItem`, `getItem`, `hasItem`, `deleteItem`, `getAllItems`, `clearService`, `getSupportedSecurityLevels`, `rotateKeys`, `getKeyVersion`, type exports |
362+
| `react-native-sensitive-info/hooks` | Every React hook (`useSecret`, `useSecureStorage`, `useKeyRotation`, …) |
363+
| `react-native-sensitive-info/errors` | Typed error classes + `instanceof` predicates |
364+
365+
There is **no default export** — import only the helpers you use and modern bundlers (Metro, Webpack, Rollup, esbuild) will drop the rest.
366+
264367
## Imperative API
265368

266369
```tsx

0 commit comments

Comments
 (0)