05/29 Update extension
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user