04/16 Upload codebase
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* sharing.js — ECDH P-256 cryptography for zero-knowledge item sharing
|
||||
*
|
||||
* Each user has a P-256 keypair:
|
||||
* - Public key : stored on server as base64 raw bytes (65-byte uncompressed point)
|
||||
* - Private key : stored on server as JWK, AES-256-GCM encrypted with the user's vault key
|
||||
*
|
||||
* When Alice shares with Bob:
|
||||
* 1. Alice fetches Bob's public key from the server
|
||||
* 2. Alice derives an ECDH shared secret: ECDH(Alice_priv, Bob_pub)
|
||||
* 3. Alice imports that shared secret as an AES-256-GCM key
|
||||
* 4. Alice encrypts the item's plaintext → (enc_data, iv)
|
||||
* 5. Alice POST /api/sharing with the ciphertext — server stores the blob
|
||||
*
|
||||
* When Bob decrypts:
|
||||
* 1. Bob fetches Alice's public key from the server (returned in inbox response)
|
||||
* 2. Bob derives the same ECDH shared secret: ECDH(Bob_priv, Alice_pub) ← commutative!
|
||||
* 3. Bob decrypts enc_data with the derived key
|
||||
*
|
||||
* The server only ever sees ciphertext. Zero-knowledge.
|
||||
*/
|
||||
|
||||
const SharingCrypto = (() => {
|
||||
const subtle = window.crypto.subtle;
|
||||
|
||||
// ── Helpers (same encoding as crypto.js) ─────────────────────────────────
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ── Key generation ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generate a fresh ECDH P-256 keypair.
|
||||
* Both keys are extractable so they can be exported/stored.
|
||||
*/
|
||||
async function generateKeyPair() {
|
||||
return subtle.generateKey(
|
||||
{ name: 'ECDH', namedCurve: 'P-256' },
|
||||
true,
|
||||
['deriveBits'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the public key as raw bytes (uncompressed point, 65 bytes) → base64.
|
||||
* This is what gets stored on the server and shared with other users.
|
||||
*/
|
||||
async function exportPublicKey(publicKey) {
|
||||
const raw = await subtle.exportKey('raw', publicKey);
|
||||
return bytesToBase64(new Uint8Array(raw));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the private key JWK with the user's vault key (AES-256-GCM).
|
||||
* The resulting ciphertext is stored on the server — only the user can decrypt it.
|
||||
*/
|
||||
async function encryptPrivateKey(vaultKey, privateKey) {
|
||||
const jwk = await subtle.exportKey('jwk', privateKey);
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
const plaintext = new TextEncoder().encode(JSON.stringify(jwk));
|
||||
const ciphertext = await subtle.encrypt({ name: 'AES-GCM', iv }, vaultKey, plaintext);
|
||||
return {
|
||||
private_key_enc: bytesToBase64(new Uint8Array(ciphertext)),
|
||||
private_key_iv: bytesToBase64(iv),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the private key JWK (fetched from server) using the user's vault 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'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a remote user's public key from its base64 raw representation.
|
||||
*/
|
||||
async function importPublicKey(base64Raw) {
|
||||
return subtle.importKey(
|
||||
'raw',
|
||||
base64ToBytes(base64Raw),
|
||||
{ name: 'ECDH', namedCurve: 'P-256' },
|
||||
false,
|
||||
[], // public keys have no usages in WebCrypto ECDH
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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'],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Encrypt / Decrypt with shared key ────────────────────────────────────
|
||||
|
||||
async function encryptForShare(sharedKey, plaintextObject) {
|
||||
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
const plaintext = new TextEncoder().encode(JSON.stringify(plaintextObject));
|
||||
const ciphertext = await subtle.encrypt({ name: 'AES-GCM', iv }, sharedKey, plaintext);
|
||||
return {
|
||||
enc_data: bytesToBase64(new Uint8Array(ciphertext)),
|
||||
iv: bytesToBase64(iv),
|
||||
};
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
return {
|
||||
generateKeyPair,
|
||||
exportPublicKey,
|
||||
encryptPrivateKey,
|
||||
decryptPrivateKey,
|
||||
importPublicKey,
|
||||
deriveSharedKey,
|
||||
encryptForShare,
|
||||
decryptShare,
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* SharingSession — holds the decrypted ECDH private key for the tab lifetime.
|
||||
* Similar to VaultSession; cleared on sign-out.
|
||||
*/
|
||||
const SharingSession = (() => {
|
||||
let _privateKey = null;
|
||||
|
||||
function setKey(key) { _privateKey = key; }
|
||||
function getKey() { return _privateKey; }
|
||||
function clear() { _privateKey = null; }
|
||||
function isReady() { return _privateKey !== null; }
|
||||
|
||||
return { setKey, getKey, clear, isReady };
|
||||
})();
|
||||
Reference in New Issue
Block a user