nostr-double-ratchet 0.0.34 → 0.0.35

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/src/Session.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  Rumor,
11
11
  CHAT_MESSAGE_KIND,
12
12
  } from "./types";
13
- import { kdf } from "./utils";
13
+ import { kdf, deepCopyState } from "./utils";
14
14
 
15
15
  const MAX_SKIP = 1000;
16
16
 
@@ -319,47 +319,64 @@ export class Session {
319
319
  throw new Error("Failed to decrypt header with current and skipped header keys");
320
320
  }
321
321
 
322
+
322
323
  private handleNostrEvent(e: { tags: string[][]; pubkey: string; content: string }) {
324
+ const snapshot = deepCopyState(this.state);
325
+ let pendingSwitch = false;
326
+
323
327
  try {
324
328
  const [header, shouldRatchet, isSkipped] = this.decryptHeader(e);
329
+ if (!isSkipped && this.state.theirNextNostrPublicKey !== header.nextPublicKey) {
330
+ this.state.theirCurrentNostrPublicKey = this.state.theirNextNostrPublicKey;
331
+ this.state.theirNextNostrPublicKey = header.nextPublicKey;
332
+ pendingSwitch = true;
333
+ }
325
334
 
326
335
  if (!isSkipped) {
327
- if (this.state.theirNextNostrPublicKey !== header.nextPublicKey) {
328
- this.state.theirCurrentNostrPublicKey = this.state.theirNextNostrPublicKey;
329
- this.state.theirNextNostrPublicKey = header.nextPublicKey;
330
- this.nostrUnsubscribe?.();
331
- this.nostrUnsubscribe = this.nostrNextUnsubscribe;
332
- this.nostrNextUnsubscribe = this.nostrSubscribe(
333
- {authors: [this.state.theirNextNostrPublicKey], kinds: [MESSAGE_EVENT_KIND]},
334
- (e) => this.handleNostrEvent(e)
335
- );
336
- }
337
-
338
336
  if (shouldRatchet) {
339
337
  this.skipMessageKeys(header.previousChainLength, e.pubkey);
340
338
  this.ratchetStep();
341
339
  }
342
340
  } else {
343
341
  if (!this.state.skippedKeys[e.pubkey]?.messageKeys[header.number]) {
344
- // Maybe we already processed this message — no error
345
- return
342
+ return;
346
343
  }
347
344
  }
348
345
 
349
346
  const text = this.ratchetDecrypt(header, e.content, e.pubkey);
350
347
  const innerEvent = JSON.parse(text);
348
+
351
349
  if (!validateEvent(innerEvent)) {
350
+ this.state = snapshot;
352
351
  return;
353
352
  }
354
-
355
353
  if (innerEvent.id !== getEventHash(innerEvent)) {
354
+ this.state = snapshot;
356
355
  return;
357
356
  }
358
357
 
359
- this.internalSubscriptions.forEach(callback => callback(innerEvent, e as VerifiedEvent));
358
+ if (pendingSwitch) {
359
+ this.nostrUnsubscribe?.();
360
+ this.nostrUnsubscribe = this.nostrNextUnsubscribe;
361
+ this.nostrNextUnsubscribe = this.nostrSubscribe(
362
+ { authors: [this.state.theirNextNostrPublicKey], kinds: [MESSAGE_EVENT_KIND] },
363
+ (ev) => this.handleNostrEvent(ev)
364
+ );
365
+ }
366
+
367
+ this.internalSubscriptions.forEach(callback => callback(innerEvent, e as VerifiedEvent));
360
368
  } catch (error) {
361
- if (error instanceof Error && error.message.includes("Failed to decrypt header")) {
362
- return;
369
+ this.state = snapshot;
370
+ if (error instanceof Error) {
371
+ if (error.message.includes("Failed to decrypt header")) {
372
+ return;
373
+ }
374
+
375
+ if (error.message === "invalid MAC") {
376
+ // Duplicate or stale ciphertexts can hit decrypt() again after a state restore.
377
+ // nip44 throws "invalid MAC" in that case, but the message has already been handled.
378
+ return;
379
+ }
363
380
  }
364
381
  throw error;
365
382
  }
package/src/utils.ts CHANGED
@@ -115,6 +115,41 @@ export function deserializeSessionState(data: string): SessionState {
115
115
  };
116
116
  }
117
117
 
118
+ export function deepCopyState(s: SessionState): SessionState {
119
+ return {
120
+ rootKey: new Uint8Array(s.rootKey),
121
+ theirCurrentNostrPublicKey: s.theirCurrentNostrPublicKey,
122
+ theirNextNostrPublicKey: s.theirNextNostrPublicKey,
123
+ ourCurrentNostrKey: s.ourCurrentNostrKey
124
+ ? {
125
+ publicKey: s.ourCurrentNostrKey.publicKey,
126
+ privateKey: new Uint8Array(s.ourCurrentNostrKey.privateKey),
127
+ }
128
+ : undefined,
129
+ ourNextNostrKey: {
130
+ publicKey: s.ourNextNostrKey.publicKey,
131
+ privateKey: new Uint8Array(s.ourNextNostrKey.privateKey),
132
+ },
133
+ receivingChainKey: s.receivingChainKey ? new Uint8Array(s.receivingChainKey) : undefined,
134
+ sendingChainKey: s.sendingChainKey ? new Uint8Array(s.sendingChainKey) : undefined,
135
+ sendingChainMessageNumber: s.sendingChainMessageNumber,
136
+ receivingChainMessageNumber: s.receivingChainMessageNumber,
137
+ previousSendingChainMessageCount: s.previousSendingChainMessageCount,
138
+ skippedKeys: Object.fromEntries(
139
+ Object.entries(s.skippedKeys).map(([author, entry]: any) => [
140
+ author,
141
+ {
142
+ headerKeys: entry.headerKeys.map((hk: Uint8Array) => new Uint8Array(hk)),
143
+ messageKeys: Object.fromEntries(
144
+ Object.entries(entry.messageKeys).map(([n, mk]: any) => [n, new Uint8Array(mk)])
145
+ ),
146
+ },
147
+ ])
148
+ ),
149
+ };
150
+ }
151
+
152
+
118
153
  export async function* createEventStream(session: Session): AsyncGenerator<Rumor, void, unknown> {
119
154
  const messageQueue: Rumor[] = [];
120
155
  let resolveNext: ((_value: Rumor) => void) | null = null;