
Company News
Socket Has Acquired Secure Annex
Socket has acquired Secure Annex to expand extension security across browsers, IDEs, and AI tools.
react-native-sensitive-info
Advanced tools
🔐 React Native secure storage, rebuilt with Nitro Modules ⚡️ Biometric-ready, StrongBox-aware, and metadata-rich for modern mobile apps
Modern secure storage for React Native, powered by Nitro Modules. Version 6 ships a new headless API surface, stronger security defaults, and a fully revamped example app.
[!TIP] Need the TL;DR? Jump to 🚀 Highlights and ⚙️ Installation to get productive in under five minutes.
[!WARNING] Version 6 drops Windows support. The module now targets Android plus the Apple platforms (iOS, macOS, visionOS, watchOS).
[!NOTE] Choosing between 5.6.x and 6.x
- Want the latest stable?
6.xis the current GA line. It runs on the Nitro hybrid core, auto-enforces Class 3/StrongBox biometrics, ships first-class hooks, and exposes rich metadata for every entry. Requires the Nitro toolchain (RN 0.80+, Node 18+,react-native-nitro-modules, New Architecture enabled).- Need bridge stability?
5.6.xis the last pre-Nitro release on the legacy JS bridge. It still receives critical security fixes but no new features — pin to it only if you cannot enable the New Architecture yet.- Staying back on 5.5.x? You miss the Android 13 prompt fixes and the manual credential fallback restoration — migrate to
5.6.xat minimum before planning the jump to 6.x.
setItem, getItem, hasItem, getAllItems, clearService).react-native-builder-bob.[!NOTE] All APIs are fully typed. Hover over any option in your editor to explore the metadata surface without leaving VS Code.
| Platform | Minimum OS | Notes |
|---|---|---|
| React Native | 0.76.0 | Requires react-native-nitro-modules for Nitro hybrid core. |
| iOS | 13.0 | Requires Face ID usage string when biometrics are enabled. |
| macOS | 11.0 (Big Sur) | Supports Catalyst and native macOS builds backed by the system keychain. |
| visionOS | 1.0 | Uses the shared Secure Enclave policies; prompts adapt to the visionOS biometric UX. |
| watchOS | 7.0 | Relies on paired-device authentication; storage syncs through the watchOS keychain. |
| Android | API 23 (Marshmallow) | StrongBox detection requires API 28+; biometrics fall back to device credential when unavailable. |
| Windows | ❌ | Removed in v6. Earlier versions may still work but are no longer maintained. |
# with npm
npm install react-native-sensitive-info react-native-nitro-modules
# or with yarn
yarn add react-native-sensitive-info react-native-nitro-modules
# or with pnpm
pnpm add react-native-sensitive-info react-native-nitro-modules
No manual linking is required. Nitro handles platform registration via autolinking.
Install pods from the root of your project:
cd ios && pod install
Add a Face ID usage description to your app’s Info.plist if you intend to use biometric prompts (already present in the example app):
<key>NSFaceIDUsageDescription</key>
<string>Face ID is used to unlock secrets stored in the secure enclave.</string>
Ensure the following permissions are present in your AndroidManifest.xml:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
If you rely on hardware-backed keystores, verify the device/emulator supports the biometrics you request.
[!WARNING] The Expo Go client does not ship native Nitro modules. Use a custom dev client (
expo run:*) or an EAS build instead.
app.json/app.config.js so prebuild toggles the new architecture for both platforms:{
"expo": {
"plugins": [
"react-native-sensitive-info"
]
}
}
npx expo prebuild --clean
npx expo run:android
npx expo run:ios
# or via EAS
eas build --profile development --platform android
The plugin enables React Native's new architecture on both platforms, ensuring the HybridSensitiveInfo Nitro class is included during compilation.
[!TIP] Use
includeValue: falseduring reads when you only care about metadata—this keeps plaintext out of memory and speeds up list views.
For a modern, reactive approach with automatic memory management and loading states, use the dedicated hooks:
import { Text, View, ActivityIndicator } from 'react-native'
import {
useSecureStorage,
useSecurityAvailability,
} from 'react-native-sensitive-info/hooks'
// Use hooks directly in any component - no provider needed!
function YourComponent() {
// Fetch and manage all secrets in a service (with CRUD)
const {
items,
isLoading,
error,
saveSecret,
removeSecret,
} = useSecureStorage({ service: 'myapp', includeValues: true })
// Query device security capabilities (cached automatically)
const { data: capabilities } = useSecurityAvailability()
if (isLoading) return <ActivityIndicator />
if (error) return <Text>Error: {error.message}</Text>
return (
<View>
{items.map(item => (
<Text key={item.key}>
{item.key}: {item.value} ({item.metadata.securityLevel})
</Text>
))}
<Text>
Biometry available: {capabilities?.biometry ? 'Yes' : 'No'}
</Text>
</View>
)
}
| Hook | Use Case | Returns |
|---|---|---|
useSecureStorage() | Manage all secrets in a service (list, add, remove) | { items, isLoading, error, saveSecret, removeSecret, clearAll, refreshItems } |
useSecretItem() | Fetch a single secret | { data, isLoading, error, refetch } |
useSecret() | Single secret + mutations | { data, isLoading, error, saveSecret, deleteSecret, refetch } |
useHasSecret() | Check if secret exists (lightweight) | { data (boolean), isLoading, error, refetch } |
useSecurityAvailability() | Query device capabilities (cached) | { data, isLoading, error, refetch } |
useKeyRotation() | Rotate the master key for a service | { lastResult, error, isRotating, rotate, readVersion } |
Memory leak prevention — All hooks automatically cancel requests and clean up resources on unmount.
Conditional fetching — Use skip: true to prevent unnecessary operations:
const { data } = useSecretItem('token', { skip: !isAuthenticated })
Optimize list views — Fetch metadata only to avoid decryption overhead:
const { items } = useSecureStorage({ includeValues: false })
Share capabilities — Query independently and results are cached automatically:
// Each component queries independently (results cached automatically)
const { data: capabilities1 } = useSecurityAvailability()
const { data: capabilities2 } = useSecurityAvailability()
// Same cached result, no duplicate native calls
For comprehensive examples and advanced patterns, see HOOKS.md.
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:
| Primitive | Responsibility |
|---|---|
useAsyncLifecycle | Mount tracking + AbortController plumbing — one job, no React state of its own. |
useAsync / useAsyncQuery | The shared "stable options → strip skip → memoize → fetch" recipe used by every read-only hook (useHasSecret, useSecretItem, useSecret, useSecureStorage, useSecurityAvailability). |
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). |
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.
Every public hook returns failures as HookError instances. Besides message, each error carries:
operation – the hook action that failed (for example, useSecureStorage.saveSecret).cause – the original native error for additional diagnostics.hint – a short suggestion shown in the example app and useful for toast copy.Biometric or device-credential prompts cancelled by the user now surface as a friendly message (Authentication prompt canceled by the user.) and do not poison hook state. Imperative calls still reject with the raw error so you can decide how to react.
import { Text } from 'react-native'
import { useSecureStorage } from 'react-native-sensitive-info/hooks'
function SecretsList() {
const { items, error } = useSecureStorage({ service: 'auth', includeValues: true })
if (error) {
if (error.message.includes('Authentication prompt canceled')) {
return <Text>The user dismissed biometric authentication.</Text>
}
return (
<Text testID="secure-error">
{error.message}
{'\n'}Hint: {error.hint ?? 'Check your secure storage configuration.'}
</Text>
)
}
return items.length === 0 ? (
<Text>No secrets stored yet.</Text>
) : (
<Text>{items.map((item) => item.key).join(', ')}</Text>
)
}
[!TIP] When using the imperative API, look for the
[E_AUTH_CANCELED]marker in the thrown error message to detect cancellations.
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.
import { rotateKeys, getKeyVersion } from 'react-native-sensitive-info'
// Lazy rotation — new writes use v+1, reads upgrade older entries as they happen
await rotateKeys({ service: 'auth' })
// Eager rotation — walks every entry in the service and re-encrypts in one go
await rotateKeys({ service: 'auth', reEncryptEagerly: true })
// Inspect the currently active version for telemetry
const version = await getKeyVersion({ service: 'auth' })
Or with the hook:
import { useKeyRotation } from 'react-native-sensitive-info/hooks'
function RotationButton() {
const { rotate, isRotating, lastResult, error } = useKeyRotation({
service: 'auth',
})
return (
<Button
title={isRotating ? 'Rotating…' : 'Rotate master key'}
onPress={rotate}
disabled={isRotating}
/>
)
}
| Concern | Android | iOS / Apple platforms |
|---|---|---|
| Master key | Android Keystore (AES/GCM, StrongBox when available) | Secure Enclave-gated (P-256) + AES-GCM |
| Authentication | BiometricPrompt (Class 3 preferred), device credential fallback | LAContext / Face ID / Touch ID / Optic ID |
| At-rest integrity | AES-GCM tag + HMAC-SHA256 metadata tag (Keystore-bound) | AES-GCM tag + HMAC-SHA256 metadata tag (Keychain-stored, after-first-unlock) |
| Replay / swap defense | AES-GCM AAD bound to service|key|v<version> | Keychain kSecAttrService + kSecAttrAccount binding |
| Device-state gating | setUnlockedDeviceRequired(true) on every key (API 28+) | kSecAttrAccessibleWhenUnlocked* defaults |
| Plaintext lifetime | Buffers zeroized after encrypt/decrypt | Data buffers zeroized via memset_s |
| Key rotation | Versioned Keystore aliases, lazy re-encryption | Versioned Keychain metadata, lazy re-wrap (preserves original access control) |
| Error classification | Typed SensitiveInfoError subclasses via /errors subpath | Same |
Tamper detection: every read recomputes the HMAC over the persisted
(service, key, version, accessControl, securityLevel, timestamp, ciphertext, iv)tuple. A mismatch raisesIntegrityViolationError(E_INTEGRITY_VIOLATION) before any biometric prompt fires, so spoofed entries can never trigger user authentication. Entries written by older library versions (nointegrityTag) are accepted on first read and upgraded on the next write or rotation.
Typed errors can be imported from the /errors subpath for tree-shakeable error handling:
import {
isNotFoundError,
isAuthenticationCanceledError,
isIntegrityViolationError,
isKeyInvalidatedError,
} from 'react-native-sensitive-info/errors'
try {
await getItem('token', { service: 'auth' })
} catch (error) {
if (isAuthenticationCanceledError(error)) return
if (isKeyInvalidatedError(error)) {
// The hardware key was invalidated (e.g. biometrics re-enrolled).
// Delete the affected entry and ask the user to re-enter.
await deleteItem('token', { service: 'auth' })
}
throw error
}
Every entry point is side-effect-free ("sideEffects": false) and split into focused subpaths:
| Import | Contents |
|---|---|
react-native-sensitive-info | setItem, getItem, hasItem, deleteItem, getAllItems, clearService, getSupportedSecurityLevels, rotateKeys, getKeyVersion, type exports |
react-native-sensitive-info/hooks | Every React hook (useSecret, useSecureStorage, useKeyRotation, …) |
react-native-sensitive-info/errors | Typed error classes + instanceof predicates |
There is no default export — import only the helpers you use and modern bundlers (Metro, Webpack, Rollup, esbuild) will drop the rest.
import React, { useEffect } from 'react'
import { SensitiveInfo, setItem, getItem } from 'react-native-sensitive-info'
export function SecureTokenExample() {
useEffect(() => {
async function bootstrap() {
await setItem('session-token', 'super-secret', {
service: 'auth',
accessControl: 'secureEnclaveBiometry',
authenticationPrompt: {
title: 'Authenticate to unlock your session',
cancel: 'Cancel',
},
})
const item = await getItem('session-token', {
service: 'auth',
includeValue: false,
})
console.log('Stored metadata', item?.metadata)
}
void bootstrap()
}, [])
return null
}
// Optionally access the singleton hybrid object directly
void SensitiveInfo.clearService({ service: 'auth' })
All functions live at the top level export and return Promises.
| Method | Signature | Description |
|---|---|---|
setItem | (key, value, options?) => Promise<MutationResult> | Writes a secret using the strongest available security policy. |
getItem | `(key, options?) => Promise<SensitiveInfoItem \ | null>` |
hasItem | (key, options?) => Promise<boolean> | Checks whether a secret exists for the given key. |
deleteItem | (key, options?) => Promise<boolean> | Removes a secret. Returns true if something was deleted. |
getAllItems | (options?) => Promise<SensitiveInfoItem[]> | Enumerates all secrets scoped to a service. Use includeValues to return decrypted payloads. |
clearService | (options?) => Promise<void> | Removes every secret within a service namespace. |
getSupportedSecurityLevels | () => Promise<SecurityAvailability> | Returns a snapshot of platform capabilities (secure enclave, biometrics, etc.). |
service (default: bundle identifier or default) — logical namespace for secrets.accessControl (default: secureEnclaveBiometry) — preferred policy; the native layer chooses the strongest supported fallback.authenticationPrompt — localized strings for biometric/device credential prompts.iosSynchronizable — enable iCloud Keychain sync.keychainGroup — custom Keychain access group.Android automatically enforces Class 3 biometrics whenever the hardware supports them, falling back to the strongest available authenticator on older devices.
See src/sensitive-info.nitro.ts for full TypeScript definitions.
MutationResult and SensitiveInfoItem.metadata surface how a value was stored:
secureEnclave, strongBox, biometry, deviceCredential, software.keychain, androidKeystore, encryptedSharedPreferences.secureEnclaveBiometry, biometryCurrentSet, biometryAny, devicePasscode, none.Use getSupportedSecurityLevels() to tailor UX before prompting users. For example, disable Secure Enclave options on simulators. For richer enrollment-state detection (so you can distinguish "hardware missing" from "user hasn't enrolled yet"), see 👁️ Biometrics.
[!TIP] Need to demo biometrics on a simulator? Use Xcode’s “Features → Face ID” and Android Studio’s “Fingerprints” toggles to simulate successful scans.
The library disambiguates capability from enrollment so you can render the right UX without false positives. SecurityAvailability exposes both a quick boolean (biometry) and a fine-grained biometryStatus enum:
biometryStatus | Meaning | Recommended UX |
|---|---|---|
'available' | Hardware present, enrolled, currently usable. | Enable the biometric toggle. |
'notEnrolled' | Hardware present but no fingerprint/face is registered. | Show a “Set up Face ID / fingerprint” CTA that deep-links to settings. |
'notAvailable' | Missing or permanently disabled (no hardware, admin policy, passcode unset). | Hide the biometric toggle entirely. |
'lockedOut' | Too many failed attempts; transiently locked. iOS only at probe time — Android surfaces lockout via BiometricPrompt failures. | Show “Try again later” and offer a devicePasscode fallback. |
'unknown' | Probe could not classify the device. | Treat as notAvailable for gating; log for diagnostics. |
Invariant:
biometry === (biometryStatus === 'available'). Both fields come from the same native probe.
canUseAccessControl(policy) predicts whether a future setItem write with the requested policy will succeed on the current device. It maps the policy onto a SecurityAvailability snapshot — pure TS, no native call — but if you don't pass a snapshot it first fetches one via getSupportedSecurityLevels(). Pass the snapshot you already hold (e.g. from useSecurityAvailability) to skip that round-trip:
import { canUseAccessControl, setItem } from 'react-native-sensitive-info'
if (await canUseAccessControl('secureEnclaveBiometry')) {
await setItem('session', token, { accessControl: 'secureEnclaveBiometry' })
} else {
// Graceful fallback so the user can still sign in.
await setItem('session', token, { accessControl: 'devicePasscode' })
}
If you already hold a snapshot from useSecurityAvailability, use the synchronous variant inside render:
import { canUseAccessControlSync } from 'react-native-sensitive-info'
import { useSecurityAvailability } from 'react-native-sensitive-info/hooks'
const { data: caps } = useSecurityAvailability()
const canEnable = caps ? canUseAccessControlSync('secureEnclaveBiometry', caps) : false
Users commonly leave the app to enroll a fingerprint and come back. Opt into foreground auto-refresh so the toggle reflects the new state without a manual refetch():
const { data: caps } = useSecurityAvailability({ refreshOnForeground: true })
if (caps?.biometryStatus === 'notEnrolled') {
return <SetupFaceIdCta onPress={() => Linking.openSettings()} />
}
The hook subscribes to AppState only when the option is enabled, debounces back-to-back active transitions (~500 ms), and unsubscribes on unmount.
useBiometryStatusWatcher is a transition-only callback (fires once per actual BiometryStatus change, never on every render):
import { useBiometryStatusWatcher } from 'react-native-sensitive-info/hooks'
useBiometryStatusWatcher((next, previous) => {
analytics.track('biometry_status_changed', { from: previous, to: next })
if (previous === 'notEnrolled' && next === 'available') showToast('Face ID is ready.')
})
It lives in its own module, so apps that don’t need transition tracking don’t pay for it (sideEffects: false + named exports keep tree-shaking honest).
[!IMPORTANT] Simulators are great for flows, but only physical hardware validates secure hardware policies such as StrongBox and Secure Enclave. Run your final regression tests on devices before shipping.
Always validate security behavior on the physical devices you ship to customers.
Explore the full feature set with the bundled example app. It showcases capability detection, metadata inspection, and error surface normalization for every API call.
[!TIP] Prefer Expo? The same Nitro module works inside bare Expo projects—just install via
expo installand run the commands below fromexample/.
The Nitro rewrite in v6 removes the classic React Native bridge bottleneck that previous releases (v5 and earlier) relied on.
| Operation (10k iterations) | v5 classic bridge | v6 Nitro hybrid | Improvement |
|---|---|---|---|
setItem (string payload, metadata write) | 812 ms | 247 ms | 3.3× faster |
getItem (with value) | 768 ms | 231 ms | 3.3× faster |
hasItem | 544 ms | 158 ms | 3.4× faster |
getAllItems (25 entries, metadata only) | 612 ms | 204 ms | 3.0× faster |
Benchmark setup
On both platforms, Nitro’s C++/Swift/Kotlin hybrid path keeps the secure storage calls close to their native implementations, cutting marshalling overhead and reducing GC pressure compared to the legacy JS module façade.
An interactive demo lives under example. It showcases every API surface, metadata inspection, and capability refresh.
cd example
yarn install
# iOS
yarn ios
# Android
yarn android
The example includes required permissions and the NSFaceIDUsageDescription string out of the box (see example/android/app/src/main/AndroidManifest.xml and example/ios/SensitiveInfoExample/Info.plist).
[!TIP] Run
yarn codegen --watchin one terminal and your platform build in another to regenerate bindings automatically during native development.
# Install dependencies
yarn install
# Regenerate Nitro bindings and build outputs
yarn codegen
# Type-check TypeScript sources
yarn typecheck
# Build the distributable packages
yarn build
The project uses Nitrogen for code generation and react-native-builder-bob for packaging CommonJS/ESM bundles.
devicePasscode where appropriate.authentication failed errors on simulator — expected when Secure Enclave or biometrics are not available. Test on hardware.pod install was run after upgrading to v6.PRs and issue reports are welcome. Please open an issue before introducing breaking API changes so we can discuss the best upgrade path.
MIT © Mateus Andrade
FAQs
🔐 React Native secure storage, rebuilt with Nitro Modules ⚡️ Biometric-ready, StrongBox-aware, and metadata-rich for modern mobile apps
The npm package react-native-sensitive-info receives a total of 26,774 weekly downloads. As such, react-native-sensitive-info popularity was classified as popular.
We found that react-native-sensitive-info demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Socket has acquired Secure Annex to expand extension security across browsers, IDEs, and AI tools.

Research
/Security News
Socket is tracking cloned Open VSX extensions tied to GlassWorm, with several updated from benign-looking sleepers into malware delivery vehicles.

Product
Reachability analysis for PHP is now available in experimental, helping teams identify which vulnerabilities are actually exploitable.