04/16 Upload codebase
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user