/* * vault1984 — shared crypto module * Runs in both QuickJS (CLI) and browser (extension). * * In CLI (QuickJS): native_* functions provided by jsbridge.c via BearSSL. * All calls are synchronous. * In browser: Web Crypto API used directly (async). * * This file is the single source of truth for L2/L3 field crypto. */ /* Detect environment */ const IS_BROWSER = typeof globalThis.crypto !== 'undefined' && typeof globalThis.crypto.subtle !== 'undefined'; /* --- base64 helpers --- */ function uint8_to_base64(bytes) { if (IS_BROWSER) { return btoa(String.fromCharCode.apply(null, bytes)); } else { return native_base64_encode(bytes); } } function base64_to_uint8(str) { if (IS_BROWSER) { const bin = atob(str); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return bytes; } else { return native_base64_decode(str); } } /* --- AES-GCM --- */ /** * Encrypt plaintext with AES-GCM. * @param {Uint8Array} key - 16 bytes (AES-128) or 32 bytes (AES-256) * @param {Uint8Array} plaintext * @returns {Uint8Array|Promise} nonce(12) || ciphertext || tag(16) */ function aes_gcm_encrypt(key, plaintext) { if (IS_BROWSER) { const iv = crypto.getRandomValues(new Uint8Array(12)); return crypto.subtle.importKey( 'raw', key, { name: 'AES-GCM' }, false, ['encrypt'] ).then(function(cryptoKey) { return crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, cryptoKey, plaintext); }).then(function(ct) { const result = new Uint8Array(12 + ct.byteLength); result.set(iv, 0); result.set(new Uint8Array(ct), 12); return result; }); } else { /* QuickJS: synchronous BearSSL binding */ return native_aes_gcm_encrypt(key, plaintext); } } /** * Decrypt AES-GCM ciphertext. * @param {Uint8Array} key - 16 or 32 bytes * @param {Uint8Array} data - nonce(12) || ciphertext || tag(16) * @returns {Uint8Array|Promise} plaintext */ function aes_gcm_decrypt(key, data) { if (data.length < 28) throw new Error('ciphertext too short'); /* Use subarray for typed array compatibility (QuickJS) */ var iv, ct; if (typeof data.subarray === 'function') { iv = new Uint8Array(data.subarray(0, 12)); ct = new Uint8Array(data.subarray(12)); } else { iv = data.slice(0, 12); ct = data.slice(12); } if (IS_BROWSER) { return crypto.subtle.importKey( 'raw', key, { name: 'AES-GCM' }, false, ['decrypt'] ).then(function(cryptoKey) { return crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, cryptoKey, ct); }).then(function(pt) { return new Uint8Array(pt); }); } else { /* Pass full data blob to native — C splits nonce/ct internally */ return native_aes_gcm_decrypt_blob(key, data); } } /* --- HKDF-SHA256 --- */ /** * HKDF-SHA256 extract + expand. * @param {Uint8Array} ikm - input key material * @param {Uint8Array|null} salt - optional salt * @param {Uint8Array} info - context info * @param {number} length - output length in bytes * @returns {Uint8Array|Promise} */ function hkdf_sha256(ikm, salt, info, length) { if (IS_BROWSER) { return crypto.subtle.importKey( 'raw', ikm, 'HKDF', false, ['deriveBits'] ).then(function(cryptoKey) { return crypto.subtle.deriveBits( { name: 'HKDF', hash: 'SHA-256', salt: salt || new Uint8Array(0), info: info }, cryptoKey, length * 8 ); }).then(function(bits) { return new Uint8Array(bits); }); } else { return native_hkdf_sha256(ikm, salt, info, length); } } /* --- L2 field encryption/decryption --- */ /** * Encrypt a field value with L2 key (AES-128-GCM). * In QuickJS: returns base64 string (synchronous). * In browser: returns Promise. */ function l2_encrypt_field(l2_key, entry_id, field_label, plaintext) { var info_str = 'vault1984-l2-' + entry_id + '-' + field_label; if (IS_BROWSER) { var enc = new TextEncoder(); var info = enc.encode(info_str); return hkdf_sha256(l2_key, null, info, 16).then(function(field_key) { return aes_gcm_encrypt(field_key, enc.encode(plaintext)); }).then(function(ct) { return uint8_to_base64(ct); }); } else { var info = native_encode_utf8(info_str); var field_key = native_hkdf_sha256(l2_key, null, info, 16); var pt_bytes = native_encode_utf8(plaintext); var ct = native_aes_gcm_encrypt(field_key, pt_bytes); return native_base64_encode(ct); } } /** * Decrypt a field value with L2 key (AES-128-GCM). * In QuickJS: returns plaintext string (synchronous). * In browser: returns Promise. */ function l2_decrypt_field(l2_key, entry_id, field_label, ciphertext_b64) { var info_str = 'vault1984-l2-' + entry_id + '-' + field_label; if (IS_BROWSER) { var enc = new TextEncoder(); var dec = new TextDecoder(); var info = enc.encode(info_str); return hkdf_sha256(l2_key, null, info, 16).then(function(field_key) { var ct = base64_to_uint8(ciphertext_b64); return aes_gcm_decrypt(field_key, ct); }).then(function(pt) { return dec.decode(pt); }); } else { var info = native_encode_utf8(info_str); var field_key = native_hkdf_sha256(l2_key, null, info, 16); var ct = native_base64_decode(ciphertext_b64); var pt = native_aes_gcm_decrypt_blob(field_key, ct); return native_decode_utf8(pt); } } /* Export for both environments */ if (typeof globalThis.vault1984 === 'undefined') globalThis.vault1984 = {}; globalThis.vault1984.crypto = { aes_gcm_encrypt: aes_gcm_encrypt, aes_gcm_decrypt: aes_gcm_decrypt, hkdf_sha256: hkdf_sha256, l2_encrypt_field: l2_encrypt_field, l2_decrypt_field: l2_decrypt_field, uint8_to_base64: uint8_to_base64, base64_to_uint8: base64_to_uint8 };