fix: replace X25519 with ECDH P-256 for Electron compatibility
Build & Release / Docker / backend (push) Has been cancelled
Build & Release / Docker / frontend (push) Has been cancelled
Build & Release / Desktop / Linux & Windows (push) Has been cancelled
Build & Release / Deploy to server (push) Has been cancelled
Build & Release / Create release (push) Has been cancelled
Build & Release / Docker / backend (push) Has been cancelled
Build & Release / Docker / frontend (push) Has been cancelled
Build & Release / Desktop / Linux & Windows (push) Has been cancelled
Build & Release / Deploy to server (push) Has been cancelled
Build & Release / Create release (push) Has been cancelled
X25519 Web Crypto support is not available in Electron's Chromium build
despite being in Chrome 113+. P-256 ECDH is universally supported across
all browsers and Electron versions with equivalent security.
- Replace { name: 'X25519' } with { name: 'ECDH', namedCurve: 'P-256' }
throughout the keypair generation, sealing, and opening functions
- Wire format updated: ephemeral pub key grows from 32 to 65 bytes
(uncompressed P-256 point: 0x04 || x || y)
- Existing accounts with X25519 keys must re-register
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
84ff218a30
commit
7351f9048a
2 files changed
+23
-38
No files matched your search
@@ -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
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user