nostr-double-ratchet 0.0.11 → 0.0.13

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/Invite.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { generateSecretKey, getPublicKey, nip44, finalizeEvent, VerifiedEvent, UnsignedEvent, verifyEvent, Filter } from "nostr-tools";
2
2
  import { INVITE_EVENT_KIND, NostrSubscribe, Unsubscribe } from "./types";
3
3
  import { getConversationKey } from "nostr-tools/nip44";
4
- import { Channel } from "./Channel";
4
+ import { Session } from "./Session.ts";
5
5
  import { MESSAGE_EVENT_KIND } from "./types";
6
6
  import { EncryptFunction, DecryptFunction } from "./types";
7
7
  import { hexToBytes, bytesToHex } from "@noble/hashes/utils";
8
8
 
9
9
  /**
10
- * Invite is a safe way to exchange session keys and initiate secret channels.
10
+ * Invite is a safe way to exchange session keys and initiate secret sessions.
11
11
  *
12
12
  * It can be shared privately as an URL (e.g. QR code) or published as a Nostr event.
13
13
  *
@@ -18,10 +18,10 @@ import { hexToBytes, bytesToHex } from "@noble/hashes/utils";
18
18
  */
19
19
  export class Invite {
20
20
  constructor(
21
- public inviterSessionPublicKey: string,
21
+ public inviterEphemeralPublicKey: string,
22
22
  public linkSecret: string,
23
23
  public inviter: string,
24
- public inviterSessionPrivateKey?: Uint8Array,
24
+ public inviterEphemeralPrivateKey?: Uint8Array,
25
25
  public label?: string,
26
26
  public maxUses?: number,
27
27
  public usedBy: string[] = [],
@@ -31,14 +31,14 @@ export class Invite {
31
31
  if (!inviter) {
32
32
  throw new Error("Inviter public key is required");
33
33
  }
34
- const inviterSessionPrivateKey = generateSecretKey();
35
- const inviterSessionPublicKey = getPublicKey(inviterSessionPrivateKey);
34
+ const inviterEphemeralPrivateKey = generateSecretKey();
35
+ const inviterEphemeralPublicKey = getPublicKey(inviterEphemeralPrivateKey);
36
36
  const linkSecret = bytesToHex(generateSecretKey());
37
37
  return new Invite(
38
- inviterSessionPublicKey,
38
+ inviterEphemeralPublicKey,
39
39
  linkSecret,
40
40
  inviter,
41
- inviterSessionPrivateKey,
41
+ inviterEphemeralPrivateKey,
42
42
  label,
43
43
  maxUses
44
44
  );
@@ -59,13 +59,13 @@ export class Invite {
59
59
  throw new Error("Invite data in URL hash is not valid JSON: " + err);
60
60
  }
61
61
 
62
- const { inviter, sessionKey, linkSecret } = data;
63
- if (!inviter || !sessionKey || !linkSecret) {
64
- throw new Error("Missing required fields (inviter, sessionKey, linkSecret) in invite data.");
62
+ const { inviter, ephemeralKey, linkSecret } = data;
63
+ if (!inviter || !ephemeralKey || !linkSecret) {
64
+ throw new Error("Missing required fields (inviter, ephemeralKey, linkSecret) in invite data.");
65
65
  }
66
66
 
67
67
  return new Invite(
68
- sessionKey,
68
+ ephemeralKey,
69
69
  linkSecret,
70
70
  inviter
71
71
  );
@@ -74,10 +74,10 @@ export class Invite {
74
74
  static deserialize(json: string): Invite {
75
75
  const data = JSON.parse(json);
76
76
  return new Invite(
77
- data.inviterSessionPublicKey,
77
+ data.inviterEphemeralPublicKey,
78
78
  data.linkSecret,
79
79
  data.inviter,
80
- data.inviterSessionPrivateKey ? new Uint8Array(data.inviterSessionPrivateKey) : undefined,
80
+ data.inviterEphemeralPrivateKey ? new Uint8Array(data.inviterEphemeralPrivateKey) : undefined,
81
81
  data.label,
82
82
  data.maxUses,
83
83
  data.usedBy
@@ -92,16 +92,16 @@ export class Invite {
92
92
  throw new Error("Event signature is invalid");
93
93
  }
94
94
  const { tags } = event;
95
- const inviterSessionPublicKey = tags.find(([key]) => key === 'sessionKey')?.[1];
95
+ const inviterEphemeralPublicKey = tags.find(([key]) => key === 'ephemeralKey')?.[1];
96
96
  const linkSecret = tags.find(([key]) => key === 'linkSecret')?.[1];
97
97
  const inviter = event.pubkey;
98
98
 
99
- if (!inviterSessionPublicKey || !linkSecret) {
99
+ if (!inviterEphemeralPublicKey || !linkSecret) {
100
100
  throw new Error("Invalid invite event: missing session key or link secret");
101
101
  }
102
102
 
103
103
  return new Invite(
104
- inviterSessionPublicKey,
104
+ inviterEphemeralPublicKey,
105
105
  linkSecret,
106
106
  inviter
107
107
  );
@@ -124,7 +124,7 @@ export class Invite {
124
124
  const inviteLink = Invite.fromEvent(event);
125
125
  onInvite(inviteLink);
126
126
  } catch (error) {
127
- console.error("Error processing invite:", error);
127
+ console.error("Error processing invite:", error, "event:", event);
128
128
  }
129
129
  });
130
130
 
@@ -136,10 +136,10 @@ export class Invite {
136
136
  */
137
137
  serialize(): string {
138
138
  return JSON.stringify({
139
- inviterSessionPublicKey: this.inviterSessionPublicKey,
139
+ inviterEphemeralPublicKey: this.inviterEphemeralPublicKey,
140
140
  linkSecret: this.linkSecret,
141
141
  inviter: this.inviter,
142
- inviterSessionPrivateKey: this.inviterSessionPrivateKey ? Array.from(this.inviterSessionPrivateKey) : undefined,
142
+ inviterEphemeralPrivateKey: this.inviterEphemeralPrivateKey ? Array.from(this.inviterEphemeralPrivateKey) : undefined,
143
143
  label: this.label,
144
144
  maxUses: this.maxUses,
145
145
  usedBy: this.usedBy,
@@ -152,7 +152,7 @@ export class Invite {
152
152
  getUrl(root = "https://iris.to") {
153
153
  const data = {
154
154
  inviter: this.inviter,
155
- sessionKey: this.inviterSessionPublicKey,
155
+ ephemeralKey: this.inviterEphemeralPublicKey,
156
156
  linkSecret: this.linkSecret
157
157
  };
158
158
  const url = new URL(root);
@@ -167,16 +167,16 @@ export class Invite {
167
167
  pubkey: this.inviter,
168
168
  content: "",
169
169
  created_at: Math.floor(Date.now() / 1000),
170
- tags: [['sessionKey', this.inviterSessionPublicKey], ['linkSecret', this.linkSecret], ['d', 'nostr-double-ratchet/invite']],
170
+ tags: [['ephemeralKey', this.inviterEphemeralPublicKey], ['linkSecret', this.linkSecret], ['d', 'nostr-double-ratchet/invite']],
171
171
  };
172
172
  }
173
173
 
174
174
  /**
175
- * Called by the invitee. Accepts the invite and creates a new channel with the inviter.
175
+ * Called by the invitee. Accepts the invite and creates a new session with the inviter.
176
176
  *
177
177
  * @param inviteeSecretKey - The invitee's secret key or a signing function
178
178
  * @param nostrSubscribe - A function to subscribe to Nostr events
179
- * @returns An object containing the new channel and an event to be published
179
+ * @returns An object containing the new session and an event to be published
180
180
  *
181
181
  * 1. Inner event: No signature, content encrypted with DH(inviter, invitee).
182
182
  * Purpose: Authenticate invitee. Contains invitee session key.
@@ -184,19 +184,19 @@ export class Invite {
184
184
  * Purpose: Contains inner event. Hides invitee from others who might have the shared Nostr key.
185
185
 
186
186
  * Note: You need to publish the returned event on Nostr using NDK or another Nostr system of your choice,
187
- * so the inviter can create the channel on their side.
187
+ * so the inviter can create the session on their side.
188
188
  */
189
189
  async accept(
190
190
  nostrSubscribe: NostrSubscribe,
191
191
  inviteePublicKey: string,
192
192
  inviteeSecretKey: Uint8Array | EncryptFunction,
193
- ): Promise<{ channel: Channel, event: VerifiedEvent }> {
193
+ ): Promise<{ session: Session, event: VerifiedEvent }> {
194
194
  const inviteeSessionKey = generateSecretKey();
195
195
  const inviteeSessionPublicKey = getPublicKey(inviteeSessionKey);
196
- const inviterPublicKey = this.inviter || this.inviterSessionPublicKey;
196
+ const inviterPublicKey = this.inviter || this.inviterEphemeralPublicKey;
197
197
 
198
198
  const sharedSecret = hexToBytes(this.linkSecret);
199
- const channel = Channel.init(nostrSubscribe, this.inviterSessionPublicKey, inviteeSessionKey, true, sharedSecret, undefined);
199
+ const session = Session.init(nostrSubscribe, this.inviterEphemeralPublicKey, inviteeSessionKey, true, sharedSecret, undefined);
200
200
 
201
201
  // Create a random keypair for the envelope sender
202
202
  const randomSenderKey = generateSecretKey();
@@ -216,22 +216,22 @@ export class Invite {
216
216
  const envelope = {
217
217
  kind: MESSAGE_EVENT_KIND,
218
218
  pubkey: randomSenderPublicKey,
219
- content: nip44.encrypt(JSON.stringify(innerEvent), getConversationKey(randomSenderKey, this.inviterSessionPublicKey)),
219
+ content: nip44.encrypt(JSON.stringify(innerEvent), getConversationKey(randomSenderKey, this.inviterEphemeralPublicKey)),
220
220
  created_at: Math.floor(Date.now() / 1000),
221
- tags: [['p', this.inviterSessionPublicKey]],
221
+ tags: [['p', this.inviterEphemeralPublicKey]],
222
222
  };
223
223
 
224
- return { channel, event: finalizeEvent(envelope, randomSenderKey) };
224
+ return { session, event: finalizeEvent(envelope, randomSenderKey) };
225
225
  }
226
226
 
227
- listen(inviterSecretKey: Uint8Array | DecryptFunction, nostrSubscribe: NostrSubscribe, onChannel: (channel: Channel, identity?: string) => void): Unsubscribe {
228
- if (!this.inviterSessionPrivateKey) {
227
+ listen(inviterSecretKey: Uint8Array | DecryptFunction, nostrSubscribe: NostrSubscribe, onSession: (session: Session, identity?: string) => void): Unsubscribe {
228
+ if (!this.inviterEphemeralPrivateKey) {
229
229
  throw new Error("Inviter session key is not available");
230
230
  }
231
231
 
232
232
  const filter = {
233
233
  kinds: [MESSAGE_EVENT_KIND],
234
- '#p': [this.inviterSessionPublicKey],
234
+ '#p': [this.inviterEphemeralPublicKey],
235
235
  };
236
236
 
237
237
  return nostrSubscribe(filter, async (event) => {
@@ -241,7 +241,7 @@ export class Invite {
241
241
  return;
242
242
  }
243
243
 
244
- const decrypted = await nip44.decrypt(event.content, getConversationKey(this.inviterSessionPrivateKey!, event.pubkey));
244
+ const decrypted = await nip44.decrypt(event.content, getConversationKey(this.inviterEphemeralPrivateKey!, event.pubkey));
245
245
  const innerEvent = JSON.parse(decrypted);
246
246
 
247
247
  if (!innerEvent.tags || !innerEvent.tags.some(([key, value]: [string, string]) => key === 'secret' && value === this.linkSecret)) {
@@ -260,11 +260,11 @@ export class Invite {
260
260
  const sharedSecret = hexToBytes(this.linkSecret);
261
261
 
262
262
  const name = event.id;
263
- const channel = Channel.init(nostrSubscribe, inviteeSessionPublicKey, this.inviterSessionPrivateKey!, false, sharedSecret, name);
263
+ const session = Session.init(nostrSubscribe, inviteeSessionPublicKey, this.inviterEphemeralPrivateKey!, false, sharedSecret, name);
264
264
 
265
- onChannel(channel, inviteeIdentity);
265
+ onSession(session, inviteeIdentity);
266
266
  } catch (error) {
267
- console.error("Error processing invite message:", error);
267
+ console.error("Error processing invite message:", error, "event", event);
268
268
  }
269
269
  });
270
270
  }
@@ -1,7 +1,7 @@
1
1
  import { generateSecretKey, getPublicKey, nip44, finalizeEvent, VerifiedEvent } from "nostr-tools";
2
2
  import { bytesToHex } from "@noble/hashes/utils";
3
3
  import {
4
- ChannelState,
4
+ SessionState,
5
5
  Header,
6
6
  Unsubscribe,
7
7
  NostrSubscribe,
@@ -13,12 +13,12 @@ import { kdf, skippedMessageIndexKey } from "./utils";
13
13
  const MAX_SKIP = 1000;
14
14
 
15
15
  /**
16
- * Double ratchet secure communication channel over Nostr
16
+ * Double ratchet secure communication session over Nostr
17
17
  *
18
18
  * Very similar to Signal's "Double Ratchet with header encryption"
19
19
  * https://signal.org/docs/specifications/doubleratchet/
20
20
  */
21
- export class Channel {
21
+ export class Session {
22
22
  private nostrUnsubscribe?: Unsubscribe;
23
23
  private nostrNextUnsubscribe?: Unsubscribe;
24
24
  private internalSubscriptions = new Map<number, MessageCallback>();
@@ -26,21 +26,21 @@ export class Channel {
26
26
  public name: string;
27
27
 
28
28
  // 1. CHANNEL PUBLIC INTERFACE
29
- constructor(private nostrSubscribe: NostrSubscribe, public state: ChannelState) {
29
+ constructor(private nostrSubscribe: NostrSubscribe, public state: SessionState) {
30
30
  this.name = Math.random().toString(36).substring(2, 6);
31
31
  }
32
32
 
33
33
  /**
34
- * Initializes a new secure communication channel
34
+ * Initializes a new secure communication session
35
35
  * @param nostrSubscribe Function to subscribe to Nostr events
36
36
  * @param theirNostrPublicKey The public key of the other party
37
37
  * @param ourCurrentPrivateKey Our current private key for Nostr
38
38
  * @param isInitiator Whether we are initiating the conversation (true) or responding (false)
39
39
  * @param sharedSecret Initial shared secret for securing the first message chain
40
- * @param name Optional name for the channel (for debugging)
41
- * @returns A new Channel instance
40
+ * @param name Optional name for the session (for debugging)
41
+ * @returns A new Session instance
42
42
  */
43
- static init(nostrSubscribe: NostrSubscribe, theirNostrPublicKey: string, ourCurrentPrivateKey: Uint8Array, isInitiator: boolean, sharedSecret: Uint8Array, name?: string): Channel {
43
+ static init(nostrSubscribe: NostrSubscribe, theirNostrPublicKey: string, ourCurrentPrivateKey: Uint8Array, isInitiator: boolean, sharedSecret: Uint8Array, name?: string): Session {
44
44
  const ourNextPrivateKey = generateSecretKey();
45
45
  const [rootKey, sendingChainKey] = kdf(sharedSecret, nip44.getConversationKey(ourNextPrivateKey, theirNostrPublicKey), 2);
46
46
  let ourCurrentNostrKey;
@@ -51,7 +51,7 @@ export class Channel {
51
51
  } else {
52
52
  ourNextNostrKey = { publicKey: getPublicKey(ourCurrentPrivateKey), privateKey: ourCurrentPrivateKey };
53
53
  }
54
- const state: ChannelState = {
54
+ const state: SessionState = {
55
55
  rootKey: isInitiator ? rootKey : sharedSecret,
56
56
  theirNostrPublicKey,
57
57
  ourCurrentNostrKey,
@@ -64,13 +64,13 @@ export class Channel {
64
64
  skippedMessageKeys: {},
65
65
  skippedHeaderKeys: {},
66
66
  };
67
- const channel = new Channel(nostrSubscribe, state);
68
- if (name) channel.name = name;
69
- return channel;
67
+ const session = new Session(nostrSubscribe, state);
68
+ if (name) session.name = name;
69
+ return session;
70
70
  }
71
71
 
72
72
  /**
73
- * Sends an encrypted message through the channel
73
+ * Sends an encrypted message through the session
74
74
  * @param data The plaintext message to send
75
75
  * @returns A verified Nostr event containing the encrypted message
76
76
  * @throws Error if we are not the initiator and trying to send the first message
@@ -96,7 +96,7 @@ export class Channel {
96
96
  }
97
97
 
98
98
  /**
99
- * Subscribes to incoming messages on this channel
99
+ * Subscribes to incoming messages on this session
100
100
  * @param callback Function to be called when a message is received
101
101
  * @returns Unsubscribe function to stop receiving messages
102
102
  */
File without changes
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from "./Channel"
1
+ export * from "./Session.ts"
2
2
  export * from "./Invite"
3
3
  export * from "./types"
4
4
  export * from "./utils"
package/src/types.ts CHANGED
@@ -26,16 +26,16 @@ export type KeyPair = {
26
26
  }
27
27
 
28
28
  /**
29
- * State of a Double Ratchet channel between two parties. Needed for persisting channels.
29
+ * State of a Double Ratchet session between two parties. Needed for persisting sessions.
30
30
  */
31
- export interface ChannelState {
31
+ export interface SessionState {
32
32
  /** Root key used to derive new sending / receiving chain keys */
33
33
  rootKey: Uint8Array;
34
34
 
35
35
  /** The other party's current Nostr public key */
36
36
  theirNostrPublicKey: string;
37
37
 
38
- /** Our current Nostr keypair used for this channel */
38
+ /** Our current Nostr keypair used for this session */
39
39
  ourCurrentNostrKey?: KeyPair;
40
40
 
41
41
  /** Our next Nostr keypair, used when ratcheting forward. It is advertised in messages we send. */
package/src/utils.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { hexToBytes, bytesToHex } from "@noble/hashes/utils";
2
- import { ChannelState, Message } from "./types";
3
- import { Channel } from "./Channel";
2
+ import { SessionState, Message } from "./types";
3
+ 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
- export function serializeChannelState(state: ChannelState): string {
7
+ export function serializeSessionState(state: SessionState): string {
8
8
  return JSON.stringify({
9
9
  rootKey: bytesToHex(state.rootKey),
10
10
  theirNostrPublicKey: state.theirNostrPublicKey,
@@ -36,7 +36,7 @@ export function serializeChannelState(state: ChannelState): string {
36
36
  });
37
37
  }
38
38
 
39
- export function deserializeChannelState(data: string): ChannelState {
39
+ export function deserializeSessionState(data: string): SessionState {
40
40
  const state = JSON.parse(data);
41
41
  return {
42
42
  rootKey: hexToBytes(state.rootKey),
@@ -69,11 +69,11 @@ export function deserializeChannelState(data: string): ChannelState {
69
69
  };
70
70
  }
71
71
 
72
- export async function* createMessageStream(channel: Channel): AsyncGenerator<Message, void, unknown> {
72
+ export async function* createMessageStream(session: Session): AsyncGenerator<Message, void, unknown> {
73
73
  const messageQueue: Message[] = [];
74
74
  let resolveNext: ((value: Message) => void) | null = null;
75
75
 
76
- const unsubscribe = channel.onMessage((message) => {
76
+ const unsubscribe = session.onMessage((message) => {
77
77
  if (resolveNext) {
78
78
  resolveNext(message);
79
79
  resolveNext = null;