v6.0.0
π v6.0.0 β Release Notes (5.6.2 β 6.0.0) π
react-native-sensitive-info 6 is a from-scratch rewrite on top of Nitro Modules and the React Native New Architecture. It is not a drop-in upgrade from 5.6.2 β the API surface is intentionally narrower, fully typed, and metadata-rich. Plan a migration window and use the migration guide.
β‘ TL;DR
- New runtime: Nitro hybrid object replaces the legacy bridge module. Requires React Native β₯ 0.80 with the New Architecture enabled.
- Promise-based API throughout; every read/write returns rich
StorageMetadata. - Typed errors (
SensitiveInfoErrorsubclasses) withinstanceofpredicates β no more string-matching. - First-class React hooks under
react-native-sensitive-info/hooks. - Versioned key rotation with lazy re-encryption.
- Defense-in-depth hardening: HMAC integrity tag, AES-GCM AAD binding,
setUnlockedDeviceRequired, plaintext zeroization, constant-time comparisons. - Tree-shakeable subpath exports (
.,/hooks,/errors);"sideEffects": false. - Windows is no longer supported. v6 targets Android + Apple platforms (iOS, macOS, visionOS, watchOS).
- First-class Expo config plugin β Face ID usage description, biometric permissions, and New Architecture flags wired up automatically. Expo Go is not supported; use a Dev Client or EAS Build.
π€ Why upgrade?
| 5.6.2 | 6.0.0 | |
|---|---|---|
| Runtime | Legacy bridge module (Paper-compatible) | Nitro hybrid object (New Architecture only) |
| Architecture | Old + Fabric (bridge fallback) | New Architecture required |
| API surface | Mixed sync/async, manual flag soup (kSecAccessControl*, keystore) |
Promise-based, typed, single accessControl enum |
| Return types | void writes, raw value reads |
MutationResult writes, SensitiveInfoItem | null reads with metadata |
| Errors | Plain Error instances, message string-matching |
Typed subclasses + is*Error predicates, dedicated /errors subpath |
| Metadata | None | securityLevel, backend, accessControl, keyVersion, integrityTag, timestamp |
| Key rotation | Not available | rotateKeys() + getKeyVersion() with lazy / eager re-encryption |
| Integrity | AES-GCM tag only | AES-GCM tag + HMAC-SHA256 over (service, key, version, accessControl, securityLevel, timestamp, ciphertext, iv) |
| Replay/swap defense | None | AES-GCM AAD bound to service|key|v<version> (Android); kSecAttrService + kSecAttrAccount (iOS) |
| Plaintext lifetime | Best-effort | Zeroized on both platforms after encrypt/decrypt |
| React hooks | None | useSecret, useSecretItem, useHasSecret, useSecureStorage, useSecurityAvailability, useKeyRotation, useSecureOperation |
| Expo support | None / community plugin | First-party app.plugin.js (Info.plist, AndroidManifest, new-arch flags) |
| Windows support | Yes (legacy) | Removed |
| Bundle size | Single CJS entry | "sideEffects": false, subpath exports, no default export |
| Lint/format toolchain | ESLint + Prettier | Biome 2 (single config) |
| TypeScript | Loose typings | Strict, exactOptionalPropertyTypes, declaration maps |
β¨ Highlights
ποΈ Nitro hybrid core (RN 0.80+ / New Architecture)
The core moves from the legacy module bridge to a Nitro hybrid object. Native calls bypass the JS bridge serialization layer, so secure-storage operations run with significantly lower marshalling overhead and predictable latency.
- iOS / Apple platforms: Swift + CryptoKit + Keychain, Secure Enclave-gated AES-GCM.
- Android: Kotlin + Keystore (StrongBox-aware) with
EncryptedSharedPreferencessoftware fallback.
π¦ Promise-based API with metadata
import { setItem, getItem } from 'react-native-sensitive-info'
const result = await setItem('session-token', 'super-secret', {
service: 'auth',
accessControl: 'secureEnclaveBiometry',
})
// result.metadata: { securityLevel, backend, accessControl, keyVersion, integrityTag, ... }
const item = await getItem('session-token', { service: 'auth' })
// item: { key, service, value?, metadata } | nullEvery read/write returns StorageMetadata, so apps can confirm the actual security level applied (e.g. refuse to store secrets when metadata.securityLevel === 'software').
π¨ Typed errors
import {
isAuthenticationCanceledError,
isIntegrityViolationError,
isKeyInvalidatedError,
} from 'react-native-sensitive-info/errors'
try {
await getItem('token', { service: 'auth' })
} catch (error) {
if (isAuthenticationCanceledError(error)) return
if (isKeyInvalidatedError(error)) {
// Hardware key invalidated (e.g. biometrics re-enrolled)
}
throw error
}Predicates: isNotFoundError, isAuthenticationCanceledError, isIntegrityViolationError, isKeyInvalidatedError, isRotationFailedError, isInvalidArgumentError.
βοΈ React hooks
A focused hooks API ships at react-native-sensitive-info/hooks:
| Hook | Use case |
|---|---|
useSecureStorage |
List + CRUD over a service |
useSecret |
Single secret + save/delete |
useSecretItem |
Single secret read |
useHasSecret |
Lightweight existence check |
useSecurityAvailability |
Device capability snapshot |
useKeyRotation |
Bump and inspect master-key version |
useSecureOperation |
Wrap arbitrary imperative calls in a state machine |
All hooks share the same lifecycle/abort/error pipeline (useAsyncQuery, useMutation) and never tear state on unmount.
π Versioned key rotation
import { rotateKeys, getKeyVersion } from 'react-native-sensitive-info'
await rotateKeys({ service: 'auth' }) // lazy, upgrades on next read
await rotateKeys({ service: 'auth', reEncryptEagerly: true }) // eager
const version = await getKeyVersion({ service: 'auth' })- iOS:
SecItemUpdatepreserves existing access-control attributes while bumpingkeyVersion. - Android: mints a fresh per-entry Keystore alias (
SensitiveInfo_<hash>_v<version>) and deletes the stale one after a successful rewrite.
π‘οΈ Defense-in-depth hardening
Applied transparently to new writes and via lazy upgrade on rotation. Backwards compatible β entries written by older 6.x RC builds decode without verification and are upgraded on the next write or rotation.
- HMAC-SHA256 integrity tag bound to every entry's metadata + ciphertext (
StorageMetadata.integrityTag). Tampering raisesIntegrityViolationErrorbefore any biometric prompt is shown. - AES-GCM AAD on Android binds ciphertext to
service|key|v<version>, defeating cross-entry swap attacks. setUnlockedDeviceRequired(true)on every Android Keystore key (API 28+), mirroring iOSkSecAttrAccessibleWhenUnlockedsemantics.- Plaintext byte buffers zeroized after use on both platforms.
- Constant-time HMAC comparison (
MessageDigest.isEqual/ manualUInt8XOR fold).
π³ Tree-shaking & ESM-first packaging
"sideEffects": false everywhere, with three focused subpath entries:
| Import | Contents |
|---|---|
react-native-sensitive-info |
setItem, getItem, hasItem, deleteItem, getAllItems, clearService, getSupportedSecurityLevels, rotateKeys, getKeyVersion, types |
react-native-sensitive-info/hooks |
Every React hook |
react-native-sensitive-info/errors |
Typed error classes + instanceof predicates |
The default export is gone β use named imports.
π§© Expo config plugin
A first-party app.plugin.js ships in the package. Add it to app.json / app.config.ts:
{
"expo": {
"plugins": [
["react-native-sensitive-info", {
"faceIDPermission": "Authenticate to unlock your account.",
"enableNewArchitecture": true
}]
]
}
}Handles: NSFaceIDUsageDescription, USE_BIOMETRIC + legacy USE_FINGERPRINT (with maxSdkVersion=28), and New Architecture flags (newArchEnabled, RCT_NEW_ARCH_ENABLED). See docs/EXPO.md.
π₯ Breaking changes
A migration table for the most common call sites:
| 5.6.2 | 6.0.0 |
|---|---|
| Bridge module, Old Architecture compatible | Nitro hybrid object, New Architecture required |
setItem(key, value, options) returns void |
setItem(key, value, options) returns Promise<MutationResult> |
getItem(key, options) returns the raw value or null |
getItem(key, options) returns SensitiveInfoItem | null ({ key, service, value, metadata }) |
getAllItems(options) returns Record<string, string> |
getAllItems(options) returns SensitiveInfoItem[] |
deleteItem(key, options) returns void |
deleteItem(key, options) returns Promise<boolean> |
sharedPreferencesName (Android) / keychainService (iOS) |
unified as service |
kSecAccessControl* strings + Android keystore config object |
single accessControl: 'secureEnclaveBiometry' | 'biometryCurrentSet' | 'biometryAny' | 'devicePasscode' | 'none' |
setInvalidatedByBiometricEnrollment opt-out |
Always-on; biometric-bound entries raise KeyInvalidatedError deterministically |
Errors are plain Error instances |
Typed SensitiveInfoError subclasses + is*Error predicates |
| Default + named exports from package root | Named exports only; no default export; hooks moved to /hooks |
| Windows supported | Windows removed |
π Access-control flag mapping
5.6.2 (kSecAccessControlβ¦) |
6.0.0 (accessControl) |
|---|---|
kSecAccessControlBiometryCurrentSet + Secure Enclave class |
secureEnclaveBiometry |
kSecAccessControlBiometryCurrentSet |
biometryCurrentSet |
kSecAccessControlBiometryAny |
biometryAny |
kSecAccessControlDevicePasscode |
devicePasscode |
| (no policy) | none |
π Data migration
5.6.2 β 6.0.0 is not wire-compatible. Existing entries written by 5.6.x are not readable by 6.x because the metadata envelope is different.
Recommended approach:
- Easiest: ship a release that simply re-prompts the user to authenticate (most secrets are short-lived by design).
- Or: ship a one-time migration that reads with a vendored 5.6 helper and re-writes via 6.x
setItem.
See docs/MIGRATION.md for the step-by-step diff-based guide.
π Requirements
- React Native β₯ 0.80 (New Architecture enabled).
- Node β₯ 18.
react-native-nitro-modulesas a peer dependency.- iOS β₯ 13.0 Β· macOS β₯ 11.0 Β· visionOS β₯ 1.0 Β· watchOS β₯ 7.0.
- Android API 23+ (StrongBox detection requires API 28+).
- Expo SDK 52+ (Dev Client or EAS Build β Expo Go is not supported).
π₯ Installation
npm install react-native-sensitive-info react-native-nitro-modules
# or
yarn add react-native-sensitive-info react-native-nitro-modules
# or
pnpm add react-native-sensitive-info react-native-nitro-modulesiOS: cd ios && pod install. Expo: add "react-native-sensitive-info" to app.json plugins and run npx expo prebuild --clean.
π§ Quick API tour
import {
setItem,
getItem,
hasItem,
deleteItem,
getAllItems,
clearService,
getSupportedSecurityLevels,
rotateKeys,
getKeyVersion,
} from 'react-native-sensitive-info'| Method | Signature |
|---|---|
setItem |
(key, value, options?) => Promise<MutationResult> |
getItem |
(key, options?) => Promise<SensitiveInfoItem | null> |
hasItem |
(key, options?) => Promise<boolean> |
deleteItem |
(key, options?) => Promise<boolean> |
getAllItems |
(options?) => Promise<SensitiveInfoItem[]> |
clearService |
(options?) => Promise<void> |
getSupportedSecurityLevels |
() => Promise<SecurityAvailability> |
rotateKeys |
(options?) => Promise<RotationResult> |
getKeyVersion |
(options?) => Promise<number> |