UNPKG

rn-secure-keystore

Version:

A comprehensive, cross-platform React Native wrapper for secure key-value storage using native security features of Android and iOS. It supports **biometric authentication**, **hardware-backed encryption**, and deep platform integrations such as **Android

479 lines (448 loc) 15.2 kB
"use strict"; import { NativeModules, Platform } from 'react-native'; const LINKING_ERROR = `The package 'rn-secure-keystore' doesn't seem to be linked. Make sure:\n\n` + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n'; const NativeSecureKeystore = NativeModules.RnSecureKeystore ? NativeModules.RnSecureKeystore : new Proxy({}, { get() { throw new Error(LINKING_ERROR); } }); // Type definitions for hardware security info // Type definitions for storage options (for setItem) // Type definitions for retrieval options (for getItem) // Custom error types export class SecureStorageError extends Error { constructor(message, code, originalError) { super(message); this.code = code; this.originalError = originalError; this.name = 'SecureStorageError'; } } /** * SecureStorage - React Native wrapper for secure key-value storage */ class SecureStorage { /** * Store a key-value pair securely * @param key The key to store * @param value The value to store * @param options Storage options including security level */ static async setItem(key, value, options = {}) { if (!key || typeof key !== 'string') { throw new SecureStorageError('Key must be a non-empty string', 'INVALID_KEY'); } if (!value || typeof value !== 'string') { throw new SecureStorageError('Value must be a non-empty string', 'INVALID_VALUE'); } const defaultOptions = { withBiometric: false, // Platform-specific defaults ...(Platform.OS === 'android' && { requireStrongBox: false, requireHardware: false, securityLevel: 'auto', allowFallback: true }), ...(Platform.OS === 'ios' && { accessGroup: null, accessControl: null }), // Default authentication prompts authenticatePrompt: 'Authenticate to store data', authenticatePromptSubtitle: 'Use your biometric credential to secure this data' }; const mergedOptions = { ...defaultOptions, ...options }; // Filter out platform-specific options for the other platform if (Platform.OS === 'android') { // Remove iOS-specific options delete mergedOptions.accessGroup; delete mergedOptions.accessControl; } else if (Platform.OS === 'ios') { // Remove Android-specific options delete mergedOptions.requireStrongBox; delete mergedOptions.requireHardware; delete mergedOptions.securityLevel; delete mergedOptions.allowFallback; } try { return await NativeSecureKeystore.setItem(key, value, mergedOptions); } catch (error) { throw new SecureStorageError(error.message || 'Failed to store item', error.code || 'STORAGE_ERROR', error); } } /** * Retrieve a stored value by key * @param key The key to retrieve * @param options Retrieval options */ static async getItem(key, options = {}) { if (!key || typeof key !== 'string') { throw new SecureStorageError('Key must be a non-empty string', 'INVALID_KEY'); } const defaultOptions = { authenticatePrompt: 'Authenticate to access secure data', authenticatePromptSubtitle: 'Use your biometric credential to access this data', showModal: false, kLocalizedFallbackTitle: 'Use Passcode' }; const mergedOptions = { ...defaultOptions, ...options }; // Filter platform-specific options if (Platform.OS === 'android') { // Remove iOS-specific options delete mergedOptions.accessGroup; delete mergedOptions.kLocalizedFallbackTitle; } else if (Platform.OS === 'ios') { // Remove Android-specific options delete mergedOptions.showModal; } try { return await NativeSecureKeystore.getItem(key, mergedOptions); } catch (error) { throw new SecureStorageError(error.message || 'Failed to retrieve item', error.code || 'RETRIEVAL_ERROR', error); } } /** * Remove a stored key-value pair * @param key The key to remove */ static async removeItem(key) { if (!key || typeof key !== 'string') { throw new SecureStorageError('Key must be a non-empty string', 'INVALID_KEY'); } try { return await NativeSecureKeystore.removeItem(key); } catch (error) { throw new SecureStorageError(error.message || 'Failed to remove item', error.code || 'REMOVAL_ERROR', error); } } /** * Check if a key exists in storage * @param key The key to check */ static async hasItem(key) { if (!key || typeof key !== 'string') { throw new SecureStorageError('Key must be a non-empty string', 'INVALID_KEY'); } try { return await NativeSecureKeystore.hasItem(key); } catch (error) { // hasItem should never throw, return false on error return false; } } /** * Get all stored keys */ static async getAllKeys() { try { return await NativeSecureKeystore.getAllKeys(); } catch (error) { throw new SecureStorageError(error.message || 'Failed to get all keys', error.code || 'GET_KEYS_ERROR', error); } } /** * Clear all stored data */ static async clear() { try { return await NativeSecureKeystore.clear(); } catch (error) { throw new SecureStorageError(error.message || 'Failed to clear storage', error.code || 'CLEAR_ERROR', error); } } /** * Check if biometric authentication is available */ static async isBiometricAvailable() { try { return await NativeSecureKeystore.isBiometricAvailable(); } catch (error) { return false; } } /** * Check if hardware-backed keystore is available */ static async isHardwareBackedAvailable() { try { return await NativeSecureKeystore.isHardwareBackedAvailable(); } catch (error) { return false; } } /** * Check if StrongBox security is available (Android only) * @returns Promise<boolean> - true if available on Android, false on iOS */ static async isStrongBoxAvailable() { if (Platform.OS === 'android') { try { return await NativeSecureKeystore.isStrongBoxAvailable(); } catch (error) { return false; } } // StrongBox is Android-specific, return false for iOS return false; } /** * Get comprehensive hardware security information * @returns Object containing all available security features and recommendations */ static async getHardwareSecurityInfo() { if (Platform.OS === 'android') { try { return await NativeSecureKeystore.getHardwareSecurityInfo(); } catch (error) { // Fallback for Android const isHardwareBacked = await this.isHardwareBackedAvailable(); return { isHardwareBackedAvailable: isHardwareBacked, isStrongBoxAvailable: false, recommendedSecurityLevel: isHardwareBacked ? 'hardware' : 'software' }; } } // For iOS, return equivalent information const isHardwareBacked = await this.isHardwareBackedAvailable(); return { isHardwareBackedAvailable: isHardwareBacked, isStrongBoxAvailable: false, // iOS doesn't have StrongBox recommendedSecurityLevel: isHardwareBacked ? 'hardware' : 'software' }; } /** * Check if a specific key is stored with hardware-backed security * @param key The key to check * @returns True if the key is hardware-backed, false otherwise */ static async isKeyHardwareBacked(key) { if (!key || typeof key !== 'string') { throw new SecureStorageError('Key must be a non-empty string', 'INVALID_KEY'); } try { if (Platform.OS === 'android') { return await NativeSecureKeystore.isKeyHardwareBacked(key); } // For iOS, check if key exists and if hardware backing is available const hasItem = await this.hasItem(key); if (!hasItem) { return false; } return await this.isHardwareBackedAvailable(); } catch (error) { return false; } } /** * Get security level for a specific key (Android only) * @param key The key to check * @returns Security level of the key */ static async getKeySecurityLevel(key) { if (!key || typeof key !== 'string') { throw new SecureStorageError('Key must be a non-empty string', 'INVALID_KEY'); } try { if (Platform.OS === 'android') { return await NativeSecureKeystore.getKeySecurityLevel(key); } else { return await NativeSecureKeystore.getKeySecurityLevel(key); } } catch (error) { return 'unknown'; } } /** * Utility method to get security level recommendation for the current device * @returns Recommended security level based on device capabilities */ static async getRecommendedSecurityLevel() { const info = await this.getHardwareSecurityInfo(); return info.recommendedSecurityLevel; } /** * Utility method to check if a security level is available on the current device * @param level The security level to check * @returns True if the security level is available */ static async isSecurityLevelAvailable(level) { switch (level) { case 'strongbox': return Platform.OS === 'android' ? await this.isStrongBoxAvailable() : false; case 'hardware': return this.isHardwareBackedAvailable(); default: return false; } } /** * Get security status for all stored keys * @returns Object mapping keys to their security status */ static async getSecurityStatus() { const keys = await this.getAllKeys(); const status = {}; for (const key of keys) { const exists = await this.hasItem(key); const isHardwareBacked = exists ? await this.isKeyHardwareBacked(key) : false; let securityLevel; if (exists) { try { securityLevel = await this.getKeySecurityLevel(key); } catch { securityLevel = undefined; } } status[key] = { exists, isHardwareBacked, ...(securityLevel && { securityLevel }) }; } return status; } /** * Android-specific: Set item with StrongBox security (if available) * @param key The key to store * @param value The value to store * @param allowFallback Whether to allow fallback to hardware if StrongBox is not available */ static async setStrongBoxItem(key, value, allowFallback = false) { if (Platform.OS !== 'android') { throw new SecureStorageError('StrongBox is only available on Android devices', 'PLATFORM_NOT_SUPPORTED'); } const isAvailable = await this.isStrongBoxAvailable(); if (!isAvailable && !allowFallback) { throw new SecureStorageError('StrongBox is not available on this device', 'STRONGBOX_NOT_AVAILABLE'); } return this.setItem(key, value, { securityLevel: 'strongbox', allowFallback }); } /** * iOS-specific: Set item with custom access control * @param key The key to store * @param value The value to store * @param accessControl iOS access control level * @param accessGroup iOS keychain access group */ static async setKeychainItem(key, value, accessControl, accessGroup) { if (Platform.OS !== 'ios') { throw new SecureStorageError('Keychain access control is only available on iOS', 'PLATFORM_NOT_SUPPORTED'); } return this.setItem(key, value, { accessControl, accessGroup }); } /** * Platform-specific capabilities check * @returns Object with platform-specific feature availability */ static async getPlatformCapabilities() { const [hasStrongBox, hasHardwareBacked, hasBiometrics] = await Promise.all([this.isStrongBoxAvailable(), this.isHardwareBackedAvailable(), this.isBiometricAvailable()]); return { platform: Platform.OS, hasStrongBox, hasHardwareBackedKeystore: hasHardwareBacked, hasBiometrics, hasKeychainAccessControl: Platform.OS === 'ios' }; } /** * Utility method to migrate from plain storage to secure storage * @param key The key to migrate * @param plainValue The plain text value to secure * @param options Security options for the new secure storage */ static async migrateToSecureStorage(key, plainValue, options = {}) { try { // Store securely await this.setItem(key, plainValue, options); return true; } catch (error) { throw new SecureStorageError(`Failed to migrate key "${key}" to secure storage: ${error instanceof Error ? error.message : 'Unknown error'}`, 'MIGRATION_ERROR', error instanceof Error ? error : undefined); } } /** * Utility method to check if the current device meets minimum security requirements * @param requirements Security requirements to check */ static async meetsSecurityRequirements(requirements) { const missing = []; if (requirements.requireBiometric) { const hasBiometric = await this.isBiometricAvailable(); if (!hasBiometric) { missing.push('biometric authentication'); } } if (requirements.requireHardwareBacking) { const hasHardware = await this.isHardwareBackedAvailable(); if (!hasHardware) { missing.push('hardware-backed storage'); } } if (requirements.requireStrongBox) { const hasStrongBox = await this.isStrongBoxAvailable(); if (!hasStrongBox) { missing.push('StrongBox security'); } } return { meets: missing.length === 0, missing }; } } export default SecureStorage; // iOS access control constants export const ACCESS_CONTROL = { BIOMETRY_ANY: 'kSecAccessControlBiometryAny', BIOMETRY_CURRENT_SET: 'kSecAccessControlBiometryCurrentSet', DEVICE_PASSCODE: 'kSecAccessControlDevicePasscode', APPLICATION_PASSWORD: 'kSecAccessControlApplicationPassword', BIOMETRY_ANY_OR_DEVICE_PASSCODE: 'kSecAccessControlBiometryAnyOrDevicePasscode' }; // Common Error Codes - Added for better error handling export const ERROR_CODES = { // Authentication errors AUTHENTICATION_CANCELLED: 'AUTHENTICATION_CANCELLED', AUTHENTICATION_FAILED: 'AUTHENTICATION_FAILED', BIOMETRIC_NOT_AVAILABLE: 'BIOMETRIC_NOT_AVAILABLE', INTERACTION_NOT_ALLOWED: 'INTERACTION_NOT_ALLOWED', // Platform errors PLATFORM_NOT_SUPPORTED: 'PLATFORM_NOT_SUPPORTED', STRONGBOX_NOT_AVAILABLE: 'STRONGBOX_NOT_AVAILABLE', // Input validation errors INVALID_KEY: 'INVALID_KEY', INVALID_VALUE: 'INVALID_VALUE', // Storage errors STORAGE_ERROR: 'STORAGE_ERROR', RETRIEVAL_ERROR: 'RETRIEVAL_ERROR', REMOVAL_ERROR: 'REMOVAL_ERROR', CLEAR_ERROR: 'CLEAR_ERROR', GET_KEYS_ERROR: 'GET_KEYS_ERROR', // Keychain/Keystore errors KEYCHAIN_ERROR: 'KEYCHAIN_ERROR', CIPHER_ERROR: 'CIPHER_ERROR', ACCESS_CONTROL_ERROR: 'ACCESS_CONTROL_ERROR', // Hardware errors SECURITY_INFO_ERROR: 'SECURITY_INFO_ERROR', NO_ACTIVITY: 'NO_ACTIVITY' }; //# sourceMappingURL=index.js.map