@moltdm/client 1.3.0 → 1.3.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.
- package/dist/index.d.mts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +267 -20
- package/dist/index.mjs +267 -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,196 @@ 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 && encryptedSenderKeys[this.moltbotId]) {
|
|
486
|
+
if (!receivedKey || receivedKey.version !== senderKeyVersion) {
|
|
487
|
+
const chainKey = await this.decryptChainKey(encryptedSenderKeys[this.moltbotId]);
|
|
488
|
+
if (chainKey) {
|
|
489
|
+
receivedKey = {
|
|
490
|
+
chainKey,
|
|
491
|
+
version: senderKeyVersion,
|
|
492
|
+
messageIndex: 0
|
|
493
|
+
};
|
|
494
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
495
|
+
await this.saveSenderKeys();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (!receivedKey) {
|
|
500
|
+
console.error(`No sender key for ${keyId}`);
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
if (messageIndex > receivedKey.messageIndex) {
|
|
504
|
+
const steps = messageIndex - receivedKey.messageIndex + 1;
|
|
505
|
+
const { chainKey, messageKeys } = this.ratchetChainKeyN(receivedKey.chainKey, steps);
|
|
506
|
+
const messageKey = messageKeys[messageKeys.length - 1];
|
|
507
|
+
receivedKey.chainKey = chainKey;
|
|
508
|
+
receivedKey.messageIndex = messageIndex + 1;
|
|
509
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
510
|
+
await this.saveSenderKeys();
|
|
511
|
+
return this.decrypt(ciphertext, messageKey);
|
|
512
|
+
} else if (messageIndex === receivedKey.messageIndex) {
|
|
513
|
+
const messageKey = this.deriveMessageKey(receivedKey.chainKey);
|
|
514
|
+
receivedKey.chainKey = this.ratchetChainKey(receivedKey.chainKey);
|
|
515
|
+
receivedKey.messageIndex++;
|
|
516
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
517
|
+
await this.saveSenderKeys();
|
|
518
|
+
return this.decrypt(ciphertext, messageKey);
|
|
519
|
+
} else {
|
|
520
|
+
console.error(`Message index ${messageIndex} is in the past (current: ${receivedKey.messageIndex})`);
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
282
524
|
async getMessages(conversationId, options) {
|
|
283
525
|
this.ensureInitialized();
|
|
284
526
|
const params = new URLSearchParams();
|
|
@@ -346,7 +588,8 @@ var MoltDMClient = class {
|
|
|
346
588
|
method: "POST",
|
|
347
589
|
body: JSON.stringify({ expiresIn: options?.expiresIn })
|
|
348
590
|
});
|
|
349
|
-
|
|
591
|
+
const data = await response.json();
|
|
592
|
+
return { token: data.invite.token, url: data.url };
|
|
350
593
|
}
|
|
351
594
|
async listInvites(conversationId) {
|
|
352
595
|
this.ensureInitialized();
|
|
@@ -439,12 +682,14 @@ var MoltDMClient = class {
|
|
|
439
682
|
this.ensureInitialized();
|
|
440
683
|
const senderKeysObj = {};
|
|
441
684
|
for (const [convId, keyData] of this.senderKeys) {
|
|
442
|
-
senderKeysObj[convId] = toBase64(keyData.
|
|
685
|
+
senderKeysObj[convId] = toBase64(keyData.initialChainKey);
|
|
443
686
|
}
|
|
444
687
|
const encryptionKeys = {
|
|
445
688
|
identityKey: this.identity.publicKey,
|
|
446
689
|
privateKey: this.identity.privateKey,
|
|
447
|
-
//
|
|
690
|
+
// Ed25519 private key for signing
|
|
691
|
+
signedPreKeyPrivate: this.identity.signedPreKey.privateKey,
|
|
692
|
+
// X25519 private key for decrypting sender keys
|
|
448
693
|
senderKeys: senderKeysObj
|
|
449
694
|
};
|
|
450
695
|
const response = await this.fetch("/api/pair/approve", {
|
|
@@ -490,7 +735,8 @@ var MoltDMClient = class {
|
|
|
490
735
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
491
736
|
const encoder = new TextEncoder();
|
|
492
737
|
const data = encoder.encode(plaintext);
|
|
493
|
-
const
|
|
738
|
+
const keyBuffer = new Uint8Array(key).buffer;
|
|
739
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, [
|
|
494
740
|
"encrypt"
|
|
495
741
|
]);
|
|
496
742
|
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, cryptoKey, data);
|
|
@@ -503,7 +749,8 @@ var MoltDMClient = class {
|
|
|
503
749
|
const combined = fromBase64(ciphertext);
|
|
504
750
|
const iv = combined.slice(0, 12);
|
|
505
751
|
const encrypted = combined.slice(12);
|
|
506
|
-
const
|
|
752
|
+
const keyBuffer = new Uint8Array(key).buffer;
|
|
753
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, [
|
|
507
754
|
"decrypt"
|
|
508
755
|
]);
|
|
509
756
|
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,196 @@ 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 && encryptedSenderKeys[this.moltbotId]) {
|
|
451
|
+
if (!receivedKey || receivedKey.version !== senderKeyVersion) {
|
|
452
|
+
const chainKey = await this.decryptChainKey(encryptedSenderKeys[this.moltbotId]);
|
|
453
|
+
if (chainKey) {
|
|
454
|
+
receivedKey = {
|
|
455
|
+
chainKey,
|
|
456
|
+
version: senderKeyVersion,
|
|
457
|
+
messageIndex: 0
|
|
458
|
+
};
|
|
459
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
460
|
+
await this.saveSenderKeys();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (!receivedKey) {
|
|
465
|
+
console.error(`No sender key for ${keyId}`);
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
if (messageIndex > receivedKey.messageIndex) {
|
|
469
|
+
const steps = messageIndex - receivedKey.messageIndex + 1;
|
|
470
|
+
const { chainKey, messageKeys } = this.ratchetChainKeyN(receivedKey.chainKey, steps);
|
|
471
|
+
const messageKey = messageKeys[messageKeys.length - 1];
|
|
472
|
+
receivedKey.chainKey = chainKey;
|
|
473
|
+
receivedKey.messageIndex = messageIndex + 1;
|
|
474
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
475
|
+
await this.saveSenderKeys();
|
|
476
|
+
return this.decrypt(ciphertext, messageKey);
|
|
477
|
+
} else if (messageIndex === receivedKey.messageIndex) {
|
|
478
|
+
const messageKey = this.deriveMessageKey(receivedKey.chainKey);
|
|
479
|
+
receivedKey.chainKey = this.ratchetChainKey(receivedKey.chainKey);
|
|
480
|
+
receivedKey.messageIndex++;
|
|
481
|
+
this.receivedSenderKeys.set(keyId, receivedKey);
|
|
482
|
+
await this.saveSenderKeys();
|
|
483
|
+
return this.decrypt(ciphertext, messageKey);
|
|
484
|
+
} else {
|
|
485
|
+
console.error(`Message index ${messageIndex} is in the past (current: ${receivedKey.messageIndex})`);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
247
489
|
async getMessages(conversationId, options) {
|
|
248
490
|
this.ensureInitialized();
|
|
249
491
|
const params = new URLSearchParams();
|
|
@@ -311,7 +553,8 @@ var MoltDMClient = class {
|
|
|
311
553
|
method: "POST",
|
|
312
554
|
body: JSON.stringify({ expiresIn: options?.expiresIn })
|
|
313
555
|
});
|
|
314
|
-
|
|
556
|
+
const data = await response.json();
|
|
557
|
+
return { token: data.invite.token, url: data.url };
|
|
315
558
|
}
|
|
316
559
|
async listInvites(conversationId) {
|
|
317
560
|
this.ensureInitialized();
|
|
@@ -404,12 +647,14 @@ var MoltDMClient = class {
|
|
|
404
647
|
this.ensureInitialized();
|
|
405
648
|
const senderKeysObj = {};
|
|
406
649
|
for (const [convId, keyData] of this.senderKeys) {
|
|
407
|
-
senderKeysObj[convId] = toBase64(keyData.
|
|
650
|
+
senderKeysObj[convId] = toBase64(keyData.initialChainKey);
|
|
408
651
|
}
|
|
409
652
|
const encryptionKeys = {
|
|
410
653
|
identityKey: this.identity.publicKey,
|
|
411
654
|
privateKey: this.identity.privateKey,
|
|
412
|
-
//
|
|
655
|
+
// Ed25519 private key for signing
|
|
656
|
+
signedPreKeyPrivate: this.identity.signedPreKey.privateKey,
|
|
657
|
+
// X25519 private key for decrypting sender keys
|
|
413
658
|
senderKeys: senderKeysObj
|
|
414
659
|
};
|
|
415
660
|
const response = await this.fetch("/api/pair/approve", {
|
|
@@ -455,7 +700,8 @@ var MoltDMClient = class {
|
|
|
455
700
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
456
701
|
const encoder = new TextEncoder();
|
|
457
702
|
const data = encoder.encode(plaintext);
|
|
458
|
-
const
|
|
703
|
+
const keyBuffer = new Uint8Array(key).buffer;
|
|
704
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, [
|
|
459
705
|
"encrypt"
|
|
460
706
|
]);
|
|
461
707
|
const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, cryptoKey, data);
|
|
@@ -468,7 +714,8 @@ var MoltDMClient = class {
|
|
|
468
714
|
const combined = fromBase64(ciphertext);
|
|
469
715
|
const iv = combined.slice(0, 12);
|
|
470
716
|
const encrypted = combined.slice(12);
|
|
471
|
-
const
|
|
717
|
+
const keyBuffer = new Uint8Array(key).buffer;
|
|
718
|
+
const cryptoKey = await crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, [
|
|
472
719
|
"decrypt"
|
|
473
720
|
]);
|
|
474
721
|
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, cryptoKey, encrypted);
|