123 lines
4.0 KiB
JavaScript
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,
|
|
};
|
|
})();
|