100 lines
3.8 KiB
JavaScript
100 lines
3.8 KiB
JavaScript
/**
|
|
* extension/shared/crypto.js — Zero-knowledge cryptography for the extension.
|
|
*
|
|
* Mirrors the web app's crypto.js but:
|
|
* - Uses `crypto.subtle` (no `window.`) so it works in both popup and service worker.
|
|
* - deriveVaultKey uses extractable:true so the key can be serialised to
|
|
* chrome.storage.session (exportVaultKey / importVaultKey).
|
|
*/
|
|
|
|
const ExtCrypto = (() => {
|
|
const subtle = crypto.subtle;
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
function strToBytes(str) { return new TextEncoder().encode(str); }
|
|
|
|
function base64ToBytes(b64) {
|
|
const bin = atob(b64);
|
|
const out = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
return out;
|
|
}
|
|
|
|
function bytesToBase64(bytes) {
|
|
let bin = '';
|
|
bytes.forEach(b => (bin += String.fromCharCode(b)));
|
|
return btoa(bin);
|
|
}
|
|
|
|
// ── PBKDF2 base key ─────────────────────────────────────────────────────────
|
|
|
|
async function importPbkdf2Key(password) {
|
|
return subtle.importKey('raw', strToBytes(password), 'PBKDF2', false, ['deriveBits', 'deriveKey']);
|
|
}
|
|
|
|
// ── Auth hash (sent to server for login/register) ───────────────────────────
|
|
|
|
async function deriveAuthHash(password, email) {
|
|
const baseKey = await importPbkdf2Key(password);
|
|
const bits = await subtle.deriveBits(
|
|
{ name: 'PBKDF2', salt: strToBytes(email.toLowerCase()), iterations: 100_000, hash: 'SHA-256' },
|
|
baseKey,
|
|
256
|
|
);
|
|
return bytesToBase64(new Uint8Array(bits));
|
|
}
|
|
|
|
// ── Vault key (AES-256-GCM, extractable for session storage) ────────────────
|
|
|
|
async function deriveVaultKey(password, enc_key_salt) {
|
|
const baseKey = await importPbkdf2Key(password);
|
|
return subtle.deriveKey(
|
|
{ name: 'PBKDF2', salt: base64ToBytes(enc_key_salt), iterations: 600_000, hash: 'SHA-256' },
|
|
baseKey,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
true, // extractable — needed to serialise into chrome.storage.session
|
|
['encrypt', 'decrypt']
|
|
);
|
|
}
|
|
|
|
/** Serialise a CryptoKey to a JSON string for chrome.storage.session. */
|
|
async function exportVaultKey(vaultKey) {
|
|
const jwk = await subtle.exportKey('jwk', vaultKey);
|
|
return JSON.stringify(jwk);
|
|
}
|
|
|
|
/** Deserialise a CryptoKey from chrome.storage.session. */
|
|
async function importVaultKey(jwkStr) {
|
|
const jwk = JSON.parse(jwkStr);
|
|
return subtle.importKey('jwk', jwk, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
|
|
}
|
|
|
|
// ── Encrypt / Decrypt ────────────────────────────────────────────────────────
|
|
|
|
async function encryptItem(vaultKey, plainObj) {
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
const ct = await subtle.encrypt(
|
|
{ name: 'AES-GCM', iv },
|
|
vaultKey,
|
|
strToBytes(JSON.stringify(plainObj))
|
|
);
|
|
return { enc_data: bytesToBase64(new Uint8Array(ct)), iv: bytesToBase64(iv) };
|
|
}
|
|
|
|
async function decryptItem(vaultKey, enc_data, iv) {
|
|
const pt = await subtle.decrypt(
|
|
{ name: 'AES-GCM', iv: base64ToBytes(iv) },
|
|
vaultKey,
|
|
base64ToBytes(enc_data)
|
|
);
|
|
return JSON.parse(new TextDecoder().decode(pt));
|
|
}
|
|
|
|
function generateSalt(byteLength = 16) {
|
|
return bytesToBase64(crypto.getRandomValues(new Uint8Array(byteLength)));
|
|
}
|
|
|
|
return { deriveAuthHash, deriveVaultKey, exportVaultKey, importVaultKey, encryptItem, decryptItem, generateSalt };
|
|
})();
|