nostr-double-ratchet 0.0.1 → 0.0.3

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/Channel.ts CHANGED
@@ -1,143 +1,231 @@
1
1
  import { generateSecretKey, getPublicKey, nip44, finalizeEvent, VerifiedEvent } from "nostr-tools";
2
- import { hexToBytes } from "@noble/hashes/utils";
2
+ import { bytesToHex } from "@noble/hashes/utils";
3
3
  import {
4
4
  ChannelState,
5
- RatchetMessage,
5
+ Header,
6
6
  Unsubscribe,
7
7
  NostrSubscribe,
8
8
  MessageCallback,
9
9
  EVENT_KIND,
10
- KeyPair,
11
- Sender,
12
- KeyType,
13
10
  } from "./types";
14
- import { kdf } from "./utils";
11
+ import { kdf, skippedMessageIndexKey } from "./utils";
15
12
 
13
+ const MAX_SKIP = 1000;
14
+
15
+ /**
16
+ * Similar to Signal's "Double Ratchet with header encryption"
17
+ * https://signal.org/docs/specifications/doubleratchet/
18
+ */
16
19
  export class Channel {
17
- nostrUnsubscribe: Unsubscribe | undefined
18
- nostrNextUnsubscribe: Unsubscribe | undefined
19
- currentInternalSubscriptionId = 0
20
- internalSubscriptions = new Map<number, MessageCallback>()
21
- name = Math.random().toString(36).substring(2, 6)
20
+ private nostrUnsubscribe?: Unsubscribe;
21
+ private nostrNextUnsubscribe?: Unsubscribe;
22
+ private internalSubscriptions = new Map<number, MessageCallback>();
23
+ private currentInternalSubscriptionId = 0;
24
+ public name: string;
22
25
 
23
26
  constructor(private nostrSubscribe: NostrSubscribe, public state: ChannelState) {
24
- this.name = Math.random().toString(36).substring(2, 6)
27
+ this.name = Math.random().toString(36).substring(2, 6);
25
28
  }
26
29
 
27
30
  /**
28
- * To preserve forward secrecy, do not use long-term keys for channel initialization. Use e.g. InviteLink to exchange session keys.
31
+ * @param sharedSecret optional, but useful to keep the first chain of messages secure. Unlike the Nostr keys, it can be forgotten after the 1st message in the chain.
32
+ * @param isInitiator determines which chain key is used for sending vs receiving
29
33
  */
30
- static init(nostrSubscribe: NostrSubscribe, theirCurrentNostrPublicKey: string, ourCurrentPrivateKey: Uint8Array, name?: string): Channel {
31
- const ourNextPrivateKey = generateSecretKey()
34
+ static init(nostrSubscribe: NostrSubscribe, theirNostrPublicKey: string, ourCurrentPrivateKey: Uint8Array, sharedSecret = new Uint8Array(), name?: string, isInitiator = true): Channel {
35
+ const ourNextPrivateKey = generateSecretKey();
36
+ const [rootKey, sendingChainKey] = kdf(sharedSecret, nip44.getConversationKey(ourNextPrivateKey, theirNostrPublicKey), 2);
37
+ let ourCurrentNostrKey;
38
+ let ourNextNostrKey;
39
+ if (isInitiator) {
40
+ ourCurrentNostrKey = { publicKey: getPublicKey(ourCurrentPrivateKey), privateKey: ourCurrentPrivateKey };
41
+ ourNextNostrKey = { publicKey: getPublicKey(ourNextPrivateKey), privateKey: ourNextPrivateKey };
42
+ } else {
43
+ ourNextNostrKey = { publicKey: getPublicKey(ourCurrentPrivateKey), privateKey: ourCurrentPrivateKey };
44
+ }
32
45
  const state: ChannelState = {
33
- theirCurrentNostrPublicKey,
34
- ourCurrentNostrKey: { publicKey: getPublicKey(ourCurrentPrivateKey), privateKey: ourCurrentPrivateKey },
35
- ourNextNostrKey: { publicKey: getPublicKey(ourNextPrivateKey), privateKey: ourNextPrivateKey },
36
- receivingChainKey: new Uint8Array(),
37
- nextReceivingChainKey: new Uint8Array(),
38
- sendingChainKey: new Uint8Array(),
46
+ rootKey: isInitiator ? rootKey : sharedSecret,
47
+ theirNostrPublicKey,
48
+ ourCurrentNostrKey,
49
+ ourNextNostrKey,
50
+ receivingChainKey: undefined,
51
+ sendingChainKey: isInitiator ? sendingChainKey : undefined,
39
52
  sendingChainMessageNumber: 0,
40
53
  receivingChainMessageNumber: 0,
41
54
  previousSendingChainMessageCount: 0,
42
- skippedMessageKeys: {}
55
+ skippedMessageKeys: {},
56
+ };
57
+ const channel = new Channel(nostrSubscribe, state);
58
+ if (name) channel.name = name;
59
+ console.log(channel.name, 'initial root key', bytesToHex(state.rootKey).slice(0,4))
60
+ return channel;
61
+ }
62
+
63
+ send(data: string): VerifiedEvent {
64
+ if (!this.state.theirNostrPublicKey || !this.state.ourCurrentNostrKey) {
65
+ throw new Error("we are not the initiator, so we can't send the first message");
43
66
  }
44
- const channel = new Channel(nostrSubscribe, state)
45
- channel.updateTheirCurrentNostrPublicKey(theirCurrentNostrPublicKey)
46
- if (name) channel.name = name
47
- return channel
67
+
68
+ const [header, encryptedData] = this.ratchetEncrypt(data);
69
+
70
+ const sharedSecret = nip44.getConversationKey(this.state.ourCurrentNostrKey.privateKey, this.state.theirNostrPublicKey);
71
+ const encryptedHeader = nip44.encrypt(JSON.stringify(header), sharedSecret);
72
+
73
+ const nostrEvent = finalizeEvent({
74
+ content: encryptedData,
75
+ kind: EVENT_KIND,
76
+ tags: [["header", encryptedHeader]],
77
+ created_at: Math.floor(Date.now() / 1000)
78
+ }, this.state.ourCurrentNostrKey.privateKey);
79
+
80
+ return nostrEvent;
81
+ }
82
+
83
+ onMessage(callback: MessageCallback): Unsubscribe {
84
+ const id = this.currentInternalSubscriptionId++
85
+ this.internalSubscriptions.set(id, callback)
86
+ this.subscribeToNostrEvents()
87
+ return () => this.internalSubscriptions.delete(id)
88
+ }
89
+
90
+ private ratchetEncrypt(plaintext: string): [Header, string] {
91
+ const [newSendingChainKey, messageKey] = kdf(this.state.sendingChainKey!, new Uint8Array([1]), 2);
92
+ this.state.sendingChainKey = newSendingChainKey;
93
+ const header: Header = {
94
+ number: this.state.sendingChainMessageNumber++,
95
+ nextPublicKey: this.state.ourNextNostrKey.publicKey,
96
+ time: Date.now(),
97
+ previousChainLength: this.state.previousSendingChainMessageCount
98
+ };
99
+ return [header, nip44.encrypt(plaintext, messageKey)];
48
100
  }
49
101
 
50
- updateTheirCurrentNostrPublicKey(theirNewPublicKey: string) {
51
- this.state.theirCurrentNostrPublicKey = theirNewPublicKey
52
- this.state.previousSendingChainMessageCount = this.state.sendingChainMessageNumber
53
- this.state.sendingChainMessageNumber = 0
54
- this.state.receivingChainMessageNumber = 0
55
- this.state.receivingChainKey = this.getNostrSenderKeypair(Sender.Them, KeyType.Current).privateKey
56
- this.state.nextReceivingChainKey = this.getNostrSenderKeypair(Sender.Them, KeyType.Next).privateKey
57
- this.state.sendingChainKey = this.getNostrSenderKeypair(Sender.Us, KeyType.Current).privateKey
102
+ private ratchetDecrypt(header: Header, ciphertext: string, nostrSender: string): string {
103
+ const plaintext = this.trySkippedMessageKeys(header, ciphertext, nostrSender);
104
+ if (plaintext) return plaintext;
105
+
106
+ this.skipMessageKeys(header.number, nostrSender);
107
+
108
+ const [newReceivingChainKey, messageKey] = kdf(this.state.receivingChainKey!, new Uint8Array([1]), 2);
109
+ this.state.receivingChainKey = newReceivingChainKey;
110
+ this.state.receivingChainMessageNumber++;
111
+
112
+ try {
113
+ return nip44.decrypt(ciphertext, messageKey);
114
+ } catch (error) {
115
+ console.error(this.name, 'Decryption failed:', error, {
116
+ messageKey: bytesToHex(messageKey).slice(0, 4),
117
+ receivingChainKey: bytesToHex(this.state.receivingChainKey).slice(0, 4),
118
+ sendingChainKey: this.state.sendingChainKey && bytesToHex(this.state.sendingChainKey).slice(0, 4),
119
+ rootKey: bytesToHex(this.state.rootKey).slice(0, 4)
120
+ });
121
+ throw error;
122
+ }
58
123
  }
59
124
 
60
- private rotateOurCurrentNostrKey() {
61
- this.state.ourCurrentNostrKey = this.state.ourNextNostrKey
62
- const ourNextSecretKey = generateSecretKey()
125
+ private ratchetStep(theirNostrPublicKey: string) {
126
+ this.state.previousSendingChainMessageCount = this.state.sendingChainMessageNumber;
127
+ this.state.sendingChainMessageNumber = 0;
128
+ this.state.receivingChainMessageNumber = 0;
129
+ this.state.theirNostrPublicKey = theirNostrPublicKey;
130
+
131
+ const conversationKey1 = nip44.getConversationKey(this.state.ourNextNostrKey.privateKey, this.state.theirNostrPublicKey!);
132
+ const [intermediateRootKey, receivingChainKey] = kdf(this.state.rootKey, conversationKey1, 3);
133
+
134
+ this.state.receivingChainKey = receivingChainKey;
135
+
136
+ this.state.ourCurrentNostrKey = this.state.ourNextNostrKey;
137
+ const ourNextSecretKey = generateSecretKey();
63
138
  this.state.ourNextNostrKey = {
64
139
  publicKey: getPublicKey(ourNextSecretKey),
65
140
  privateKey: ourNextSecretKey
66
- }
141
+ };
142
+
143
+ const conversationKey2 = nip44.getConversationKey(this.state.ourNextNostrKey.privateKey, this.state.theirNostrPublicKey!);
144
+ const [rootKey2, sendingChainKey] = kdf(intermediateRootKey, conversationKey2, 3);
145
+ this.state.rootKey = rootKey2;
146
+ this.state.sendingChainKey = sendingChainKey;
67
147
  }
68
148
 
69
- getNostrSenderKeypair(sender: Sender, keyType: KeyType): KeyPair {
70
- if (sender === Sender.Us && keyType === KeyType.Next) {
71
- throw new Error("We don't have their next key")
149
+ private skipMessageKeys(until: number, nostrSender: string) {
150
+ if (this.state.receivingChainMessageNumber + MAX_SKIP < until) {
151
+ throw new Error("Too many skipped messages");
72
152
  }
73
- const ourPrivate = keyType === KeyType.Current ? this.state.ourCurrentNostrKey.privateKey : this.state.ourNextNostrKey.privateKey
74
- const theirPublic = this.state.theirCurrentNostrPublicKey
75
- const senderPubKey = sender === Sender.Us ? getPublicKey(ourPrivate) : theirPublic
76
- const privateKey = kdf(nip44.getConversationKey(ourPrivate, theirPublic), hexToBytes(senderPubKey))
77
- return {
78
- publicKey: getPublicKey(privateKey),
79
- privateKey
153
+ while (this.state.receivingChainMessageNumber < until) {
154
+ const [newReceivingChainKey, messageKey] = kdf(this.state.receivingChainKey!, new Uint8Array([1]), 2);
155
+ this.state.receivingChainKey = newReceivingChainKey;
156
+ const key = skippedMessageIndexKey(nostrSender, this.state.receivingChainMessageNumber);
157
+ this.state.skippedMessageKeys[key] = messageKey;
158
+ this.state.receivingChainMessageNumber++;
80
159
  }
81
160
  }
82
161
 
83
- private nostrSubscribeNext() {
84
- const nextReceivingPublicKey = this.getNostrSenderKeypair(Sender.Them, KeyType.Next).publicKey
85
- const decryptKey = this.state.nextReceivingChainKey
86
- this.nostrNextUnsubscribe = this.nostrSubscribe({authors: [nextReceivingPublicKey], kinds: [EVENT_KIND]}, (e) => {
87
- // they acknowledged our next key and sent with the corresponding new nostr sender key
88
- const msg = JSON.parse(nip44.decrypt(e.content, decryptKey)) as RatchetMessage
89
- if (msg.nextPublicKey !== this.state.theirCurrentNostrPublicKey) {
90
- this.rotateOurCurrentNostrKey()
91
- this.updateTheirCurrentNostrPublicKey(msg.nextPublicKey)
92
- this.nostrUnsubscribe?.()
93
- this.nostrUnsubscribe = this.nostrNextUnsubscribe
94
- this.nostrSubscribeNext()
95
- }
96
- this.internalSubscriptions.forEach(callback => callback({id: e.id, data: msg.data, pubkey: msg.nextPublicKey, time: msg.time}))
97
- })
162
+ private trySkippedMessageKeys(header: Header, ciphertext: string, nostrSender: string): string | null {
163
+ const key = skippedMessageIndexKey(nostrSender, header.number);
164
+ if (key in this.state.skippedMessageKeys) {
165
+ const mk = this.state.skippedMessageKeys[key];
166
+ delete this.state.skippedMessageKeys[key];
167
+ return nip44.decrypt(ciphertext, mk);
168
+ }
169
+ return null;
98
170
  }
99
171
 
100
- private subscribeToNostrEvents() {
101
- if (this.nostrUnsubscribe) {
102
- return
103
- }
104
- const receivingPublicKey = this.getNostrSenderKeypair(Sender.Them, KeyType.Current).publicKey
105
- const decryptKey = this.state.receivingChainKey
106
- this.nostrUnsubscribe = this.nostrSubscribe({authors: [receivingPublicKey], kinds: [EVENT_KIND]}, (e) => {
107
- const msg = JSON.parse(nip44.decrypt(e.content, decryptKey)) as RatchetMessage
108
- if (msg.nextPublicKey !== this.state.theirCurrentNostrPublicKey) {
109
- // they announced their next key: we will use it to derive the next nostr sender key
110
- this.updateTheirCurrentNostrPublicKey(msg.nextPublicKey)
172
+ private decryptHeader(e: any): [Header, boolean] {
173
+ const encryptedHeader = e.tags[0][1];
174
+ if (this.state.ourCurrentNostrKey) {
175
+ const currentSecret = nip44.getConversationKey(this.state.ourCurrentNostrKey.privateKey, e.pubkey);
176
+ try {
177
+ const header = JSON.parse(nip44.decrypt(encryptedHeader, currentSecret)) as Header;
178
+ return [header, false];
179
+ } catch (error) {
180
+ // Decryption with currentSecret failed, try with nextSecret
111
181
  }
112
- this.internalSubscriptions.forEach(callback => callback({id: e.id, data: msg.data, pubkey: msg.nextPublicKey, time: msg.time}))
113
- })
114
- this.nostrSubscribeNext()
115
- }
182
+ }
116
183
 
117
- onMessage(callback: MessageCallback): Unsubscribe {
118
- const id = this.currentInternalSubscriptionId++
119
- this.internalSubscriptions.set(id, callback)
120
- this.subscribeToNostrEvents()
121
- return () => this.internalSubscriptions.delete(id)
184
+ const nextSecret = nip44.getConversationKey(this.state.ourNextNostrKey.privateKey, e.pubkey);
185
+ try {
186
+ const header = JSON.parse(nip44.decrypt(encryptedHeader, nextSecret)) as Header;
187
+ return [header, true];
188
+ } catch (error) {
189
+ // Decryption with nextSecret also failed
190
+ }
191
+
192
+ throw new Error("Failed to decrypt header with both current and next secrets");
122
193
  }
123
194
 
124
- send(data: string): VerifiedEvent {
125
- const message: RatchetMessage = {
126
- number: this.state.sendingChainMessageNumber,
127
- data: data,
128
- nextPublicKey: this.state.ourNextNostrKey.publicKey,
129
- time: Date.now()
195
+ private handleNostrEvent(e: any) {
196
+ const [header, shouldRatchet] = this.decryptHeader(e);
197
+
198
+ if (this.state.theirNostrPublicKey !== header.nextPublicKey) {
199
+ this.state.theirNostrPublicKey = header.nextPublicKey;
200
+ this.nostrUnsubscribe?.(); // should we keep this open for a while? maybe as long as we have skipped messages?
201
+ this.nostrUnsubscribe = this.nostrNextUnsubscribe;
202
+ this.nostrNextUnsubscribe = this.nostrSubscribe(
203
+ {authors: [this.state.theirNostrPublicKey], kinds: [EVENT_KIND]},
204
+ (e) => this.handleNostrEvent(e)
205
+ );
130
206
  }
131
- this.state.sendingChainMessageNumber++
132
- const sendingPrivateKey = this.getNostrSenderKeypair(Sender.Us, KeyType.Current).privateKey
133
- const encryptedData = nip44.encrypt(JSON.stringify(message), this.state.sendingChainKey)
134
- const nostrEvent = finalizeEvent({
135
- content: encryptedData,
136
- kind: EVENT_KIND,
137
- tags: [],
138
- created_at: Math.floor(Date.now() / 1000)
139
- }, sendingPrivateKey)
140
207
 
141
- return nostrEvent
208
+ if (shouldRatchet) {
209
+ this.skipMessageKeys(header.previousChainLength, e.pubkey);
210
+ this.ratchetStep(header.nextPublicKey);
211
+ }
212
+
213
+ const data = this.ratchetDecrypt(header, e.content, e.pubkey);
214
+
215
+ this.internalSubscriptions.forEach(callback => callback({id: e.id, data, pubkey: header.nextPublicKey, time: header.time}));
216
+ }
217
+
218
+ private subscribeToNostrEvents() {
219
+ if (this.nostrNextUnsubscribe) return;
220
+ if (this.state.theirNostrPublicKey) {
221
+ this.nostrUnsubscribe = this.nostrSubscribe(
222
+ {authors: [this.state.theirNostrPublicKey], kinds: [EVENT_KIND]},
223
+ (e) => this.handleNostrEvent(e)
224
+ );
225
+ }
226
+ this.nostrNextUnsubscribe = this.nostrSubscribe(
227
+ {authors: [this.state.theirNostrPublicKey], kinds: [EVENT_KIND]},
228
+ (e) => this.handleNostrEvent(e)
229
+ );
142
230
  }
143
231
  }
package/src/InviteLink.ts CHANGED
@@ -127,7 +127,7 @@ export class InviteLink {
127
127
  const inviteeSessionPublicKey = getPublicKey(inviteeSessionKey);
128
128
  const inviterPublicKey = this.inviter || this.inviterSessionPublicKey;
129
129
 
130
- const channel = Channel.init(nostrSubscribe, this.inviterSessionPublicKey, inviteeSessionKey);
130
+ const channel = Channel.init(nostrSubscribe, this.inviterSessionPublicKey, inviteeSessionKey, new Uint8Array(), undefined, true);
131
131
 
132
132
  // Create a random keypair for the envelope sender
133
133
  const randomSenderKey = generateSecretKey();
@@ -181,7 +181,8 @@ export class InviteLink {
181
181
 
182
182
  const inviteeSessionPublicKey = await innerDecrypt(innerEvent.content, innerEvent.pubkey);
183
183
 
184
- const channel = Channel.init(nostrSubscribe, inviteeSessionPublicKey, this.inviterSessionPrivateKey!);
184
+ const name = event.id;
185
+ const channel = Channel.init(nostrSubscribe, inviteeSessionPublicKey, this.inviterSessionPrivateKey!, new Uint8Array(), name, false);
185
186
 
186
187
  onChannel(channel, innerEvent.pubkey);
187
188
  } catch (error) {
package/src/types.ts CHANGED
@@ -7,9 +7,9 @@ export type Message = {
7
7
  time: number; // unlike Nostr, we use milliseconds instead of seconds
8
8
  }
9
9
 
10
- export type RatchetMessage = {
10
+ export type Header = {
11
11
  number: number;
12
- data: string;
12
+ previousChainLength: number;
13
13
  nextPublicKey: string;
14
14
  time: number;
15
15
  }
@@ -24,17 +24,39 @@ export type KeyPair = {
24
24
  privateKey: Uint8Array;
25
25
  }
26
26
 
27
+ /**
28
+ * Represents the state of a Double Ratchet channel between two parties. Needed for persisting channels.
29
+ */
27
30
  export interface ChannelState {
28
- theirCurrentNostrPublicKey: string;
29
- ourCurrentNostrKey: KeyPair;
31
+ /** Root key used to derive new sending / receiving chain keys */
32
+ rootKey: Uint8Array;
33
+
34
+ /** The other party's current Nostr public key */
35
+ theirNostrPublicKey: string;
36
+
37
+ /** Our current Nostr keypair used for this channel */
38
+ ourCurrentNostrKey?: KeyPair;
39
+
40
+ /** Our next Nostr keypair, used when ratcheting forward. It is advertised in messages we send. */
30
41
  ourNextNostrKey: KeyPair;
31
- receivingChainKey: Uint8Array;
32
- nextReceivingChainKey: Uint8Array;
33
- sendingChainKey: Uint8Array;
42
+
43
+ /** Key for decrypting incoming messages in current chain */
44
+ receivingChainKey?: Uint8Array;
45
+
46
+ /** Key for encrypting outgoing messages in current chain */
47
+ sendingChainKey?: Uint8Array;
48
+
49
+ /** Number of messages sent in current sending chain */
34
50
  sendingChainMessageNumber: number;
51
+
52
+ /** Number of messages received in current receiving chain */
35
53
  receivingChainMessageNumber: number;
54
+
55
+ /** Number of messages sent in previous sending chain */
36
56
  previousSendingChainMessageCount: number;
37
- skippedMessageKeys: Record<number, Uint8Array>;
57
+
58
+ /** Cache of message keys for handling out-of-order messages */
59
+ skippedMessageKeys: Record<string, Uint8Array>;
38
60
  }
39
61
 
40
62
  export type Unsubscribe = () => void;
@@ -59,10 +81,5 @@ export enum Sender {
59
81
  Them
60
82
  }
61
83
 
62
- export enum KeyType {
63
- Current,
64
- Next
65
- }
66
-
67
84
  export type EncryptFunction = (plaintext: string, pubkey: string) => Promise<string>;
68
85
  export type DecryptFunction = (ciphertext: string, pubkey: string) => Promise<string>;
package/src/utils.ts CHANGED
@@ -6,18 +6,18 @@ import { sha256 } from '@noble/hashes/sha256';
6
6
 
7
7
  export function serializeChannelState(state: ChannelState): string {
8
8
  return JSON.stringify({
9
- theirCurrentNostrPublicKey: state.theirCurrentNostrPublicKey,
10
- ourCurrentNostrKey: {
9
+ rootKey: bytesToHex(state.rootKey),
10
+ theirNostrPublicKey: state.theirNostrPublicKey,
11
+ ourCurrentNostrKey: state.ourCurrentNostrKey ? {
11
12
  publicKey: state.ourCurrentNostrKey.publicKey,
12
13
  privateKey: bytesToHex(state.ourCurrentNostrKey.privateKey),
13
- },
14
+ } : undefined,
14
15
  ourNextNostrKey: {
15
16
  publicKey: state.ourNextNostrKey.publicKey,
16
17
  privateKey: bytesToHex(state.ourNextNostrKey.privateKey),
17
18
  },
18
- receivingChainKey: bytesToHex(state.receivingChainKey),
19
- nextReceivingChainKey: bytesToHex(state.nextReceivingChainKey),
20
- sendingChainKey: bytesToHex(state.sendingChainKey),
19
+ receivingChainKey: state.receivingChainKey ? bytesToHex(state.receivingChainKey) : undefined,
20
+ sendingChainKey: state.sendingChainKey ? bytesToHex(state.sendingChainKey) : undefined,
21
21
  sendingChainMessageNumber: state.sendingChainMessageNumber,
22
22
  receivingChainMessageNumber: state.receivingChainMessageNumber,
23
23
  previousSendingChainMessageCount: state.previousSendingChainMessageCount,
@@ -33,18 +33,18 @@ export function serializeChannelState(state: ChannelState): string {
33
33
  export function deserializeChannelState(data: string): ChannelState {
34
34
  const state = JSON.parse(data);
35
35
  return {
36
- theirCurrentNostrPublicKey: state.theirCurrentNostrPublicKey,
37
- ourCurrentNostrKey: {
36
+ rootKey: hexToBytes(state.rootKey),
37
+ theirNostrPublicKey: state.theirNostrPublicKey,
38
+ ourCurrentNostrKey: state.ourCurrentNostrKey ? {
38
39
  publicKey: state.ourCurrentNostrKey.publicKey,
39
40
  privateKey: hexToBytes(state.ourCurrentNostrKey.privateKey),
40
- },
41
+ } : undefined,
41
42
  ourNextNostrKey: {
42
43
  publicKey: state.ourNextNostrKey.publicKey,
43
44
  privateKey: hexToBytes(state.ourNextNostrKey.privateKey),
44
45
  },
45
- receivingChainKey: hexToBytes(state.receivingChainKey),
46
- nextReceivingChainKey: hexToBytes(state.nextReceivingChainKey),
47
- sendingChainKey: hexToBytes(state.sendingChainKey),
46
+ receivingChainKey: state.receivingChainKey ? hexToBytes(state.receivingChainKey) : undefined,
47
+ sendingChainKey: state.sendingChainKey ? hexToBytes(state.sendingChainKey) : undefined,
48
48
  sendingChainMessageNumber: state.sendingChainMessageNumber,
49
49
  receivingChainMessageNumber: state.receivingChainMessageNumber,
50
50
  previousSendingChainMessageCount: state.previousSendingChainMessageCount,
@@ -85,7 +85,16 @@ export async function* createMessageStream(channel: Channel): AsyncGenerator<Mes
85
85
  }
86
86
  }
87
87
 
88
- export function kdf(input1: Uint8Array, input2: Uint8Array = new Uint8Array(32)) {
89
- const prk = hkdf_extract(sha256, input1, input2)
90
- return hkdf_expand(sha256, prk, new Uint8Array([1]), 32)
88
+ export function kdf(input1: Uint8Array, input2: Uint8Array = new Uint8Array(32), numOutputs: number = 1): Uint8Array[] {
89
+ const prk = hkdf_extract(sha256, input1, input2);
90
+
91
+ const outputs: Uint8Array[] = [];
92
+ for (let i = 1; i <= numOutputs; i++) {
93
+ outputs.push(hkdf_expand(sha256, prk, new Uint8Array([i]), 32));
94
+ }
95
+ return outputs;
91
96
  }
97
+
98
+ export function skippedMessageIndexKey(nostrSender: string, number: number): string {
99
+ return `${nostrSender}:${number}`;
100
+ }