nostr-double-ratchet 0.0.21 → 0.0.23
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/Session.d.ts.map +1 -1
- package/dist/nostr-double-ratchet.es.js +1142 -1112
- package/dist/nostr-double-ratchet.umd.js +1 -1
- package/dist/types.d.ts +9 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Session.ts +49 -49
- package/src/types.ts +7 -5
- package/src/utils.ts +64 -20
package/src/Session.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
Rumor,
|
|
11
11
|
CHAT_MESSAGE_KIND,
|
|
12
12
|
} from "./types";
|
|
13
|
-
import { kdf
|
|
13
|
+
import { kdf } from "./utils";
|
|
14
14
|
|
|
15
15
|
const MAX_SKIP = 1000;
|
|
16
16
|
|
|
@@ -66,8 +66,7 @@ export class Session {
|
|
|
66
66
|
sendingChainMessageNumber: 0,
|
|
67
67
|
receivingChainMessageNumber: 0,
|
|
68
68
|
previousSendingChainMessageCount: 0,
|
|
69
|
-
|
|
70
|
-
skippedHeaderKeys: {},
|
|
69
|
+
skippedKeys: {},
|
|
71
70
|
};
|
|
72
71
|
const session = new Session(nostrSubscribe, state);
|
|
73
72
|
if (name) session.name = name;
|
|
@@ -220,43 +219,45 @@ export class Session {
|
|
|
220
219
|
if (this.state.receivingChainMessageNumber + MAX_SKIP < until) {
|
|
221
220
|
throw new Error("Too many skipped messages");
|
|
222
221
|
}
|
|
222
|
+
|
|
223
|
+
if (!this.state.skippedKeys[nostrSender]) {
|
|
224
|
+
this.state.skippedKeys[nostrSender] = {
|
|
225
|
+
headerKeys: [],
|
|
226
|
+
messageKeys: {}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Store header keys
|
|
230
|
+
if (this.state.ourCurrentNostrKey) {
|
|
231
|
+
const currentSecret = nip44.getConversationKey(this.state.ourCurrentNostrKey.privateKey, nostrSender);
|
|
232
|
+
this.state.skippedKeys[nostrSender].headerKeys.push(currentSecret);
|
|
233
|
+
}
|
|
234
|
+
const nextSecret = nip44.getConversationKey(this.state.ourNextNostrKey.privateKey, nostrSender);
|
|
235
|
+
this.state.skippedKeys[nostrSender].headerKeys.push(nextSecret);
|
|
236
|
+
}
|
|
237
|
+
|
|
223
238
|
while (this.state.receivingChainMessageNumber < until) {
|
|
224
239
|
const [newReceivingChainKey, messageKey] = kdf(this.state.receivingChainKey!, new Uint8Array([1]), 2);
|
|
225
240
|
this.state.receivingChainKey = newReceivingChainKey;
|
|
226
|
-
|
|
227
|
-
this.state.skippedMessageKeys[key] = messageKey;
|
|
228
|
-
|
|
229
|
-
if (!this.state.skippedHeaderKeys[nostrSender]) {
|
|
230
|
-
const secrets: Uint8Array[] = [];
|
|
231
|
-
if (this.state.ourCurrentNostrKey) {
|
|
232
|
-
const currentSecret = nip44.getConversationKey(this.state.ourCurrentNostrKey.privateKey, nostrSender);
|
|
233
|
-
secrets.push(currentSecret);
|
|
234
|
-
}
|
|
235
|
-
const nextSecret = nip44.getConversationKey(this.state.ourNextNostrKey.privateKey, nostrSender);
|
|
236
|
-
secrets.push(nextSecret);
|
|
237
|
-
this.state.skippedHeaderKeys[nostrSender] = secrets;
|
|
238
|
-
}
|
|
239
|
-
|
|
241
|
+
this.state.skippedKeys[nostrSender].messageKeys[this.state.receivingChainMessageNumber] = messageKey;
|
|
240
242
|
this.state.receivingChainMessageNumber++;
|
|
241
243
|
}
|
|
242
244
|
}
|
|
243
245
|
|
|
244
246
|
private trySkippedMessageKeys(header: Header, ciphertext: string, nostrSender: string): string | null {
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return nip44.decrypt(ciphertext, mk);
|
|
247
|
+
const skippedKeys = this.state.skippedKeys[nostrSender];
|
|
248
|
+
if (!skippedKeys) return null;
|
|
249
|
+
|
|
250
|
+
const messageKey = skippedKeys.messageKeys[header.number];
|
|
251
|
+
if (!messageKey) return null;
|
|
252
|
+
|
|
253
|
+
delete skippedKeys.messageKeys[header.number];
|
|
254
|
+
|
|
255
|
+
// Clean up if no more skipped messages
|
|
256
|
+
if (Object.keys(skippedKeys.messageKeys).length === 0) {
|
|
257
|
+
delete this.state.skippedKeys[nostrSender];
|
|
258
258
|
}
|
|
259
|
-
|
|
259
|
+
|
|
260
|
+
return nip44.decrypt(ciphertext, messageKey);
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
// 4. NOSTR EVENT HANDLING
|
|
@@ -280,9 +281,9 @@ export class Session {
|
|
|
280
281
|
// Decryption with nextSecret also failed
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
const
|
|
284
|
-
if (
|
|
285
|
-
for (const key of
|
|
284
|
+
const skippedKeys = this.state.skippedKeys[event.pubkey];
|
|
285
|
+
if (skippedKeys?.headerKeys) {
|
|
286
|
+
for (const key of skippedKeys.headerKeys) {
|
|
286
287
|
try {
|
|
287
288
|
const header = JSON.parse(nip44.decrypt(encryptedHeader, key)) as Header;
|
|
288
289
|
return [header, false, true];
|
|
@@ -302,7 +303,7 @@ export class Session {
|
|
|
302
303
|
if (this.state.theirNextNostrPublicKey !== header.nextPublicKey) {
|
|
303
304
|
this.state.theirCurrentNostrPublicKey = this.state.theirNextNostrPublicKey;
|
|
304
305
|
this.state.theirNextNostrPublicKey = header.nextPublicKey;
|
|
305
|
-
this.nostrUnsubscribe?.();
|
|
306
|
+
this.nostrUnsubscribe?.();
|
|
306
307
|
this.nostrUnsubscribe = this.nostrNextUnsubscribe;
|
|
307
308
|
this.nostrNextUnsubscribe = this.nostrSubscribe(
|
|
308
309
|
{authors: [this.state.theirNextNostrPublicKey], kinds: [MESSAGE_EVENT_KIND]},
|
|
@@ -314,11 +315,6 @@ export class Session {
|
|
|
314
315
|
this.skipMessageKeys(header.previousChainLength, e.pubkey);
|
|
315
316
|
this.ratchetStep(header.nextPublicKey);
|
|
316
317
|
}
|
|
317
|
-
} else {
|
|
318
|
-
const key = skippedMessageIndexKey(e.pubkey, header.number);
|
|
319
|
-
if (!(key in this.state.skippedMessageKeys)) {
|
|
320
|
-
return // maybe we already processed this message
|
|
321
|
-
}
|
|
322
318
|
}
|
|
323
319
|
|
|
324
320
|
const text = this.ratchetDecrypt(header, e.content, e.pubkey);
|
|
@@ -343,15 +339,19 @@ export class Session {
|
|
|
343
339
|
(e) => this.handleNostrEvent(e)
|
|
344
340
|
);
|
|
345
341
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
342
|
+
if (this.state.theirCurrentNostrPublicKey) {
|
|
343
|
+
this.nostrUnsubscribe = this.nostrSubscribe(
|
|
344
|
+
{authors: [this.state.theirCurrentNostrPublicKey], kinds: [MESSAGE_EVENT_KIND]},
|
|
345
|
+
(e) => this.handleNostrEvent(e)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const skippedAuthors = Object.keys(this.state.skippedKeys);
|
|
350
|
+
if (skippedAuthors.length) {
|
|
351
|
+
this.nostrSubscribe(
|
|
352
|
+
{authors: skippedAuthors, kinds: [MESSAGE_EVENT_KIND]},
|
|
353
|
+
(e) => this.handleNostrEvent(e)
|
|
354
|
+
);
|
|
349
355
|
}
|
|
350
|
-
// do we want this unsubscribed on rotation or should we keep it open
|
|
351
|
-
// in case more skipped messages are found by relays or peers?
|
|
352
|
-
this.nostrUnsubscribe = this.nostrSubscribe(
|
|
353
|
-
{authors, kinds: [MESSAGE_EVENT_KIND]},
|
|
354
|
-
(e) => this.handleNostrEvent(e)
|
|
355
|
-
);
|
|
356
356
|
}
|
|
357
357
|
}
|
package/src/types.ts
CHANGED
|
@@ -48,11 +48,13 @@ export interface SessionState {
|
|
|
48
48
|
/** Number of messages sent in previous sending chain */
|
|
49
49
|
previousSendingChainMessageCount: number;
|
|
50
50
|
|
|
51
|
-
/** Cache of message keys for handling out-of-order messages */
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
/** Cache of message & header keys for handling out-of-order messages */
|
|
52
|
+
skippedKeys: {
|
|
53
|
+
[pubKey: string]: {
|
|
54
|
+
headerKeys: Uint8Array[],
|
|
55
|
+
messageKeys: {[msgIndex: number]: Uint8Array}
|
|
56
|
+
};
|
|
57
|
+
};
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/**
|
package/src/utils.ts
CHANGED
|
@@ -4,8 +4,11 @@ import { Session } from "./Session.ts";
|
|
|
4
4
|
import { extract as hkdf_extract, expand as hkdf_expand } from '@noble/hashes/hkdf';
|
|
5
5
|
import { sha256 } from '@noble/hashes/sha256';
|
|
6
6
|
|
|
7
|
+
const VERSION_NUMBER = 1;
|
|
8
|
+
|
|
7
9
|
export function serializeSessionState(state: SessionState): string {
|
|
8
10
|
return JSON.stringify({
|
|
11
|
+
version: VERSION_NUMBER,
|
|
9
12
|
rootKey: bytesToHex(state.rootKey),
|
|
10
13
|
theirCurrentNostrPublicKey: state.theirCurrentNostrPublicKey,
|
|
11
14
|
theirNextNostrPublicKey: state.theirNextNostrPublicKey,
|
|
@@ -22,16 +25,18 @@ export function serializeSessionState(state: SessionState): string {
|
|
|
22
25
|
sendingChainMessageNumber: state.sendingChainMessageNumber,
|
|
23
26
|
receivingChainMessageNumber: state.receivingChainMessageNumber,
|
|
24
27
|
previousSendingChainMessageCount: state.previousSendingChainMessageCount,
|
|
25
|
-
|
|
26
|
-
Object.entries(state.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
skippedKeys: Object.fromEntries(
|
|
29
|
+
Object.entries(state.skippedKeys).map(([pubKey, value]) => [
|
|
30
|
+
pubKey,
|
|
31
|
+
{
|
|
32
|
+
headerKeys: value.headerKeys.map(bytes => bytesToHex(bytes)),
|
|
33
|
+
messageKeys: Object.fromEntries(
|
|
34
|
+
Object.entries(value.messageKeys).map(([msgIndex, bytes]) => [
|
|
35
|
+
msgIndex,
|
|
36
|
+
bytesToHex(bytes)
|
|
37
|
+
])
|
|
38
|
+
)
|
|
39
|
+
}
|
|
35
40
|
])
|
|
36
41
|
),
|
|
37
42
|
});
|
|
@@ -39,6 +44,43 @@ export function serializeSessionState(state: SessionState): string {
|
|
|
39
44
|
|
|
40
45
|
export function deserializeSessionState(data: string): SessionState {
|
|
41
46
|
const state = JSON.parse(data);
|
|
47
|
+
|
|
48
|
+
// Handle version 0 (legacy format)
|
|
49
|
+
if (!state.version) {
|
|
50
|
+
const skippedKeys: SessionState['skippedKeys'] = {};
|
|
51
|
+
|
|
52
|
+
// Migrate old skipped keys format to new structure
|
|
53
|
+
if (state.skippedMessageKeys) {
|
|
54
|
+
Object.entries(state.skippedMessageKeys).forEach(([pubKey, messageKeys]: [string, any]) => {
|
|
55
|
+
skippedKeys[pubKey] = {
|
|
56
|
+
headerKeys: state.skippedHeaderKeys?.[pubKey] || [],
|
|
57
|
+
messageKeys: messageKeys
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
rootKey: hexToBytes(state.rootKey),
|
|
64
|
+
theirCurrentNostrPublicKey: state.theirCurrentNostrPublicKey,
|
|
65
|
+
theirNextNostrPublicKey: state.theirNextNostrPublicKey,
|
|
66
|
+
ourCurrentNostrKey: state.ourCurrentNostrKey ? {
|
|
67
|
+
publicKey: state.ourCurrentNostrKey.publicKey,
|
|
68
|
+
privateKey: hexToBytes(state.ourCurrentNostrKey.privateKey),
|
|
69
|
+
} : undefined,
|
|
70
|
+
ourNextNostrKey: {
|
|
71
|
+
publicKey: state.ourNextNostrKey.publicKey,
|
|
72
|
+
privateKey: hexToBytes(state.ourNextNostrKey.privateKey),
|
|
73
|
+
},
|
|
74
|
+
receivingChainKey: state.receivingChainKey ? hexToBytes(state.receivingChainKey) : undefined,
|
|
75
|
+
sendingChainKey: state.sendingChainKey ? hexToBytes(state.sendingChainKey) : undefined,
|
|
76
|
+
sendingChainMessageNumber: state.sendingChainMessageNumber,
|
|
77
|
+
receivingChainMessageNumber: state.receivingChainMessageNumber,
|
|
78
|
+
previousSendingChainMessageCount: state.previousSendingChainMessageCount,
|
|
79
|
+
skippedKeys
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle current version
|
|
42
84
|
return {
|
|
43
85
|
rootKey: hexToBytes(state.rootKey),
|
|
44
86
|
theirCurrentNostrPublicKey: state.theirCurrentNostrPublicKey,
|
|
@@ -56,16 +98,18 @@ export function deserializeSessionState(data: string): SessionState {
|
|
|
56
98
|
sendingChainMessageNumber: state.sendingChainMessageNumber,
|
|
57
99
|
receivingChainMessageNumber: state.receivingChainMessageNumber,
|
|
58
100
|
previousSendingChainMessageCount: state.previousSendingChainMessageCount,
|
|
59
|
-
|
|
60
|
-
Object.entries(state.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
101
|
+
skippedKeys: Object.fromEntries(
|
|
102
|
+
Object.entries(state.skippedKeys || {}).map(([pubKey, value]) => [
|
|
103
|
+
pubKey,
|
|
104
|
+
{
|
|
105
|
+
headerKeys: (value as any).headerKeys.map((hex: string) => hexToBytes(hex)),
|
|
106
|
+
messageKeys: Object.fromEntries(
|
|
107
|
+
Object.entries((value as any).messageKeys).map(([msgIndex, hex]) => [
|
|
108
|
+
msgIndex,
|
|
109
|
+
hexToBytes(hex as string)
|
|
110
|
+
])
|
|
111
|
+
)
|
|
112
|
+
}
|
|
69
113
|
])
|
|
70
114
|
),
|
|
71
115
|
};
|