Securely store and retrieve sensitive information on React Native with biometric authentication, AES-256-GCM encryption, and hardware-backed key storage.
๐ Securely store sensitive information on React Native with automatic encryption, biometric authentication, and hardware-backed key storage.
v5.6.0 - Production Ready | iOS 13+ | Android 7+ | macOS 10.15+ | visionOS 1.0+ | watchOS 6+
[!TIP] v6.0.0 (Nitro Preview)
- ๐ Adds full Nitro Module and Nitro View compatibility
- ๐งช Currently stabilizing on the
masterbranch- โณ Preview quality while documentation and samples are updated
[!IMPORTANT] v5.6.0 (Current)
- โ New Architecture ready (RN 0.73+)
- โ Universal Android authentication (API 24+)
- โ Biometric + device credential support
- โ AES-256-GCM hardware-backed encryption
- ๐ฆ Distributed as a TurboModule
[!NOTE] v5.5.x and older
- โ Old Architecture only
- ๐ Bug fixes only, no new features
- โฌ๏ธ Install when you cannot move to RN New Architecture yet
Install the right build for your project:
# React Native 0.73+ (New Architecture)
npm install react-native-sensitive-info@5.6.0
# React Native <0.73 (Old Architecture)
npm install react-native-sensitive-info@5.5.x
| Feature | iOS | Android | macOS | visionOS | watchOS |
|---|---|---|---|---|---|
| Secure Storage | โ | โ | โ | โ | โ |
| AES-256 Encryption | โ | โ | โ | โ | โ |
| Hardware-Backed Keys | โ | โ API 24+ | โ | โ | โ |
| Biometric Auth | โ Face/Touch ID | โ Fingerprint & device credential | โ Touch ID | โ Optic ID | โ Passcode only |
| Automatic Migration | โ | โ | โ | โ | โ |
| Zero Dependencies | โ | โ | โ | โ | โ |
App Code
โ
SensitiveInfo.setItem(key, value)
โ
AES-256-GCM Encryption (random IV per operation)
โ
Hardware-Backed Key Storage
โโ iOS: Keychain + Secure Enclave (iOS 16+)
โโ Android: AndroidKeyStore (StrongBox when available)
โโ Optional: Biometric authentication required
โ
Encrypted data stored securely
Real-world use cases:
| Android Version | API Level | Prompt Strategy | Storage Backend |
|---|---|---|---|
| Android 14 - 10 | 34 - 29 | Keystore-gated biometric/device credential prompt shown when decrypting | AndroidKeyStore (hardware-backed, StrongBox when available) |
| Android 9 - 7 | 28 - 24 | Manual prompt displayed before key use; library waits for user confirmation | AndroidKeyStore (hardware-backed) |
[!TIP] Supply localized
authenticationPromptcopy even on Android 10+ so users see consistent messaging across API levels.
[!CAUTION] Android 7-9 throws
E_AUTHENTICATION_REQUIREDif you skip the prompt configuration because the OS does not present a system dialog automatically.
npm install react-native-sensitive-info@5.6.0
# or
yarn add react-native-sensitive-info@5.6.0
npx react-native link react-native-sensitive-info
Add permissions to AndroidManifest.xml:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
[!NOTE] Android 7-9 rely on a manual authentication dialog. Provide
authenticationPromptstrings whenever you request biometric/device credential protection so the library can display localized UI ahead of decrypting secrets.
Add to Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>We need Face ID to protect your authentication token</string>
import { SensitiveInfo } from 'react-native-sensitive-info';
// Store with biometric protection
await SensitiveInfo.setItem('auth-token', 'jwt-token-xyz', {
keychainService: 'myapp',
accessControl: 'biometryOrDevicePasscode',
authenticationPrompt: {
title: 'Authenticate',
subtitle: 'Please authenticate to protect your token',
description: 'Your token is encrypted and requires biometric verification'
}
});
// User sees biometric prompt (Face ID/Touch ID/Fingerprint)
// After authentication, value is stored encrypted
// Retrieve decrypted value
const result = await SensitiveInfo.getItem('auth-token', {
keychainService: 'myapp'
});
console.log(result.value); // 'jwt-token-xyz'
console.log(result.metadata); // { securityLevel: 'strongBox', backend: 'androidKeystore', timestamp: 1700000000 }
// If biometric-protected: OS shows prompt automatically
// User authenticates โ value is decrypted and returned
// Check if exists
const exists = await SensitiveInfo.hasItem('auth-token', {
keychainService: 'myapp'
});
// Get all keys in service
const keys = await SensitiveInfo.getAllItems({
keychainService: 'myapp'
});
// Returns: ['auth-token', 'refresh-token', ...]
// Delete specific item
await SensitiveInfo.deleteItem('auth-token', {
keychainService: 'myapp'
});
// Clear entire service
await SensitiveInfo.clearService({
keychainService: 'myapp'
});
| Platform | Storage | Hardware-Backed | Notes |
|---|---|---|---|
| iOS 16+ | Keychain + Secure Enclave | โ Yes | Isolated, tamper-resistant |
| iOS 13-15 | Keychain only | โ Yes | Device passcode/biometric |
| Android 14-10 | AndroidKeyStore + StrongBox | โ Yes | System prompt wraps keystore auth |
| Android 9-7 | AndroidKeyStore | โ Yes | Manual prompt required before key use |
| macOS 13+ | Keychain + Secure Enclave | โ Yes | Touch ID support |
| visionOS | Keychain + Secure Enclave | โ Yes | Optic ID support |
| watchOS | Keychain | โ Partial | Shared with paired iPhone |
When enabled, biometric authentication is required to access encrypted data:
[!NOTE] On Android 7-9 the library displays its own dialog before touching the keystore. On Android 10+ the OS handles the biometric/device credential UI directly.
Protection against:
setItem(key, value, options?)Stores an encrypted value with optional biometric protection.
interface SetOptions {
keychainService?: string; // Service namespace (app package by default)
accessControl?: string; // 'biometryOrDevicePasscode' | 'devicePasscode' | 'none'
authenticationPrompt?: {
title: string; // Required: "Authenticate" (mandatory on Android 7-9)
subtitle?: string; // "Scan your fingerprint"
description?: string; // "Required to protect this data" (shown in manual dialog)
};
}
interface SetResult {
metadata: {
securityLevel: string; // 'biometry' | 'deviceCredential' | 'software'
accessControl: string; // Policy applied
backend: string; // 'keychain' | 'preferences'
timestamp: number; // UNIX timestamp
};
}
getItem(key, options?)Retrieves and decrypts a stored value.
interface GetOptions {
keychainService?: string; // Service namespace
}
interface GetResult {
value: string; // Decrypted value
metadata: {
securityLevel: string;
accessControl: string;
backend: string;
timestamp: number;
};
}
// Returns: GetResult | null (null if not found)
hasItem(key, options?)Checks if a value exists in storage.
const exists = await SensitiveInfo.hasItem('key', { keychainService: 'myapp' });
// Returns: boolean
getAllItems(options?)Lists all keys in a service namespace.
const keys = await SensitiveInfo.getAllItems({ keychainService: 'myapp' });
// Returns: string[] (array of key names)
deleteItem(key, options?)Deletes a specific value and its encryption key.
await SensitiveInfo.deleteItem('key', { keychainService: 'myapp' });
clearService(options?)Deletes all values in a service namespace.
await SensitiveInfo.clearService({ keychainService: 'myapp' });
try {
await SensitiveInfo.setItem('token', 'value', {
authenticationPrompt: { title: 'Authenticate' }
});
} catch (error) {
switch (error.code) {
case 'E_BIOMETRIC_NOT_AVAILABLE':
// Device doesn't support biometric - use password instead
showPasswordPrompt();
break;
case 'E_BIOMETRIC_LOCKOUT':
// Too many failed attempts - try again later
console.log('Locked out for ~30 seconds');
break;
case 'E_USER_CANCELLED':
// User dismissed the biometric prompt - normal behavior
console.log('User cancelled');
break;
case 'E_NOT_FOUND':
// (getItem only) Value doesn't exist
console.log('Not stored yet');
break;
case 'E_ENCRYPTION_FAILED':
// Encryption operation failed
showError('Failed to store secure data');
break;
}
}
# Android Emulator - Simulate fingerprint
adb shell cmd finger simulate 1
# iOS Simulator - Tap biometric in menu or press โU
// Test with biometric
await testWithBiometric();
// Test without biometric (disable in settings)
await testWithoutBiometric();
// Test error cases
await testBiometricCancellation();
await testBiometricTimeout();
[!TIP] Run your suite on both an Android 13+ emulator and an Android 8/9 emulator to validate the automatic keystore dialog and the manual pre-auth dialog paths.
ActivityContextHolder.setActivity(this) runs in MainActivity.onCreate so prompts attach to the foreground activity.USE_BIOMETRIC and USE_FINGERPRINT in AndroidManifest.xml.Good news! v5.6.0 is 100% backward compatible and fully automatic:
// v5.5.0 code works unchanged in v5.6.0
const token = await SensitiveInfo.getItem('auth-token');
// Behind the scenes:
// 1. Detects old fixed-IV encryption
// 2. Decrypts with old algorithm
// 3. Re-encrypts with random IV (secure!)
// 4. Updates storage transparently
// Users see no difference โจ
No code changes required - just upgrade and everything works better!
// โ
Use service namespaces to organize secrets
await SensitiveInfo.setItem('auth-token', token, {
keychainService: 'authentication'
});
await SensitiveInfo.setItem('api-key', key, {
keychainService: 'api'
});
// โ
Always provide biometric prompts with clear messages
await SensitiveInfo.setItem('sensitive-data', data, {
authenticationPrompt: {
title: 'Secure Your Account',
description: 'Biometric verification required'
}
});
// โ
Handle errors gracefully
try {
const result = await SensitiveInfo.getItem('token');
if (!result) {
// Item not found - redirect to login
}
} catch (e) {
if (e.code === 'E_BIOMETRIC_LOCKOUT') {
// Guide user through unlock process
}
}
// โ Don't hardcode service names - use constants
const SERVICE = 'myapp-auth'; // โ Define once, reuse everywhere
// โ Don't skip error handling for biometric
await SensitiveInfo.setItem('key', 'value', {
authenticationPrompt: { title: 'Auth' }
// โ Must catch errors
// โ Don't store passwords in plain text
const password = 'user-password'; // โ DON'T DO THIS
// Instead: Use OAuth tokens, never store passwords locally
// โ Don't log sensitive values
console.log(token); // โ Never log decrypted values
Cause: BiometricAuthenticator can't access Activity
Solution: Ensure ActivityContextHolder.setActivity(this) is called in MainActivity
// In MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ActivityContextHolder.setActivity(this)
}
Cause: Device lacks fingerprint sensor or biometric not enrolled Solution: Fall back to password authentication
try {
await setItemWithBiometric();
} catch (e) {
if (e.code === 'E_BIOMETRIC_NOT_AVAILABLE') {
await setItemWithPassword(); // Fallback
}
}
E_AUTHENTICATION_REQUIRED on Android 7-9Cause: authenticationPrompt text is missing, so the manual dialog cannot be rendered before hitting the keystore.
Solution: Provide at least a title (and ideally description) when storing or reading biometric-protected secrets.
Cause: Biometric enrollment changed (finger added/removed) Solution: Delete old key, recreate on next store
try {
const value = await SensitiveInfo.getItem('token');
} catch (e) {
if (e.code === 'E_KEY_INVALIDATED') {
await SensitiveInfo.deleteItem('token');
// User must re-authenticate to create new key
}
}
MIT ยฉ mCodex
Contributions welcome! Please see CONTRIBUTING.md for guidelines.
Built with โค๏ธ for React Native