refactor(relay): remove unnecessary awaits from synchronous crypto functions (#687)

This commit is contained in:
Mohamed Boudra
2026-05-05 19:56:51 +07:00
parent b45f0a9ad1
commit 2b75df6132
5 changed files with 114 additions and 126 deletions
+58 -64
View File
@@ -10,113 +10,107 @@ import {
describe("crypto", () => { describe("crypto", () => {
describe("generateKeyPair", () => { describe("generateKeyPair", () => {
it("generates a valid keypair", async () => { it("generates a valid keypair", () => {
const keypair = await generateKeyPair(); const keypair = generateKeyPair();
expect(keypair.secretKey).toBeDefined(); expect(keypair.secretKey).toBeDefined();
expect(keypair.publicKey).toBeDefined(); expect(keypair.publicKey).toBeDefined();
}); });
}); });
describe("exportPublicKey / importPublicKey", () => { describe("exportPublicKey / importPublicKey", () => {
it("roundtrips public key through base64", async () => { it("roundtrips public key through base64", () => {
const keypair = await generateKeyPair(); const keypair = generateKeyPair();
const exported = await exportPublicKey(keypair.publicKey); const exported = exportPublicKey(keypair.publicKey);
expect(typeof exported).toBe("string"); expect(typeof exported).toBe("string");
expect(exported.length).toBeGreaterThan(0); expect(exported.length).toBeGreaterThan(0);
const imported = await importPublicKey(exported); const imported = importPublicKey(exported);
expect(imported).toBeDefined(); expect(imported).toBeDefined();
// Re-export should match // Re-export should match
const reExported = await exportPublicKey(imported); const reExported = exportPublicKey(imported);
expect(reExported).toBe(exported); expect(reExported).toBe(exported);
}); });
}); });
describe("deriveSharedKey", () => { describe("deriveSharedKey", () => {
it("derives the same key on both sides", async () => { it("derives the same key on both sides", () => {
// Simulate daemon and client // Simulate daemon and client
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const clientKeyPair = await generateKeyPair(); const clientKeyPair = generateKeyPair();
// Export public keys (what would go over the wire) // Export public keys (what would go over the wire)
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey); const clientPubKeyB64 = exportPublicKey(clientKeyPair.publicKey);
// Import peer's public key // Import peer's public key
const daemonSeesClientPubKey = await importPublicKey(clientPubKeyB64); const daemonSeesClientPubKey = importPublicKey(clientPubKeyB64);
const clientSeesDaemonPubKey = await importPublicKey(daemonPubKeyB64); const clientSeesDaemonPubKey = importPublicKey(daemonPubKeyB64);
// Derive shared keys // Derive shared keys
const daemonSharedKey = await deriveSharedKey( const daemonSharedKey = deriveSharedKey(daemonKeyPair.secretKey, daemonSeesClientPubKey);
daemonKeyPair.secretKey, const clientSharedKey = deriveSharedKey(clientKeyPair.secretKey, clientSeesDaemonPubKey);
daemonSeesClientPubKey,
);
const clientSharedKey = await deriveSharedKey(
clientKeyPair.secretKey,
clientSeesDaemonPubKey,
);
// Both should derive the same key - test by encrypting with one, decrypting with other // Both should derive the same key - test by encrypting with one, decrypting with other
const testMessage = "Hello, encrypted world!"; const testMessage = "Hello, encrypted world!";
const encrypted = await encrypt(daemonSharedKey, testMessage); const encrypted = encrypt(daemonSharedKey, testMessage);
const decrypted = await decrypt(clientSharedKey, encrypted); const decrypted = decrypt(clientSharedKey, encrypted);
expect(decrypted).toBe(testMessage); expect(decrypted).toBe(testMessage);
}); });
}); });
describe("encrypt / decrypt", () => { describe("encrypt / decrypt", () => {
it("roundtrips a string message", async () => { it("roundtrips a string message", () => {
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const clientKeyPair = await generateKeyPair(); const clientKeyPair = generateKeyPair();
const sharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientKeyPair.publicKey); const sharedKey = deriveSharedKey(daemonKeyPair.secretKey, clientKeyPair.publicKey);
const plaintext = "Test message with unicode: 你好世界 🎉"; const plaintext = "Test message with unicode: 你好世界 🎉";
const ciphertext = await encrypt(sharedKey, plaintext); const ciphertext = encrypt(sharedKey, plaintext);
expect(ciphertext).toBeInstanceOf(ArrayBuffer); expect(ciphertext).toBeInstanceOf(ArrayBuffer);
expect(ciphertext.byteLength).toBeGreaterThan(plaintext.length); expect(ciphertext.byteLength).toBeGreaterThan(plaintext.length);
const decrypted = await decrypt(sharedKey, ciphertext); const decrypted = decrypt(sharedKey, ciphertext);
expect(decrypted).toBe(plaintext); expect(decrypted).toBe(plaintext);
}); });
it("roundtrips binary data", async () => { it("roundtrips binary data", () => {
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const clientKeyPair = await generateKeyPair(); const clientKeyPair = generateKeyPair();
const sharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientKeyPair.publicKey); const sharedKey = deriveSharedKey(daemonKeyPair.secretKey, clientKeyPair.publicKey);
const binary = new Uint8Array([0, 1, 2, 255, 254, 253]); const binary = new Uint8Array([0, 1, 2, 255, 254, 253]);
const ciphertext = await encrypt(sharedKey, binary.buffer); const ciphertext = encrypt(sharedKey, binary.buffer);
const decrypted = await decrypt(sharedKey, ciphertext); const decrypted = decrypt(sharedKey, ciphertext);
expect(new Uint8Array(decrypted as ArrayBuffer)).toEqual(binary); expect(new Uint8Array(decrypted as ArrayBuffer)).toEqual(binary);
}); });
it("fails to decrypt with wrong key", async () => { it("fails to decrypt with wrong key", () => {
const keypair1 = await generateKeyPair(); const keypair1 = generateKeyPair();
const keypair2 = await generateKeyPair(); const keypair2 = generateKeyPair();
const keypair3 = await generateKeyPair(); const keypair3 = generateKeyPair();
const correctKey = await deriveSharedKey(keypair1.secretKey, keypair2.publicKey); const correctKey = deriveSharedKey(keypair1.secretKey, keypair2.publicKey);
const wrongKey = await deriveSharedKey(keypair1.secretKey, keypair3.publicKey); const wrongKey = deriveSharedKey(keypair1.secretKey, keypair3.publicKey);
const ciphertext = await encrypt(correctKey, "secret"); const ciphertext = encrypt(correctKey, "secret");
const tryDecrypt = () => decrypt(wrongKey, ciphertext); const tryDecrypt = () => decrypt(wrongKey, ciphertext);
expect(tryDecrypt).toThrow(); expect(tryDecrypt).toThrow();
}); });
it("produces different ciphertext for same plaintext (random IV)", async () => { it("produces different ciphertext for same plaintext (random IV)", () => {
const keypair1 = await generateKeyPair(); const keypair1 = generateKeyPair();
const keypair2 = await generateKeyPair(); const keypair2 = generateKeyPair();
const sharedKey = await deriveSharedKey(keypair1.secretKey, keypair2.publicKey); const sharedKey = deriveSharedKey(keypair1.secretKey, keypair2.publicKey);
const plaintext = "Same message"; const plaintext = "Same message";
const ciphertext1 = await encrypt(sharedKey, plaintext); const ciphertext1 = encrypt(sharedKey, plaintext);
const ciphertext2 = await encrypt(sharedKey, plaintext); const ciphertext2 = encrypt(sharedKey, plaintext);
// Should be different due to random IV // Should be different due to random IV
const arr1 = new Uint8Array(ciphertext1); const arr1 = new Uint8Array(ciphertext1);
@@ -124,49 +118,49 @@ describe("crypto", () => {
expect(arr1).not.toEqual(arr2); expect(arr1).not.toEqual(arr2);
// But both should decrypt to same plaintext // But both should decrypt to same plaintext
expect(await decrypt(sharedKey, ciphertext1)).toBe(plaintext); expect(decrypt(sharedKey, ciphertext1)).toBe(plaintext);
expect(await decrypt(sharedKey, ciphertext2)).toBe(plaintext); expect(decrypt(sharedKey, ciphertext2)).toBe(plaintext);
}); });
}); });
describe("full handshake simulation", () => { describe("full handshake simulation", () => {
it("simulates complete daemon<->client key exchange", async () => { it("simulates complete daemon<->client key exchange", () => {
// === DAEMON SIDE (generates session) === // === DAEMON SIDE (generates session) ===
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
// QR code would contain: { serverId, daemonPubKeyB64, relay: { endpoint } } // QR code would contain: { serverId, daemonPubKeyB64, relay: { endpoint } }
// === CLIENT SIDE (scans QR) === // === CLIENT SIDE (scans QR) ===
const clientKeyPair = await generateKeyPair(); const clientKeyPair = generateKeyPair();
const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey); const clientPubKeyB64 = exportPublicKey(clientKeyPair.publicKey);
// Client imports daemon's public key from QR // Client imports daemon's public key from QR
const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64); const daemonPubKeyOnClient = importPublicKey(daemonPubKeyB64);
// Client derives shared key // Client derives shared key
const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient); const clientSharedKey = deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient);
// Client sends hello: { type: "hello", key: clientPubKeyB64 } // Client sends hello: { type: "hello", key: clientPubKeyB64 }
// === DAEMON SIDE (receives hello) === // === DAEMON SIDE (receives hello) ===
// Daemon imports client's public key from hello message // Daemon imports client's public key from hello message
const clientPubKeyOnDaemon = await importPublicKey(clientPubKeyB64); const clientPubKeyOnDaemon = importPublicKey(clientPubKeyB64);
// Daemon derives shared key // Daemon derives shared key
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon); const daemonSharedKey = deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon);
// === VERIFY BOTH HAVE SAME KEY === // === VERIFY BOTH HAVE SAME KEY ===
const testFromDaemon = "Message from daemon"; const testFromDaemon = "Message from daemon";
const testFromClient = "Message from client"; const testFromClient = "Message from client";
// Daemon encrypts, client decrypts // Daemon encrypts, client decrypts
const encryptedFromDaemon = await encrypt(daemonSharedKey, testFromDaemon); const encryptedFromDaemon = encrypt(daemonSharedKey, testFromDaemon);
expect(await decrypt(clientSharedKey, encryptedFromDaemon)).toBe(testFromDaemon); expect(decrypt(clientSharedKey, encryptedFromDaemon)).toBe(testFromDaemon);
// Client encrypts, daemon decrypts // Client encrypts, daemon decrypts
const encryptedFromClient = await encrypt(clientSharedKey, testFromClient); const encryptedFromClient = encrypt(clientSharedKey, testFromClient);
expect(await decrypt(daemonSharedKey, encryptedFromClient)).toBe(testFromClient); expect(decrypt(daemonSharedKey, encryptedFromClient)).toBe(testFromClient);
}); });
}); });
}); });
+33 -33
View File
@@ -234,8 +234,8 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
// === DAEMON SIDE === // === DAEMON SIDE ===
// Generate keypair (public key goes in QR) // Generate keypair (public key goes in QR)
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
// QR would contain: { serverId, daemonPubKeyB64, relay: { endpoint } } // QR would contain: { serverId, daemonPubKeyB64, relay: { endpoint } }
@@ -252,12 +252,12 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
// === CLIENT SIDE === // === CLIENT SIDE ===
// Client scans QR, gets daemon's public key and session ID // Client scans QR, gets daemon's public key and session ID
// Client generates own keypair // Client generates own keypair
const clientKeyPair = await generateKeyPair(); const clientKeyPair = generateKeyPair();
const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey); const clientPubKeyB64 = exportPublicKey(clientKeyPair.publicKey);
// Client imports daemon's public key and derives shared secret // Client imports daemon's public key and derives shared secret
const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64); const daemonPubKeyOnClient = importPublicKey(daemonPubKeyB64);
const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient); const clientSharedKey = deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient);
const waitForClientSeen = new Promise<void>((resolve, reject) => { const waitForClientSeen = new Promise<void>((resolve, reject) => {
const timeout = setTimeout( const timeout = setTimeout(
@@ -324,21 +324,21 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
expect(hello.key).toBe(clientPubKeyB64); expect(hello.key).toBe(clientPubKeyB64);
// Daemon imports client's public key and derives shared secret // Daemon imports client's public key and derives shared secret
const clientPubKeyOnDaemon = await importPublicKey(hello.key); const clientPubKeyOnDaemon = importPublicKey(hello.key);
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon); const daemonSharedKey = deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon);
// === VERIFY BOTH HAVE SAME KEY - Exchange encrypted messages === // === VERIFY BOTH HAVE SAME KEY - Exchange encrypted messages ===
// Daemon sends encrypted "ready" message // Daemon sends encrypted "ready" message
const readyPlaintext = JSON.stringify({ type: "ready" }); const readyPlaintext = JSON.stringify({ type: "ready" });
const readyCiphertext = await encrypt(daemonSharedKey, readyPlaintext); const readyCiphertext = encrypt(daemonSharedKey, readyPlaintext);
daemonWs.send(Buffer.from(readyCiphertext)); daemonWs.send(Buffer.from(readyCiphertext));
// Client receives and decrypts // Client receives and decrypts
const clientReceivedReady = await new Promise<Buffer>((resolve) => { const clientReceivedReady = await new Promise<Buffer>((resolve) => {
clientWs.once("message", (data) => resolve(data as Buffer)); clientWs.once("message", (data) => resolve(data as Buffer));
}); });
const decryptedReady = await decrypt( const decryptedReady = decrypt(
clientSharedKey, clientSharedKey,
clientReceivedReady.buffer.slice( clientReceivedReady.buffer.slice(
clientReceivedReady.byteOffset, clientReceivedReady.byteOffset,
@@ -349,14 +349,14 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
// Client sends encrypted message // Client sends encrypted message
const clientMessage = "Hello from client!"; const clientMessage = "Hello from client!";
const clientCiphertext = await encrypt(clientSharedKey, clientMessage); const clientCiphertext = encrypt(clientSharedKey, clientMessage);
clientWs.send(Buffer.from(clientCiphertext)); clientWs.send(Buffer.from(clientCiphertext));
// Daemon receives and decrypts // Daemon receives and decrypts
const daemonReceivedMsg = await new Promise<Buffer>((resolve) => { const daemonReceivedMsg = await new Promise<Buffer>((resolve) => {
daemonWs.once("message", (data) => resolve(data as Buffer)); daemonWs.once("message", (data) => resolve(data as Buffer));
}); });
const decryptedClientMsg = await decrypt( const decryptedClientMsg = decrypt(
daemonSharedKey, daemonSharedKey,
daemonReceivedMsg.buffer.slice( daemonReceivedMsg.buffer.slice(
daemonReceivedMsg.byteOffset, daemonReceivedMsg.byteOffset,
@@ -367,14 +367,14 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
// Daemon sends encrypted response // Daemon sends encrypted response
const daemonMessage = "Hello from daemon!"; const daemonMessage = "Hello from daemon!";
const daemonCiphertext = await encrypt(daemonSharedKey, daemonMessage); const daemonCiphertext = encrypt(daemonSharedKey, daemonMessage);
daemonWs.send(Buffer.from(daemonCiphertext)); daemonWs.send(Buffer.from(daemonCiphertext));
// Client receives and decrypts // Client receives and decrypts
const clientReceivedMsg = await new Promise<Buffer>((resolve) => { const clientReceivedMsg = await new Promise<Buffer>((resolve) => {
clientWs.once("message", (data) => resolve(data as Buffer)); clientWs.once("message", (data) => resolve(data as Buffer));
}); });
const decryptedDaemonMsg = await decrypt( const decryptedDaemonMsg = decrypt(
clientSharedKey, clientSharedKey,
clientReceivedMsg.buffer.slice( clientReceivedMsg.buffer.slice(
clientReceivedMsg.byteOffset, clientReceivedMsg.byteOffset,
@@ -394,17 +394,17 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
const connectionId = "clt_opaque_" + Date.now() + "_" + Math.random().toString(36).slice(2); const connectionId = "clt_opaque_" + Date.now() + "_" + Math.random().toString(36).slice(2);
// Setup keys // Setup keys
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const clientKeyPair = await generateKeyPair(); const clientKeyPair = generateKeyPair();
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey); const clientPubKeyB64 = exportPublicKey(clientKeyPair.publicKey);
const clientPubKey = await importPublicKey(clientPubKeyB64); const clientPubKey = importPublicKey(clientPubKeyB64);
const daemonPubKey = await importPublicKey(daemonPubKeyB64); const daemonPubKey = importPublicKey(daemonPubKeyB64);
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKey); const daemonSharedKey = deriveSharedKey(daemonKeyPair.secretKey, clientPubKey);
const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKey); const clientSharedKey = deriveSharedKey(clientKeyPair.secretKey, daemonPubKey);
const daemonControlWs = new WebSocket( const daemonControlWs = new WebSocket(
`ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`, `ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`,
@@ -458,7 +458,7 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
// Send encrypted secret // Send encrypted secret
const secret = "This is a secret that relay cannot read"; const secret = "This is a secret that relay cannot read";
const ciphertext = await encrypt(clientSharedKey, secret); const ciphertext = encrypt(clientSharedKey, secret);
clientWs.send(Buffer.from(ciphertext)); clientWs.send(Buffer.from(ciphertext));
// Daemon receives // Daemon receives
@@ -471,7 +471,7 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
expect(rawString).not.toContain(secret); expect(rawString).not.toContain(secret);
// But daemon can decrypt // But daemon can decrypt
const decrypted = await decrypt( const decrypted = decrypt(
daemonSharedKey, daemonSharedKey,
received.buffer.slice(received.byteOffset, received.byteOffset + received.byteLength), received.buffer.slice(received.byteOffset, received.byteOffset + received.byteLength),
); );
@@ -482,22 +482,22 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise<void> {
clientWs.close(); clientWs.close();
}); });
it("wrong key cannot decrypt", async () => { it("wrong key cannot decrypt", () => {
// Setup - daemon and client with correct keys // Setup - daemon and client with correct keys
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const clientKeyPair = await generateKeyPair(); const clientKeyPair = generateKeyPair();
const attackerKeyPair = await generateKeyPair(); const attackerKeyPair = generateKeyPair();
const clientPubKey = await importPublicKey(await exportPublicKey(clientKeyPair.publicKey)); const clientPubKey = importPublicKey(exportPublicKey(clientKeyPair.publicKey));
const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKey); const daemonSharedKey = deriveSharedKey(daemonKeyPair.secretKey, clientPubKey);
// Attacker tries to derive key with their own keypair // Attacker tries to derive key with their own keypair
const attackerPubKey = await importPublicKey(await exportPublicKey(attackerKeyPair.publicKey)); const attackerPubKey = importPublicKey(exportPublicKey(attackerKeyPair.publicKey));
const attackerKey = await deriveSharedKey(attackerKeyPair.secretKey, attackerPubKey); const attackerKey = deriveSharedKey(attackerKeyPair.secretKey, attackerPubKey);
// Encrypt with daemon's key // Encrypt with daemon's key
const secret = "Top secret message"; const secret = "Top secret message";
const ciphertext = await encrypt(daemonSharedKey, secret); const ciphertext = encrypt(daemonSharedKey, secret);
// Attacker cannot decrypt // Attacker cannot decrypt
expect(() => decrypt(attackerKey, ciphertext)).toThrow(); expect(() => decrypt(attackerKey, ciphertext)).toThrow();
+9 -9
View File
@@ -40,8 +40,8 @@ describe("EncryptedChannel", () => {
const [daemonTransport, clientTransport] = createMockTransportPair(); const [daemonTransport, clientTransport] = createMockTransportPair();
// Daemon generates keypair (public key goes in QR) // Daemon generates keypair (public key goes in QR)
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
let clientOpenedResolve: (() => void) | null = null; let clientOpenedResolve: (() => void) | null = null;
const clientOpened = new Promise<void>((resolve) => { const clientOpened = new Promise<void>((resolve) => {
@@ -67,8 +67,8 @@ describe("EncryptedChannel", () => {
it("exchanges encrypted messages bidirectionally", async () => { it("exchanges encrypted messages bidirectionally", async () => {
const [daemonTransport, clientTransport] = createMockTransportPair(); const [daemonTransport, clientTransport] = createMockTransportPair();
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
const daemonMessages: (string | ArrayBuffer)[] = []; const daemonMessages: (string | ArrayBuffer)[] = [];
const clientMessages: (string | ArrayBuffer)[] = []; const clientMessages: (string | ArrayBuffer)[] = [];
@@ -105,8 +105,8 @@ describe("EncryptedChannel", () => {
it("encrypted messages are opaque to transport", async () => { it("encrypted messages are opaque to transport", async () => {
const [daemonTransport, clientTransport] = createMockTransportPair(); const [daemonTransport, clientTransport] = createMockTransportPair();
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
let clientOpenedResolve: (() => void) | null = null; let clientOpenedResolve: (() => void) | null = null;
const clientOpened = new Promise<void>((resolve) => { const clientOpened = new Promise<void>((resolve) => {
@@ -142,8 +142,8 @@ describe("EncryptedChannel", () => {
it("does not throw uncaught when handshake hello retry send fails", async () => { it("does not throw uncaught when handshake hello retry send fails", async () => {
vi.useFakeTimers(); vi.useFakeTimers();
try { try {
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
const transport: Transport = { const transport: Transport = {
send: vi.fn(), send: vi.fn(),
@@ -183,7 +183,7 @@ describe("EncryptedChannel", () => {
it("fails handshake on invalid hello", async () => { it("fails handshake on invalid hello", async () => {
const [daemonTransport] = createMockTransportPair(); const [daemonTransport] = createMockTransportPair();
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const daemonChannelPromise = createDaemonChannel(daemonTransport, daemonKeyPair); const daemonChannelPromise = createDaemonChannel(daemonTransport, daemonKeyPair);
+2 -2
View File
@@ -331,7 +331,7 @@ export class EncryptedChannel {
})(); })();
if (ciphertext) { if (ciphertext) {
const plaintext = await decrypt(this.sharedKey, ciphertext); const plaintext = decrypt(this.sharedKey, ciphertext);
this.events.onmessage?.(plaintext); this.events.onmessage?.(plaintext);
} }
} catch (error) { } catch (error) {
@@ -361,7 +361,7 @@ export class EncryptedChannel {
throw new Error("Channel not open"); throw new Error("Channel not open");
} }
const ciphertext = await encrypt(this.sharedKey, data); const ciphertext = encrypt(this.sharedKey, data);
// Send as base64 for WebSocket text compatibility // Send as base64 for WebSocket text compatibility
this.transport.send(arrayBufferToBase64(ciphertext)); this.transport.send(arrayBufferToBase64(ciphertext));
} }
+12 -18
View File
@@ -104,17 +104,14 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
)}&role=client&connectionId=${encodeURIComponent(connectionId)}&v=2`; )}&role=client&connectionId=${encodeURIComponent(connectionId)}&v=2`;
// === Key setup === // === Key setup ===
const daemonKeyPair = await generateKeyPair(); const daemonKeyPair = generateKeyPair();
const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
const clientKeyPair = await generateKeyPair(); const clientKeyPair = generateKeyPair();
const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey); const clientPubKeyB64 = exportPublicKey(clientKeyPair.publicKey);
const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64); const daemonPubKeyOnClient = importPublicKey(daemonPubKeyB64);
const clientSharedKey = await deriveSharedKey( const clientSharedKey = deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient);
clientKeyPair.secretKey,
daemonPubKeyOnClient,
);
// === Connect === // === Connect ===
const daemonControlWs = new WebSocket(serverControlUrl); const daemonControlWs = new WebSocket(serverControlUrl);
@@ -149,15 +146,12 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
expect(hello.type).toBe("hello"); expect(hello.type).toBe("hello");
expect(typeof hello.key).toBe("string"); expect(typeof hello.key).toBe("string");
const clientPubKeyOnDaemon = await importPublicKey(hello.key!); const clientPubKeyOnDaemon = importPublicKey(hello.key!);
const daemonSharedKey = await deriveSharedKey( const daemonSharedKey = deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon);
daemonKeyPair.secretKey,
clientPubKeyOnDaemon,
);
// === Encrypted exchange === // === Encrypted exchange ===
const plaintextFromClient = "hello-from-client"; const plaintextFromClient = "hello-from-client";
const ciphertextFromClient = await encrypt(clientSharedKey, plaintextFromClient); const ciphertextFromClient = encrypt(clientSharedKey, plaintextFromClient);
clientWs.send(Buffer.from(ciphertextFromClient)); clientWs.send(Buffer.from(ciphertextFromClient));
const daemonReceivedCiphertext = await waitForOnceMessage( const daemonReceivedCiphertext = await waitForOnceMessage(
@@ -166,7 +160,7 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
"Timed out waiting for encrypted message", "Timed out waiting for encrypted message",
); );
const decryptedOnDaemon = await decrypt( const decryptedOnDaemon = decrypt(
daemonSharedKey, daemonSharedKey,
daemonReceivedCiphertext.buffer.slice( daemonReceivedCiphertext.buffer.slice(
daemonReceivedCiphertext.byteOffset, daemonReceivedCiphertext.byteOffset,
@@ -176,7 +170,7 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
expect(decryptedOnDaemon).toBe(plaintextFromClient); expect(decryptedOnDaemon).toBe(plaintextFromClient);
const plaintextFromDaemon = "hello-from-daemon"; const plaintextFromDaemon = "hello-from-daemon";
const ciphertextFromDaemon = await encrypt(daemonSharedKey, plaintextFromDaemon); const ciphertextFromDaemon = encrypt(daemonSharedKey, plaintextFromDaemon);
daemonWs.send(Buffer.from(ciphertextFromDaemon)); daemonWs.send(Buffer.from(ciphertextFromDaemon));
const clientReceivedCiphertext = await waitForOnceMessage( const clientReceivedCiphertext = await waitForOnceMessage(
@@ -185,7 +179,7 @@ describe("Live relay (relay.paseo.sh) E2E", () => {
"Timed out waiting for encrypted response", "Timed out waiting for encrypted response",
); );
const decryptedOnClient = await decrypt( const decryptedOnClient = decrypt(
clientSharedKey, clientSharedKey,
clientReceivedCiphertext.buffer.slice( clientReceivedCiphertext.buffer.slice(
clientReceivedCiphertext.byteOffset, clientReceivedCiphertext.byteOffset,