nostr-double-ratchet 0.0.14 → 0.0.15
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 +13 -6
- package/dist/Session.d.ts.map +1 -1
- package/dist/nostr-double-ratchet.es.js +575 -554
- package/dist/nostr-double-ratchet.umd.js +1 -1
- package/dist/types.d.ts +12 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +2 -2
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Session.ts +48 -11
- package/src/types.ts +14 -18
- package/src/utils.ts +8 -8
package/src/Session.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
|
-
import { generateSecretKey, getPublicKey, nip44, finalizeEvent, VerifiedEvent } from "nostr-tools";
|
|
1
|
+
import { generateSecretKey, getPublicKey, nip44, finalizeEvent, VerifiedEvent, UnsignedEvent, getEventHash } from "nostr-tools";
|
|
2
2
|
import { bytesToHex } from "@noble/hashes/utils";
|
|
3
3
|
import {
|
|
4
4
|
SessionState,
|
|
5
5
|
Header,
|
|
6
6
|
Unsubscribe,
|
|
7
7
|
NostrSubscribe,
|
|
8
|
-
|
|
8
|
+
EventCallback,
|
|
9
9
|
MESSAGE_EVENT_KIND,
|
|
10
|
+
Rumor,
|
|
11
|
+
CHAT_MESSAGE_KIND,
|
|
10
12
|
} from "./types";
|
|
11
13
|
import { kdf, skippedMessageIndexKey } from "./utils";
|
|
12
14
|
|
|
13
15
|
const MAX_SKIP = 1000;
|
|
14
16
|
|
|
17
|
+
// 64 zeros
|
|
18
|
+
const DUMMY_PUBKEY = '0000000000000000000000000000000000000000000000000000000000000000'
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
21
|
* Double ratchet secure communication session over Nostr
|
|
17
22
|
*
|
|
@@ -21,7 +26,7 @@ const MAX_SKIP = 1000;
|
|
|
21
26
|
export class Session {
|
|
22
27
|
private nostrUnsubscribe?: Unsubscribe;
|
|
23
28
|
private nostrNextUnsubscribe?: Unsubscribe;
|
|
24
|
-
private internalSubscriptions = new Map<number,
|
|
29
|
+
private internalSubscriptions = new Map<number, EventCallback>();
|
|
25
30
|
private currentInternalSubscriptionId = 0;
|
|
26
31
|
public name: string;
|
|
27
32
|
|
|
@@ -70,17 +75,49 @@ export class Session {
|
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
/**
|
|
73
|
-
* Sends
|
|
74
|
-
* @param
|
|
78
|
+
* Sends a text message through the encrypted session
|
|
79
|
+
* @param text The plaintext message to send
|
|
80
|
+
* @returns A verified Nostr event containing the encrypted message
|
|
81
|
+
* @throws Error if we are not the initiator and trying to send the first message
|
|
82
|
+
*/
|
|
83
|
+
send(text: string): VerifiedEvent {
|
|
84
|
+
return this.sendEvent({
|
|
85
|
+
content: text,
|
|
86
|
+
kind: CHAT_MESSAGE_KIND
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Send a Nostr event through the encrypted session
|
|
92
|
+
* @param event Partial Nostr event to send. Must be unsigned. Id and will be generated if not provided.
|
|
75
93
|
* @returns A verified Nostr event containing the encrypted message
|
|
76
94
|
* @throws Error if we are not the initiator and trying to send the first message
|
|
77
95
|
*/
|
|
78
|
-
|
|
96
|
+
sendEvent(event: Partial<UnsignedEvent>): VerifiedEvent {
|
|
79
97
|
if (!this.state.theirNextNostrPublicKey || !this.state.ourCurrentNostrKey) {
|
|
80
98
|
throw new Error("we are not the initiator, so we can't send the first message");
|
|
81
99
|
}
|
|
82
100
|
|
|
83
|
-
|
|
101
|
+
if ("sig" in event) {
|
|
102
|
+
throw new Error("Event must be unsigned " + JSON.stringify(event));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const rumor: Partial<Rumor> = {
|
|
106
|
+
...event,
|
|
107
|
+
content: event.content || "",
|
|
108
|
+
kind: event.kind || MESSAGE_EVENT_KIND,
|
|
109
|
+
created_at: event.created_at || Math.floor(Date.now() / 1000),
|
|
110
|
+
tags: event.tags || [],
|
|
111
|
+
pubkey: event.pubkey || DUMMY_PUBKEY,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!rumor.tags!.some(([k]) => k === "ms")) {
|
|
115
|
+
rumor.tags!.push(["ms", Date.now().toString()])
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
rumor.id = getEventHash(rumor as Rumor);
|
|
119
|
+
|
|
120
|
+
const [header, encryptedData] = this.ratchetEncrypt(JSON.stringify(event));
|
|
84
121
|
|
|
85
122
|
const sharedSecret = nip44.getConversationKey(this.state.ourCurrentNostrKey.privateKey, this.state.theirNextNostrPublicKey);
|
|
86
123
|
const encryptedHeader = nip44.encrypt(JSON.stringify(header), sharedSecret);
|
|
@@ -100,7 +137,7 @@ export class Session {
|
|
|
100
137
|
* @param callback Function to be called when a message is received
|
|
101
138
|
* @returns Unsubscribe function to stop receiving messages
|
|
102
139
|
*/
|
|
103
|
-
|
|
140
|
+
onEvent(callback: EventCallback): Unsubscribe {
|
|
104
141
|
const id = this.currentInternalSubscriptionId++
|
|
105
142
|
this.internalSubscriptions.set(id, callback)
|
|
106
143
|
this.subscribeToNostrEvents()
|
|
@@ -122,7 +159,6 @@ export class Session {
|
|
|
122
159
|
const header: Header = {
|
|
123
160
|
number: this.state.sendingChainMessageNumber++,
|
|
124
161
|
nextPublicKey: this.state.ourNextNostrKey.publicKey,
|
|
125
|
-
time: Date.now(),
|
|
126
162
|
previousChainLength: this.state.previousSendingChainMessageCount
|
|
127
163
|
};
|
|
128
164
|
return [header, nip44.encrypt(plaintext, messageKey)];
|
|
@@ -283,9 +319,10 @@ export class Session {
|
|
|
283
319
|
}
|
|
284
320
|
}
|
|
285
321
|
|
|
286
|
-
const
|
|
322
|
+
const text = this.ratchetDecrypt(header, e.content, e.pubkey);
|
|
323
|
+
const innerEvent = JSON.parse(text);
|
|
287
324
|
|
|
288
|
-
this.internalSubscriptions.forEach(callback => callback(
|
|
325
|
+
this.internalSubscriptions.forEach(callback => callback(innerEvent, e));
|
|
289
326
|
}
|
|
290
327
|
|
|
291
328
|
private subscribeToNostrEvents() {
|
package/src/types.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
import { Filter, UnsignedEvent, VerifiedEvent } from "nostr-tools";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* An event that has been verified to be from the Nostr network.
|
|
5
|
-
*/
|
|
6
|
-
export type Message = {
|
|
7
|
-
id: string;
|
|
8
|
-
data: string;
|
|
9
|
-
pubkey: string;
|
|
10
|
-
time: number; // unlike Nostr, we use milliseconds instead of seconds
|
|
11
|
-
}
|
|
12
|
-
|
|
13
3
|
export type Header = {
|
|
14
4
|
number: number;
|
|
15
5
|
previousChainLength: number;
|
|
16
6
|
nextPublicKey: string;
|
|
17
|
-
time: number;
|
|
18
7
|
}
|
|
19
8
|
|
|
20
9
|
/**
|
|
@@ -76,14 +65,26 @@ export type Unsubscribe = () => void;
|
|
|
76
65
|
*/
|
|
77
66
|
export type NostrSubscribe = (filter: Filter, onEvent: (e: VerifiedEvent) => void) => Unsubscribe;
|
|
78
67
|
|
|
68
|
+
export type Rumor = UnsignedEvent & { id: string }
|
|
69
|
+
|
|
79
70
|
/**
|
|
80
71
|
* Callback function for handling decrypted messages
|
|
81
72
|
* @param message - The decrypted message object
|
|
82
73
|
*/
|
|
83
|
-
export type
|
|
74
|
+
export type EventCallback = (event: Rumor, outerEvent: VerifiedEvent) => void;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Message event kind
|
|
78
|
+
*/
|
|
79
|
+
export const MESSAGE_EVENT_KIND = 30078;
|
|
84
80
|
|
|
85
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Invite event kind
|
|
83
|
+
*/
|
|
86
84
|
export const INVITE_EVENT_KIND = 30078;
|
|
85
|
+
|
|
86
|
+
export const CHAT_MESSAGE_KIND = 14;
|
|
87
|
+
|
|
87
88
|
export const MAX_SKIP = 100;
|
|
88
89
|
|
|
89
90
|
export type NostrEvent = {
|
|
@@ -96,11 +97,6 @@ export type NostrEvent = {
|
|
|
96
97
|
sig: string;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
export enum Sender {
|
|
100
|
-
Us,
|
|
101
|
-
Them
|
|
102
|
-
}
|
|
103
|
-
|
|
104
100
|
export type EncryptFunction = (plaintext: string, pubkey: string) => Promise<string>;
|
|
105
101
|
export type DecryptFunction = (ciphertext: string, pubkey: string) => Promise<string>;
|
|
106
102
|
export type NostrPublish = (event: UnsignedEvent) => Promise<VerifiedEvent>;
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hexToBytes, bytesToHex } from "@noble/hashes/utils";
|
|
2
|
-
import {
|
|
2
|
+
import { Rumor, SessionState } from "./types";
|
|
3
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';
|
|
@@ -71,16 +71,16 @@ export function deserializeSessionState(data: string): SessionState {
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
export async function*
|
|
75
|
-
const messageQueue:
|
|
76
|
-
let resolveNext: ((value:
|
|
74
|
+
export async function* createEventStream(session: Session): AsyncGenerator<Rumor, void, unknown> {
|
|
75
|
+
const messageQueue: Rumor[] = [];
|
|
76
|
+
let resolveNext: ((value: Rumor) => void) | null = null;
|
|
77
77
|
|
|
78
|
-
const unsubscribe = session.
|
|
78
|
+
const unsubscribe = session.onEvent((event) => {
|
|
79
79
|
if (resolveNext) {
|
|
80
|
-
resolveNext(
|
|
80
|
+
resolveNext(event);
|
|
81
81
|
resolveNext = null;
|
|
82
82
|
} else {
|
|
83
|
-
messageQueue.push(
|
|
83
|
+
messageQueue.push(event);
|
|
84
84
|
}
|
|
85
85
|
});
|
|
86
86
|
|
|
@@ -89,7 +89,7 @@ export async function* createMessageStream(session: Session): AsyncGenerator<Mes
|
|
|
89
89
|
if (messageQueue.length > 0) {
|
|
90
90
|
yield messageQueue.shift()!;
|
|
91
91
|
} else {
|
|
92
|
-
yield new Promise<
|
|
92
|
+
yield new Promise<Rumor>(resolve => {
|
|
93
93
|
resolveNext = resolve;
|
|
94
94
|
});
|
|
95
95
|
}
|