@immahq/aegis 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,606 @@
1
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
2
+ import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";
3
+ import { blake3 } from "@noble/hashes/blake3.js";
4
+ import { randomBytes } from "@noble/post-quantum/utils.js";
5
+ import { bytesToHex, concatBytes, utf8ToBytes } from "@noble/hashes/utils.js";
6
+ import { Logger } from "./logger.js";
7
+ import { MAX_MESSAGE_AGE } from "./constants.js";
8
+ export class GroupManager {
9
+ constructor(storage) {
10
+ Object.defineProperty(this, "storage", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: void 0
15
+ });
16
+ Object.defineProperty(this, "identity", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: null
21
+ });
22
+ Object.defineProperty(this, "sentMessageNumbers", {
23
+ enumerable: true,
24
+ configurable: true,
25
+ writable: true,
26
+ value: void 0
27
+ }); // groupId -> senderId -> messageNumber
28
+ this.storage = storage;
29
+ this.sentMessageNumbers = new Map();
30
+ }
31
+ async initialize(identity) {
32
+ this.identity = identity;
33
+ }
34
+ async createGroup(name, members, memberKemPublicKeys, memberDsaPublicKeys) {
35
+ if (!this.identity) {
36
+ throw new Error("GroupManager not initialized with identity");
37
+ }
38
+ if (members.length < 2) {
39
+ throw new Error("Group must have at least 2 members");
40
+ }
41
+ // Generate a random shared key for the group
42
+ const sharedKey = randomBytes(32);
43
+ // Create group ID based on name and members
44
+ const groupId = "GROUP_" +
45
+ bytesToHex(blake3(concatBytes(utf8ToBytes(name), ...members.map((m) => utf8ToBytes(String(m)))), { dkLen: 32 }));
46
+ // Check if group already exists in storage
47
+ const existingGroup = await this.storage.getSession(groupId);
48
+ if (existingGroup) {
49
+ throw new Error("Group already exists");
50
+ }
51
+ // Create member keys - encrypt the shared key for each member
52
+ const memberKeys = new Map();
53
+ const memberPublicKeysMap = new Map();
54
+ const memberDsaPublicKeysMap = new Map();
55
+ // Use the provided member public keys
56
+ for (const memberId of members) {
57
+ const kemPublicKey = memberKemPublicKeys.get(memberId);
58
+ if (!kemPublicKey) {
59
+ throw new Error(`KEM public key not provided for member: ${memberId}`);
60
+ }
61
+ memberPublicKeysMap.set(memberId, kemPublicKey);
62
+ const dsaPublicKey = memberDsaPublicKeys.get(memberId);
63
+ if (!dsaPublicKey) {
64
+ throw new Error(`DSA public key not provided for member: ${memberId}`);
65
+ }
66
+ memberDsaPublicKeysMap.set(memberId, dsaPublicKey);
67
+ // Encrypt the shared key with the member's KEM public key using ML-KEM
68
+ const encryptedSharedKey = await this.encryptKeyWithPublicKey(sharedKey, kemPublicKey);
69
+ memberKeys.set(memberId, encryptedSharedKey);
70
+ }
71
+ const group = {
72
+ groupId,
73
+ name,
74
+ members,
75
+ sharedKey, // This is the actual shared key for the group owner
76
+ createdAt: Date.now(),
77
+ lastUpdated: Date.now(),
78
+ owner: this.identity.userId,
79
+ memberKeys, // These are encrypted with each member's KEM public key
80
+ memberPublicKeys: memberPublicKeysMap,
81
+ memberDsaPublicKeys: memberDsaPublicKeysMap, // DSA public keys for signature verification
82
+ receivedMessageNumbers: new Map(), // Initialize received message numbers tracking
83
+ };
84
+ // Store the group in the storage with a special format
85
+ // We'll store it as a session with additional group-specific data
86
+ await this.storage.saveSession(groupId, {
87
+ sessionId: groupId,
88
+ peerUserId: "GROUP", // Special marker for group sessions
89
+ peerDsaPublicKey: this.identity.dsaKeyPair.publicKey,
90
+ rootKey: group.sharedKey, // The actual shared key for the group owner
91
+ currentRatchetKeyPair: null,
92
+ peerRatchetPublicKey: null,
93
+ sendingChain: null,
94
+ receivingChain: null,
95
+ previousSendingChainLength: 0,
96
+ skippedMessageKeys: new Map(),
97
+ highestReceivedMessageNumber: -1,
98
+ maxSkippedMessages: 100,
99
+ createdAt: group.createdAt,
100
+ lastUsed: Date.now(),
101
+ isInitiator: true,
102
+ ratchetCount: 0,
103
+ state: "ACTIVE",
104
+ confirmed: true,
105
+ // Store group-specific data in additional fields
106
+ groupData: {
107
+ name: group.name,
108
+ members: group.members,
109
+ owner: group.owner,
110
+ memberKeys: Array.from(group.memberKeys.entries()), // Encrypted keys
111
+ memberPublicKeys: Array.from(group.memberPublicKeys.entries()),
112
+ memberDsaPublicKeys: Array.from(group.memberDsaPublicKeys.entries()),
113
+ receivedMessageNumbers: Array.from(group.receivedMessageNumbers.entries()),
114
+ },
115
+ receivedMessageIds: new Set(),
116
+ replayWindowSize: 100,
117
+ lastProcessedTimestamp: Date.now(),
118
+ });
119
+ Logger.log("GroupManager", "Group created successfully", {
120
+ groupId: groupId.substring(0, 16) + "...",
121
+ name,
122
+ membersCount: members.length,
123
+ });
124
+ return group;
125
+ }
126
+ async addMember(groupId, userId, _session, // Unused parameter, using underscore prefix
127
+ userPublicKey // New parameter for the user's public key
128
+ ) {
129
+ if (!this.identity) {
130
+ throw new Error("GroupManager not initialized with identity");
131
+ }
132
+ const group = await this.getGroup(groupId);
133
+ if (!group) {
134
+ throw new Error("Group not found");
135
+ }
136
+ // Only owner can add members
137
+ if (group.owner !== this.identity.userId) {
138
+ throw new Error("Only group owner can add members");
139
+ }
140
+ // Check if user is already a member
141
+ if (group.members.includes(userId)) {
142
+ throw new Error("User is already a member of this group");
143
+ }
144
+ // Add user to members list
145
+ group.members.push(userId);
146
+ group.lastUpdated = Date.now();
147
+ // Encrypt the shared key with the new member's public key
148
+ const encryptedSharedKey = await this.encryptKeyWithPublicKey(group.sharedKey, userPublicKey);
149
+ group.memberKeys.set(userId, encryptedSharedKey);
150
+ // Update member public keys with the provided public key
151
+ group.memberPublicKeys.set(userId, userPublicKey);
152
+ // Save updated group to storage
153
+ await this.storage.saveSession(groupId, {
154
+ sessionId: groupId,
155
+ peerUserId: "GROUP", // Special marker for group sessions
156
+ peerDsaPublicKey: this.identity.dsaKeyPair.publicKey,
157
+ rootKey: group.sharedKey,
158
+ currentRatchetKeyPair: null,
159
+ peerRatchetPublicKey: null,
160
+ sendingChain: null,
161
+ receivingChain: null,
162
+ previousSendingChainLength: 0,
163
+ skippedMessageKeys: new Map(),
164
+ highestReceivedMessageNumber: -1,
165
+ maxSkippedMessages: 100,
166
+ createdAt: group.createdAt,
167
+ lastUsed: Date.now(),
168
+ isInitiator: true,
169
+ ratchetCount: 0,
170
+ state: "ACTIVE",
171
+ confirmed: true,
172
+ // Store group-specific data in additional fields
173
+ groupData: {
174
+ name: group.name,
175
+ members: group.members,
176
+ owner: group.owner,
177
+ memberKeys: Array.from(group.memberKeys.entries()),
178
+ memberPublicKeys: Array.from(group.memberPublicKeys.entries()),
179
+ memberDsaPublicKeys: Array.from(group.memberDsaPublicKeys.entries()),
180
+ receivedMessageNumbers: Array.from(group.receivedMessageNumbers.entries()),
181
+ },
182
+ receivedMessageIds: new Set(),
183
+ replayWindowSize: 100,
184
+ lastProcessedTimestamp: Date.now(),
185
+ });
186
+ Logger.log("GroupManager", "Member added to group", {
187
+ groupId: groupId.substring(0, 16) + "...",
188
+ userId,
189
+ membersCount: group.members.length,
190
+ });
191
+ }
192
+ async removeMember(groupId, userId) {
193
+ if (!this.identity) {
194
+ throw new Error("GroupManager not initialized with identity");
195
+ }
196
+ const group = await this.getGroup(groupId);
197
+ if (!group) {
198
+ throw new Error("Group not found");
199
+ }
200
+ // Only owner can remove members
201
+ if (group.owner !== this.identity.userId) {
202
+ throw new Error("Only group owner can remove members");
203
+ }
204
+ // Check if user is a member
205
+ const memberIndex = group.members.indexOf(userId);
206
+ if (memberIndex === -1) {
207
+ throw new Error("User is not a member of this group");
208
+ }
209
+ // Remove user from members list
210
+ group.members.splice(memberIndex, 1);
211
+ group.lastUpdated = Date.now();
212
+ // Remove member key
213
+ group.memberKeys.delete(userId);
214
+ // Remove member public key
215
+ group.memberPublicKeys.delete(userId);
216
+ // Save updated group to storage
217
+ await this.storage.saveSession(groupId, {
218
+ sessionId: groupId,
219
+ peerUserId: "GROUP", // Special marker for group sessions
220
+ peerDsaPublicKey: this.identity.dsaKeyPair.publicKey,
221
+ rootKey: group.sharedKey,
222
+ currentRatchetKeyPair: null,
223
+ peerRatchetPublicKey: null,
224
+ sendingChain: null,
225
+ receivingChain: null,
226
+ previousSendingChainLength: 0,
227
+ skippedMessageKeys: new Map(),
228
+ highestReceivedMessageNumber: -1,
229
+ maxSkippedMessages: 100,
230
+ createdAt: group.createdAt,
231
+ lastUsed: Date.now(),
232
+ isInitiator: true,
233
+ ratchetCount: 0,
234
+ state: "ACTIVE",
235
+ confirmed: true,
236
+ // Store group-specific data in additional fields
237
+ groupData: {
238
+ name: group.name,
239
+ members: group.members,
240
+ owner: group.owner,
241
+ memberKeys: Array.from(group.memberKeys.entries()),
242
+ memberPublicKeys: Array.from(group.memberPublicKeys.entries()),
243
+ memberDsaPublicKeys: Array.from(group.memberDsaPublicKeys.entries()),
244
+ receivedMessageNumbers: Array.from(group.receivedMessageNumbers.entries()),
245
+ },
246
+ receivedMessageIds: new Set(),
247
+ replayWindowSize: 100,
248
+ lastProcessedTimestamp: Date.now(),
249
+ });
250
+ Logger.log("GroupManager", "Member removed from group", {
251
+ groupId: groupId.substring(0, 16) + "...",
252
+ userId,
253
+ membersCount: group.members.length,
254
+ });
255
+ }
256
+ async updateGroupKey(groupId) {
257
+ if (!this.identity) {
258
+ throw new Error("GroupManager not initialized with identity");
259
+ }
260
+ const group = await this.getGroup(groupId);
261
+ if (!group) {
262
+ throw new Error("Group not found");
263
+ }
264
+ // Only owner can update group key
265
+ if (group.owner !== this.identity.userId) {
266
+ throw new Error("Only group owner can update group key");
267
+ }
268
+ // Generate new shared key
269
+ const newSharedKey = randomBytes(32);
270
+ // Update member keys for all members - encrypt the new key with each member's public key
271
+ for (const memberId of group.members) {
272
+ const memberPublicKey = group.memberPublicKeys.get(memberId);
273
+ if (!memberPublicKey) {
274
+ throw new Error(`Public key not found for member: ${memberId}`);
275
+ }
276
+ // Encrypt the new shared key with the member's public key
277
+ const encryptedNewSharedKey = await this.encryptKeyWithPublicKey(newSharedKey, memberPublicKey);
278
+ group.memberKeys.set(memberId, encryptedNewSharedKey);
279
+ }
280
+ // Note: We don't update public keys when updating the group key
281
+ // Update the group shared key
282
+ group.sharedKey = newSharedKey;
283
+ group.lastUpdated = Date.now();
284
+ // Save updated group to storage
285
+ await this.storage.saveSession(groupId, {
286
+ sessionId: groupId,
287
+ peerUserId: "GROUP", // Special marker for group sessions
288
+ peerDsaPublicKey: this.identity.dsaKeyPair.publicKey,
289
+ rootKey: group.sharedKey,
290
+ currentRatchetKeyPair: null,
291
+ peerRatchetPublicKey: null,
292
+ sendingChain: null,
293
+ receivingChain: null,
294
+ previousSendingChainLength: 0,
295
+ skippedMessageKeys: new Map(),
296
+ highestReceivedMessageNumber: -1,
297
+ maxSkippedMessages: 100,
298
+ createdAt: group.createdAt,
299
+ lastUsed: Date.now(),
300
+ isInitiator: true,
301
+ ratchetCount: 0,
302
+ state: "ACTIVE",
303
+ confirmed: true,
304
+ // Store group-specific data in additional fields
305
+ groupData: {
306
+ name: group.name,
307
+ members: group.members,
308
+ owner: group.owner,
309
+ memberKeys: Array.from(group.memberKeys.entries()),
310
+ memberPublicKeys: Array.from(group.memberPublicKeys.entries()),
311
+ memberDsaPublicKeys: Array.from(group.memberDsaPublicKeys.entries()),
312
+ receivedMessageNumbers: Array.from(group.receivedMessageNumbers.entries()),
313
+ },
314
+ receivedMessageIds: new Set(),
315
+ replayWindowSize: 100,
316
+ lastProcessedTimestamp: Date.now(),
317
+ });
318
+ Logger.log("GroupManager", "Group key updated", {
319
+ groupId: groupId.substring(0, 16) + "...",
320
+ });
321
+ }
322
+ async encryptMessage(groupId, message) {
323
+ if (!this.identity) {
324
+ throw new Error("GroupManager not initialized with identity");
325
+ }
326
+ const group = await this.getGroup(groupId);
327
+ if (!group) {
328
+ throw new Error("Group not found");
329
+ }
330
+ // Check if user is a member of the group
331
+ if (!group.members.includes(this.identity.userId)) {
332
+ throw new Error("User is not a member of this group");
333
+ }
334
+ const messageBytes = typeof message === "string" ? utf8ToBytes(message) : message;
335
+ // Get the group's actual shared key for encryption
336
+ // For the group owner, they have the key directly
337
+ // For other members, they would need to decrypt their encrypted key
338
+ let encryptionKey;
339
+ if (group.owner === this.identity.userId) {
340
+ // Group owner has the key directly
341
+ encryptionKey = group.sharedKey;
342
+ }
343
+ else {
344
+ // Other members need to decrypt their copy of the key
345
+ const encryptedSharedKey = group.memberKeys.get(this.identity.userId);
346
+ if (!encryptedSharedKey) {
347
+ throw new Error("No encrypted shared key found for user");
348
+ }
349
+ if (!this.identity.kemKeyPair.secretKey) {
350
+ throw new Error("User's secret key not available");
351
+ }
352
+ encryptionKey = await this.decryptKeyWithSecretKey(encryptedSharedKey, this.identity.kemKeyPair.secretKey);
353
+ }
354
+ // Encrypt the message with the shared key
355
+ const nonce = randomBytes(24);
356
+ const cipher = xchacha20poly1305(encryptionKey, nonce);
357
+ const ciphertext = cipher.encrypt(messageBytes);
358
+ const fullCiphertext = concatBytes(nonce, ciphertext);
359
+ // Create message header with a simple incrementing number per sender (in memory, not stored)
360
+ // For a real implementation, you'd want a more robust approach to message numbering
361
+ if (!this.sentMessageNumbers) {
362
+ this.sentMessageNumbers = new Map(); // groupId -> senderId -> messageNumber
363
+ }
364
+ if (!this.sentMessageNumbers.has(groupId)) {
365
+ this.sentMessageNumbers.set(groupId, new Map());
366
+ }
367
+ const groupSentNumbers = this.sentMessageNumbers.get(groupId);
368
+ const currentMessageNumber = (groupSentNumbers.get(this.identity.userId) || 0) + 1;
369
+ groupSentNumbers.set(this.identity.userId, currentMessageNumber);
370
+ // Create message header
371
+ const header = {
372
+ messageId: bytesToHex(blake3(fullCiphertext, { dkLen: 32 })),
373
+ timestamp: Date.now(),
374
+ senderId: this.identity.userId,
375
+ messageNumber: currentMessageNumber,
376
+ };
377
+ // Note: We don't update the stored group state here because sent message numbers
378
+ // are tracked separately per participant
379
+ // Sign the message
380
+ const headerBytes = this.serializeGroupHeader(header);
381
+ const messageToSign = concatBytes(headerBytes, fullCiphertext);
382
+ const signature = ml_dsa65.sign(messageToSign, this.identity.dsaKeyPair.secretKey);
383
+ Logger.log("GroupManager", "Group message encrypted", {
384
+ groupId: groupId.substring(0, 16) + "...",
385
+ messageId: header.messageId.substring(0, 16) + "...",
386
+ senderId: this.identity.userId,
387
+ });
388
+ return {
389
+ groupId,
390
+ message: fullCiphertext,
391
+ header,
392
+ signature,
393
+ };
394
+ }
395
+ async decryptMessage(groupId, encrypted) {
396
+ if (!this.identity) {
397
+ throw new Error("GroupManager not initialized with identity");
398
+ }
399
+ const group = await this.getGroup(groupId);
400
+ if (!group) {
401
+ throw new Error("Group not found");
402
+ }
403
+ // Check if user is a member of the group
404
+ if (!group.members.includes(this.identity.userId)) {
405
+ throw new Error("User is not a member of this group");
406
+ }
407
+ // Get the encrypted shared key for this user and decrypt it
408
+ const encryptedSharedKey = group.memberKeys.get(this.identity.userId);
409
+ if (!encryptedSharedKey) {
410
+ throw new Error("No encrypted shared key found for user");
411
+ }
412
+ // We need the user's KEM secret key to decrypt the shared group key
413
+ // For this demo, we'll assume we have access to the user's identity secret key
414
+ // In a real implementation, this would be securely stored and accessed
415
+ if (!this.identity.kemKeyPair.secretKey) {
416
+ throw new Error("User's secret key not available");
417
+ }
418
+ const sharedKey = await this.decryptKeyWithSecretKey(encryptedSharedKey, this.identity.kemKeyPair.secretKey);
419
+ // Get the sender's public key from the group
420
+ const senderPublicKey = await this.getSenderPublicKey(groupId, encrypted.header.senderId);
421
+ if (!senderPublicKey) {
422
+ throw new Error("Could not retrieve sender's public key");
423
+ }
424
+ // Verify signature
425
+ const headerBytes = this.serializeGroupHeader(encrypted.header);
426
+ const messageToVerify = concatBytes(headerBytes, encrypted.message);
427
+ const isValid = ml_dsa65.verify(encrypted.signature, messageToVerify, senderPublicKey);
428
+ if (!isValid) {
429
+ throw new Error("Invalid message signature");
430
+ }
431
+ // Check message freshness
432
+ const now = Date.now();
433
+ const messageAge = now - encrypted.header.timestamp;
434
+ if (messageAge > MAX_MESSAGE_AGE) {
435
+ throw new Error(`Message too old: ${Math.round(messageAge / 1000)}s`);
436
+ }
437
+ // Check message ordering/replay protection
438
+ const lastMessageNumber = group.receivedMessageNumbers.get(encrypted.header.senderId) || 0;
439
+ if (encrypted.header.messageNumber <= lastMessageNumber) {
440
+ throw new Error(`Message number too low: ${encrypted.header.messageNumber} <= ${lastMessageNumber}`);
441
+ }
442
+ // Update the received message number for this sender
443
+ group.receivedMessageNumbers.set(encrypted.header.senderId, encrypted.header.messageNumber);
444
+ // Update the group in storage with the new received message number
445
+ await this.storage.saveSession(groupId, {
446
+ sessionId: groupId,
447
+ peerUserId: "GROUP", // Special marker for group sessions
448
+ peerDsaPublicKey: this.identity.dsaKeyPair.publicKey,
449
+ rootKey: group.sharedKey,
450
+ currentRatchetKeyPair: null,
451
+ peerRatchetPublicKey: null,
452
+ sendingChain: null,
453
+ receivingChain: null,
454
+ previousSendingChainLength: 0,
455
+ skippedMessageKeys: new Map(),
456
+ highestReceivedMessageNumber: -1,
457
+ maxSkippedMessages: 100,
458
+ createdAt: group.createdAt,
459
+ lastUsed: Date.now(),
460
+ isInitiator: true,
461
+ ratchetCount: 0,
462
+ state: "ACTIVE",
463
+ confirmed: true,
464
+ // Store group-specific data in additional fields
465
+ groupData: {
466
+ name: group.name,
467
+ members: group.members,
468
+ owner: group.owner,
469
+ memberKeys: Array.from(group.memberKeys.entries()),
470
+ memberPublicKeys: Array.from(group.memberPublicKeys.entries()),
471
+ memberDsaPublicKeys: Array.from(group.memberDsaPublicKeys.entries()),
472
+ receivedMessageNumbers: Array.from(group.receivedMessageNumbers.entries()),
473
+ },
474
+ receivedMessageIds: new Set(),
475
+ replayWindowSize: 100,
476
+ lastProcessedTimestamp: Date.now(),
477
+ });
478
+ // Decrypt the message
479
+ const nonce = encrypted.message.slice(0, 24);
480
+ const encryptedData = encrypted.message.slice(24);
481
+ const cipher = xchacha20poly1305(sharedKey, nonce);
482
+ const plaintext = cipher.decrypt(encryptedData);
483
+ Logger.log("GroupManager", "Group message decrypted", {
484
+ groupId: groupId.substring(0, 16) + "...",
485
+ messageId: encrypted.header.messageId.substring(0, 16) + "...",
486
+ senderId: encrypted.header.senderId,
487
+ messageNumber: encrypted.header.messageNumber,
488
+ });
489
+ return plaintext;
490
+ }
491
+ async getGroup(groupId) {
492
+ const session = await this.storage.getSession(groupId);
493
+ if (!session || session.peerUserId !== "GROUP") {
494
+ return null;
495
+ }
496
+ // Reconstruct the group from the stored session data
497
+ const groupData = session.groupData;
498
+ if (!groupData) {
499
+ return null;
500
+ }
501
+ // Convert the stored arrays back to Maps
502
+ const memberKeys = new Map(groupData.memberKeys);
503
+ const memberPublicKeys = new Map(groupData.memberPublicKeys);
504
+ // Initialize received message numbers
505
+ const receivedMessageNumbers = new Map();
506
+ if (groupData.receivedMessageNumbers) {
507
+ for (const [senderId, number] of groupData.receivedMessageNumbers) {
508
+ receivedMessageNumbers.set(senderId, number);
509
+ }
510
+ }
511
+ // Initialize DSA public keys
512
+ const memberDsaPublicKeys = new Map(groupData.memberDsaPublicKeys || []);
513
+ return {
514
+ groupId: session.sessionId,
515
+ name: groupData.name,
516
+ members: groupData.members,
517
+ sharedKey: session.rootKey, // This is the actual shared key for the group owner
518
+ createdAt: session.createdAt,
519
+ lastUpdated: session.lastUsed,
520
+ owner: groupData.owner,
521
+ memberKeys, // These are the encrypted keys for each member
522
+ memberPublicKeys,
523
+ memberDsaPublicKeys,
524
+ receivedMessageNumbers, // This is now always initialized
525
+ };
526
+ }
527
+ async getGroups() {
528
+ const sessionIds = await this.storage.listSessions();
529
+ const groups = [];
530
+ for (const sessionId of sessionIds) {
531
+ if (sessionId.startsWith("GROUP_")) {
532
+ // Only retrieve group sessions
533
+ const group = await this.getGroup(sessionId);
534
+ if (group) {
535
+ groups.push(group);
536
+ }
537
+ }
538
+ }
539
+ return groups;
540
+ }
541
+ async getSenderPublicKey(groupId, senderId) {
542
+ const group = await this.getGroup(groupId);
543
+ if (!group) {
544
+ return null;
545
+ }
546
+ return group.memberDsaPublicKeys.get(senderId) || null;
547
+ }
548
+ // Encrypt a key with a public key using ML-KEM
549
+ async encryptKeyWithPublicKey(key, publicKey) {
550
+ const { ml_kem768 } = await import("@noble/post-quantum/ml-kem.js");
551
+ const { blake3 } = await import("@noble/hashes/blake3.js");
552
+ const { xchacha20poly1305 } = await import("@noble/ciphers/chacha.js");
553
+ const { randomBytes, concatBytes } = await import("@noble/hashes/utils.js");
554
+ // Use ML-KEM to generate a shared secret with the recipient's public key
555
+ const result = ml_kem768.encapsulate(publicKey);
556
+ const sharedSecret = result.sharedSecret;
557
+ const ciphertext = result.cipherText;
558
+ // Use the shared secret to derive a symmetric key for encrypting the actual key
559
+ const encryptionKey = blake3(sharedSecret, { dkLen: 32 });
560
+ // Generate a random nonce for encryption
561
+ const nonce = randomBytes(24);
562
+ // Encrypt the key using ChaCha20-Poly1305 with the derived key
563
+ const cipher = xchacha20poly1305(encryptionKey, nonce);
564
+ const encryptedKey = cipher.encrypt(key);
565
+ // Return ciphertext (for decapsulation) + nonce + encrypted key
566
+ return concatBytes(ciphertext, nonce, encryptedKey);
567
+ }
568
+ // Decrypt an encrypted key with a secret key using ML-KEM
569
+ async decryptKeyWithSecretKey(encryptedKey, secretKey) {
570
+ const { ml_kem768 } = await import("@noble/post-quantum/ml-kem.js");
571
+ const { blake3 } = await import("@noble/hashes/blake3.js");
572
+ const { xchacha20poly1305 } = await import("@noble/ciphers/chacha.js");
573
+ // Extract components: first ML-KEM ciphertext (1088 bytes for ML-KEM 768)
574
+ // Then 24-byte nonce, then the rest is the encrypted key
575
+ const kemCiphertextLength = 1088; // Length of ML-KEM 768 ciphertext
576
+ const nonceLength = 24;
577
+ if (encryptedKey.length < kemCiphertextLength + nonceLength) {
578
+ throw new Error("Invalid encrypted key format");
579
+ }
580
+ const kemCiphertext = encryptedKey.slice(0, kemCiphertextLength);
581
+ const nonce = encryptedKey.slice(kemCiphertextLength, kemCiphertextLength + nonceLength);
582
+ const encryptedData = encryptedKey.slice(kemCiphertextLength + nonceLength);
583
+ // Use ML-KEM to decapsulate and get the shared secret
584
+ const sharedSecret = ml_kem768.decapsulate(kemCiphertext, secretKey);
585
+ // Use the shared secret to derive the symmetric key
586
+ const encryptionKey = blake3(sharedSecret, { dkLen: 32 });
587
+ // Decrypt the key using ChaCha20-Poly1305 with the derived key
588
+ const cipher = xchacha20poly1305(encryptionKey, nonce);
589
+ return cipher.decrypt(encryptedData);
590
+ }
591
+ serializeGroupHeader(header) {
592
+ // More robust serialization format
593
+ const timestampBytes = new Uint8Array(8);
594
+ new DataView(timestampBytes.buffer).setBigUint64(0, BigInt(header.timestamp), true);
595
+ const messageNumberBytes = new Uint8Array(8);
596
+ new DataView(messageNumberBytes.buffer).setBigUint64(0, BigInt(header.messageNumber), true);
597
+ const senderIdBytes = utf8ToBytes(header.senderId);
598
+ const messageIdBytes = utf8ToBytes(header.messageId);
599
+ // Format: [messageId length][messageId][timestamp][senderId length][senderId][messageNumber]
600
+ const messageIdLength = new Uint8Array(4);
601
+ new DataView(messageIdLength.buffer).setUint32(0, messageIdBytes.length, true);
602
+ const senderIdLength = new Uint8Array(4);
603
+ new DataView(senderIdLength.buffer).setUint32(0, senderIdBytes.length, true);
604
+ return concatBytes(messageIdLength, messageIdBytes, timestampBytes, senderIdLength, senderIdBytes, messageNumberBytes);
605
+ }
606
+ }