Files
PassKeeper/extension/shared/sharing-crypto.js
T
2026-06-01 09:17:40 -04:00

123 lines
4.0 KiB
JavaScript

/**
* extension/shared/sharing-crypto.js
*
* ECDH P-256 cryptography for zero-knowledge item sharing — extension port.
* Mirrors app/static/js/sharing.js exactly but uses `crypto.subtle`
* (no `window.`) so it works in both the popup and the service worker.
*
* Only the primitives needed by the extension are included:
* importPublicKey, decryptPrivateKey, deriveSharedKey, decryptShare, decryptName
*/
const ExtSharingCrypto = (() => {
const subtle = crypto.subtle;
// ── Helpers ────────────────────────────────────────────────────────────────
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;
}
// ── Key import ─────────────────────────────────────────────────────────────
/**
* Import a remote user's ECDH public key from base64 raw bytes (65-byte
* uncompressed P-256 point).
*/
async function importPublicKey(base64Raw) {
return subtle.importKey(
'raw',
base64ToBytes(base64Raw),
{ name: 'ECDH', namedCurve: 'P-256' },
false,
[], // public keys have no usages in WebCrypto ECDH
);
}
/**
* Decrypt the user's ECDH private key JWK (fetched from server as
* private_key_enc / private_key_iv) using the vault AES-256-GCM key.
* Returns a CryptoKey usable for ECDH deriveBits.
*/
async function decryptPrivateKey(vaultKey, private_key_enc, private_key_iv) {
const plaintext = await subtle.decrypt(
{ name: 'AES-GCM', iv: base64ToBytes(private_key_iv) },
vaultKey,
base64ToBytes(private_key_enc),
);
const jwk = JSON.parse(new TextDecoder().decode(plaintext));
return subtle.importKey(
'jwk',
jwk,
{ name: 'ECDH', namedCurve: 'P-256' },
false,
['deriveBits'],
);
}
// ── Shared-secret derivation ───────────────────────────────────────────────
/**
* Derive an AES-256-GCM CryptoKey from the ECDH shared secret.
* ECDH is commutative: ECDH(A_priv, B_pub) === ECDH(B_priv, A_pub).
*/
async function deriveSharedKey(myPrivateKey, theirPublicKey) {
const bits = await subtle.deriveBits(
{ name: 'ECDH', public: theirPublicKey },
myPrivateKey,
256,
);
return subtle.importKey(
'raw',
bits,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt'],
);
}
// ── Decrypt ────────────────────────────────────────────────────────────────
/**
* Decrypt a shared item payload (JSON object) using the ECDH shared key.
*/
async function decryptShare(sharedKey, enc_data, iv) {
const plaintext = await subtle.decrypt(
{ name: 'AES-GCM', iv: base64ToBytes(iv) },
sharedKey,
base64ToBytes(enc_data),
);
return JSON.parse(new TextDecoder().decode(plaintext));
}
/**
* Decrypt an encrypted item display name.
* Returns null on failure (e.g. legacy share without enc_name).
*/
async function decryptName(sharedKey, enc_name, iv_name) {
try {
const plaintext = await subtle.decrypt(
{ name: 'AES-GCM', iv: base64ToBytes(iv_name) },
sharedKey,
base64ToBytes(enc_name),
);
return new TextDecoder().decode(plaintext);
} catch {
return null;
}
}
// ── Public API ─────────────────────────────────────────────────────────────
return {
importPublicKey,
decryptPrivateKey,
deriveSharedKey,
decryptShare,
decryptName,
};
})();