|
|
|
@@ -50,24 +50,23 @@ export async function decryptMasterKey(encB64: string, derived: CryptoKey): Prom
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── X25519 ECDH keypair ──────────────────────────────────────────────────────
|
|
|
|
|
// ── ECDH P-256 keypair ───────────────────────────────────────────────────────
|
|
|
|
|
// P-256 is universally supported across all browsers and Electron versions.
|
|
|
|
|
// Raw public key is 65 bytes (uncompressed point: 0x04 || x || y).
|
|
|
|
|
|
|
|
|
|
const ECDH_ALGO = { name: 'ECDH', namedCurve: 'P-256' } as const;
|
|
|
|
|
const PUB_KEY_BYTES = 65; // uncompressed P-256 point
|
|
|
|
|
|
|
|
|
|
export async function generateX25519Keypair(): Promise<{
|
|
|
|
|
publicKeyB64: string;
|
|
|
|
|
privateKey: CryptoKey;
|
|
|
|
|
pkcs8Bytes: Uint8Array<ArrayBuffer>;
|
|
|
|
|
}> {
|
|
|
|
|
const kp = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, ['deriveBits']);
|
|
|
|
|
const kp = await crypto.subtle.generateKey(ECDH_ALGO, true, ['deriveBits']);
|
|
|
|
|
const publicRaw = await crypto.subtle.exportKey('raw', (kp as CryptoKeyPair).publicKey);
|
|
|
|
|
const pkcs8 = await crypto.subtle.exportKey('pkcs8', (kp as CryptoKeyPair).privateKey);
|
|
|
|
|
|
|
|
|
|
const privateKey = await crypto.subtle.importKey(
|
|
|
|
|
'pkcs8',
|
|
|
|
|
pkcs8,
|
|
|
|
|
{ name: 'X25519' } as any,
|
|
|
|
|
false,
|
|
|
|
|
['deriveBits']
|
|
|
|
|
);
|
|
|
|
|
const privateKey = await crypto.subtle.importKey('pkcs8', pkcs8, ECDH_ALGO, false, ['deriveBits']);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
publicKeyB64: bufToB64(publicRaw),
|
|
|
|
@@ -85,12 +84,12 @@ export async function decryptPrivateKeyToCryptoKey(
|
|
|
|
|
master: CryptoKey
|
|
|
|
|
): Promise<CryptoKey> {
|
|
|
|
|
const pkcs8 = await aesDecryptRaw(encB64, master);
|
|
|
|
|
return crypto.subtle.importKey('pkcs8', pkcs8, { name: 'X25519' } as any, false, ['deriveBits']);
|
|
|
|
|
return crypto.subtle.importKey('pkcs8', pkcs8, ECDH_ALGO, false, ['deriveBits']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Conversation key exchange (ephemeral X25519 ECDH sealed box) ─────────────
|
|
|
|
|
// ── Conversation key exchange (ephemeral ECDH P-256 sealed box) ───────────────
|
|
|
|
|
//
|
|
|
|
|
// Wire format: ephemeral_pub_key(32) | nonce(12) | ciphertext
|
|
|
|
|
// Wire format: ephemeral_pub_key(65) | nonce(12) | ciphertext
|
|
|
|
|
|
|
|
|
|
export function generateConvKeyRaw(): Uint8Array<ArrayBuffer> {
|
|
|
|
|
return crypto.getRandomValues(new Uint8Array(32));
|
|
|
|
@@ -100,23 +99,15 @@ export async function sealConvKey(
|
|
|
|
|
keyRaw: Uint8Array<ArrayBuffer>,
|
|
|
|
|
recipientPublicKeyB64: string
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const recipientPubKey = await crypto.subtle.importKey(
|
|
|
|
|
'raw',
|
|
|
|
|
b64ToBuf(recipientPublicKeyB64),
|
|
|
|
|
{ name: 'X25519' } as any,
|
|
|
|
|
false,
|
|
|
|
|
[]
|
|
|
|
|
);
|
|
|
|
|
const recipientPubKey = await crypto.subtle.importKey('raw', b64ToBuf(recipientPublicKeyB64), ECDH_ALGO, false, []);
|
|
|
|
|
|
|
|
|
|
const ephemeral = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, [
|
|
|
|
|
'deriveBits',
|
|
|
|
|
]);
|
|
|
|
|
const ephemeral = await crypto.subtle.generateKey(ECDH_ALGO, true, ['deriveBits']);
|
|
|
|
|
const ephemeralPubRaw = new Uint8Array(
|
|
|
|
|
await crypto.subtle.exportKey('raw', (ephemeral as CryptoKeyPair).publicKey)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const sharedBits = await crypto.subtle.deriveBits(
|
|
|
|
|
{ name: 'X25519', public: recipientPubKey } as any,
|
|
|
|
|
{ name: 'ECDH', public: recipientPubKey },
|
|
|
|
|
(ephemeral as CryptoKeyPair).privateKey,
|
|
|
|
|
256
|
|
|
|
|
);
|
|
|
|
@@ -126,10 +117,10 @@ export async function sealConvKey(
|
|
|
|
|
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
|
|
|
|
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, aesKey, keyRaw);
|
|
|
|
|
|
|
|
|
|
const out = new Uint8Array(32 + 12 + ct.byteLength);
|
|
|
|
|
const out = new Uint8Array(PUB_KEY_BYTES + 12 + ct.byteLength);
|
|
|
|
|
out.set(ephemeralPubRaw, 0);
|
|
|
|
|
out.set(nonce, 32);
|
|
|
|
|
out.set(new Uint8Array(ct), 44);
|
|
|
|
|
out.set(nonce, PUB_KEY_BYTES);
|
|
|
|
|
out.set(new Uint8Array(ct), PUB_KEY_BYTES + 12);
|
|
|
|
|
return bufToB64(out);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -138,20 +129,14 @@ export async function openSealedConvKey(
|
|
|
|
|
myPrivateKey: CryptoKey
|
|
|
|
|
): Promise<Uint8Array<ArrayBuffer>> {
|
|
|
|
|
const data = b64ToBuf(sealedB64);
|
|
|
|
|
const ephemeralPubRaw = u8(data, 0, 32);
|
|
|
|
|
const nonce = u8(data, 32, 44);
|
|
|
|
|
const ct = u8(data, 44);
|
|
|
|
|
const ephemeralPubRaw = u8(data, 0, PUB_KEY_BYTES);
|
|
|
|
|
const nonce = u8(data, PUB_KEY_BYTES, PUB_KEY_BYTES + 12);
|
|
|
|
|
const ct = u8(data, PUB_KEY_BYTES + 12);
|
|
|
|
|
|
|
|
|
|
const ephemeralPubKey = await crypto.subtle.importKey(
|
|
|
|
|
'raw',
|
|
|
|
|
ephemeralPubRaw,
|
|
|
|
|
{ name: 'X25519' } as any,
|
|
|
|
|
false,
|
|
|
|
|
[]
|
|
|
|
|
);
|
|
|
|
|
const ephemeralPubKey = await crypto.subtle.importKey('raw', ephemeralPubRaw, ECDH_ALGO, false, []);
|
|
|
|
|
|
|
|
|
|
const sharedBits = await crypto.subtle.deriveBits(
|
|
|
|
|
{ name: 'X25519', public: ephemeralPubKey } as any,
|
|
|
|
|
{ name: 'ECDH', public: ephemeralPubKey },
|
|
|
|
|
myPrivateKey,
|
|
|
|
|
256
|
|
|
|
|
);
|
|
|
|
|