Secure storage abstractions for React Native - provides secure storage for sensitive data (encrypted seeds, keys) using react-native-keychain.
- 🔒 Secure storage using native keychain/keystore
- 📱 iOS Keychain integration with selective iCloud sync
- 🤖 Android Keystore integration with selective Google Cloud backup
- 🔐 Biometric authentication support
- 💾 Encrypted data storage at rest
- ✅ Comprehensive input validation
- 📊 Structured logging support
- ⏱️ Configurable timeouts
- 🎯 TypeScript support with full type safety
npm install @tetherto/wdk-react-native-secure-storageIf the package is not yet published to npm, you can install directly from GitHub:
npm install https://github.com/tetherto/wdk-react-native-secure-storage.gitOr add to your package.json:
{
"dependencies": {
"@tetherto/wdk-react-native-secure-storage": "github:tetherto/wdk-react-native-secure-storage"
}
}Then run npm install.
npm install react-native@">=0.70.0"import { createSecureStorage } from '@tetherto/wdk-react-native-secure-storage'
// Create storage instance
const storage = createSecureStorage()
// Store encryption key
await storage.setEncryptionKey('my-encryption-key', 'user@example.com')
// Retrieve encryption key
const key = await storage.getEncryptionKey('user@example.com')
if (key) {
console.log('Key retrieved:', key)
}
// Store encrypted seed
await storage.setEncryptedSeed('encrypted-seed-data', 'user@example.com')
// Store encrypted entropy
await storage.setEncryptedEntropy('encrypted-entropy-data', 'user@example.com')
// Get all encrypted data
const allData = await storage.getAllEncrypted('user@example.com')
console.log('All data:', allData)
// Check if wallet exists
const exists = await storage.hasWallet('user@example.com')
// Delete wallet
await storage.deleteWallet('user@example.com')import { createSecureStorage, defaultLogger, LogLevel } from '@tetherto/wdk-react-native-secure-storage'
// Configure logger
defaultLogger.setLevel(LogLevel.INFO)
// Create storage with custom options
const storage = createSecureStorage({
logger: customLogger, // Optional custom logger
authentication: {
promptMessage: 'Authenticate to access your wallet',
cancelLabel: 'Cancel',
disableDeviceFallback: false,
},
timeoutMs: 30000, // 30 seconds default
})
// Use storage
await storage.setEncryptionKey('key', 'user@example.com')import {
createSecureStorage,
ValidationError,
KeychainWriteError,
KeychainReadError,
AuthenticationError,
TimeoutError,
} from '@tetherto/wdk-react-native-secure-storage'
const storage = createSecureStorage()
try {
await storage.setEncryptionKey('my-key', 'user@example.com')
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid input:', error.message)
} else if (error instanceof KeychainWriteError) {
console.error('Failed to write to keychain:', error.message)
} else if (error instanceof TimeoutError) {
console.error('Operation timed out:', error.message)
} else {
console.error('Unexpected error:', error)
}
}
try {
const key = await storage.getEncryptionKey('user@example.com')
if (!key) {
console.log('Key not found')
}
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Authentication failed:', error.message)
} else if (error instanceof KeychainReadError) {
console.error('Failed to read from keychain:', error.message)
}
}The identifier parameter allows you to support multiple wallets:
// Store data for different users
await storage.setEncryptionKey('key1', 'user1@example.com')
await storage.setEncryptionKey('key2', 'user2@example.com')
// Retrieve specific user's data
const key1 = await storage.getEncryptionKey('user1@example.com')
const key2 = await storage.getEncryptionKey('user2@example.com')Creates a new instance of secure storage. Each call returns a new instance with the specified options. For most apps, you should create one instance and reuse it throughout your application.
Options:
logger?: Logger- Custom logger instanceauthentication?: AuthenticationOptions- Authentication prompt configurationtimeoutMs?: number- Timeout for keychain operations (default: 30000ms, min: 1000ms, max: 300000ms)
Returns: SecureStorage instance
Stores an encryption key securely.
Parameters:
key: string- The encryption key (max 10KB, non-empty)identifier?: string- Optional identifier for multiple wallets (max 256 chars)
Throws:
ValidationError- If input is invalidKeychainWriteError- If keychain operation failsTimeoutError- If operation times out
Retrieves an encryption key.
Parameters:
identifier?: string- Optional identifier
Returns: The encryption key or null if not found
Throws:
ValidationError- If identifier is invalidAuthenticationError- If authentication failsKeychainReadError- If keychain operation failsTimeoutError- If operation times out
Stores encrypted seed data.
Retrieves encrypted seed data.
Stores encrypted entropy data.
Retrieves encrypted entropy data.
getAllEncrypted(identifier?: string): Promise<{encryptedSeed: string | null, encryptedEntropy: string | null, encryptionKey: string | null}>
Retrieves all encrypted wallet data at once.
Checks if wallet credentials exist.
Deletes all wallet credentials.
Throws:
ValidationError- If identifier is invalidSecureStorageError- If deletion fails (with details of which items failed)TimeoutError- If operation times out
Checks if biometric authentication is available.
Authenticates with biometrics. Returns true if successful, false otherwise.
Throws:
AuthenticationError- If authentication fails
The module provides a Logger interface for structured logging. The default logger can be configured:
import { defaultLogger, LogLevel } from '@tetherto/wdk-react-native-secure-storage'
// Set the minimum log level (logs below this level will be ignored)
defaultLogger.setLevel(LogLevel.INFO)
// Available log levels: DEBUG, INFO, WARN, ERRORYou can also provide a custom logger that implements the Logger interface:
const customLogger: Logger = {
debug: (message, context) => { /* ... */ },
info: (message, context) => { /* ... */ },
warn: (message, context) => { /* ... */ },
error: (message, error, context) => { /* ... */ },
setLevel: (level) => { /* optional */ },
}
const storage = createSecureStorage({ logger: customLogger })The module has no shared state or cleanup requirements. Each storage instance is independent and can be used without any module-level lifecycle management.
- All inputs are validated before processing
- Maximum length limits enforced (10KB for values, 256 chars for identifiers)
- Invalid characters rejected
- Type checking at runtime
- All validation happens before any side effects
- Comprehensive error types for different failure scenarios
- Detailed error messages
- Proper error propagation
- Structured logging for security events
- Configurable log levels
- No sensitive data in logs
SecureStorageError- Base error classKeychainError- Keychain operation errorsKeychainWriteError- Keychain write failuresKeychainReadError- Keychain read failuresAuthenticationError- Authentication failuresValidationError- Input validation failuresTimeoutError- Operation timeout errors
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage# Type checking
npm run typecheck
# Linting
npm run lint
npm run lint:fix
# Formatting
npm run format
npm run format:checkNote: ESLint and Prettier are configured but need to be installed as dev dependencies:
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin prettierThis module is production-ready and includes:
✅ Built and tested - All code is compiled to JavaScript with TypeScript definitions
✅ Proper exports - Only necessary files are included in the npm package
✅ Security hardened - Input validation and secure storage with device-level protections
✅ Error handling - Comprehensive error types for all failure scenarios
✅ Logging - Structured logging with configurable levels (defaults to ERROR in production)
✅ Type safety - Full TypeScript support with exported types
✅ Documentation - Complete API documentation and usage examples
-
Configure Logging: Set appropriate log levels for your environment:
import { defaultLogger, LogLevel } from '@tetherto/wdk-react-native-secure-storage' // In production, only log errors and warnings defaultLogger.setLevel(LogLevel.WARN) // In development, you might want more verbose logging if (__DEV__) { defaultLogger.setLevel(LogLevel.DEBUG) }
-
Error Handling: Always handle errors appropriately:
try { await storage.setEncryptionKey(key, identifier) } catch (error) { // Log error to your error tracking service (e.g., Sentry) // Never log sensitive data like keys or seeds if (error instanceof ValidationError) { // Handle validation errors } else if (error instanceof KeychainWriteError) { // Handle keychain errors } }
-
Single Instance: Create one storage instance and reuse it:
// Good: Create once and reuse const storage = createSecureStorage({ logger: customLogger }) // Avoid: Creating multiple instances unnecessarily
- Data is encrypted at rest by iOS Keychain / Android Keystore
- Cloud sync behavior:
- Encryption key: Synced via iCloud Keychain (iOS) and Google Cloud backup (Android)
- Encrypted seed and entropy: Device-only storage (not synced across devices)
- Biometric authentication required when available
- Device-level keychain/keystore provides rate limiting and lockout mechanisms
- No sensitive data is logged - The logger only logs error messages and metadata
-
Timeout Resource Usage: The timeout implementation uses
Promise.race()which does NOT cancel the underlying keychain operation. The operation continues executing even after timeout, though its result is ignored. This means:- Under extreme load, timed-out keychain operations may continue executing in the background
- Memory and resources are not immediately freed on timeout
- This is generally acceptable because:
- Keychain operations are typically fast (< 1 second)
- They are bounded in duration by the OS
- Timeouts are a safety mechanism, not a normal occurrence
- React Native's single-threaded nature limits concurrent operations
- Monitoring: In production, monitor timeout frequency. If timeouts occur frequently, investigate keychain performance or increase timeout values.
-
Device Authentication: On devices without authentication (no PIN/password/biometrics), data is still encrypted at rest but accessible when the device is unlocked. This is a limitation of the underlying platform security model.
-
Device-Level Rate Limiting: The module relies on device-level keychain/keystore rate limiting and lockout mechanisms. These are more robust than app-level rate limiting and persist across app restarts.
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests (
npm test) - Run type checking (
npm run typecheck) - Run linting (
npm run lint) - Format code (
npm run format) - Build the project (
npm run build) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
# Clone the repository
git clone https://github.com/tetherto/wdk-react-native-secure-storage.git
cd wdk-react-native-secure-storage
# Install dependencies
npm install
# Run tests
npm test
# Run type checking
npm run typecheck
# Run linting
npm run lint
# Format code
npm run format
# Build
npm run buildApache-2.0