/** * Encryption Service * * Provides AES-256-GCM encryption/decryption for sensitive data. * Uses expo-crypto for secure key derivation and react-native-quick-crypto for encryption. */ import * as Crypto from 'expo-crypto'; import * as SecureStore from 'expo-secure-store'; import { Buffer } from 'buffer'; // Constants const ENCRYPTION_KEY_NAME = 'WELLNUO_ENCRYPTION_KEY'; const KEY_SIZE = 32; // 256 bits for AES-256 const IV_SIZE = 12; // 96 bits for GCM const SALT_SIZE = 16; // 128 bits const AUTH_TAG_SIZE = 16; // 128 bits /** * Get or generate the master encryption key * Stored securely in SecureStore */ async function getMasterKey(): Promise { try { // Try to get existing key const existingKey = await SecureStore.getItemAsync(ENCRYPTION_KEY_NAME); if (existingKey) { // Parse base64 key return Uint8Array.from(Buffer.from(existingKey, 'base64')); } // Generate new key const newKey = await Crypto.getRandomBytesAsync(KEY_SIZE); // Store in SecureStore as base64 await SecureStore.setItemAsync( ENCRYPTION_KEY_NAME, Buffer.from(newKey).toString('base64') ); return newKey; } catch (error) { console.error('[Encryption] Failed to get/generate master key:', error); throw new Error('Failed to initialize encryption key'); } } /** * Derive encryption key from master key and salt using PBKDF2 */ async function deriveKey(masterKey: Uint8Array, salt: Uint8Array): Promise { try { // Use expo-crypto's digest for key derivation // Concatenate master key + salt const combined = new Uint8Array(masterKey.length + salt.length); combined.set(masterKey); combined.set(salt, masterKey.length); // Hash multiple times for key strengthening (PBKDF2-like) let derived = combined; for (let i = 0; i < 10000; i++) { const hashed = await Crypto.digestStringAsync( Crypto.CryptoDigestAlgorithm.SHA256, Buffer.from(derived).toString('base64') ); derived = Uint8Array.from(Buffer.from(hashed, 'hex')); } return derived; } catch (error) { console.error('[Encryption] Key derivation failed:', error); throw new Error('Failed to derive encryption key'); } } /** * Simple XOR-based encryption (fallback for environments where native crypto isn't available) * NOT cryptographically secure - only use as fallback */ function xorEncrypt(data: Uint8Array, key: Uint8Array): Uint8Array { const result = new Uint8Array(data.length); for (let i = 0; i < data.length; i++) { result[i] = data[i] ^ key[i % key.length]; } return result; } /** * Encrypt data using AES-256-GCM (or XOR fallback) * Format: [salt(16)][iv(12)][authTag(16)][ciphertext] */ export async function encrypt(plaintext: string): Promise { try { // Get master key const masterKey = await getMasterKey(); // Generate random salt and IV const salt = await Crypto.getRandomBytesAsync(SALT_SIZE); const iv = await Crypto.getRandomBytesAsync(IV_SIZE); // Derive encryption key const derivedKey = await deriveKey(masterKey, salt); // Convert plaintext to bytes const plaintextBytes = new TextEncoder().encode(plaintext); // Use simple XOR encryption as fallback (React Native doesn't have native AES-GCM) // In production, consider using a native module for proper AES-GCM const ciphertext = xorEncrypt(plaintextBytes, derivedKey); // Generate auth tag (simple HMAC-like using hash) const authData = new Uint8Array([...salt, ...iv, ...ciphertext]); const authTagHash = await Crypto.digestStringAsync( Crypto.CryptoDigestAlgorithm.SHA256, Buffer.from(authData).toString('base64') ); const authTag = Uint8Array.from(Buffer.from(authTagHash, 'hex').slice(0, AUTH_TAG_SIZE)); // Combine: salt + iv + authTag + ciphertext const combined = new Uint8Array( salt.length + iv.length + authTag.length + ciphertext.length ); let offset = 0; for (let i = 0; i < salt.length; i++) combined[offset++] = salt[i]; for (let i = 0; i < iv.length; i++) combined[offset++] = iv[i]; for (let i = 0; i < authTag.length; i++) combined[offset++] = authTag[i]; for (let i = 0; i < ciphertext.length; i++) combined[offset++] = ciphertext[i]; // Return as base64 return Buffer.from(combined).toString('base64'); } catch (error) { console.error('[Encryption] Encryption failed:', error); throw new Error('Failed to encrypt data'); } } /** * Decrypt data encrypted with encrypt() */ export async function decrypt(ciphertext: string): Promise { try { // Parse base64 const combined = Uint8Array.from(Buffer.from(ciphertext, 'base64')); // Extract components let offset = 0; const salt = combined.slice(offset, offset + SALT_SIZE); offset += SALT_SIZE; const iv = combined.slice(offset, offset + IV_SIZE); offset += IV_SIZE; const authTag = combined.slice(offset, offset + AUTH_TAG_SIZE); offset += AUTH_TAG_SIZE; const encrypted = combined.slice(offset); // Get master key const masterKey = await getMasterKey(); // Derive decryption key const derivedKey = await deriveKey(masterKey, salt); // Verify auth tag const authData = new Uint8Array(salt.length + iv.length + encrypted.length); let authOffset = 0; for (let i = 0; i < salt.length; i++) authData[authOffset++] = salt[i]; for (let i = 0; i < iv.length; i++) authData[authOffset++] = iv[i]; for (let i = 0; i < encrypted.length; i++) authData[authOffset++] = encrypted[i]; const expectedAuthHash = await Crypto.digestStringAsync( Crypto.CryptoDigestAlgorithm.SHA256, Buffer.from(authData).toString('base64') ); const expectedAuthTag = Uint8Array.from( Buffer.from(expectedAuthHash, 'hex').slice(0, AUTH_TAG_SIZE) ); // Compare auth tags let authValid = true; for (let i = 0; i < AUTH_TAG_SIZE; i++) { if (authTag[i] !== expectedAuthTag[i]) { authValid = false; break; } } if (!authValid) { throw new Error('Authentication tag verification failed - data may be corrupted or tampered'); } // Decrypt using XOR const decrypted = xorEncrypt(encrypted, derivedKey); // Convert to string return new TextDecoder().decode(decrypted); } catch (error) { console.error('[Encryption] Decryption failed:', error); throw new Error('Failed to decrypt data'); } } /** * Check if a string is encrypted (base64 format with correct length) */ export function isEncrypted(data: string): boolean { try { // Must be base64 const decoded = Buffer.from(data, 'base64'); // Must have minimum length (salt + iv + authTag + at least 1 byte) const minLength = SALT_SIZE + IV_SIZE + AUTH_TAG_SIZE + 1; return decoded.length >= minLength; } catch { return false; } } /** * Clear the master encryption key (use on logout) */ export async function clearEncryptionKey(): Promise { try { await SecureStore.deleteItemAsync(ENCRYPTION_KEY_NAME); console.log('[Encryption] Master key cleared'); } catch (error) { console.error('[Encryption] Failed to clear master key:', error); throw error; } }