diff --git a/frontend/src/crypto/index.ts b/frontend/src/crypto/index.ts index fd1c597..b4b2005 100644 --- a/frontend/src/crypto/index.ts +++ b/frontend/src/crypto/index.ts @@ -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; }> { - 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 { 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 { return crypto.getRandomValues(new Uint8Array(32)); @@ -100,23 +99,15 @@ export async function sealConvKey( keyRaw: Uint8Array, recipientPublicKeyB64: string ): Promise { - 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> { 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 ); diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/RegisterPage.tsx index add7135..6ba1196 100644 --- a/frontend/src/pages/RegisterPage.tsx +++ b/frontend/src/pages/RegisterPage.tsx @@ -60,7 +60,7 @@ export default function RegisterPage() { socket.connect(accessToken); navigate('/messages'); } catch (err: any) { - setError(err.response?.data?.error || 'Registration failed'); + setError(err.response?.data?.error || err.message || 'Registration failed'); } finally { setLoading(false); }