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/README.md +7 -4
- package/dist/Channel.d.ts +21 -13
- package/dist/Channel.d.ts.map +1 -1
- package/dist/InviteLink.d.ts.map +1 -1
- package/dist/nostr-double-ratchet.es.js +1126 -1067
- package/dist/nostr-double-ratchet.umd.js +1 -1
- package/dist/types.d.ts +21 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.d.ts.map +1 -1
- package/package.json +2 -3
- package/src/Channel.ts +187 -99
- package/src/InviteLink.ts +3 -2
- package/src/types.ts +30 -13
- package/src/utils.ts +24 -15
- package/data/profileData.json +0 -1
- package/data/profileIndex.json +0 -1
- package/data/socialGraph.json +0 -1
package/src/Channel.ts
CHANGED
|
@@ -1,143 +1,231 @@
|
|
|
1
1
|
import { generateSecretKey, getPublicKey, nip44, finalizeEvent, VerifiedEvent } from "nostr-tools";
|
|
2
|
-
import {
|
|
2
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
3
3
|
import {
|
|
4
4
|
ChannelState,
|
|
5
|
-
|
|
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
|
|
18
|
-
nostrNextUnsubscribe
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
name
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
sendingChainKey:
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
51
|
-
this.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
this.state.
|
|
57
|
-
this.state.
|
|
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
|
|
61
|
-
this.state.
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
if (
|
|
71
|
-
throw new Error("
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
})
|
|
114
|
-
this.nostrSubscribeNext()
|
|
115
|
-
}
|
|
182
|
+
}
|
|
116
183
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
10
|
+
export type Header = {
|
|
11
11
|
number: number;
|
|
12
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|