@moltdm/client 1.3.0 → 1.3.2
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.
- package/dist/index.d.mts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +274 -20
- package/dist/index.mjs +274 -20
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -36,6 +36,7 @@ interface Message {
|
|
|
36
36
|
replyTo?: string;
|
|
37
37
|
expiresAt?: string;
|
|
38
38
|
createdAt: string;
|
|
39
|
+
encryptedSenderKeys?: Record<string, string>;
|
|
39
40
|
}
|
|
40
41
|
interface DecryptedMessage {
|
|
41
42
|
id: string;
|
|
@@ -122,6 +123,7 @@ declare class MoltDMClient {
|
|
|
122
123
|
private relayUrl;
|
|
123
124
|
private identity;
|
|
124
125
|
private senderKeys;
|
|
126
|
+
private receivedSenderKeys;
|
|
125
127
|
constructor(options?: MoltDMClientOptions);
|
|
126
128
|
get address(): string;
|
|
127
129
|
get moltbotId(): string;
|
|
@@ -130,6 +132,20 @@ declare class MoltDMClient {
|
|
|
130
132
|
private createIdentity;
|
|
131
133
|
private loadSenderKeys;
|
|
132
134
|
private saveSenderKeys;
|
|
135
|
+
/**
|
|
136
|
+
* Derive message key from chain key using HMAC
|
|
137
|
+
* message_key = HMAC-SHA256(chain_key, 0x01)
|
|
138
|
+
*/
|
|
139
|
+
private deriveMessageKey;
|
|
140
|
+
/**
|
|
141
|
+
* Ratchet chain key forward
|
|
142
|
+
* new_chain_key = HMAC-SHA256(chain_key, 0x02)
|
|
143
|
+
*/
|
|
144
|
+
private ratchetChainKey;
|
|
145
|
+
/**
|
|
146
|
+
* Ratchet a chain key forward N steps (for catching up on missed messages)
|
|
147
|
+
*/
|
|
148
|
+
private ratchetChainKeyN;
|
|
133
149
|
startConversation(memberIds: string[], options?: {
|
|
134
150
|
name?: string;
|
|
135
151
|
type?: 'dm' | 'group';
|
|
@@ -153,6 +169,22 @@ declare class MoltDMClient {
|
|
|
153
169
|
}): Promise<{
|
|
154
170
|
messageId: string;
|
|
155
171
|
}>;
|
|
172
|
+
/**
|
|
173
|
+
* Rotate sender key for a conversation (call when membership changes)
|
|
174
|
+
*/
|
|
175
|
+
rotateSenderKey(conversationId: string): Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* Encrypt chain key for each conversation member using X25519 ECDH
|
|
178
|
+
*/
|
|
179
|
+
private encryptChainKeyForRecipients;
|
|
180
|
+
/**
|
|
181
|
+
* Decrypt a received chain key using our X25519 private key
|
|
182
|
+
*/
|
|
183
|
+
private decryptChainKey;
|
|
184
|
+
/**
|
|
185
|
+
* Decrypt a message using Sender Keys protocol
|
|
186
|
+
*/
|
|
187
|
+
decryptMessage(message: Message): Promise<string | null>;
|
|
156
188
|
getMessages(conversationId: string, options?: {
|
|
157
189
|
since?: string;
|
|
158
190
|
limit?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -36,6 +36,7 @@ interface Message {
|
|
|
36
36
|
replyTo?: string;
|
|
37
37
|
expiresAt?: string;
|
|
38
38
|
createdAt: string;
|
|
39
|
+
encryptedSenderKeys?: Record<string, string>;
|
|
39
40
|
}
|
|
40
41
|
interface DecryptedMessage {
|
|
41
42
|
id: string;
|
|
@@ -122,6 +123,7 @@ declare class MoltDMClient {
|
|
|
122
123
|
private relayUrl;
|
|
123
124
|
private identity;
|
|
124
125
|
private senderKeys;
|
|
126
|
+
private receivedSenderKeys;
|
|
125
127
|
constructor(options?: MoltDMClientOptions);
|
|
126
128
|
get address(): string;
|
|
127
129
|
get moltbotId(): string;
|
|
@@ -130,6 +132,20 @@ declare class MoltDMClient {
|
|
|
130
132
|
private createIdentity;
|
|
131
133
|
private loadSenderKeys;
|
|
132
134
|
private saveSenderKeys;
|
|
135
|
+
/**
|
|
136
|
+
* Derive message key from chain key using HMAC
|
|
137
|
+
* message_key = HMAC-SHA256(chain_key, 0x01)
|
|
138
|
+
*/
|
|
139
|
+
private deriveMessageKey;
|
|
140
|
+
/**
|
|
141
|
+
* Ratchet chain key forward
|
|
142
|
+
* new_chain_key = HMAC-SHA256(chain_key, 0x02)
|
|
143
|
+
*/
|
|
144
|
+
private ratchetChainKey;
|
|
145
|
+
/**
|
|
146
|
+
* Ratchet a chain key forward N steps (for catching up on missed messages)
|
|
147
|
+
*/
|
|
148
|
+
private ratchetChainKeyN;
|
|
133
149
|
startConversation(memberIds: string[], options?: {
|
|
134
150
|
name?: string;
|
|
135
151
|
type?: 'dm' | 'group';
|
|
@@ -153,6 +169,22 @@ declare class MoltDMClient {
|
|
|
153
169
|
}): Promise<{
|
|
154
170
|
messageId: string;
|
|
155
171
|
}>;
|
|
172
|
+
/**
|
|
173
|
+
* Rotate sender key for a conversation (call when membership changes)
|
|
174
|
+
*/
|
|
175
|
+
rotateSenderKey(conversationId: string): Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* Encrypt chain key for each conversation member using X25519 ECDH
|
|
178
|
+
*/
|
|
179
|
+
private encryptChainKeyForRecipients;
|
|
180
|
+
/**
|
|
181
|
+
* Decrypt a received chain key using our X25519 private key
|
|
182
|
+
*/
|
|
183
|
+
private decryptChainKey;
|
|
184
|
+
/**
|
|
185
|
+
* Decrypt a message using Sender Keys protocol
|
|
186
|
+
*/
|
|
187
|
+
decryptMessage(message: Message): Promise<string | null>;
|
|
156
188
|
getMessages(conversationId: string, options?: {
|
|
157
189
|
since?: string;
|
|
158
190
|
limit?: number;
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ var import_ed25519 = require("@noble/curves/ed25519");
|
|
|
39
39
|
var fs = __toESM(require("fs"));
|
|
40
40
|
var path = __toESM(require("path"));
|
|
41
41
|
var os = __toESM(require("os"));
|
|
42
|
+
var import_crypto = require("crypto");
|
|
42
43
|
function toBase64(bytes) {
|
|
43
44
|
return Buffer.from(bytes).toString("base64");
|
|
44
45
|
}
|
|
@@ -49,7 +50,10 @@ var MoltDMClient = class {
|
|
|
49
50
|
storagePath;
|
|
50
51
|
relayUrl;
|
|
51
52
|
identity = null;
|
|
53
|
+
// Our sender keys (for messages we send)
|
|
52
54
|
senderKeys = /* @__PURE__ */ new Map();
|
|
55
|
+
// Received sender keys (for messages from others) - keyed by `${convId}:${fromId}`
|
|
56
|
+
receivedSenderKeys = /* @__PURE__ */ new Map();
|
|
53
57
|
constructor(options = {}) {
|
|
54
58
|
const defaultStoragePath = process.env.OPENCLAW_STATE_DIR ? path.join(process.env.OPENCLAW_STATE_DIR, ".moltdm") : path.join(os.homedir(), ".moltdm");
|
|
55
59
|
this.storagePath = options.storagePath || defaultStoragePath;
|
|
@@ -151,10 +155,25 @@ var MoltDMClient = class {
|
|
|
151
155
|
const keys = JSON.parse(data);
|
|
152
156
|
for (const [convId, keyData] of Object.entries(keys)) {
|
|
153
157
|
const k = keyData;
|
|
158
|
+
const chainKey = fromBase64(k.chainKey || k.key || "");
|
|
154
159
|
this.senderKeys.set(convId, {
|
|
155
|
-
|
|
160
|
+
chainKey,
|
|
161
|
+
initialChainKey: k.initialChainKey ? fromBase64(k.initialChainKey) : chainKey,
|
|
156
162
|
version: k.version,
|
|
157
|
-
|
|
163
|
+
messageIndex: k.messageIndex ?? k.index ?? 0
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const receivedPath = path.join(this.storagePath, "received_sender_keys.json");
|
|
168
|
+
if (fs.existsSync(receivedPath)) {
|
|
169
|
+
const data = fs.readFileSync(receivedPath, "utf-8");
|
|
170
|
+
const keys = JSON.parse(data);
|
|
171
|
+
for (const [key, keyData] of Object.entries(keys)) {
|
|
172
|
+
const k = keyData;
|
|
173
|
+
this.receivedSenderKeys.set(key, {
|
|
174
|
+
chainKey: fromBase64(k.chainKey),
|
|
175
|
+
version: k.version,
|
|
176
|
+
messageIndex: k.messageIndex
|
|
158
177
|
});
|
|
159
178
|
}
|
|
160
179
|
}
|
|
@@ -164,12 +183,64 @@ var MoltDMClient = class {
|
|
|
164
183
|
const obj = {};
|
|
165
184
|
for (const [convId, keyData] of this.senderKeys) {
|
|
166
185
|
obj[convId] = {
|
|
167
|
-
|
|
186
|
+
chainKey: toBase64(keyData.chainKey),
|
|
187
|
+
initialChainKey: toBase64(keyData.initialChainKey),
|
|
168
188
|
version: keyData.version,
|
|
169
|
-
|
|
189
|
+
messageIndex: keyData.messageIndex
|
|
170
190
|
};
|
|
171
191
|
}
|
|
172
192
|
fs.writeFileSync(keysPath, JSON.stringify(obj, null, 2));
|
|
193
|
+
const receivedPath = path.join(this.storagePath, "received_sender_keys.json");
|
|
194
|
+
const receivedObj = {};
|
|
195
|
+
for (const [key, keyData] of this.receivedSenderKeys) {
|
|
196
|
+
receivedObj[key] = {
|
|
197
|
+
chainKey: toBase64(keyData.chainKey),
|
|
198
|
+
version: keyData.version,
|
|
199
|
+
messageIndex: keyData.messageIndex
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
fs.writeFileSync(receivedPath, JSON.stringify(receivedObj, null, 2));
|
|
203
|
+
}
|
|
204
|
+
// ============================================
|
|
205
|
+
// Sender Keys Protocol (Signal-style)
|
|
206
|
+
// ============================================
|
|
207
|
+
/**
|
|
208
|
+
* Derive message key from chain key using HMAC
|
|
209
|
+
* message_key = HMAC-SHA256(chain_key, 0x01)
|
|
210
|
+
*/
|
|
211
|
+
deriveMessageKey(chainKey) {
|
|
212
|
+
const keyBuffer = Buffer.from(chainKey);
|
|
213
|
+
const hmac = (0, import_crypto.createHmac)("sha256", keyBuffer);
|
|
214
|
+
hmac.update(Buffer.from([1]));
|
|
215
|
+
const digest = hmac.digest();
|
|
216
|
+
const result = new Uint8Array(32);
|
|
217
|
+
result.set(new Uint8Array(digest));
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Ratchet chain key forward
|
|
222
|
+
* new_chain_key = HMAC-SHA256(chain_key, 0x02)
|
|
223
|
+
*/
|
|
224
|
+
ratchetChainKey(chainKey) {
|
|
225
|
+
const keyBuffer = Buffer.from(chainKey);
|
|
226
|
+
const hmac = (0, import_crypto.createHmac)("sha256", keyBuffer);
|
|
227
|
+
hmac.update(Buffer.from([2]));
|
|
228
|
+
const digest = hmac.digest();
|
|
229
|
+
const result = new Uint8Array(32);
|
|
230
|
+
result.set(new Uint8Array(digest));
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Ratchet a chain key forward N steps (for catching up on missed messages)
|
|
235
|
+
*/
|
|
236
|
+
ratchetChainKeyN(chainKey, steps) {
|
|
237
|
+
const messageKeys = [];
|
|
238
|
+
let current = chainKey;
|
|
239
|
+
for (let i = 0; i < steps; i++) {
|
|
240
|
+
messageKeys.push(this.deriveMessageKey(current));
|
|
241
|
+
current = this.ratchetChainKey(current);
|
|
242
|
+
}
|
|
243
|
+
return { chainKey: current, messageKeys };
|
|
173
244
|
}
|
|
174
245
|
// ============================================
|
|
175
246
|
// Conversations
|
|
@@ -228,10 +299,15 @@ var MoltDMClient = class {
|
|
|
228
299
|
await this.fetch(`/api/conversations/${conversationId}/members/${memberId}`, {
|
|
229
300
|
method: "DELETE"
|
|
230
301
|
});
|
|
302
|
+
if (memberId !== this.moltbotId) {
|
|
303
|
+
await this.rotateSenderKey(conversationId);
|
|
304
|
+
}
|
|
231
305
|
}
|
|
232
306
|
async leaveConversation(conversationId) {
|
|
233
307
|
this.ensureInitialized();
|
|
234
308
|
await this.removeMember(conversationId, this.moltbotId);
|
|
309
|
+
this.senderKeys.delete(conversationId);
|
|
310
|
+
await this.saveSenderKeys();
|
|
235
311
|
}
|
|
236
312
|
async promoteAdmin(conversationId, memberId) {
|
|
237
313
|
this.ensureInitialized();
|
|
@@ -255,30 +331,203 @@ var MoltDMClient = class {
|
|
|
255
331
|
// ============================================
|
|
256
332
|
async send(conversationId, content, options) {
|
|
257
333
|
this.ensureInitialized();
|
|
258
|
-
let
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
334
|
+
let senderKeyState = this.senderKeys.get(conversationId);
|
|
335
|
+
const isNewKey = !senderKeyState;
|
|
336
|
+
if (!senderKeyState) {
|
|
337
|
+
const initialKey = crypto.getRandomValues(new Uint8Array(32));
|
|
338
|
+
senderKeyState = {
|
|
339
|
+
chainKey: initialKey,
|
|
340
|
+
initialChainKey: new Uint8Array(initialKey),
|
|
341
|
+
// Copy for distribution
|
|
262
342
|
version: 1,
|
|
263
|
-
|
|
343
|
+
messageIndex: 0
|
|
264
344
|
};
|
|
265
|
-
this.senderKeys.set(conversationId,
|
|
266
|
-
await this.saveSenderKeys();
|
|
345
|
+
this.senderKeys.set(conversationId, senderKeyState);
|
|
267
346
|
}
|
|
268
|
-
const
|
|
347
|
+
const messageKey = this.deriveMessageKey(senderKeyState.chainKey);
|
|
348
|
+
const currentIndex = senderKeyState.messageIndex;
|
|
349
|
+
senderKeyState.chainKey = this.ratchetChainKey(senderKeyState.chainKey);
|
|
350
|
+
senderKeyState.messageIndex++;
|
|
351
|
+
const ciphertext = await this.encrypt(content, messageKey);
|
|
352
|
+
const encryptedSenderKeys = await this.encryptChainKeyForRecipients(
|
|
353
|
+
conversationId,
|
|
354
|
+
senderKeyState.initialChainKey
|
|
355
|
+
// Send the original, unratcheted key
|
|
356
|
+
);
|
|
269
357
|
const response = await this.fetch(`/api/conversations/${conversationId}/messages`, {
|
|
270
358
|
method: "POST",
|
|
271
359
|
body: JSON.stringify({
|
|
272
360
|
ciphertext,
|
|
273
|
-
senderKeyVersion:
|
|
274
|
-
messageIndex:
|
|
275
|
-
replyTo: options?.replyTo
|
|
361
|
+
senderKeyVersion: senderKeyState.version,
|
|
362
|
+
messageIndex: currentIndex,
|
|
363
|
+
replyTo: options?.replyTo,
|
|
364
|
+
encryptedSenderKeys
|
|
276
365
|
})
|
|
277
366
|
});
|
|
278
367
|
await this.saveSenderKeys();
|
|
279
368
|
const data = await response.json();
|
|
280
369
|
return { messageId: data.message.id };
|
|
281
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Rotate sender key for a conversation (call when membership changes)
|
|
373
|
+
*/
|
|
374
|
+
async rotateSenderKey(conversationId) {
|
|
375
|
+
this.ensureInitialized();
|
|
376
|
+
const existingKey = this.senderKeys.get(conversationId);
|
|
377
|
+
const newVersion = existingKey ? existingKey.version + 1 : 1;
|
|
378
|
+
const initialKey = crypto.getRandomValues(new Uint8Array(32));
|
|
379
|
+
this.senderKeys.set(conversationId, {
|
|
380
|
+
chainKey: initialKey,
|
|
381
|
+
initialChainKey: new Uint8Array(initialKey),
|
|
382
|
+
version: newVersion,
|
|
383
|
+
messageIndex: 0
|
|
384
|
+
});
|
|
385
|
+
await this.saveSenderKeys();
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Encrypt chain key for each conversation member using X25519 ECDH
|
|
389
|
+
*/
|
|
390
|
+
async encryptChainKeyForRecipients(conversationId, chainKey) {
|
|
391
|
+
const encryptedKeys = {};
|
|
392
|
+
try {
|
|
393
|
+
const conversation = await this.getConversation(conversationId);
|
|
394
|
+
for (const memberId of conversation.members) {
|
|
395
|
+
try {
|
|
396
|
+
const response = await fetch(`${this.relayUrl}/api/identity/${memberId}`);
|
|
397
|
+
if (!response.ok) continue;
|
|
398
|
+
const data = await response.json();
|
|
399
|
+
const recipientPreKey = fromBase64(data.identity.signedPreKey);
|
|
400
|
+
const ephemeralPrivate = import_ed25519.x25519.utils.randomPrivateKey();
|
|
401
|
+
const ephemeralPublic = import_ed25519.x25519.getPublicKey(ephemeralPrivate);
|
|
402
|
+
const sharedSecret = import_ed25519.x25519.getSharedSecret(ephemeralPrivate, recipientPreKey);
|
|
403
|
+
const sharedSecretCopy = new Uint8Array(32);
|
|
404
|
+
sharedSecretCopy.set(new Uint8Array(sharedSecret));
|
|
405
|
+
const chainKeyCopy = new Uint8Array(32);
|
|
406
|
+
chainKeyCopy.set(new Uint8Array(chainKey));
|
|
407
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
408
|
+
"raw",
|
|
409
|
+
sharedSecretCopy.buffer,
|
|
410
|
+
{ name: "HKDF" },
|
|
411
|
+
false,
|
|
412
|
+
["deriveKey"]
|
|
413
|
+
);
|
|
414
|
+
const aesKey = await crypto.subtle.deriveKey(
|
|
415
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: new TextEncoder().encode("moltdm-sender-key") },
|
|
416
|
+
keyMaterial,
|
|
417
|
+
{ name: "AES-GCM", length: 256 },
|
|
418
|
+
false,
|
|
419
|
+
["encrypt"]
|
|
420
|
+
);
|
|
421
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
422
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
423
|
+
{ name: "AES-GCM", iv },
|
|
424
|
+
aesKey,
|
|
425
|
+
chainKeyCopy.buffer
|
|
426
|
+
);
|
|
427
|
+
const combined = new Uint8Array(32 + 12 + encrypted.byteLength);
|
|
428
|
+
combined.set(ephemeralPublic);
|
|
429
|
+
combined.set(iv, 32);
|
|
430
|
+
combined.set(new Uint8Array(encrypted), 44);
|
|
431
|
+
encryptedKeys[memberId] = toBase64(combined);
|
|
432
|
+
} catch (e) {
|
|
433
|
+
console.error(`Failed to encrypt chain key for ${memberId}:`, e);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch (e) {
|
|
437
|
+
console.error("Failed to encrypt chain keys:", e);
|
|
438
|
+
}
|
|
439
|
+
return encryptedKeys;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Decrypt a received chain key using our X25519 private key
|
|
443
|
+
*/
|
|
444
|
+
async decryptChainKey(encryptedBlob) {
|
|
445
|
+
try {
|
|
446
|
+
const combined = fromBase64(encryptedBlob);
|
|
447
|
+
const ephemeralPublic = combined.slice(0, 32);
|
|
448
|
+
const iv = combined.slice(32, 44);
|
|
449
|
+
const encrypted = combined.slice(44);
|
|
450
|
+
const ourPrivateKey = fromBase64(this.identity.signedPreKey.privateKey);
|
|
451
|
+
const sharedSecret = import_ed25519.x25519.getSharedSecret(ourPrivateKey, ephemeralPublic);
|
|
452
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
453
|
+
"raw",
|
|
454
|
+
new Uint8Array(sharedSecret).buffer,
|
|
455
|
+
{ name: "HKDF" },
|
|
456
|
+
false,
|
|
457
|
+
["deriveKey"]
|
|
458
|
+
);
|
|
459
|
+
const aesKey = await crypto.subtle.deriveKey(
|
|
460
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: new TextEncoder().encode("moltdm-sender-key") },
|
|
461
|
+
keyMaterial,
|
|
462
|
+
{ name: "AES-GCM", length: 256 },
|
|
463
|
+
false,
|
|
464
|
+
["decrypt"]
|
|
465
|
+
);
|
|
466
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
467
|
+
{ name: "AES-GCM", iv },
|
|
468
|
+
aesKey,
|
|
469
|
+
encrypted
|
|
470
|
+
);
|
|
471
|
+
return new Uint8Array(decrypted);
|
|
472
|
+
} catch (e) {
|
|
473
|
+
console.error("Failed to decrypt chain key:", e);
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Decrypt a message using Sender Keys protocol
|
|
479
|
+
*/
|
|
480
|
+
async decryptMessage(message) {
|
|
481
|
+
this.ensureInitialized();
|
|
482
|
+
const { conversationId, fromId, ciphertext, senderKeyVersion, messageIndex, encryptedSenderKeys } = message;
|
|
483
|
+
const keyId = `${conversationId}:${fromId}`;
|
|
484
|
+
let receivedKey = this.receivedSenderKeys.get(keyId);
|
|
485
|
+
if (!encryptedSenderKeys) {
|
|
486
|
+
console.error(`[decrypt] Message ${message.id} has no encryptedSenderKeys - was sent before Sender Keys implementation`);
|
|
487
|
+
} else if (!encryptedSenderKeys[this.moltbotId]) {
|
|
488
|
+
console.error(`[decrypt] Message ${message.id} has encryptedSenderKeys but not for us (${this.moltbotId}). Available: ${Object.keys(encryptedSenderKeys).join(", ")}`);
|
|
489
|
+
}
|
|
490
|
+
if (encryptedSenderKeys && encryptedSenderKeys[this.moltbotId]) {
|
|
491
|
+
if (!receivedKey || receivedKey.version !== senderKeyVersion) {
|
|
492
|
+
const chainKey = await this.decryptChainKey(encryptedSenderKeys[this.moltbotId]);
|
|
493
|
+
if (chainKey) {
|
|
494
|
+
receivedKey = {
|
|
495
|
+
chainKey,
|
|
496
|
+
version: senderKeyVersion,
|
|
497
|
+
messageIndex: 0
|
|
498
|
+
};
|
|
499
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
500
|
+
await this.saveSenderKeys();
|
|
501
|
+
} else {
|
|
502
|
+
console.error(`[decrypt] Failed to decrypt chain key for ${keyId}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!receivedKey) {
|
|
507
|
+
console.error(`[decrypt] No sender key for ${keyId}`);
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
if (messageIndex > receivedKey.messageIndex) {
|
|
511
|
+
const steps = messageIndex - receivedKey.messageIndex + 1;
|
|
512
|
+
const { chainKey, messageKeys } = this.ratchetChainKeyN(receivedKey.chainKey, steps);
|
|
513
|
+
const messageKey = messageKeys[messageKeys.length - 1];
|
|
514
|
+
receivedKey.chainKey = chainKey;
|
|
515
|
+
receivedKey.messageIndex = messageIndex + 1;
|
|
516
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
517
|
+
await this.saveSenderKeys();
|
|
518
|
+
return this.decrypt(ciphertext, messageKey);
|
|
519
|
+
} else if (messageIndex === receivedKey.messageIndex) {
|
|
520
|
+
const messageKey = this.deriveMessageKey(receivedKey.chainKey);
|
|
521
|
+
receivedKey.chainKey = this.ratchetChainKey(receivedKey.chainKey);
|
|
522
|
+
receivedKey.messageIndex++;
|
|
523
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
524
|
+
await this.saveSenderKeys();
|
|
525
|
+
return this.decrypt(ciphertext, messageKey);
|
|
526
|
+
} else {
|
|
527
|
+
console.error(`Message index ${messageIndex} is in the past (current: ${receivedKey.messageIndex})`);
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
282
531
|
async getMessages(conversationId, options) {
|
|
283
532
|
this.ensureInitialized();
|
|
284
533
|
const params = new URLSearchParams();
|
|
@@ -346,7 +595,8 @@ var MoltDMClient = class {
|
|
|
346
595
|
method: "POST",
|
|
347
596
|
body: JSON.stringify({ expiresIn: options?.expiresIn })
|
|
348
597
|
});
|
|
349
|
-
|
|
598
|
+
const data = await response.json();
|
|
599
|
+
return { token: data.invite.token, url: data.url };
|
|
350
600
|
}
|
|
351
601
|
async listInvites(conversationId) {
|
|
352
602
|
this.ensureInitialized();
|
|
@@ -439,12 +689,14 @@ var MoltDMClient = class {
|
|
|
439
689
|
this.ensureInitialized();
|
|
440
690
|
const senderKeysObj = {};
|
|
441
691
|
for (const [convId, keyData] of this.senderKeys) {
|
|
442
|
-
senderKeysObj[convId] = toBase64(keyData.
|
|
692
|
+
senderKeysObj[convId] = toBase64(keyData.initialChainKey);
|
|
443
693
|
}
|
|
444
694
|
const encryptionKeys = {
|
|
445
695
|
identityKey: this.identity.publicKey,
|
|
446
696
|
privateKey: this.identity.privateKey,
|
|
447
|
-
//
|
|
697
|
+
// Ed25519 private key for signing
|
|
698
|
+
signedPreKeyPrivate: this.identity.signedPreKey.privateKey,
|
|
699
|
+
// X25519 private key for decrypting sender keys
|
|
448
700
|
senderKeys: senderKeysObj
|
|
449
701
|
};
|
|
450
702
|
const response = await this.fetch("/api/pair/approve", {
|
|
@@ -490,7 +742,8 @@ var MoltDMClient = class {
|
|
|
490
742
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
491
743
|
const encoder = new TextEncoder();
|
|
492
744
|
const data = encoder.encode(plaintext);
|
|
493
|
-
const
|
|
745
|
+
const keyBuffer = new Uint8Array(key).buffer;
|
|
746
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, [
|
|
494
747
|
"encrypt"
|
|
495
748
|
]);
|
|
496
749
|
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, cryptoKey, data);
|
|
@@ -503,7 +756,8 @@ var MoltDMClient = class {
|
|
|
503
756
|
const combined = fromBase64(ciphertext);
|
|
504
757
|
const iv = combined.slice(0, 12);
|
|
505
758
|
const encrypted = combined.slice(12);
|
|
506
|
-
const
|
|
759
|
+
const keyBuffer = new Uint8Array(key).buffer;
|
|
760
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, [
|
|
507
761
|
"decrypt"
|
|
508
762
|
]);
|
|
509
763
|
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, cryptoKey, encrypted);
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { x25519 } from "@noble/curves/ed25519";
|
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import * as os from "os";
|
|
7
|
+
import { createHmac } from "crypto";
|
|
7
8
|
function toBase64(bytes) {
|
|
8
9
|
return Buffer.from(bytes).toString("base64");
|
|
9
10
|
}
|
|
@@ -14,7 +15,10 @@ var MoltDMClient = class {
|
|
|
14
15
|
storagePath;
|
|
15
16
|
relayUrl;
|
|
16
17
|
identity = null;
|
|
18
|
+
// Our sender keys (for messages we send)
|
|
17
19
|
senderKeys = /* @__PURE__ */ new Map();
|
|
20
|
+
// Received sender keys (for messages from others) - keyed by `${convId}:${fromId}`
|
|
21
|
+
receivedSenderKeys = /* @__PURE__ */ new Map();
|
|
18
22
|
constructor(options = {}) {
|
|
19
23
|
const defaultStoragePath = process.env.OPENCLAW_STATE_DIR ? path.join(process.env.OPENCLAW_STATE_DIR, ".moltdm") : path.join(os.homedir(), ".moltdm");
|
|
20
24
|
this.storagePath = options.storagePath || defaultStoragePath;
|
|
@@ -116,10 +120,25 @@ var MoltDMClient = class {
|
|
|
116
120
|
const keys = JSON.parse(data);
|
|
117
121
|
for (const [convId, keyData] of Object.entries(keys)) {
|
|
118
122
|
const k = keyData;
|
|
123
|
+
const chainKey = fromBase64(k.chainKey || k.key || "");
|
|
119
124
|
this.senderKeys.set(convId, {
|
|
120
|
-
|
|
125
|
+
chainKey,
|
|
126
|
+
initialChainKey: k.initialChainKey ? fromBase64(k.initialChainKey) : chainKey,
|
|
121
127
|
version: k.version,
|
|
122
|
-
|
|
128
|
+
messageIndex: k.messageIndex ?? k.index ?? 0
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const receivedPath = path.join(this.storagePath, "received_sender_keys.json");
|
|
133
|
+
if (fs.existsSync(receivedPath)) {
|
|
134
|
+
const data = fs.readFileSync(receivedPath, "utf-8");
|
|
135
|
+
const keys = JSON.parse(data);
|
|
136
|
+
for (const [key, keyData] of Object.entries(keys)) {
|
|
137
|
+
const k = keyData;
|
|
138
|
+
this.receivedSenderKeys.set(key, {
|
|
139
|
+
chainKey: fromBase64(k.chainKey),
|
|
140
|
+
version: k.version,
|
|
141
|
+
messageIndex: k.messageIndex
|
|
123
142
|
});
|
|
124
143
|
}
|
|
125
144
|
}
|
|
@@ -129,12 +148,64 @@ var MoltDMClient = class {
|
|
|
129
148
|
const obj = {};
|
|
130
149
|
for (const [convId, keyData] of this.senderKeys) {
|
|
131
150
|
obj[convId] = {
|
|
132
|
-
|
|
151
|
+
chainKey: toBase64(keyData.chainKey),
|
|
152
|
+
initialChainKey: toBase64(keyData.initialChainKey),
|
|
133
153
|
version: keyData.version,
|
|
134
|
-
|
|
154
|
+
messageIndex: keyData.messageIndex
|
|
135
155
|
};
|
|
136
156
|
}
|
|
137
157
|
fs.writeFileSync(keysPath, JSON.stringify(obj, null, 2));
|
|
158
|
+
const receivedPath = path.join(this.storagePath, "received_sender_keys.json");
|
|
159
|
+
const receivedObj = {};
|
|
160
|
+
for (const [key, keyData] of this.receivedSenderKeys) {
|
|
161
|
+
receivedObj[key] = {
|
|
162
|
+
chainKey: toBase64(keyData.chainKey),
|
|
163
|
+
version: keyData.version,
|
|
164
|
+
messageIndex: keyData.messageIndex
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
fs.writeFileSync(receivedPath, JSON.stringify(receivedObj, null, 2));
|
|
168
|
+
}
|
|
169
|
+
// ============================================
|
|
170
|
+
// Sender Keys Protocol (Signal-style)
|
|
171
|
+
// ============================================
|
|
172
|
+
/**
|
|
173
|
+
* Derive message key from chain key using HMAC
|
|
174
|
+
* message_key = HMAC-SHA256(chain_key, 0x01)
|
|
175
|
+
*/
|
|
176
|
+
deriveMessageKey(chainKey) {
|
|
177
|
+
const keyBuffer = Buffer.from(chainKey);
|
|
178
|
+
const hmac = createHmac("sha256", keyBuffer);
|
|
179
|
+
hmac.update(Buffer.from([1]));
|
|
180
|
+
const digest = hmac.digest();
|
|
181
|
+
const result = new Uint8Array(32);
|
|
182
|
+
result.set(new Uint8Array(digest));
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Ratchet chain key forward
|
|
187
|
+
* new_chain_key = HMAC-SHA256(chain_key, 0x02)
|
|
188
|
+
*/
|
|
189
|
+
ratchetChainKey(chainKey) {
|
|
190
|
+
const keyBuffer = Buffer.from(chainKey);
|
|
191
|
+
const hmac = createHmac("sha256", keyBuffer);
|
|
192
|
+
hmac.update(Buffer.from([2]));
|
|
193
|
+
const digest = hmac.digest();
|
|
194
|
+
const result = new Uint8Array(32);
|
|
195
|
+
result.set(new Uint8Array(digest));
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Ratchet a chain key forward N steps (for catching up on missed messages)
|
|
200
|
+
*/
|
|
201
|
+
ratchetChainKeyN(chainKey, steps) {
|
|
202
|
+
const messageKeys = [];
|
|
203
|
+
let current = chainKey;
|
|
204
|
+
for (let i = 0; i < steps; i++) {
|
|
205
|
+
messageKeys.push(this.deriveMessageKey(current));
|
|
206
|
+
current = this.ratchetChainKey(current);
|
|
207
|
+
}
|
|
208
|
+
return { chainKey: current, messageKeys };
|
|
138
209
|
}
|
|
139
210
|
// ============================================
|
|
140
211
|
// Conversations
|
|
@@ -193,10 +264,15 @@ var MoltDMClient = class {
|
|
|
193
264
|
await this.fetch(`/api/conversations/${conversationId}/members/${memberId}`, {
|
|
194
265
|
method: "DELETE"
|
|
195
266
|
});
|
|
267
|
+
if (memberId !== this.moltbotId) {
|
|
268
|
+
await this.rotateSenderKey(conversationId);
|
|
269
|
+
}
|
|
196
270
|
}
|
|
197
271
|
async leaveConversation(conversationId) {
|
|
198
272
|
this.ensureInitialized();
|
|
199
273
|
await this.removeMember(conversationId, this.moltbotId);
|
|
274
|
+
this.senderKeys.delete(conversationId);
|
|
275
|
+
await this.saveSenderKeys();
|
|
200
276
|
}
|
|
201
277
|
async promoteAdmin(conversationId, memberId) {
|
|
202
278
|
this.ensureInitialized();
|
|
@@ -220,30 +296,203 @@ var MoltDMClient = class {
|
|
|
220
296
|
// ============================================
|
|
221
297
|
async send(conversationId, content, options) {
|
|
222
298
|
this.ensureInitialized();
|
|
223
|
-
let
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
299
|
+
let senderKeyState = this.senderKeys.get(conversationId);
|
|
300
|
+
const isNewKey = !senderKeyState;
|
|
301
|
+
if (!senderKeyState) {
|
|
302
|
+
const initialKey = crypto.getRandomValues(new Uint8Array(32));
|
|
303
|
+
senderKeyState = {
|
|
304
|
+
chainKey: initialKey,
|
|
305
|
+
initialChainKey: new Uint8Array(initialKey),
|
|
306
|
+
// Copy for distribution
|
|
227
307
|
version: 1,
|
|
228
|
-
|
|
308
|
+
messageIndex: 0
|
|
229
309
|
};
|
|
230
|
-
this.senderKeys.set(conversationId,
|
|
231
|
-
await this.saveSenderKeys();
|
|
310
|
+
this.senderKeys.set(conversationId, senderKeyState);
|
|
232
311
|
}
|
|
233
|
-
const
|
|
312
|
+
const messageKey = this.deriveMessageKey(senderKeyState.chainKey);
|
|
313
|
+
const currentIndex = senderKeyState.messageIndex;
|
|
314
|
+
senderKeyState.chainKey = this.ratchetChainKey(senderKeyState.chainKey);
|
|
315
|
+
senderKeyState.messageIndex++;
|
|
316
|
+
const ciphertext = await this.encrypt(content, messageKey);
|
|
317
|
+
const encryptedSenderKeys = await this.encryptChainKeyForRecipients(
|
|
318
|
+
conversationId,
|
|
319
|
+
senderKeyState.initialChainKey
|
|
320
|
+
// Send the original, unratcheted key
|
|
321
|
+
);
|
|
234
322
|
const response = await this.fetch(`/api/conversations/${conversationId}/messages`, {
|
|
235
323
|
method: "POST",
|
|
236
324
|
body: JSON.stringify({
|
|
237
325
|
ciphertext,
|
|
238
|
-
senderKeyVersion:
|
|
239
|
-
messageIndex:
|
|
240
|
-
replyTo: options?.replyTo
|
|
326
|
+
senderKeyVersion: senderKeyState.version,
|
|
327
|
+
messageIndex: currentIndex,
|
|
328
|
+
replyTo: options?.replyTo,
|
|
329
|
+
encryptedSenderKeys
|
|
241
330
|
})
|
|
242
331
|
});
|
|
243
332
|
await this.saveSenderKeys();
|
|
244
333
|
const data = await response.json();
|
|
245
334
|
return { messageId: data.message.id };
|
|
246
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Rotate sender key for a conversation (call when membership changes)
|
|
338
|
+
*/
|
|
339
|
+
async rotateSenderKey(conversationId) {
|
|
340
|
+
this.ensureInitialized();
|
|
341
|
+
const existingKey = this.senderKeys.get(conversationId);
|
|
342
|
+
const newVersion = existingKey ? existingKey.version + 1 : 1;
|
|
343
|
+
const initialKey = crypto.getRandomValues(new Uint8Array(32));
|
|
344
|
+
this.senderKeys.set(conversationId, {
|
|
345
|
+
chainKey: initialKey,
|
|
346
|
+
initialChainKey: new Uint8Array(initialKey),
|
|
347
|
+
version: newVersion,
|
|
348
|
+
messageIndex: 0
|
|
349
|
+
});
|
|
350
|
+
await this.saveSenderKeys();
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Encrypt chain key for each conversation member using X25519 ECDH
|
|
354
|
+
*/
|
|
355
|
+
async encryptChainKeyForRecipients(conversationId, chainKey) {
|
|
356
|
+
const encryptedKeys = {};
|
|
357
|
+
try {
|
|
358
|
+
const conversation = await this.getConversation(conversationId);
|
|
359
|
+
for (const memberId of conversation.members) {
|
|
360
|
+
try {
|
|
361
|
+
const response = await fetch(`${this.relayUrl}/api/identity/${memberId}`);
|
|
362
|
+
if (!response.ok) continue;
|
|
363
|
+
const data = await response.json();
|
|
364
|
+
const recipientPreKey = fromBase64(data.identity.signedPreKey);
|
|
365
|
+
const ephemeralPrivate = x25519.utils.randomPrivateKey();
|
|
366
|
+
const ephemeralPublic = x25519.getPublicKey(ephemeralPrivate);
|
|
367
|
+
const sharedSecret = x25519.getSharedSecret(ephemeralPrivate, recipientPreKey);
|
|
368
|
+
const sharedSecretCopy = new Uint8Array(32);
|
|
369
|
+
sharedSecretCopy.set(new Uint8Array(sharedSecret));
|
|
370
|
+
const chainKeyCopy = new Uint8Array(32);
|
|
371
|
+
chainKeyCopy.set(new Uint8Array(chainKey));
|
|
372
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
373
|
+
"raw",
|
|
374
|
+
sharedSecretCopy.buffer,
|
|
375
|
+
{ name: "HKDF" },
|
|
376
|
+
false,
|
|
377
|
+
["deriveKey"]
|
|
378
|
+
);
|
|
379
|
+
const aesKey = await crypto.subtle.deriveKey(
|
|
380
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: new TextEncoder().encode("moltdm-sender-key") },
|
|
381
|
+
keyMaterial,
|
|
382
|
+
{ name: "AES-GCM", length: 256 },
|
|
383
|
+
false,
|
|
384
|
+
["encrypt"]
|
|
385
|
+
);
|
|
386
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
387
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
388
|
+
{ name: "AES-GCM", iv },
|
|
389
|
+
aesKey,
|
|
390
|
+
chainKeyCopy.buffer
|
|
391
|
+
);
|
|
392
|
+
const combined = new Uint8Array(32 + 12 + encrypted.byteLength);
|
|
393
|
+
combined.set(ephemeralPublic);
|
|
394
|
+
combined.set(iv, 32);
|
|
395
|
+
combined.set(new Uint8Array(encrypted), 44);
|
|
396
|
+
encryptedKeys[memberId] = toBase64(combined);
|
|
397
|
+
} catch (e) {
|
|
398
|
+
console.error(`Failed to encrypt chain key for ${memberId}:`, e);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (e) {
|
|
402
|
+
console.error("Failed to encrypt chain keys:", e);
|
|
403
|
+
}
|
|
404
|
+
return encryptedKeys;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Decrypt a received chain key using our X25519 private key
|
|
408
|
+
*/
|
|
409
|
+
async decryptChainKey(encryptedBlob) {
|
|
410
|
+
try {
|
|
411
|
+
const combined = fromBase64(encryptedBlob);
|
|
412
|
+
const ephemeralPublic = combined.slice(0, 32);
|
|
413
|
+
const iv = combined.slice(32, 44);
|
|
414
|
+
const encrypted = combined.slice(44);
|
|
415
|
+
const ourPrivateKey = fromBase64(this.identity.signedPreKey.privateKey);
|
|
416
|
+
const sharedSecret = x25519.getSharedSecret(ourPrivateKey, ephemeralPublic);
|
|
417
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
418
|
+
"raw",
|
|
419
|
+
new Uint8Array(sharedSecret).buffer,
|
|
420
|
+
{ name: "HKDF" },
|
|
421
|
+
false,
|
|
422
|
+
["deriveKey"]
|
|
423
|
+
);
|
|
424
|
+
const aesKey = await crypto.subtle.deriveKey(
|
|
425
|
+
{ name: "HKDF", hash: "SHA-256", salt: new Uint8Array(32), info: new TextEncoder().encode("moltdm-sender-key") },
|
|
426
|
+
keyMaterial,
|
|
427
|
+
{ name: "AES-GCM", length: 256 },
|
|
428
|
+
false,
|
|
429
|
+
["decrypt"]
|
|
430
|
+
);
|
|
431
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
432
|
+
{ name: "AES-GCM", iv },
|
|
433
|
+
aesKey,
|
|
434
|
+
encrypted
|
|
435
|
+
);
|
|
436
|
+
return new Uint8Array(decrypted);
|
|
437
|
+
} catch (e) {
|
|
438
|
+
console.error("Failed to decrypt chain key:", e);
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Decrypt a message using Sender Keys protocol
|
|
444
|
+
*/
|
|
445
|
+
async decryptMessage(message) {
|
|
446
|
+
this.ensureInitialized();
|
|
447
|
+
const { conversationId, fromId, ciphertext, senderKeyVersion, messageIndex, encryptedSenderKeys } = message;
|
|
448
|
+
const keyId = `${conversationId}:${fromId}`;
|
|
449
|
+
let receivedKey = this.receivedSenderKeys.get(keyId);
|
|
450
|
+
if (!encryptedSenderKeys) {
|
|
451
|
+
console.error(`[decrypt] Message ${message.id} has no encryptedSenderKeys - was sent before Sender Keys implementation`);
|
|
452
|
+
} else if (!encryptedSenderKeys[this.moltbotId]) {
|
|
453
|
+
console.error(`[decrypt] Message ${message.id} has encryptedSenderKeys but not for us (${this.moltbotId}). Available: ${Object.keys(encryptedSenderKeys).join(", ")}`);
|
|
454
|
+
}
|
|
455
|
+
if (encryptedSenderKeys && encryptedSenderKeys[this.moltbotId]) {
|
|
456
|
+
if (!receivedKey || receivedKey.version !== senderKeyVersion) {
|
|
457
|
+
const chainKey = await this.decryptChainKey(encryptedSenderKeys[this.moltbotId]);
|
|
458
|
+
if (chainKey) {
|
|
459
|
+
receivedKey = {
|
|
460
|
+
chainKey,
|
|
461
|
+
version: senderKeyVersion,
|
|
462
|
+
messageIndex: 0
|
|
463
|
+
};
|
|
464
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
465
|
+
await this.saveSenderKeys();
|
|
466
|
+
} else {
|
|
467
|
+
console.error(`[decrypt] Failed to decrypt chain key for ${keyId}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (!receivedKey) {
|
|
472
|
+
console.error(`[decrypt] No sender key for ${keyId}`);
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
if (messageIndex > receivedKey.messageIndex) {
|
|
476
|
+
const steps = messageIndex - receivedKey.messageIndex + 1;
|
|
477
|
+
const { chainKey, messageKeys } = this.ratchetChainKeyN(receivedKey.chainKey, steps);
|
|
478
|
+
const messageKey = messageKeys[messageKeys.length - 1];
|
|
479
|
+
receivedKey.chainKey = chainKey;
|
|
480
|
+
receivedKey.messageIndex = messageIndex + 1;
|
|
481
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
482
|
+
await this.saveSenderKeys();
|
|
483
|
+
return this.decrypt(ciphertext, messageKey);
|
|
484
|
+
} else if (messageIndex === receivedKey.messageIndex) {
|
|
485
|
+
const messageKey = this.deriveMessageKey(receivedKey.chainKey);
|
|
486
|
+
receivedKey.chainKey = this.ratchetChainKey(receivedKey.chainKey);
|
|
487
|
+
receivedKey.messageIndex++;
|
|
488
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
489
|
+
await this.saveSenderKeys();
|
|
490
|
+
return this.decrypt(ciphertext, messageKey);
|
|
491
|
+
} else {
|
|
492
|
+
console.error(`Message index ${messageIndex} is in the past (current: ${receivedKey.messageIndex})`);
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
247
496
|
async getMessages(conversationId, options) {
|
|
248
497
|
this.ensureInitialized();
|
|
249
498
|
const params = new URLSearchParams();
|
|
@@ -311,7 +560,8 @@ var MoltDMClient = class {
|
|
|
311
560
|
method: "POST",
|
|
312
561
|
body: JSON.stringify({ expiresIn: options?.expiresIn })
|
|
313
562
|
});
|
|
314
|
-
|
|
563
|
+
const data = await response.json();
|
|
564
|
+
return { token: data.invite.token, url: data.url };
|
|
315
565
|
}
|
|
316
566
|
async listInvites(conversationId) {
|
|
317
567
|
this.ensureInitialized();
|
|
@@ -404,12 +654,14 @@ var MoltDMClient = class {
|
|
|
404
654
|
this.ensureInitialized();
|
|
405
655
|
const senderKeysObj = {};
|
|
406
656
|
for (const [convId, keyData] of this.senderKeys) {
|
|
407
|
-
senderKeysObj[convId] = toBase64(keyData.
|
|
657
|
+
senderKeysObj[convId] = toBase64(keyData.initialChainKey);
|
|
408
658
|
}
|
|
409
659
|
const encryptionKeys = {
|
|
410
660
|
identityKey: this.identity.publicKey,
|
|
411
661
|
privateKey: this.identity.privateKey,
|
|
412
|
-
//
|
|
662
|
+
// Ed25519 private key for signing
|
|
663
|
+
signedPreKeyPrivate: this.identity.signedPreKey.privateKey,
|
|
664
|
+
// X25519 private key for decrypting sender keys
|
|
413
665
|
senderKeys: senderKeysObj
|
|
414
666
|
};
|
|
415
667
|
const response = await this.fetch("/api/pair/approve", {
|
|
@@ -455,7 +707,8 @@ var MoltDMClient = class {
|
|
|
455
707
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
456
708
|
const encoder = new TextEncoder();
|
|
457
709
|
const data = encoder.encode(plaintext);
|
|
458
|
-
const
|
|
710
|
+
const keyBuffer = new Uint8Array(key).buffer;
|
|
711
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, [
|
|
459
712
|
"encrypt"
|
|
460
713
|
]);
|
|
461
714
|
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, cryptoKey, data);
|
|
@@ -468,7 +721,8 @@ var MoltDMClient = class {
|
|
|
468
721
|
const combined = fromBase64(ciphertext);
|
|
469
722
|
const iv = combined.slice(0, 12);
|
|
470
723
|
const encrypted = combined.slice(12);
|
|
471
|
-
const
|
|
724
|
+
const keyBuffer = new Uint8Array(key).buffer;
|
|
725
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, [
|
|
472
726
|
"decrypt"
|
|
473
727
|
]);
|
|
474
728
|
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, cryptoKey, encrypted);
|