/** * Vault crypto: PBKDF2 key derivation + AES-256-GCM encrypt/decrypt. * Matches backend SentinelVault semantics (PBKDF2 from mnemonic, AES-GCM). * Uses Web Crypto API (crypto.subtle). Requires secure context / React Native polyfill if needed. */ const SALT = new TextEncoder().encode('Sentinel_Salt_2026'); const PBKDF2_ITERATIONS = 100000; const AES_KEY_LEN = 256; const GCM_IV_LEN = 16; const GCM_TAG_LEN = 16; function getCrypto(): Crypto { if (typeof crypto !== 'undefined' && crypto.subtle) return crypto; throw new Error('vaultCrypto: crypto.subtle not available'); } /** * Derive a 32-byte AES key from mnemonic phrase (space-separated words). */ export async function deriveKey(mnemonicPhrase: string, salt: Uint8Array = SALT): Promise { const crypto = getCrypto(); const keyMaterial = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(mnemonicPhrase), 'PBKDF2', false, ['deriveBits'] ); const saltBuf = salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength) as ArrayBuffer; const bits = await crypto.subtle.deriveBits( { name: 'PBKDF2', salt: saltBuf, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256', }, keyMaterial, AES_KEY_LEN ); return bits; } /** * Encrypt plaintext with AES-256-GCM. Returns nonce(16) + tag(16) + ciphertext (matches Python SentinelVault). */ export async function encryptDataGCM(key: ArrayBuffer, plaintext: string): Promise { const crypto = getCrypto(); const iv = crypto.getRandomValues(new Uint8Array(GCM_IV_LEN)); const cryptoKey = await crypto.subtle.importKey( 'raw', key, { name: 'AES-GCM' }, false, ['encrypt'] ); const encoded = new TextEncoder().encode(plaintext); const ciphertextWithTag = await crypto.subtle.encrypt( { name: 'AES-GCM', iv, tagLength: GCM_TAG_LEN * 8 }, cryptoKey, encoded ); const out = new Uint8Array(iv.length + ciphertextWithTag.byteLength); out.set(iv, 0); out.set(new Uint8Array(ciphertextWithTag), iv.length); return out; } /** * Decrypt blob from encryptDataGCM (nonce(16) + ciphertext+tag). */ export async function decryptDataGCM(key: ArrayBuffer, blob: Uint8Array): Promise { const crypto = getCrypto(); const iv = blob.subarray(0, GCM_IV_LEN); const ciphertextWithTag = blob.subarray(GCM_IV_LEN); const ivBuf = iv.buffer.slice(iv.byteOffset, iv.byteOffset + iv.byteLength) as ArrayBuffer; const ctBuf = ciphertextWithTag.buffer.slice( ciphertextWithTag.byteOffset, ciphertextWithTag.byteOffset + ciphertextWithTag.byteLength ) as ArrayBuffer; const cryptoKey = await crypto.subtle.importKey( 'raw', key, { name: 'AES-GCM' }, false, ['decrypt'] ); const dec = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: ivBuf, tagLength: GCM_TAG_LEN * 8 }, cryptoKey, ctBuf ); return new TextDecoder().decode(dec); } export function bytesToHex(bytes: Uint8Array): string { return Array.from(bytes) .map((b) => b.toString(16).padStart(2, '0')) .join(''); } export function hexToBytes(hex: string): Uint8Array { const len = hex.length / 2; const out = new Uint8Array(len); for (let i = 0; i < len; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); return out; }