nostr-double-ratchet 0.0.36 → 0.0.38
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/DeviceManager.d.ts +127 -0
- package/dist/DeviceManager.d.ts.map +1 -0
- package/dist/Invite.d.ts +3 -2
- package/dist/Invite.d.ts.map +1 -1
- package/dist/InviteList.d.ts +43 -0
- package/dist/InviteList.d.ts.map +1 -0
- package/dist/SessionManager.d.ts +86 -30
- package/dist/SessionManager.d.ts.map +1 -1
- package/dist/StorageAdapter.d.ts +9 -0
- package/dist/StorageAdapter.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/inviteUtils.d.ts +122 -0
- package/dist/inviteUtils.d.ts.map +1 -0
- package/dist/nostr-double-ratchet.es.js +3361 -2233
- package/dist/nostr-double-ratchet.umd.js +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/DeviceManager.ts +565 -0
- package/src/Invite.ts +63 -84
- package/src/InviteList.ts +333 -0
- package/src/Session.ts +1 -1
- package/src/SessionManager.ts +780 -347
- package/src/StorageAdapter.ts +64 -6
- package/src/index.ts +5 -1
- package/src/inviteUtils.ts +270 -0
- package/src/types.ts +13 -1
- package/src/utils.ts +3 -3
package/src/Invite.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { finalizeEvent, VerifiedEvent, UnsignedEvent, verifyEvent, Filter } from "nostr-tools";
|
|
2
2
|
import { INVITE_EVENT_KIND, NostrSubscribe, Unsubscribe, EncryptFunction, DecryptFunction, INVITE_RESPONSE_KIND } from "./types";
|
|
3
|
-
import { getConversationKey } from "nostr-tools/nip44";
|
|
4
3
|
import { Session } from "./Session.ts";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import {
|
|
5
|
+
generateEphemeralKeypair,
|
|
6
|
+
generateSharedSecret,
|
|
7
|
+
encryptInviteResponse,
|
|
8
|
+
decryptInviteResponse,
|
|
9
|
+
createSessionFromAccept,
|
|
10
|
+
} from "./inviteUtils";
|
|
8
11
|
|
|
9
12
|
const now = () => Math.round(Date.now() / 1000)
|
|
10
|
-
const randomNow = () => Math.round(now() - Math.random() * TWO_DAYS)
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Invite is a safe way to exchange session keys and initiate secret sessions.
|
|
@@ -36,14 +38,13 @@ export class Invite {
|
|
|
36
38
|
if (!inviter) {
|
|
37
39
|
throw new Error("Inviter public key is required");
|
|
38
40
|
}
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const sharedSecret = bytesToHex(generateSecretKey());
|
|
41
|
+
const ephemeralKeypair = generateEphemeralKeypair();
|
|
42
|
+
const sharedSecret = generateSharedSecret();
|
|
42
43
|
return new Invite(
|
|
43
|
-
|
|
44
|
+
ephemeralKeypair.publicKey,
|
|
44
45
|
sharedSecret,
|
|
45
46
|
inviter,
|
|
46
|
-
|
|
47
|
+
ephemeralKeypair.privateKey,
|
|
47
48
|
deviceId,
|
|
48
49
|
maxUses
|
|
49
50
|
);
|
|
@@ -197,8 +198,9 @@ export class Invite {
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
/**
|
|
200
|
-
* Creates
|
|
201
|
-
*
|
|
201
|
+
* Creates a tombstone event that replaces the invite, signaling device revocation.
|
|
202
|
+
* The tombstone has the same d-tag but no keys, making it invalid as an invite.
|
|
203
|
+
* Used during migration to InviteList or when revoking a device.
|
|
202
204
|
*/
|
|
203
205
|
getDeletionEvent(): UnsignedEvent {
|
|
204
206
|
if (!this.deviceId) {
|
|
@@ -207,12 +209,11 @@ export class Invite {
|
|
|
207
209
|
return {
|
|
208
210
|
kind: INVITE_EVENT_KIND,
|
|
209
211
|
pubkey: this.inviter,
|
|
210
|
-
content: "",
|
|
212
|
+
content: "",
|
|
211
213
|
created_at: Math.floor(Date.now() / 1000),
|
|
212
214
|
tags: [
|
|
213
|
-
['
|
|
214
|
-
['
|
|
215
|
-
['d', 'double-ratchet/invites/' + this.deviceId], // same d tag
|
|
215
|
+
['d', 'double-ratchet/invites/' + this.deviceId],
|
|
216
|
+
['l', 'double-ratchet/invites'],
|
|
216
217
|
],
|
|
217
218
|
};
|
|
218
219
|
}
|
|
@@ -240,52 +241,39 @@ export class Invite {
|
|
|
240
241
|
encryptor: Uint8Array | EncryptFunction,
|
|
241
242
|
deviceId?: string,
|
|
242
243
|
): Promise<{ session: Session, event: VerifiedEvent }> {
|
|
243
|
-
const
|
|
244
|
-
const inviteeSessionPublicKey = getPublicKey(inviteeSessionKey);
|
|
244
|
+
const inviteeSessionKeypair = generateEphemeralKeypair();
|
|
245
245
|
const inviterPublicKey = this.inviter || this.inviterEphemeralPublicKey;
|
|
246
246
|
|
|
247
|
-
const
|
|
248
|
-
|
|
247
|
+
const session = createSessionFromAccept({
|
|
248
|
+
nostrSubscribe,
|
|
249
|
+
theirPublicKey: this.inviterEphemeralPublicKey,
|
|
250
|
+
ourSessionPrivateKey: inviteeSessionKeypair.privateKey,
|
|
251
|
+
sharedSecret: this.sharedSecret,
|
|
252
|
+
isSender: true,
|
|
253
|
+
});
|
|
249
254
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const encrypt = typeof encryptor === 'function' ?
|
|
253
|
-
encryptor :
|
|
254
|
-
(plaintext: string, pubkey: string) => Promise.resolve(nip44.encrypt(plaintext, getConversationKey(encryptor, pubkey)));
|
|
255
|
+
const encrypt = typeof encryptor === 'function' ? encryptor : undefined;
|
|
256
|
+
const inviteePrivateKey = typeof encryptor === 'function' ? undefined : encryptor;
|
|
255
257
|
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
const encrypted = await encryptInviteResponse({
|
|
259
|
+
inviteeSessionPublicKey: inviteeSessionKeypair.publicKey,
|
|
260
|
+
inviteePublicKey,
|
|
261
|
+
inviteePrivateKey,
|
|
262
|
+
inviterPublicKey,
|
|
263
|
+
inviterEphemeralPublicKey: this.inviterEphemeralPublicKey,
|
|
264
|
+
sharedSecret: this.sharedSecret,
|
|
265
|
+
deviceId,
|
|
266
|
+
encrypt,
|
|
259
267
|
});
|
|
260
|
-
const dhEncrypted = await encrypt(payload, inviterPublicKey);
|
|
261
268
|
|
|
262
|
-
|
|
263
|
-
pubkey: inviteePublicKey,
|
|
264
|
-
content: await nip44.encrypt(dhEncrypted, sharedSecret),
|
|
265
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
266
|
-
};
|
|
267
|
-
const innerJson = JSON.stringify(innerEvent);
|
|
268
|
-
|
|
269
|
-
// Create a random keypair for the envelope sender
|
|
270
|
-
const randomSenderKey = generateSecretKey();
|
|
271
|
-
const randomSenderPublicKey = getPublicKey(randomSenderKey);
|
|
272
|
-
|
|
273
|
-
const envelope = {
|
|
274
|
-
kind: INVITE_RESPONSE_KIND,
|
|
275
|
-
pubkey: randomSenderPublicKey,
|
|
276
|
-
content: nip44.encrypt(innerJson, getConversationKey(randomSenderKey, this.inviterEphemeralPublicKey)),
|
|
277
|
-
created_at: randomNow(),
|
|
278
|
-
tags: [['p', this.inviterEphemeralPublicKey]],
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
return { session, event: finalizeEvent(envelope, randomSenderKey) };
|
|
269
|
+
return { session, event: finalizeEvent(encrypted.envelope, encrypted.randomSenderPrivateKey) };
|
|
282
270
|
}
|
|
283
271
|
|
|
284
272
|
listen(decryptor: Uint8Array | DecryptFunction, nostrSubscribe: NostrSubscribe, onSession: (_session: Session, _identity: string, _deviceId?: string) => void): Unsubscribe {
|
|
285
273
|
if (!this.inviterEphemeralPrivateKey) {
|
|
286
274
|
throw new Error("Inviter session key is not available");
|
|
287
275
|
}
|
|
288
|
-
|
|
276
|
+
|
|
289
277
|
const filter = {
|
|
290
278
|
kinds: [INVITE_RESPONSE_KIND],
|
|
291
279
|
'#p': [this.inviterEphemeralPublicKey],
|
|
@@ -297,39 +285,30 @@ export class Invite {
|
|
|
297
285
|
return;
|
|
298
286
|
}
|
|
299
287
|
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
deviceId = parsed.deviceId;
|
|
325
|
-
} catch {
|
|
326
|
-
inviteeSessionPublicKey = decryptedPayload;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const name = event.id;
|
|
330
|
-
const session = Session.init(nostrSubscribe, inviteeSessionPublicKey, this.inviterEphemeralPrivateKey!, false, sharedSecret, name);
|
|
331
|
-
|
|
332
|
-
onSession(session, inviteeIdentity, deviceId);
|
|
288
|
+
const decrypt = typeof decryptor === 'function' ? decryptor : undefined;
|
|
289
|
+
const inviterPrivateKey = typeof decryptor === 'function' ? undefined : decryptor;
|
|
290
|
+
|
|
291
|
+
const decrypted = await decryptInviteResponse({
|
|
292
|
+
envelopeContent: event.content,
|
|
293
|
+
envelopeSenderPubkey: event.pubkey,
|
|
294
|
+
inviterEphemeralPrivateKey: this.inviterEphemeralPrivateKey!,
|
|
295
|
+
inviterPrivateKey,
|
|
296
|
+
sharedSecret: this.sharedSecret,
|
|
297
|
+
decrypt,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
this.usedBy.push(decrypted.inviteeIdentity);
|
|
301
|
+
|
|
302
|
+
const session = createSessionFromAccept({
|
|
303
|
+
nostrSubscribe,
|
|
304
|
+
theirPublicKey: decrypted.inviteeSessionPublicKey,
|
|
305
|
+
ourSessionPrivateKey: this.inviterEphemeralPrivateKey!,
|
|
306
|
+
sharedSecret: this.sharedSecret,
|
|
307
|
+
isSender: false,
|
|
308
|
+
name: event.id,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
onSession(session, decrypted.inviteeIdentity, decrypted.deviceId);
|
|
333
312
|
} catch {
|
|
334
313
|
}
|
|
335
314
|
});
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { finalizeEvent, VerifiedEvent, UnsignedEvent, verifyEvent } from "nostr-tools"
|
|
2
|
+
import {
|
|
3
|
+
NostrSubscribe,
|
|
4
|
+
Unsubscribe,
|
|
5
|
+
EncryptFunction,
|
|
6
|
+
DecryptFunction,
|
|
7
|
+
INVITE_LIST_EVENT_KIND,
|
|
8
|
+
INVITE_RESPONSE_KIND,
|
|
9
|
+
} from "./types"
|
|
10
|
+
import { Session } from "./Session"
|
|
11
|
+
import {
|
|
12
|
+
generateEphemeralKeypair,
|
|
13
|
+
generateSharedSecret,
|
|
14
|
+
generateDeviceId,
|
|
15
|
+
encryptInviteResponse,
|
|
16
|
+
decryptInviteResponse,
|
|
17
|
+
createSessionFromAccept,
|
|
18
|
+
} from "./inviteUtils"
|
|
19
|
+
|
|
20
|
+
const now = () => Math.round(Date.now() / 1000)
|
|
21
|
+
|
|
22
|
+
type DeviceTag = [
|
|
23
|
+
type: "device",
|
|
24
|
+
ephemeralPublicKey: string,
|
|
25
|
+
sharedSecret: string,
|
|
26
|
+
deviceId: string,
|
|
27
|
+
createdAt: string,
|
|
28
|
+
identityPubkey: string,
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
type RemovedTag = [type: "removed", deviceId: string]
|
|
32
|
+
|
|
33
|
+
const isDeviceTag = (tag: string[]): tag is DeviceTag =>
|
|
34
|
+
tag.length >= 6 &&
|
|
35
|
+
tag[0] === "device" &&
|
|
36
|
+
tag.slice(1, 6).every((v) => typeof v === "string")
|
|
37
|
+
|
|
38
|
+
const isRemovedTag = (tag: string[]): tag is RemovedTag =>
|
|
39
|
+
tag.length >= 2 &&
|
|
40
|
+
tag[0] === "removed" &&
|
|
41
|
+
typeof tag[1] === "string"
|
|
42
|
+
|
|
43
|
+
export interface DeviceEntry {
|
|
44
|
+
ephemeralPublicKey: string
|
|
45
|
+
/** Only stored locally, not published */
|
|
46
|
+
ephemeralPrivateKey?: Uint8Array
|
|
47
|
+
sharedSecret: string
|
|
48
|
+
deviceId: string
|
|
49
|
+
deviceLabel: string
|
|
50
|
+
createdAt: number
|
|
51
|
+
/** Owner's pubkey for owner devices, delegate's own pubkey for delegate devices */
|
|
52
|
+
identityPubkey: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SerializedDeviceEntry extends Omit<DeviceEntry, 'ephemeralPrivateKey'> {
|
|
56
|
+
ephemeralPrivateKey?: number[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Manages a consolidated list of device invites (kind 10078).
|
|
61
|
+
* Single atomic event containing all device invites for a user.
|
|
62
|
+
* Uses union merge strategy for conflict resolution.
|
|
63
|
+
*/
|
|
64
|
+
export class InviteList {
|
|
65
|
+
private devices: Map<string, DeviceEntry> = new Map()
|
|
66
|
+
private removedDeviceIds: Set<string> = new Set()
|
|
67
|
+
|
|
68
|
+
constructor(
|
|
69
|
+
public readonly ownerPublicKey: string,
|
|
70
|
+
devices: DeviceEntry[] = [],
|
|
71
|
+
removedDeviceIds: string[] = [],
|
|
72
|
+
) {
|
|
73
|
+
this.removedDeviceIds = new Set(removedDeviceIds)
|
|
74
|
+
devices
|
|
75
|
+
.filter((device) => !this.removedDeviceIds.has(device.deviceId))
|
|
76
|
+
.forEach((device) => this.devices.set(device.deviceId, device))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
createDevice(label: string, deviceId?: string): DeviceEntry {
|
|
80
|
+
const keypair = generateEphemeralKeypair()
|
|
81
|
+
return {
|
|
82
|
+
ephemeralPublicKey: keypair.publicKey,
|
|
83
|
+
ephemeralPrivateKey: keypair.privateKey,
|
|
84
|
+
sharedSecret: generateSharedSecret(),
|
|
85
|
+
deviceId: deviceId || generateDeviceId(),
|
|
86
|
+
deviceLabel: label,
|
|
87
|
+
createdAt: now(),
|
|
88
|
+
identityPubkey: this.ownerPublicKey,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
addDevice(device: DeviceEntry): void {
|
|
93
|
+
if (this.removedDeviceIds.has(device.deviceId)) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (!this.devices.has(device.deviceId)) {
|
|
97
|
+
this.devices.set(device.deviceId, device)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
removeDevice(deviceId: string): void {
|
|
102
|
+
this.devices.delete(deviceId)
|
|
103
|
+
this.removedDeviceIds.add(deviceId)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getDevice(deviceId: string): DeviceEntry | undefined {
|
|
107
|
+
return this.devices.get(deviceId)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getAllDevices(): DeviceEntry[] {
|
|
111
|
+
return Array.from(this.devices.values())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getRemovedDeviceIds(): string[] {
|
|
115
|
+
return Array.from(this.removedDeviceIds)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
updateDeviceLabel(deviceId: string, newLabel: string): void {
|
|
119
|
+
const device = this.devices.get(deviceId)
|
|
120
|
+
if (device) {
|
|
121
|
+
device.deviceLabel = newLabel
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getEvent(): UnsignedEvent {
|
|
126
|
+
const deviceTags = this.getAllDevices().map((device) => [
|
|
127
|
+
"device",
|
|
128
|
+
device.ephemeralPublicKey,
|
|
129
|
+
device.sharedSecret,
|
|
130
|
+
device.deviceId,
|
|
131
|
+
String(device.createdAt),
|
|
132
|
+
device.identityPubkey,
|
|
133
|
+
])
|
|
134
|
+
|
|
135
|
+
const removedTags = this.getRemovedDeviceIds().map((deviceId) => [
|
|
136
|
+
"removed",
|
|
137
|
+
deviceId,
|
|
138
|
+
])
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
kind: INVITE_LIST_EVENT_KIND,
|
|
142
|
+
pubkey: this.ownerPublicKey,
|
|
143
|
+
content: "",
|
|
144
|
+
created_at: now(),
|
|
145
|
+
tags: [
|
|
146
|
+
["d", "double-ratchet/invite-list"],
|
|
147
|
+
["version", "1"],
|
|
148
|
+
...deviceTags,
|
|
149
|
+
...removedTags,
|
|
150
|
+
],
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static fromEvent(event: VerifiedEvent): InviteList {
|
|
155
|
+
if (!event.sig) {
|
|
156
|
+
throw new Error("Event is not signed")
|
|
157
|
+
}
|
|
158
|
+
if (!verifyEvent(event)) {
|
|
159
|
+
throw new Error("Event signature is invalid")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const devices = event.tags
|
|
163
|
+
.filter(isDeviceTag)
|
|
164
|
+
.map(([, ephemeralPublicKey, sharedSecret, deviceId, createdAt, identityPubkey]) => ({
|
|
165
|
+
ephemeralPublicKey,
|
|
166
|
+
sharedSecret,
|
|
167
|
+
deviceId,
|
|
168
|
+
deviceLabel: deviceId,
|
|
169
|
+
createdAt: parseInt(createdAt, 10) || event.created_at,
|
|
170
|
+
identityPubkey,
|
|
171
|
+
}))
|
|
172
|
+
|
|
173
|
+
const removedDeviceIds = event.tags
|
|
174
|
+
.filter(isRemovedTag)
|
|
175
|
+
.map(([, deviceId]) => deviceId)
|
|
176
|
+
|
|
177
|
+
return new InviteList(event.pubkey, devices, removedDeviceIds)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
serialize(): string {
|
|
181
|
+
const devices = this.getAllDevices().map((d) => ({
|
|
182
|
+
...d,
|
|
183
|
+
ephemeralPrivateKey: d.ephemeralPrivateKey
|
|
184
|
+
? Array.from(d.ephemeralPrivateKey)
|
|
185
|
+
: undefined,
|
|
186
|
+
}))
|
|
187
|
+
|
|
188
|
+
return JSON.stringify({
|
|
189
|
+
ownerPublicKey: this.ownerPublicKey,
|
|
190
|
+
devices,
|
|
191
|
+
removedDeviceIds: this.getRemovedDeviceIds(),
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static deserialize(json: string): InviteList {
|
|
196
|
+
const data = JSON.parse(json) as { ownerPublicKey: string; devices: SerializedDeviceEntry[]; removedDeviceIds?: string[] }
|
|
197
|
+
const devices: DeviceEntry[] = data.devices.map((d) => ({
|
|
198
|
+
...d,
|
|
199
|
+
ephemeralPrivateKey: d.ephemeralPrivateKey
|
|
200
|
+
? new Uint8Array(d.ephemeralPrivateKey)
|
|
201
|
+
: undefined,
|
|
202
|
+
}))
|
|
203
|
+
|
|
204
|
+
return new InviteList(data.ownerPublicKey, devices, data.removedDeviceIds || [])
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
merge(other: InviteList): InviteList {
|
|
208
|
+
const mergedRemovedIds = new Set([
|
|
209
|
+
...this.removedDeviceIds,
|
|
210
|
+
...other.removedDeviceIds,
|
|
211
|
+
])
|
|
212
|
+
|
|
213
|
+
const mergedDevices = [...this.devices.values(), ...other.devices.values()]
|
|
214
|
+
.reduce((map, device) => {
|
|
215
|
+
const existing = map.get(device.deviceId)
|
|
216
|
+
map.set(device.deviceId, existing
|
|
217
|
+
? { ...device, ephemeralPrivateKey: existing.ephemeralPrivateKey || device.ephemeralPrivateKey }
|
|
218
|
+
: device
|
|
219
|
+
)
|
|
220
|
+
return map
|
|
221
|
+
}, new Map<string, DeviceEntry>())
|
|
222
|
+
|
|
223
|
+
const activeDevices = Array.from(mergedDevices.values())
|
|
224
|
+
.filter((device) => !mergedRemovedIds.has(device.deviceId))
|
|
225
|
+
|
|
226
|
+
return new InviteList(
|
|
227
|
+
this.ownerPublicKey,
|
|
228
|
+
activeDevices,
|
|
229
|
+
Array.from(mergedRemovedIds)
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async accept(
|
|
234
|
+
deviceId: string,
|
|
235
|
+
nostrSubscribe: NostrSubscribe,
|
|
236
|
+
inviteePublicKey: string,
|
|
237
|
+
encryptor: Uint8Array | EncryptFunction,
|
|
238
|
+
inviteeDeviceId?: string
|
|
239
|
+
): Promise<{ session: Session; event: VerifiedEvent }> {
|
|
240
|
+
const device = this.devices.get(deviceId)
|
|
241
|
+
if (!device) {
|
|
242
|
+
throw new Error(`Device ${deviceId} not found in invite list`)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const inviteeSessionKeypair = generateEphemeralKeypair()
|
|
246
|
+
|
|
247
|
+
const session = createSessionFromAccept({
|
|
248
|
+
nostrSubscribe,
|
|
249
|
+
theirPublicKey: device.ephemeralPublicKey,
|
|
250
|
+
ourSessionPrivateKey: inviteeSessionKeypair.privateKey,
|
|
251
|
+
sharedSecret: device.sharedSecret,
|
|
252
|
+
isSender: true,
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
const encrypt = typeof encryptor === "function" ? encryptor : undefined
|
|
256
|
+
const inviteePrivateKey = typeof encryptor === "function" ? undefined : encryptor
|
|
257
|
+
|
|
258
|
+
const encrypted = await encryptInviteResponse({
|
|
259
|
+
inviteeSessionPublicKey: inviteeSessionKeypair.publicKey,
|
|
260
|
+
inviteePublicKey,
|
|
261
|
+
inviteePrivateKey,
|
|
262
|
+
inviterPublicKey: device.identityPubkey,
|
|
263
|
+
inviterEphemeralPublicKey: device.ephemeralPublicKey,
|
|
264
|
+
sharedSecret: device.sharedSecret,
|
|
265
|
+
deviceId: inviteeDeviceId,
|
|
266
|
+
encrypt,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
session,
|
|
271
|
+
event: finalizeEvent(encrypted.envelope, encrypted.randomSenderPrivateKey),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
listen(
|
|
276
|
+
decryptor: Uint8Array | DecryptFunction,
|
|
277
|
+
nostrSubscribe: NostrSubscribe,
|
|
278
|
+
onSession: (
|
|
279
|
+
session: Session,
|
|
280
|
+
identity: string,
|
|
281
|
+
deviceId?: string,
|
|
282
|
+
ourDeviceId?: string
|
|
283
|
+
) => void
|
|
284
|
+
): Unsubscribe {
|
|
285
|
+
const devices = this.getAllDevices()
|
|
286
|
+
const decryptableDevices = devices.filter((d) => !!d.ephemeralPrivateKey)
|
|
287
|
+
|
|
288
|
+
// If we don't have any devices we can decrypt for, do nothing gracefully
|
|
289
|
+
if (decryptableDevices.length === 0) {
|
|
290
|
+
return () => {}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const ephemeralPubkeys = decryptableDevices.map((d) => d.ephemeralPublicKey)
|
|
294
|
+
const decrypt = typeof decryptor === "function" ? decryptor : undefined
|
|
295
|
+
const ownerPrivateKey = typeof decryptor === "function" ? undefined : decryptor
|
|
296
|
+
|
|
297
|
+
return nostrSubscribe(
|
|
298
|
+
{ kinds: [INVITE_RESPONSE_KIND], "#p": ephemeralPubkeys },
|
|
299
|
+
async (event) => {
|
|
300
|
+
const targetPubkey = event.tags.find((t) => t[0] === "p")?.[1]
|
|
301
|
+
const device = decryptableDevices.find((d) => d.ephemeralPublicKey === targetPubkey)
|
|
302
|
+
|
|
303
|
+
if (!device || !device.ephemeralPrivateKey) {
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const decrypted = await decryptInviteResponse({
|
|
309
|
+
envelopeContent: event.content,
|
|
310
|
+
envelopeSenderPubkey: event.pubkey,
|
|
311
|
+
inviterEphemeralPrivateKey: device.ephemeralPrivateKey,
|
|
312
|
+
inviterPrivateKey: ownerPrivateKey,
|
|
313
|
+
sharedSecret: device.sharedSecret,
|
|
314
|
+
decrypt,
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const session = createSessionFromAccept({
|
|
318
|
+
nostrSubscribe,
|
|
319
|
+
theirPublicKey: decrypted.inviteeSessionPublicKey,
|
|
320
|
+
ourSessionPrivateKey: device.ephemeralPrivateKey,
|
|
321
|
+
sharedSecret: device.sharedSecret,
|
|
322
|
+
isSender: false,
|
|
323
|
+
name: event.id,
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
onSession(session, decrypted.inviteeIdentity, decrypted.deviceId, device.deviceId)
|
|
327
|
+
} catch {
|
|
328
|
+
// Invalid response
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
}
|
package/src/Session.ts
CHANGED
|
@@ -393,7 +393,7 @@ export class Session {
|
|
|
393
393
|
this.nostrUnsubscribe = this.nostrSubscribe(
|
|
394
394
|
{authors: [this.state.theirCurrentNostrPublicKey], kinds: [MESSAGE_EVENT_KIND]},
|
|
395
395
|
(e) => this.handleNostrEvent(e)
|
|
396
|
-
);
|
|
396
|
+
);
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
const skippedAuthors = Object.keys(this.state.skippedKeys);
|