Skip to content

Commit 9426fd2

Browse files
committed
Merge remote-tracking branch 'refs/remotes/origin/master'
2 parents 9e2e973 + 3cae24f commit 9426fd2

5 files changed

Lines changed: 89 additions & 11 deletions

File tree

backend/api/messaging/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,14 @@ router.post(
6666
if (!valid) {
6767
return res.sendStatus(404);
6868
}
69-
const existing = await db.findOneFromDB({ channel, user: sender }, PUBLIC_KEY_COLLECTION);
69+
const existing = await db.findOneFromDB<{ aesKey: string | null }>({ channel, user: sender }, PUBLIC_KEY_COLLECTION);
7070
if (existing) {
71-
return res.status(409).send({ error: "Key already registered for this session" });
71+
if (existing.aesKey) {
72+
return res.status(409).send({ error: "Key already registered for this session" });
73+
}
74+
// First call registered publicKey with aesKey: null; this call adds the encrypted AES key
75+
await db.updateOneFromDb({ channel, user: sender }, { aesKey }, PUBLIC_KEY_COLLECTION);
76+
return res.send({ status: "ok" });
7277
}
7378
await db.insertInDb({ aesKey, publicKey, user: sender, channel }, PUBLIC_KEY_COLLECTION);
7479
return res.send({ status: "ok" });

client/app.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@ const callStatusText = document.getElementById('call-status')!;
4141
const endCallBtn = document.getElementById('end-call-btn') as HTMLButtonElement;
4242
const callDuration = document.getElementById('call-duration')!;
4343

44+
// Audio notification
45+
function playBeep() {
46+
try {
47+
const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext;
48+
if (!AudioContextClass) return;
49+
const ctx = new AudioContextClass();
50+
const oscillator = ctx.createOscillator();
51+
const gainNode = ctx.createGain();
52+
oscillator.connect(gainNode);
53+
gainNode.connect(ctx.destination);
54+
oscillator.type = 'sine';
55+
oscillator.frequency.setValueAtTime(880, ctx.currentTime);
56+
gainNode.gain.setValueAtTime(0.3, ctx.currentTime);
57+
gainNode.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.4);
58+
oscillator.start(ctx.currentTime);
59+
oscillator.stop(ctx.currentTime + 0.4);
60+
} catch (err) {
61+
console.warn('Audio notification not available:', err);
62+
}
63+
}
64+
4465
// Initialize Chat
4566
async function initChat() {
4667
try {
@@ -120,6 +141,7 @@ async function checkExistingUsers() {
120141
try {
121142
const users = await chat.getUsersInChannel();
122143
if (users && users.length > 1) {
144+
playBeep();
123145
chatHeader.classList.add('active');
124146
participantInfo.textContent = 'Peer is already here. Communication is encrypted.';
125147
}
@@ -181,6 +203,7 @@ joinBtn.addEventListener('click', async () => {
181203

182204
function setupChatListeners() {
183205
chat.on('on-alice-join', () => {
206+
playBeep();
184207
chatHeader.classList.add('active');
185208
participantInfo.textContent = 'Peer joined. Communication is encrypted.';
186209
});

service/build.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ async function build() {
1818
minify: isProduction,
1919
logLevel: 'info',
2020
metafile: true,
21-
define: {
22-
'process.env.CHATE2EE_API_URL': JSON.stringify(process.env.CHATE2EE_API_URL) || 'undefined',
23-
},
2421
};
2522

2623
if (watch) {

service/src/crypto.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,62 @@ describe('AesGcmEncryption (AES-GCM) – real Web Crypto', () => {
124124
);
125125
});
126126
});
127+
128+
// ---------------------------------------------------------------------------
129+
// AES key exchange via RSA-encrypted channel
130+
// ---------------------------------------------------------------------------
131+
describe('AES key exchange via RSA-encrypted channel', () => {
132+
it('AES key exported by sender can be RSA-encrypted and decrypted by receiver, enabling symmetric decryption', async () => {
133+
// Simulate Bob (receiver): generate an RSA key pair
134+
const { publicKey: bobPublicKey, privateKey: bobPrivateKey } = await cryptoUtils.generateKeypairs();
135+
136+
// Simulate Alice (sender): generate an AES key
137+
const aliceAes = new AesGcmEncryption();
138+
await aliceAes.int();
139+
140+
// Alice exports her AES key as a JWK string (this is what getRawAesKeyToExport returns)
141+
const aesKeyJwk = await aliceAes.getRawAesKeyToExport();
142+
143+
// Alice encrypts the AES key with Bob's RSA public key before sending it to the server
144+
const encryptedAesKey = await cryptoUtils.encryptMessage(aesKeyJwk, bobPublicKey);
145+
expect(typeof encryptedAesKey).toBe('string');
146+
expect(encryptedAesKey).not.toBe(aesKeyJwk); // must be ciphertext, not plaintext
147+
148+
// Bob decrypts the AES key using his RSA private key
149+
const decryptedAesKeyJwk = await cryptoUtils.decryptMessage(encryptedAesKey, bobPrivateKey);
150+
expect(decryptedAesKeyJwk).toBe(aesKeyJwk); // recovered plaintext must match original
151+
152+
// Bob sets the decrypted AES key as his remote key
153+
const bobAes = new AesGcmEncryption();
154+
await bobAes.setRemoteAesKey(decryptedAesKeyJwk);
155+
156+
// Alice encrypts some data with her local AES key
157+
const originalText = 'Secret message over AES-GCM 🔐';
158+
const { encryptedData, iv } = await aliceAes.encryptData(
159+
new TextEncoder().encode(originalText).buffer
160+
);
161+
162+
// Bob decrypts the data using the AES key he received through the RSA-encrypted channel
163+
const decryptedBuffer = await bobAes.decryptData(encryptedData, iv);
164+
const decryptedText = new TextDecoder().decode(decryptedBuffer);
165+
166+
expect(decryptedText).toBe(originalText);
167+
});
168+
169+
it('a third party cannot decrypt the AES key without the receiver private key', async () => {
170+
const { publicKey: bobPublicKey } = await cryptoUtils.generateKeypairs();
171+
const { privateKey: evePrivateKey } = await cryptoUtils.generateKeypairs(); // attacker's key
172+
173+
const aliceAes = new AesGcmEncryption();
174+
await aliceAes.int();
175+
const aesKeyJwk = await aliceAes.getRawAesKeyToExport();
176+
177+
// Alice encrypts AES key with Bob's public key
178+
const encryptedAesKey = await cryptoUtils.encryptMessage(aesKeyJwk, bobPublicKey);
179+
180+
// Eve (third party) tries to decrypt with her own private key and fails
181+
await expect(
182+
cryptoUtils.decryptMessage(encryptedAesKey, evePrivateKey)
183+
).rejects.toThrow();
184+
});
185+
});

service/src/cryptoAES.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ export class AesGcmEncryption {
2626
return this.aesKeyRemote;
2727
}
2828

29-
/**
30-
* To Do:
31-
* this key is plain text, can be used to decrypt data.
32-
* Should not be transmitted over network.
33-
* Use cryptoUtils to encrypt the key and exchange.
34-
*/
3529
public async getRawAesKeyToExport(): Promise<string> {
3630
if (!this.aesKeyLocal) {
3731
throw new Error('AES key not generated');

0 commit comments

Comments
 (0)