04/16 Upload codebase
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* crypto.js — Zero-knowledge cryptography layer
|
||||
*
|
||||
* All encryption/decryption runs in the browser using the Web Crypto API.
|
||||
* The vault key (AES-256-GCM) is derived from the master password client-side
|
||||
* and is NEVER sent to the server. The server only stores encrypted blobs.
|
||||
*
|
||||
* Key derivation chain:
|
||||
* authHash = PBKDF2(masterPassword, email, 100_000 iter, SHA-256) → sent to server for auth
|
||||
* vaultKey = PBKDF2(masterPassword, enc_key_salt, 600_000 iter, SHA-256) → stays in memory only
|
||||
*/
|
||||
|
||||
const Crypto = (() => {
|
||||
const subtle = window.crypto.subtle;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
function strToBytes(str) {
|
||||
return new TextEncoder().encode(str);
|
||||
}
|
||||
|
||||
function base64ToBytes(b64) {
|
||||
const bin = atob(b64);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
let bin = '';
|
||||
bytes.forEach(b => (bin += String.fromCharCode(b)));
|
||||
return btoa(bin);
|
||||
}
|
||||
|
||||
// ── PBKDF2 key import ────────────────────────────────────────────────────
|
||||
|
||||
async function importPbkdf2Key(masterPassword) {
|
||||
return subtle.importKey(
|
||||
'raw',
|
||||
strToBytes(masterPassword),
|
||||
'PBKDF2',
|
||||
false, // not extractable
|
||||
['deriveBits', 'deriveKey']
|
||||
);
|
||||
}
|
||||
|
||||
// ── Auth hash (sent to server) ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Derive an auth token from the master password.
|
||||
* Used ONLY for server-side authentication — never for encryption.
|
||||
* Returns a base64 string safe to POST to /api/auth/login|register.
|
||||
*/
|
||||
async function deriveAuthHash(masterPassword, email) {
|
||||
const baseKey = await importPbkdf2Key(masterPassword);
|
||||
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 (stays in memory) ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Derive the AES-256-GCM vault key from the master password.
|
||||
* enc_key_salt is the base64-encoded 16-byte salt returned by the server on login.
|
||||
* The returned CryptoKey is marked extractable:false — raw bytes cannot be read back.
|
||||
*/
|
||||
async function deriveVaultKey(masterPassword, enc_key_salt) {
|
||||
const baseKey = await importPbkdf2Key(masterPassword);
|
||||
return subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: base64ToBytes(enc_key_salt),
|
||||
iterations: 600_000,
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
baseKey,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false, // extractable:false — key material cannot be exported
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
}
|
||||
|
||||
// ── Encrypt / Decrypt ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Encrypt a plain JS object with the vault key.
|
||||
* A fresh random 12-byte IV is generated for every call (required for GCM).
|
||||
* Returns { enc_data: base64, iv: base64 }
|
||||
*/
|
||||
async function encryptItem(vaultKey, plaintextObject) {
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
const plaintext = strToBytes(JSON.stringify(plaintextObject));
|
||||
const ciphertext = await subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
vaultKey,
|
||||
plaintext
|
||||
);
|
||||
return {
|
||||
enc_data: bytesToBase64(new Uint8Array(ciphertext)),
|
||||
iv: bytesToBase64(iv),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an encrypted vault item back to a JS object.
|
||||
* enc_data and iv must be the base64 strings stored on the server.
|
||||
*/
|
||||
async function decryptItem(vaultKey, enc_data, iv) {
|
||||
const plaintext = await subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: base64ToBytes(iv) },
|
||||
vaultKey,
|
||||
base64ToBytes(enc_data)
|
||||
);
|
||||
return JSON.parse(new TextDecoder().decode(plaintext));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cryptographically random base64 string (for enc_key_salt).
|
||||
* byteLength defaults to 16 (128-bit salt).
|
||||
*/
|
||||
function generateSalt(byteLength = 16) {
|
||||
return bytesToBase64(window.crypto.getRandomValues(new Uint8Array(byteLength)));
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
return { deriveAuthHash, deriveVaultKey, encryptItem, decryptItem, generateSalt };
|
||||
})();
|
||||
Reference in New Issue
Block a user