/* * 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); } } /* --- Field encryption/decryption --- */ /** * Encrypt a field value. * Key length determines tier: 16 bytes = L2 (AES-128), 32 bytes = L3 (AES-256). * @param {Uint8Array} key - 16 or 32 bytes * @param {string} field_label - field label (for per-field key derivation) * @param {string} plaintext - field value to encrypt * @returns {string|Promise} base64-encoded ciphertext */ /* * Normalize key for AES: 8-byte keys are doubled to 16 bytes. * AES requires 16, 24, or 32 byte keys. * HKDF output length matches the (normalized) key length. */ function normalize_key(key) { if (key.length === 8) { var doubled = new Uint8Array(16); doubled.set(key, 0); doubled.set(key, 8); return doubled; } return key; } function encrypt_field(key, field_label, plaintext) { var info_str = 'vault1984-field-' + field_label; var nkey = normalize_key(key); var aes_len = nkey.length; /* 16 or 32 */ if (IS_BROWSER) { var enc = new TextEncoder(); var info = enc.encode(info_str); return hkdf_sha256(nkey, null, info, aes_len).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(nkey, null, info, aes_len); 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. * Key length determines tier: 16 bytes = L2, 32 bytes = L3. * @param {Uint8Array} key - 16 or 32 bytes * @param {string} field_label - field label * @param {string} ciphertext_b64 - base64-encoded ciphertext * @returns {string|Promise} plaintext */ function decrypt_field(key, field_label, ciphertext_b64) { var info_str = 'vault1984-field-' + field_label; var nkey = normalize_key(key); var aes_len = nkey.length; if (IS_BROWSER) { var enc = new TextEncoder(); var dec = new TextDecoder(); var info = enc.encode(info_str); return hkdf_sha256(nkey, null, info, aes_len).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(nkey, null, info, aes_len); var ct = native_base64_decode(ciphertext_b64); var pt = native_aes_gcm_decrypt_blob(field_key, ct); return native_decode_utf8(pt); } } /* Backward compat aliases */ function l2_encrypt_field(key, entry_id, label, pt) { return encrypt_field(key, label, pt); } function l2_decrypt_field(key, entry_id, label, ct) { return decrypt_field(key, label, ct); } /* 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, encrypt_field: encrypt_field, decrypt_field: decrypt_field, l2_encrypt_field: l2_encrypt_field, l2_decrypt_field: l2_decrypt_field, uint8_to_base64: uint8_to_base64, base64_to_uint8: base64_to_uint8 };