libp2r2p 0.0.2 → 0.1.0
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/base93/index.js +193 -0
- package/package.json +23 -1
- package/AGENTS.md +0 -20
- package/tests/base16.test.js +0 -19
- package/tests/base64.test.js +0 -18
- package/tests/content-key-event.test.js +0 -48
- package/tests/fixtures/nip44v3-vectors.json +0 -1
- package/tests/helpers/test-signer.js +0 -185
- package/tests/idb-queue.test.js +0 -153
- package/tests/idb.test.js +0 -91
- package/tests/nip44-v3.test.js +0 -73
- package/tests/nip46.test.js +0 -668
- package/tests/private-channel.test.js +0 -1912
- package/tests/private-message.test.js +0 -501
- package/tests/private-messenger.test.js +0 -1737
- package/tests/queries.test.js +0 -101
- package/tests/queue-parity.test.js +0 -105
- package/tests/relay-hll.test.js +0 -32
- package/tests/relay-pool.test.js +0 -2063
- package/tests/temporary-storage.test.js +0 -89
- package/tests/web-storage-queue.test.js +0 -480
|
@@ -1,1912 +0,0 @@
|
|
|
1
|
-
import { afterEach, test } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { finalizeEvent, generateSecretKey, getEventHash, getPublicKey } from 'nostr-tools'
|
|
4
|
-
import NsecSigner, { deriveSharedKey } from './helpers/test-signer.js'
|
|
5
|
-
import { deriveDoubleDhConversationKey } from '../double-dh/index.js'
|
|
6
|
-
import {
|
|
7
|
-
EXPIRATION_SECONDS,
|
|
8
|
-
eventFromNymCarriers,
|
|
9
|
-
fetch,
|
|
10
|
-
getJsonlChunkByteSize,
|
|
11
|
-
getNymCarrierChunkSize,
|
|
12
|
-
MAX_EVENT_BYTES,
|
|
13
|
-
NYM_CARRIER_KIND,
|
|
14
|
-
PRIVATE_BROADCAST_KIND,
|
|
15
|
-
publish,
|
|
16
|
-
publishNymEvent,
|
|
17
|
-
ROUTER_KIND,
|
|
18
|
-
subscribe,
|
|
19
|
-
unwrapEvent,
|
|
20
|
-
wrapEvent,
|
|
21
|
-
wrapEvents,
|
|
22
|
-
wrapNymEvent
|
|
23
|
-
} from '../private-channel/index.js'
|
|
24
|
-
import { createReceivedChunkStore, DEFAULT_RECEIVED_CHUNK_MAX_BYTES } from '../private-channel/services/received-chunks.js'
|
|
25
|
-
import { makeContentKeyEvent, parseContentKeyEvent, verifyContentKeyProof } from '../content-key/event/index.js'
|
|
26
|
-
import { TEMPORARY_STORAGE_KEYS_KEY } from '../temporary-storage/index.js'
|
|
27
|
-
import { bytesToBase64, base64ToBytes } from '../base64/index.js'
|
|
28
|
-
import { bytesToHex, hexToBytes } from '../base16/index.js'
|
|
29
|
-
import { relayPool } from '../relay/index.js'
|
|
30
|
-
|
|
31
|
-
const originalEventsFeedGenerator = relayPool.getEventsFeedGenerator
|
|
32
|
-
const originalLiveEventsGenerator = relayPool.getLiveEventsGenerator
|
|
33
|
-
let mockedSubscribeMany = null
|
|
34
|
-
|
|
35
|
-
// Adapts the old callback fixture style to the async-generator RelayPool API.
|
|
36
|
-
// Resolving onevent waits until private-channel has consumed that event.
|
|
37
|
-
function createMockEventsGenerator (subscribeMany) {
|
|
38
|
-
return (filter, relays, { signal } = {}) => {
|
|
39
|
-
const pending = []
|
|
40
|
-
let wake = Promise.withResolvers()
|
|
41
|
-
let current = null
|
|
42
|
-
let closed = false
|
|
43
|
-
let subscription
|
|
44
|
-
|
|
45
|
-
const close = () => {
|
|
46
|
-
if (closed) return
|
|
47
|
-
closed = true
|
|
48
|
-
current?.resolve()
|
|
49
|
-
current = null
|
|
50
|
-
for (const entry of pending.splice(0)) entry.resolve()
|
|
51
|
-
subscription?.close?.()
|
|
52
|
-
wake.resolve()
|
|
53
|
-
}
|
|
54
|
-
const handlers = {
|
|
55
|
-
onevent: event => new Promise(resolve => {
|
|
56
|
-
if (closed) return resolve()
|
|
57
|
-
pending.push({ event, resolve })
|
|
58
|
-
wake.resolve()
|
|
59
|
-
wake = Promise.withResolvers()
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
subscription = subscribeMany(relays, filter, handlers)
|
|
63
|
-
signal?.addEventListener('abort', close, { once: true })
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
async next () {
|
|
67
|
-
current?.resolve()
|
|
68
|
-
current = null
|
|
69
|
-
while (!closed && pending.length === 0) await wake.promise
|
|
70
|
-
if (closed) return { done: true }
|
|
71
|
-
current = pending.shift()
|
|
72
|
-
return { value: current.event, done: false }
|
|
73
|
-
},
|
|
74
|
-
async return () {
|
|
75
|
-
close()
|
|
76
|
-
return { done: true }
|
|
77
|
-
},
|
|
78
|
-
[Symbol.asyncIterator] () { return this }
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const pool = {
|
|
84
|
-
get subscribeMany () {
|
|
85
|
-
return mockedSubscribeMany
|
|
86
|
-
},
|
|
87
|
-
set subscribeMany (subscribeMany) {
|
|
88
|
-
mockedSubscribeMany = subscribeMany
|
|
89
|
-
relayPool.getEventsFeedGenerator = subscribeMany
|
|
90
|
-
? createMockEventsGenerator(subscribeMany)
|
|
91
|
-
: originalEventsFeedGenerator
|
|
92
|
-
relayPool.getLiveEventsGenerator = subscribeMany
|
|
93
|
-
? createMockEventsGenerator(subscribeMany)
|
|
94
|
-
: originalLiveEventsGenerator
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!globalThis.localStorage) {
|
|
99
|
-
const data = new Map()
|
|
100
|
-
globalThis.localStorage = {
|
|
101
|
-
clear: () => data.clear(),
|
|
102
|
-
getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
|
|
103
|
-
key: index => [...data.keys()][index] || null,
|
|
104
|
-
removeItem: key => { data.delete(String(key)) },
|
|
105
|
-
setItem: (key, value) => { data.set(String(key), String(value)) },
|
|
106
|
-
get length () { return data.size }
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!globalThis.sessionStorage) {
|
|
111
|
-
const data = new Map()
|
|
112
|
-
globalThis.sessionStorage = {
|
|
113
|
-
clear: () => data.clear(),
|
|
114
|
-
getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
|
|
115
|
-
key: index => [...data.keys()][index] || null,
|
|
116
|
-
removeItem: key => { data.delete(String(key)) },
|
|
117
|
-
setItem: (key, value) => { data.set(String(key), String(value)) },
|
|
118
|
-
get length () { return data.size }
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!globalThis.crypto) globalThis.crypto = crypto
|
|
123
|
-
if (!globalThis.btoa) globalThis.btoa = s => Buffer.from(s, 'binary').toString('base64')
|
|
124
|
-
if (!globalThis.atob) globalThis.atob = s => Buffer.from(s, 'base64').toString('binary')
|
|
125
|
-
|
|
126
|
-
afterEach(() => {
|
|
127
|
-
pool.subscribeMany = null
|
|
128
|
-
NsecSigner.releaseAll()
|
|
129
|
-
globalThis.localStorage.clear()
|
|
130
|
-
globalThis.sessionStorage.clear()
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
function signer () {
|
|
134
|
-
return NsecSigner.getOrCreate(bytesToHex(generateSecretKey()))
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const encoder = new TextEncoder()
|
|
138
|
-
const decoder = new TextDecoder()
|
|
139
|
-
|
|
140
|
-
function textToBase64 (text) {
|
|
141
|
-
return bytesToBase64(encoder.encode(text))
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function nip44v3EncryptText (signer, peerPubkey, kind, plaintext) {
|
|
145
|
-
return signer.nip44v3Encrypt(peerPubkey, kind, '', textToBase64(plaintext))
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function nip44v3DecryptText (signer, peerPubkey, kind, ciphertext) {
|
|
149
|
-
return decoder.decode(base64ToBytes(await signer.nip44v3Decrypt(peerPubkey, kind, '', ciphertext)))
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function decryptPrivateBroadcast (signer, peerPubkey, ciphertext) {
|
|
153
|
-
return JSON.parse(await nip44v3DecryptText(signer, peerPubkey, PRIVATE_BROADCAST_KIND, ciphertext))
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function encryptPrivateBroadcast (signer, peerPubkey, value) {
|
|
157
|
-
return nip44v3EncryptText(signer, peerPubkey, PRIVATE_BROADCAST_KIND, JSON.stringify(value))
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function signerWithInternalContentKey (identitySigner, contentSigner) {
|
|
161
|
-
const identityPubkey = await identitySigner.getPublicKey()
|
|
162
|
-
NsecSigner.setContentSigners(identitySigner, [contentSigner])
|
|
163
|
-
return {
|
|
164
|
-
getPublicKey: () => identityPubkey,
|
|
165
|
-
signEvent: event => identitySigner.signEvent(event),
|
|
166
|
-
nip44Encrypt: (peerPubkey, plaintext) => identitySigner.nip44Encrypt(peerPubkey, plaintext),
|
|
167
|
-
nip44Decrypt: (peerPubkey, ciphertext) => identitySigner.nip44Decrypt(peerPubkey, ciphertext),
|
|
168
|
-
nip44v3Encrypt: (peerPubkey, kind, scope, plaintextB64) => identitySigner.nip44v3Encrypt(peerPubkey, kind, scope, plaintextB64),
|
|
169
|
-
nip44v3Decrypt: (peerPubkey, kind, scope, ciphertext) => identitySigner.nip44v3Decrypt(peerPubkey, kind, scope, ciphertext),
|
|
170
|
-
nip44EncryptDoubleDH: (...params) => identitySigner.nip44EncryptDoubleDH(...params),
|
|
171
|
-
nip44DecryptDoubleDH: (...params) => identitySigner.nip44DecryptDoubleDH(...params)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function eventFixture (content = 'hello') {
|
|
176
|
-
return { kind: 1, created_at: 1, tags: [], content }
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function unwrappedFixture (event, pubkey) {
|
|
180
|
-
const unwrapped = { ...event, pubkey }
|
|
181
|
-
return { ...unwrapped, id: getEventHash(unwrapped) }
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function noContentKeys () {
|
|
185
|
-
return {}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function routerJsonlRows (router) {
|
|
189
|
-
return routerJsonlLines(router).map(line => JSON.parse(line))
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function routerJsonlLines (router) {
|
|
193
|
-
return new TextDecoder()
|
|
194
|
-
.decode(Buffer.from(router.content, 'base64'))
|
|
195
|
-
.trim()
|
|
196
|
-
.split('\n')
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function routerRecipientRows (router) {
|
|
200
|
-
return routerJsonlRows(router).filter(row => row.length !== 1)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
test('shared-key signer derives matching deniable key from either side', async () => {
|
|
204
|
-
const alice = signer()
|
|
205
|
-
const bob = signer()
|
|
206
|
-
const alicePubkey = await alice.getPublicKey()
|
|
207
|
-
const bobPubkey = await bob.getPublicKey()
|
|
208
|
-
const aliceShared = alice.withSharedKey(bobPubkey)
|
|
209
|
-
const bobShared = bob.withSharedKey(alicePubkey)
|
|
210
|
-
|
|
211
|
-
assert.equal(await aliceShared.getPublicKey(), await bobShared.getPublicKey())
|
|
212
|
-
const sharedPubkey = await aliceShared.getPublicKey()
|
|
213
|
-
const ciphertext = await aliceShared.nip44Encrypt(sharedPubkey, 'secret')
|
|
214
|
-
assert.equal(await bobShared.nip44Decrypt(sharedPubkey, ciphertext), 'secret')
|
|
215
|
-
|
|
216
|
-
const syncShared = alice.withSharedKey(bobPubkey, 'trusted-signer-sync-v1')
|
|
217
|
-
assert.notEqual(await syncShared.getPublicKey(), sharedPubkey)
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
test('shared-key derivation matches the 48-byte reduction vector', async () => {
|
|
221
|
-
const aliceSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001')
|
|
222
|
-
const bobSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000002')
|
|
223
|
-
const alicePubkey = getPublicKey(aliceSecret)
|
|
224
|
-
const bobPubkey = getPublicKey(bobSecret)
|
|
225
|
-
|
|
226
|
-
const aliceShared = await deriveSharedKey(aliceSecret, bobPubkey, 'trusted-signer-sync-v1')
|
|
227
|
-
const bobShared = await deriveSharedKey(bobSecret, alicePubkey, 'trusted-signer-sync-v1')
|
|
228
|
-
|
|
229
|
-
assert.equal(bytesToHex(aliceShared), '10228022a480c7db02f2d68796e30f029dd77853dca392174e623ea9fc275de0')
|
|
230
|
-
assert.equal(bytesToHex(bobShared), bytesToHex(aliceShared))
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
test('double-DH signer round-trips every content-key mode', async () => {
|
|
234
|
-
const alice = signer()
|
|
235
|
-
const bob = signer()
|
|
236
|
-
const aliceContent = signer()
|
|
237
|
-
const bobContent = signer()
|
|
238
|
-
const alicePubkey = await alice.getPublicKey()
|
|
239
|
-
const bobPubkey = await bob.getPublicKey()
|
|
240
|
-
const aliceContentPubkey = await aliceContent.getPublicKey()
|
|
241
|
-
const bobContentPubkey = await bobContent.getPublicKey()
|
|
242
|
-
|
|
243
|
-
const cases = [
|
|
244
|
-
{ mode: 'identity' },
|
|
245
|
-
{ mode: 'sender-content', aliceContent },
|
|
246
|
-
{ mode: 'receiver-content', bobContent },
|
|
247
|
-
{ mode: 'both-content', aliceContent, bobContent }
|
|
248
|
-
]
|
|
249
|
-
|
|
250
|
-
for (const c of cases) {
|
|
251
|
-
NsecSigner.setContentSigners(alice, c.aliceContent ? [aliceContent] : [])
|
|
252
|
-
NsecSigner.setContentSigners(bob, c.bobContent ? [bobContent] : [])
|
|
253
|
-
const encrypted = await alice.nip44EncryptDoubleDH(
|
|
254
|
-
bobPubkey,
|
|
255
|
-
ROUTER_KIND,
|
|
256
|
-
'',
|
|
257
|
-
textToBase64(c.mode),
|
|
258
|
-
c.bobContent ? bobContentPubkey : ''
|
|
259
|
-
)
|
|
260
|
-
const decrypted = await bob.nip44DecryptDoubleDH(
|
|
261
|
-
alicePubkey,
|
|
262
|
-
ROUTER_KIND,
|
|
263
|
-
'',
|
|
264
|
-
encrypted[0],
|
|
265
|
-
encrypted[1],
|
|
266
|
-
c.bobContent ? bobContentPubkey : ''
|
|
267
|
-
)
|
|
268
|
-
|
|
269
|
-
assert.equal(encrypted[1], c.aliceContent ? aliceContentPubkey : '')
|
|
270
|
-
assert.equal(decoder.decode(base64ToBytes(decrypted)), c.mode)
|
|
271
|
-
if (c.mode === 'both-content') {
|
|
272
|
-
await assert.rejects(
|
|
273
|
-
() => bob.nip44DecryptDoubleDH(alicePubkey, ROUTER_KIND + 1, '', encrypted[0], aliceContentPubkey, bobContentPubkey),
|
|
274
|
-
/kind mismatch/
|
|
275
|
-
)
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
test('double-DH conversation key matches the fixed salt vector', () => {
|
|
281
|
-
const aliceSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000001')
|
|
282
|
-
const bobSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000002')
|
|
283
|
-
const aliceContentSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000003')
|
|
284
|
-
const bobContentSecret = hexToBytes('0000000000000000000000000000000000000000000000000000000000000004')
|
|
285
|
-
const alicePubkey = getPublicKey(aliceSecret)
|
|
286
|
-
const bobPubkey = getPublicKey(bobSecret)
|
|
287
|
-
const aliceContentPubkey = getPublicKey(aliceContentSecret)
|
|
288
|
-
const bobContentPubkey = getPublicKey(bobContentSecret)
|
|
289
|
-
const { mode, conversationKey } = deriveDoubleDhConversationKey({
|
|
290
|
-
role: 'sender',
|
|
291
|
-
identitySecretKey: aliceSecret,
|
|
292
|
-
identityPubkey: alicePubkey,
|
|
293
|
-
contentSecretKey: aliceContentSecret,
|
|
294
|
-
contentPubkey: aliceContentPubkey,
|
|
295
|
-
peerIdentityPubkey: bobPubkey,
|
|
296
|
-
peerContentPubkey: bobContentPubkey,
|
|
297
|
-
kind: ROUTER_KIND,
|
|
298
|
-
scope: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
assert.equal(mode, 'both-content')
|
|
302
|
-
assert.equal(bytesToHex(conversationKey), '995fec3e81d662f029fc4218bb3cf52305957c849c7b8356bd5d71852f4629b0')
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
test('double-DH conversation key is pair-oriented, not direction-oriented', () => {
|
|
306
|
-
const aliceSecret = generateSecretKey()
|
|
307
|
-
const bobSecret = generateSecretKey()
|
|
308
|
-
const aliceContentSecret = generateSecretKey()
|
|
309
|
-
const bobContentSecret = generateSecretKey()
|
|
310
|
-
const alicePubkey = getPublicKey(aliceSecret)
|
|
311
|
-
const bobPubkey = getPublicKey(bobSecret)
|
|
312
|
-
const aliceContentPubkey = getPublicKey(aliceContentSecret)
|
|
313
|
-
const bobContentPubkey = getPublicKey(bobContentSecret)
|
|
314
|
-
const aliceToBob = deriveDoubleDhConversationKey({
|
|
315
|
-
role: 'sender',
|
|
316
|
-
identitySecretKey: aliceSecret,
|
|
317
|
-
identityPubkey: alicePubkey,
|
|
318
|
-
contentSecretKey: aliceContentSecret,
|
|
319
|
-
contentPubkey: aliceContentPubkey,
|
|
320
|
-
peerIdentityPubkey: bobPubkey,
|
|
321
|
-
peerContentPubkey: bobContentPubkey,
|
|
322
|
-
kind: ROUTER_KIND,
|
|
323
|
-
scope: ''
|
|
324
|
-
})
|
|
325
|
-
const bobReceiving = deriveDoubleDhConversationKey({
|
|
326
|
-
role: 'receiver',
|
|
327
|
-
identitySecretKey: bobSecret,
|
|
328
|
-
identityPubkey: bobPubkey,
|
|
329
|
-
contentSecretKey: bobContentSecret,
|
|
330
|
-
contentPubkey: bobContentPubkey,
|
|
331
|
-
peerIdentityPubkey: alicePubkey,
|
|
332
|
-
peerContentPubkey: aliceContentPubkey,
|
|
333
|
-
kind: ROUTER_KIND,
|
|
334
|
-
scope: ''
|
|
335
|
-
})
|
|
336
|
-
const bobToAlice = deriveDoubleDhConversationKey({
|
|
337
|
-
role: 'sender',
|
|
338
|
-
identitySecretKey: bobSecret,
|
|
339
|
-
identityPubkey: bobPubkey,
|
|
340
|
-
contentSecretKey: bobContentSecret,
|
|
341
|
-
contentPubkey: bobContentPubkey,
|
|
342
|
-
peerIdentityPubkey: alicePubkey,
|
|
343
|
-
peerContentPubkey: aliceContentPubkey,
|
|
344
|
-
kind: ROUTER_KIND,
|
|
345
|
-
scope: ''
|
|
346
|
-
})
|
|
347
|
-
const aliceToBobInChannel = deriveDoubleDhConversationKey({
|
|
348
|
-
role: 'sender',
|
|
349
|
-
identitySecretKey: aliceSecret,
|
|
350
|
-
identityPubkey: alicePubkey,
|
|
351
|
-
contentSecretKey: aliceContentSecret,
|
|
352
|
-
contentPubkey: aliceContentPubkey,
|
|
353
|
-
peerIdentityPubkey: bobPubkey,
|
|
354
|
-
peerContentPubkey: bobContentPubkey,
|
|
355
|
-
kind: ROUTER_KIND,
|
|
356
|
-
scope: '1'.repeat(64)
|
|
357
|
-
})
|
|
358
|
-
const bobReceivingInChannel = deriveDoubleDhConversationKey({
|
|
359
|
-
role: 'receiver',
|
|
360
|
-
identitySecretKey: bobSecret,
|
|
361
|
-
identityPubkey: bobPubkey,
|
|
362
|
-
contentSecretKey: bobContentSecret,
|
|
363
|
-
contentPubkey: bobContentPubkey,
|
|
364
|
-
peerIdentityPubkey: alicePubkey,
|
|
365
|
-
peerContentPubkey: aliceContentPubkey,
|
|
366
|
-
kind: ROUTER_KIND,
|
|
367
|
-
scope: '1'.repeat(64)
|
|
368
|
-
})
|
|
369
|
-
const aliceToBobInOtherChannel = deriveDoubleDhConversationKey({
|
|
370
|
-
role: 'sender',
|
|
371
|
-
identitySecretKey: aliceSecret,
|
|
372
|
-
identityPubkey: alicePubkey,
|
|
373
|
-
contentSecretKey: aliceContentSecret,
|
|
374
|
-
contentPubkey: aliceContentPubkey,
|
|
375
|
-
peerIdentityPubkey: bobPubkey,
|
|
376
|
-
peerContentPubkey: bobContentPubkey,
|
|
377
|
-
kind: ROUTER_KIND,
|
|
378
|
-
scope: '2'.repeat(64)
|
|
379
|
-
})
|
|
380
|
-
const aliceToBobInOtherKind = deriveDoubleDhConversationKey({
|
|
381
|
-
role: 'sender',
|
|
382
|
-
identitySecretKey: aliceSecret,
|
|
383
|
-
identityPubkey: alicePubkey,
|
|
384
|
-
contentSecretKey: aliceContentSecret,
|
|
385
|
-
contentPubkey: aliceContentPubkey,
|
|
386
|
-
peerIdentityPubkey: bobPubkey,
|
|
387
|
-
peerContentPubkey: bobContentPubkey,
|
|
388
|
-
kind: ROUTER_KIND + 1,
|
|
389
|
-
scope: '1'.repeat(64)
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
assert.equal(bytesToHex(aliceToBob.conversationKey), bytesToHex(bobReceiving.conversationKey))
|
|
393
|
-
assert.equal(bytesToHex(aliceToBob.conversationKey), bytesToHex(bobToAlice.conversationKey))
|
|
394
|
-
assert.equal(bytesToHex(aliceToBobInChannel.conversationKey), bytesToHex(bobReceivingInChannel.conversationKey))
|
|
395
|
-
assert.notEqual(bytesToHex(aliceToBob.conversationKey), bytesToHex(aliceToBobInChannel.conversationKey))
|
|
396
|
-
assert.notEqual(bytesToHex(aliceToBobInChannel.conversationKey), bytesToHex(aliceToBobInOtherChannel.conversationKey))
|
|
397
|
-
assert.notEqual(bytesToHex(aliceToBobInChannel.conversationKey), bytesToHex(aliceToBobInOtherKind.conversationKey))
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
test('double-DH self-encryption with a content key requires both secrets', async () => {
|
|
401
|
-
const alice = signer()
|
|
402
|
-
const aliceContent = signer()
|
|
403
|
-
const wrongIdentity = signer()
|
|
404
|
-
const alicePubkey = await alice.getPublicKey()
|
|
405
|
-
const aliceContentPubkey = await aliceContent.getPublicKey()
|
|
406
|
-
NsecSigner.setContentSigners(alice, [aliceContent])
|
|
407
|
-
|
|
408
|
-
const encrypted = await alice.nip44EncryptDoubleDH(alicePubkey, ROUTER_KIND, '', textToBase64('note to self'), aliceContentPubkey)
|
|
409
|
-
|
|
410
|
-
assert.equal(encrypted[1], aliceContentPubkey)
|
|
411
|
-
assert.equal(decoder.decode(base64ToBytes(await alice.nip44DecryptDoubleDH(
|
|
412
|
-
alicePubkey,
|
|
413
|
-
ROUTER_KIND,
|
|
414
|
-
'',
|
|
415
|
-
encrypted[0],
|
|
416
|
-
aliceContentPubkey,
|
|
417
|
-
aliceContentPubkey
|
|
418
|
-
))), 'note to self')
|
|
419
|
-
|
|
420
|
-
await assert.rejects(
|
|
421
|
-
() => alice.nip44DecryptDoubleDH(alicePubkey, ROUTER_KIND, '', encrypted[0], aliceContentPubkey, ''),
|
|
422
|
-
/RECEIVER_CONTENT_KEY_REQUIRED/
|
|
423
|
-
)
|
|
424
|
-
|
|
425
|
-
NsecSigner.setContentSigners(wrongIdentity, [aliceContent])
|
|
426
|
-
await assert.rejects(
|
|
427
|
-
() => wrongIdentity.nip44DecryptDoubleDH(alicePubkey, ROUTER_KIND, '', encrypted[0], aliceContentPubkey, aliceContentPubkey),
|
|
428
|
-
/invalid MAC/
|
|
429
|
-
)
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
test('wrapEvent creates private broadcast events under relay event size limit', async () => {
|
|
433
|
-
const alice = signer()
|
|
434
|
-
const bob = signer()
|
|
435
|
-
const bobPubkey = await bob.getPublicKey()
|
|
436
|
-
const before = Math.floor(Date.now() / 1000)
|
|
437
|
-
const wrapped = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey], event: eventFixture('hello bob'), _getIykcProofs: noContentKeys })
|
|
438
|
-
|
|
439
|
-
assert.equal(wrapped.length, 1)
|
|
440
|
-
assert.equal(wrapped[0].kind, PRIVATE_BROADCAST_KIND)
|
|
441
|
-
assert.equal(wrapped[0].pubkey, await alice.getPublicKey())
|
|
442
|
-
assert.equal(wrapped[0].tags.some(tag => tag[0] === 's'), false)
|
|
443
|
-
assert.ok(Number(wrapped[0].tags[0][1]) >= before + EXPIRATION_SECONDS)
|
|
444
|
-
assert.ok(new TextEncoder().encode(JSON.stringify(wrapped[0])).length <= MAX_EVENT_BYTES)
|
|
445
|
-
assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
test('private broadcast wrappers share and normalize one deletion pubkey per logical message', async () => {
|
|
449
|
-
const alice = signer()
|
|
450
|
-
const bob = signer()
|
|
451
|
-
const bobPubkey = await bob.getPublicKey()
|
|
452
|
-
const deletionPubkey = 'A'.repeat(64)
|
|
453
|
-
const [wrapped] = await wrapEvent({
|
|
454
|
-
senderSigner: alice,
|
|
455
|
-
receivers: [bobPubkey],
|
|
456
|
-
deletionPubkey,
|
|
457
|
-
event: eventFixture('deletable'),
|
|
458
|
-
_getIykcProofs: noContentKeys
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
assert.deepEqual(wrapped.tags.find(tag => tag[0] === 's'), ['s', deletionPubkey.toLowerCase()])
|
|
462
|
-
assert.ok(Number(wrapped.tags.find(tag => tag[0] === 'expiration')?.[1]) > 0)
|
|
463
|
-
assert.deepEqual(
|
|
464
|
-
await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
|
|
465
|
-
unwrappedFixture(eventFixture('deletable'), await alice.getPublicKey())
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
for (const invalidDeletionPubkey of ['', 'not-a-pubkey']) {
|
|
469
|
-
await assert.rejects(
|
|
470
|
-
() => wrapEvent({
|
|
471
|
-
senderSigner: alice,
|
|
472
|
-
receivers: [bobPubkey],
|
|
473
|
-
deletionPubkey: invalidDeletionPubkey,
|
|
474
|
-
event: eventFixture('invalid'),
|
|
475
|
-
_getIykcProofs: noContentKeys
|
|
476
|
-
}),
|
|
477
|
-
/INVALID_DELETION_PUBKEY/
|
|
478
|
-
)
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
test('publish splits relay-targeted routers by receiver subset with separate router pubkeys', async () => {
|
|
484
|
-
const alice = signer()
|
|
485
|
-
const bob = signer()
|
|
486
|
-
const carol = signer()
|
|
487
|
-
const dave = signer()
|
|
488
|
-
const alicePubkey = await alice.getPublicKey()
|
|
489
|
-
const bobPubkey = await bob.getPublicKey()
|
|
490
|
-
const carolPubkey = await carol.getPublicKey()
|
|
491
|
-
const davePubkey = await dave.getPublicKey()
|
|
492
|
-
const event = eventFixture('split publish')
|
|
493
|
-
const published = []
|
|
494
|
-
const temporaryWrites = []
|
|
495
|
-
const temporaryStorageArea = {
|
|
496
|
-
getItem: key => globalThis.localStorage.getItem(key),
|
|
497
|
-
removeItem: key => globalThis.localStorage.removeItem(key),
|
|
498
|
-
setItem: (key, value) => {
|
|
499
|
-
temporaryWrites.push(key)
|
|
500
|
-
globalThis.localStorage.setItem(key, value)
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const reports = await publish({
|
|
505
|
-
senderSigner: alice,
|
|
506
|
-
receivers: [bobPubkey, carolPubkey, davePubkey],
|
|
507
|
-
relayToReceivers: new Map([
|
|
508
|
-
['wss://one.example', [bobPubkey, carolPubkey]],
|
|
509
|
-
['wss://two.example', [carolPubkey, bobPubkey]],
|
|
510
|
-
['wss://three.example', [carolPubkey, davePubkey]]
|
|
511
|
-
]),
|
|
512
|
-
recoveryRelays: ['wss://seed.example', 'wss://one.example'],
|
|
513
|
-
deletionPubkey: 'd'.repeat(64),
|
|
514
|
-
event,
|
|
515
|
-
temporaryStorageArea,
|
|
516
|
-
_getIykcProofs: noContentKeys,
|
|
517
|
-
_publish: async (outer, relays) => {
|
|
518
|
-
published.push({ outer, relays })
|
|
519
|
-
return { success: true }
|
|
520
|
-
}
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
assert.equal(published.length, 2)
|
|
524
|
-
assert.deepEqual(published.map(({ outer }) => outer.tags.find(tag => tag[0] === 's')), [
|
|
525
|
-
['s', 'd'.repeat(64)],
|
|
526
|
-
['s', 'd'.repeat(64)]
|
|
527
|
-
])
|
|
528
|
-
assert.deepEqual(reports, [{ success: true }, { success: true }])
|
|
529
|
-
assert.deepEqual(published[0].relays, ['wss://one.example', 'wss://two.example', 'wss://seed.example'])
|
|
530
|
-
assert.deepEqual(published[1].relays, ['wss://three.example', 'wss://seed.example', 'wss://one.example'])
|
|
531
|
-
|
|
532
|
-
const routers = []
|
|
533
|
-
for (const { outer } of published) {
|
|
534
|
-
routers.push(await decryptPrivateBroadcast(alice, alicePubkey, outer.content))
|
|
535
|
-
}
|
|
536
|
-
assert.deepEqual(routers.map(router => router.tags.some(tag => tag[0] === 'id')), [false, false])
|
|
537
|
-
assert.notEqual(routers[0].pubkey, routers[1].pubkey)
|
|
538
|
-
assert.deepEqual(
|
|
539
|
-
routerRecipientRows(routers[0]).map(row => row[0]).sort(),
|
|
540
|
-
[bobPubkey, carolPubkey].sort()
|
|
541
|
-
)
|
|
542
|
-
assert.deepEqual(
|
|
543
|
-
routerRecipientRows(routers[1]).map(row => row[0]),
|
|
544
|
-
[carolPubkey, davePubkey]
|
|
545
|
-
)
|
|
546
|
-
const firstLines = routerJsonlLines(routers[0])
|
|
547
|
-
const secondLines = routerJsonlLines(routers[1])
|
|
548
|
-
assert.equal(firstLines[0], secondLines[0])
|
|
549
|
-
assert.equal(
|
|
550
|
-
firstLines.find(line => JSON.parse(line)[0] === carolPubkey),
|
|
551
|
-
secondLines.find(line => JSON.parse(line)[0] === carolPubkey)
|
|
552
|
-
)
|
|
553
|
-
assert.ok(temporaryWrites.includes(TEMPORARY_STORAGE_KEYS_KEY))
|
|
554
|
-
assert.equal(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
555
|
-
assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
test('publish sends wrapped events through RelayPool.sendEvent', async () => {
|
|
559
|
-
const originalSendEvent = relayPool.sendEvent
|
|
560
|
-
const published = []
|
|
561
|
-
relayPool.sendEvent = async (outer, relays) => {
|
|
562
|
-
published.push({ outer, relays })
|
|
563
|
-
return { success: true, total: relays.length, promise: Promise.resolve({ success: true }) }
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
try {
|
|
567
|
-
const alice = signer()
|
|
568
|
-
const bob = signer()
|
|
569
|
-
await publish({
|
|
570
|
-
senderSigner: alice,
|
|
571
|
-
receivers: [await bob.getPublicKey()],
|
|
572
|
-
event: eventFixture('relay pool publish'),
|
|
573
|
-
relays: ['wss://relay.example'],
|
|
574
|
-
_getIykcProofs: noContentKeys
|
|
575
|
-
})
|
|
576
|
-
} finally {
|
|
577
|
-
relayPool.sendEvent = originalSendEvent
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
assert.equal(published.length, 1)
|
|
581
|
-
assert.equal(published[0].outer.kind, PRIVATE_BROADCAST_KIND)
|
|
582
|
-
assert.deepEqual(published[0].relays, ['wss://relay.example'])
|
|
583
|
-
})
|
|
584
|
-
|
|
585
|
-
test('fetch uses complete RelayPool reads for private-channel recovery', async () => {
|
|
586
|
-
let call
|
|
587
|
-
const events = await fetch({
|
|
588
|
-
relays: ['wss://relay.example'],
|
|
589
|
-
_getEvents: async (filter, relays, options) => {
|
|
590
|
-
call = { filter, relays, options }
|
|
591
|
-
return { result: [] }
|
|
592
|
-
}
|
|
593
|
-
})
|
|
594
|
-
|
|
595
|
-
assert.deepEqual(events, [])
|
|
596
|
-
assert.deepEqual(call.relays, ['wss://relay.example'])
|
|
597
|
-
assert.deepEqual(call.options, { timeout: 5000, timeoutAfterFirstEose: null })
|
|
598
|
-
assert.deepEqual(call.filter.kinds, [PRIVATE_BROADCAST_KIND])
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
test('publish cleans prepared rows when grouped publishing fails', async () => {
|
|
602
|
-
const alice = signer()
|
|
603
|
-
const bob = signer()
|
|
604
|
-
const carol = signer()
|
|
605
|
-
const bobPubkey = await bob.getPublicKey()
|
|
606
|
-
const carolPubkey = await carol.getPublicKey()
|
|
607
|
-
|
|
608
|
-
await assert.rejects(
|
|
609
|
-
publish({
|
|
610
|
-
senderSigner: alice,
|
|
611
|
-
receivers: [bobPubkey, carolPubkey],
|
|
612
|
-
relayToReceivers: new Map([
|
|
613
|
-
['wss://one.example', [bobPubkey]],
|
|
614
|
-
['wss://two.example', [carolPubkey]]
|
|
615
|
-
]),
|
|
616
|
-
event: eventFixture('publish fail'),
|
|
617
|
-
_getIykcProofs: noContentKeys,
|
|
618
|
-
_publish: async () => { throw new Error('relay offline') }
|
|
619
|
-
}),
|
|
620
|
-
/relay offline/
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
624
|
-
})
|
|
625
|
-
|
|
626
|
-
test('publishNymEvent mirrors carrier chunks to recovery relays', async () => {
|
|
627
|
-
const channel = signer()
|
|
628
|
-
const nym = signer()
|
|
629
|
-
const published = []
|
|
630
|
-
|
|
631
|
-
const reports = await publishNymEvent({
|
|
632
|
-
nymSigner: nym,
|
|
633
|
-
privateChannelSigner: channel,
|
|
634
|
-
event: eventFixture('nym mirror'),
|
|
635
|
-
relays: ['wss://receiver.example'],
|
|
636
|
-
recoveryRelays: ['wss://seed.example', 'wss://receiver.example'],
|
|
637
|
-
deletionPubkey: 'e'.repeat(64),
|
|
638
|
-
_publish: async (outer, relays) => {
|
|
639
|
-
published.push({ outer, relays })
|
|
640
|
-
return { success: true }
|
|
641
|
-
}
|
|
642
|
-
})
|
|
643
|
-
|
|
644
|
-
assert.equal(published.length, 1)
|
|
645
|
-
assert.deepEqual(published[0].relays, ['wss://receiver.example', 'wss://seed.example'])
|
|
646
|
-
assert.deepEqual(published[0].outer.tags.find(tag => tag[0] === 's'), ['s', 'e'.repeat(64)])
|
|
647
|
-
assert.deepEqual(reports, [{ success: true }])
|
|
648
|
-
assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
649
|
-
})
|
|
650
|
-
|
|
651
|
-
test('received chunk default cap is proportional to private-channel chunk size', () => {
|
|
652
|
-
assert.equal(getJsonlChunkByteSize(), 30159)
|
|
653
|
-
assert.equal(DEFAULT_RECEIVED_CHUNK_MAX_BYTES, Math.min(getJsonlChunkByteSize() * 64, 3 * 1024 * 1024))
|
|
654
|
-
})
|
|
655
|
-
|
|
656
|
-
test('wrapEvent supports overriding the outer expiration window', async () => {
|
|
657
|
-
const alice = signer()
|
|
658
|
-
const bob = signer()
|
|
659
|
-
const bobPubkey = await bob.getPublicKey()
|
|
660
|
-
const before = Math.floor(Date.now() / 1000)
|
|
661
|
-
const [wrapped] = await wrapEvent({
|
|
662
|
-
senderSigner: alice,
|
|
663
|
-
receivers: [bobPubkey],
|
|
664
|
-
event: eventFixture('hello bob'),
|
|
665
|
-
expirationSeconds: 7 * 24 * 60 * 60,
|
|
666
|
-
_getIykcProofs: noContentKeys
|
|
667
|
-
})
|
|
668
|
-
|
|
669
|
-
assert.ok(Number(wrapped.tags[0][1]) >= before + 7 * 24 * 60 * 60)
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
test('unwrapEvent returns the addressed receiver event or null', async () => {
|
|
673
|
-
const alice = signer()
|
|
674
|
-
const bob = signer()
|
|
675
|
-
const carol = signer()
|
|
676
|
-
const bobPubkey = await bob.getPublicKey()
|
|
677
|
-
const carolPubkey = await carol.getPublicKey()
|
|
678
|
-
const alicePubkey = await alice.getPublicKey()
|
|
679
|
-
const original = { ...eventFixture('private'), pubkey: '0'.repeat(64), id: 'f'.repeat(64) }
|
|
680
|
-
const [wrapped] = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey, carolPubkey], event: original, _getIykcProofs: noContentKeys })
|
|
681
|
-
|
|
682
|
-
assert.deepEqual(await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), unwrappedFixture(original, alicePubkey))
|
|
683
|
-
assert.deepEqual(await unwrapEvent({ receiverSigner: carol, privateChannelSigner: alice, event: wrapped, receiverPubkey: carolPubkey }), unwrappedFixture(original, alicePubkey))
|
|
684
|
-
assert.equal(await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: await signer().getPublicKey() }), null)
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
test('wrapEvent can encrypt the outer router to a separate reader key', async () => {
|
|
688
|
-
const sender = signer()
|
|
689
|
-
const channel = signer()
|
|
690
|
-
const reader = signer()
|
|
691
|
-
const bob = signer()
|
|
692
|
-
const bobPubkey = await bob.getPublicKey()
|
|
693
|
-
const readerPubkey = await reader.getPublicKey()
|
|
694
|
-
const channelPubkey = await channel.getPublicKey()
|
|
695
|
-
const original = eventFixture('reader decrypts channel')
|
|
696
|
-
const [wrapped] = await wrapEvent({
|
|
697
|
-
senderSigner: sender,
|
|
698
|
-
privateChannelSigner: channel,
|
|
699
|
-
privateChannelReaderPubkey: readerPubkey,
|
|
700
|
-
receivers: [bobPubkey],
|
|
701
|
-
event: original,
|
|
702
|
-
_getIykcProofs: noContentKeys
|
|
703
|
-
})
|
|
704
|
-
|
|
705
|
-
assert.equal(wrapped.pubkey, channelPubkey)
|
|
706
|
-
const router = await decryptPrivateBroadcast(reader, channelPubkey, wrapped.content)
|
|
707
|
-
assert.equal(router.kind, ROUTER_KIND)
|
|
708
|
-
assert.deepEqual(await unwrapEvent({
|
|
709
|
-
receiverSigner: bob,
|
|
710
|
-
privateChannelSigner: channel,
|
|
711
|
-
privateChannelReaderSigner: reader,
|
|
712
|
-
event: wrapped,
|
|
713
|
-
receiverPubkey: bobPubkey
|
|
714
|
-
}), unwrappedFixture(original, await sender.getPublicKey()))
|
|
715
|
-
assert.deepEqual(await unwrapEvent({
|
|
716
|
-
receiverSigner: bob,
|
|
717
|
-
privateChannelSigner: channel,
|
|
718
|
-
privateChannelReaderPubkey: readerPubkey,
|
|
719
|
-
event: wrapped,
|
|
720
|
-
receiverPubkey: bobPubkey
|
|
721
|
-
}), unwrappedFixture(original, await sender.getPublicKey()))
|
|
722
|
-
})
|
|
723
|
-
|
|
724
|
-
test('subscribe can read channel events with only a reader signer', async () => {
|
|
725
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
726
|
-
let handlers = null
|
|
727
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
728
|
-
handlers = nextHandlers
|
|
729
|
-
return { close: () => {} }
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
try {
|
|
733
|
-
const sender = signer()
|
|
734
|
-
const channel = signer()
|
|
735
|
-
const reader = signer()
|
|
736
|
-
const bob = signer()
|
|
737
|
-
const bobPubkey = await bob.getPublicKey()
|
|
738
|
-
const channelPubkey = await channel.getPublicKey()
|
|
739
|
-
const original = eventFixture('reader-only subscribe')
|
|
740
|
-
const [wrapped] = await wrapEvent({
|
|
741
|
-
senderSigner: sender,
|
|
742
|
-
privateChannelSigner: channel,
|
|
743
|
-
privateChannelReaderPubkey: await reader.getPublicKey(),
|
|
744
|
-
receivers: [bobPubkey],
|
|
745
|
-
event: original,
|
|
746
|
-
_getIykcProofs: noContentKeys
|
|
747
|
-
})
|
|
748
|
-
const events = []
|
|
749
|
-
|
|
750
|
-
subscribe({
|
|
751
|
-
receiverSigner: bob,
|
|
752
|
-
privateChannelSigner: null,
|
|
753
|
-
privateChannelReaderSigner: reader,
|
|
754
|
-
privateChannelPubkey: channelPubkey,
|
|
755
|
-
receiverPubkey: bobPubkey,
|
|
756
|
-
relays: ['wss://relay.example'],
|
|
757
|
-
onEvent: event => events.push(event)
|
|
758
|
-
})
|
|
759
|
-
await handlers.onevent(wrapped)
|
|
760
|
-
|
|
761
|
-
assert.deepEqual(events, [unwrappedFixture(original, await sender.getPublicKey())])
|
|
762
|
-
} finally {
|
|
763
|
-
pool.subscribeMany = originalSubscribeMany
|
|
764
|
-
}
|
|
765
|
-
})
|
|
766
|
-
|
|
767
|
-
test('live-only subscribe closes its RelayPool stream', async () => {
|
|
768
|
-
const sender = signer()
|
|
769
|
-
const bob = signer()
|
|
770
|
-
const bobPubkey = await bob.getPublicKey()
|
|
771
|
-
const channelPubkey = await sender.getPublicKey()
|
|
772
|
-
const original = eventFixture('live subscription')
|
|
773
|
-
const [wrapped] = await wrapEvent({
|
|
774
|
-
senderSigner: sender,
|
|
775
|
-
receivers: [bobPubkey],
|
|
776
|
-
event: original,
|
|
777
|
-
_getIykcProofs: noContentKeys
|
|
778
|
-
})
|
|
779
|
-
const received = []
|
|
780
|
-
let aborted = false
|
|
781
|
-
|
|
782
|
-
const subscription = subscribe({
|
|
783
|
-
receiverSigner: bob,
|
|
784
|
-
privateChannelSigner: sender,
|
|
785
|
-
privateChannelPubkey: channelPubkey,
|
|
786
|
-
receiverPubkey: bobPubkey,
|
|
787
|
-
relays: ['wss://relay.example'],
|
|
788
|
-
liveOnly: true,
|
|
789
|
-
onEvent: event => received.push(event),
|
|
790
|
-
_liveEventsGenerator: (_filter, _relays, { signal }) => (async function * () {
|
|
791
|
-
yield wrapped
|
|
792
|
-
await new Promise(resolve => signal.addEventListener('abort', () => {
|
|
793
|
-
aborted = true
|
|
794
|
-
resolve()
|
|
795
|
-
}, { once: true }))
|
|
796
|
-
})()
|
|
797
|
-
})
|
|
798
|
-
|
|
799
|
-
for (let attempt = 0; attempt < 20 && received.length === 0; attempt++) {
|
|
800
|
-
await new Promise(resolve => setImmediate(resolve))
|
|
801
|
-
}
|
|
802
|
-
assert.deepEqual(received, [unwrappedFixture(original, await sender.getPublicKey())])
|
|
803
|
-
|
|
804
|
-
subscription.close()
|
|
805
|
-
for (let attempt = 0; attempt < 20 && !aborted; attempt++) {
|
|
806
|
-
await new Promise(resolve => setImmediate(resolve))
|
|
807
|
-
}
|
|
808
|
-
assert.ok(aborted)
|
|
809
|
-
})
|
|
810
|
-
|
|
811
|
-
test('subscribe can read reader-targeted channel events with the writer signer', async () => {
|
|
812
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
813
|
-
let handlers = null
|
|
814
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
815
|
-
handlers = nextHandlers
|
|
816
|
-
return { close: () => {} }
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
try {
|
|
820
|
-
const sender = signer()
|
|
821
|
-
const channel = signer()
|
|
822
|
-
const reader = signer()
|
|
823
|
-
const bob = signer()
|
|
824
|
-
const bobPubkey = await bob.getPublicKey()
|
|
825
|
-
const readerPubkey = await reader.getPublicKey()
|
|
826
|
-
const channelPubkey = await channel.getPublicKey()
|
|
827
|
-
const original = eventFixture('writer reads reader-targeted router')
|
|
828
|
-
const [wrapped] = await wrapEvent({
|
|
829
|
-
senderSigner: sender,
|
|
830
|
-
privateChannelSigner: channel,
|
|
831
|
-
privateChannelReaderPubkey: readerPubkey,
|
|
832
|
-
receivers: [bobPubkey],
|
|
833
|
-
event: original,
|
|
834
|
-
_getIykcProofs: noContentKeys
|
|
835
|
-
})
|
|
836
|
-
const events = []
|
|
837
|
-
|
|
838
|
-
subscribe({
|
|
839
|
-
receiverSigner: bob,
|
|
840
|
-
privateChannelSigner: channel,
|
|
841
|
-
privateChannelReaderPubkey: readerPubkey,
|
|
842
|
-
privateChannelPubkey: channelPubkey,
|
|
843
|
-
receiverPubkey: bobPubkey,
|
|
844
|
-
relays: ['wss://relay.example'],
|
|
845
|
-
onEvent: event => events.push(event)
|
|
846
|
-
})
|
|
847
|
-
await handlers.onevent(wrapped)
|
|
848
|
-
|
|
849
|
-
assert.deepEqual(events, [unwrappedFixture(original, await sender.getPublicKey())])
|
|
850
|
-
} finally {
|
|
851
|
-
pool.subscribeMany = originalSubscribeMany
|
|
852
|
-
}
|
|
853
|
-
})
|
|
854
|
-
|
|
855
|
-
test('unwrapEvent preserves valid signed inner events', async () => {
|
|
856
|
-
const alice = signer()
|
|
857
|
-
const bob = signer()
|
|
858
|
-
const authorSecret = generateSecretKey()
|
|
859
|
-
const bobPubkey = await bob.getPublicKey()
|
|
860
|
-
const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [['x', '1']], content: 'signed' }, authorSecret)
|
|
861
|
-
const [wrapped] = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey], event: signed, _getIykcProofs: noContentKeys })
|
|
862
|
-
|
|
863
|
-
assert.deepEqual(await unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), signed)
|
|
864
|
-
})
|
|
865
|
-
|
|
866
|
-
test('unwrapEvent rejects invalid signed inner events', async () => {
|
|
867
|
-
const alice = signer()
|
|
868
|
-
const bob = signer()
|
|
869
|
-
const bobPubkey = await bob.getPublicKey()
|
|
870
|
-
const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'signed' }, generateSecretKey())
|
|
871
|
-
const [wrapped] = await wrapEvent({
|
|
872
|
-
senderSigner: alice,
|
|
873
|
-
receivers: [bobPubkey],
|
|
874
|
-
event: { ...signed, content: 'tampered' },
|
|
875
|
-
_getIykcProofs: noContentKeys
|
|
876
|
-
})
|
|
877
|
-
|
|
878
|
-
await assert.rejects(
|
|
879
|
-
() => unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
|
|
880
|
-
/INVALID_SIGNED_INNER_EVENT/
|
|
881
|
-
)
|
|
882
|
-
})
|
|
883
|
-
|
|
884
|
-
test('unwrapEvent rejects malformed signed inner events', async () => {
|
|
885
|
-
const alice = signer()
|
|
886
|
-
const bob = signer()
|
|
887
|
-
const bobPubkey = await bob.getPublicKey()
|
|
888
|
-
const [wrapped] = await wrapEvent({
|
|
889
|
-
senderSigner: alice,
|
|
890
|
-
receivers: [bobPubkey],
|
|
891
|
-
event: { id: 'e'.repeat(64), pubkey: 'a'.repeat(64), kind: 9002, created_at: 2, tags: [], content: 'signed', sig: 42 },
|
|
892
|
-
_getIykcProofs: noContentKeys
|
|
893
|
-
})
|
|
894
|
-
|
|
895
|
-
await assert.rejects(
|
|
896
|
-
() => unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
|
|
897
|
-
/INVALID_SIGNED_INNER_EVENT/
|
|
898
|
-
)
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
test('wrapNymEvent signs nym carriers and reconstructs nym rumors', async () => {
|
|
902
|
-
const channel = signer()
|
|
903
|
-
const reader = signer()
|
|
904
|
-
const nym = signer()
|
|
905
|
-
const channelPubkey = await channel.getPublicKey()
|
|
906
|
-
const readerPubkey = await reader.getPublicKey()
|
|
907
|
-
const nymPubkey = await nym.getPublicKey()
|
|
908
|
-
const original = eventFixture('nym rumor')
|
|
909
|
-
const [wrapped] = await wrapNymEvent({
|
|
910
|
-
nymSigner: nym,
|
|
911
|
-
privateChannelSigner: channel,
|
|
912
|
-
privateChannelReaderPubkey: readerPubkey,
|
|
913
|
-
event: original
|
|
914
|
-
})
|
|
915
|
-
|
|
916
|
-
assert.equal(wrapped.kind, PRIVATE_BROADCAST_KIND)
|
|
917
|
-
assert.equal(wrapped.pubkey, channelPubkey)
|
|
918
|
-
assert.ok(new TextEncoder().encode(JSON.stringify(wrapped)).length <= MAX_EVENT_BYTES)
|
|
919
|
-
const carrier = await decryptPrivateBroadcast(reader, channelPubkey, wrapped.content)
|
|
920
|
-
const expected = unwrappedFixture(original, nymPubkey)
|
|
921
|
-
|
|
922
|
-
assert.equal(carrier.kind, NYM_CARRIER_KIND)
|
|
923
|
-
assert.equal(carrier.pubkey, nymPubkey)
|
|
924
|
-
assert.equal(carrier.tags.find(tag => tag[0] === 'id')?.[1], expected.id)
|
|
925
|
-
assert.deepEqual(carrier.tags.find(tag => tag[0] === 'c'), ['c', '0', '1'])
|
|
926
|
-
assert.deepEqual(eventFromNymCarriers([carrier]), expected)
|
|
927
|
-
})
|
|
928
|
-
|
|
929
|
-
test('wrapNymEvent preserves signed inner event authors distinct from carrier nym', async () => {
|
|
930
|
-
const channel = signer()
|
|
931
|
-
const nym = signer()
|
|
932
|
-
const signed = finalizeEvent({ kind: 9002, created_at: 23, tags: [['x', 'signed']], content: 'signed by someone else' }, generateSecretKey())
|
|
933
|
-
const [wrapped] = await wrapNymEvent({
|
|
934
|
-
nymSigner: nym,
|
|
935
|
-
privateChannelSigner: channel,
|
|
936
|
-
event: signed
|
|
937
|
-
})
|
|
938
|
-
const carrier = await decryptPrivateBroadcast(channel, await channel.getPublicKey(), wrapped.content)
|
|
939
|
-
|
|
940
|
-
assert.notEqual(carrier.pubkey, signed.pubkey)
|
|
941
|
-
assert.equal(carrier.tags.find(tag => tag[0] === 'id')?.[1], signed.id)
|
|
942
|
-
assert.deepEqual(eventFromNymCarriers([carrier]), signed)
|
|
943
|
-
})
|
|
944
|
-
|
|
945
|
-
test('subscribe buffers out-of-order nym carrier chunks by nym pubkey and inner id', async () => {
|
|
946
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
947
|
-
let handlers = null
|
|
948
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
949
|
-
handlers = nextHandlers
|
|
950
|
-
return { close: () => {} }
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
try {
|
|
954
|
-
const channel = signer()
|
|
955
|
-
const nym = signer()
|
|
956
|
-
const nymPubkey = await nym.getPublicKey()
|
|
957
|
-
const original = eventFixture('x'.repeat(getNymCarrierChunkSize()))
|
|
958
|
-
const wrapped = await wrapNymEvent({
|
|
959
|
-
nymSigner: nym,
|
|
960
|
-
privateChannelSigner: channel,
|
|
961
|
-
deletionPubkey: 'e'.repeat(64),
|
|
962
|
-
event: original
|
|
963
|
-
})
|
|
964
|
-
const routedEvents = []
|
|
965
|
-
const nymEvents = []
|
|
966
|
-
const seeds = []
|
|
967
|
-
|
|
968
|
-
assert.ok(wrapped.length > 1)
|
|
969
|
-
for (const event of wrapped) assert.ok(new TextEncoder().encode(JSON.stringify(event)).length <= MAX_EVENT_BYTES)
|
|
970
|
-
assert.deepEqual(new Set(wrapped.map(event => event.tags.find(tag => tag[0] === 's')?.[1])), new Set(['e'.repeat(64)]))
|
|
971
|
-
|
|
972
|
-
subscribe({
|
|
973
|
-
privateChannelSigner: channel,
|
|
974
|
-
privateChannelPubkey: await channel.getPublicKey(),
|
|
975
|
-
relays: ['wss://relay.example'],
|
|
976
|
-
mode: 'seeder',
|
|
977
|
-
onEvent: event => routedEvents.push(event),
|
|
978
|
-
onNymEvent: event => nymEvents.push(event),
|
|
979
|
-
onSeedEvent: seed => seeds.push(seed)
|
|
980
|
-
})
|
|
981
|
-
for (const event of [...wrapped].reverse()) await handlers.onevent(event)
|
|
982
|
-
|
|
983
|
-
assert.deepEqual(routedEvents, [])
|
|
984
|
-
assert.deepEqual(nymEvents, [unwrappedFixture(original, nymPubkey)])
|
|
985
|
-
assert.equal(seeds.length, 1)
|
|
986
|
-
assert.equal(seeds[0].recordType, 'nymCarrier_v1')
|
|
987
|
-
assert.equal(seeds[0].carriers.length, wrapped.length)
|
|
988
|
-
} finally {
|
|
989
|
-
pool.subscribeMany = originalSubscribeMany
|
|
990
|
-
}
|
|
991
|
-
})
|
|
992
|
-
|
|
993
|
-
test('subscribe separates nym carrier groups that use different chunk totals', async () => {
|
|
994
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
995
|
-
let handlers = null
|
|
996
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
997
|
-
handlers = nextHandlers
|
|
998
|
-
return { close: () => {} }
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
try {
|
|
1002
|
-
const channel = signer()
|
|
1003
|
-
const nym = signer()
|
|
1004
|
-
const channelPubkey = await channel.getPublicKey()
|
|
1005
|
-
const nymPubkey = await nym.getPublicKey()
|
|
1006
|
-
const original = eventFixture('same inner event, different chunking')
|
|
1007
|
-
const expected = unwrappedFixture(original, nymPubkey)
|
|
1008
|
-
const encoded = Buffer.from(JSON.stringify(original)).toString('base64')
|
|
1009
|
-
const midpoint = Math.ceil(encoded.length / 2)
|
|
1010
|
-
const nymEvents = []
|
|
1011
|
-
const errors = []
|
|
1012
|
-
|
|
1013
|
-
async function carrier (index, total, content) {
|
|
1014
|
-
return nym.signEvent({
|
|
1015
|
-
kind: NYM_CARRIER_KIND,
|
|
1016
|
-
created_at: 9,
|
|
1017
|
-
tags: [['id', expected.id], ['c', String(index), String(total)]],
|
|
1018
|
-
content
|
|
1019
|
-
})
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
async function outer (carrier) {
|
|
1023
|
-
return channel.signEvent({
|
|
1024
|
-
kind: PRIVATE_BROADCAST_KIND,
|
|
1025
|
-
created_at: 10,
|
|
1026
|
-
tags: [],
|
|
1027
|
-
content: await encryptPrivateBroadcast(channel, channelPubkey, carrier)
|
|
1028
|
-
})
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
const twoChunkFirst = await outer(await carrier(0, 2, encoded.slice(0, midpoint)))
|
|
1032
|
-
const oneChunk = await outer(await carrier(0, 1, encoded))
|
|
1033
|
-
const twoChunkLast = await outer(await carrier(1, 2, encoded.slice(midpoint)))
|
|
1034
|
-
|
|
1035
|
-
subscribe({
|
|
1036
|
-
privateChannelSigner: channel,
|
|
1037
|
-
privateChannelPubkey: channelPubkey,
|
|
1038
|
-
relays: ['wss://relay.example'],
|
|
1039
|
-
onNymEvent: event => nymEvents.push(event),
|
|
1040
|
-
onError: err => errors.push(err)
|
|
1041
|
-
})
|
|
1042
|
-
await handlers.onevent(twoChunkFirst)
|
|
1043
|
-
await handlers.onevent(oneChunk)
|
|
1044
|
-
await handlers.onevent(twoChunkLast)
|
|
1045
|
-
|
|
1046
|
-
assert.deepEqual(errors, [])
|
|
1047
|
-
assert.deepEqual(nymEvents, [expected, expected])
|
|
1048
|
-
} finally {
|
|
1049
|
-
pool.subscribeMany = originalSubscribeMany
|
|
1050
|
-
}
|
|
1051
|
-
})
|
|
1052
|
-
|
|
1053
|
-
test('unwrapEvent uses imkc tag as the row encryption pubkey', async () => {
|
|
1054
|
-
const alice = signer()
|
|
1055
|
-
const imkc = signer()
|
|
1056
|
-
const bob = signer()
|
|
1057
|
-
const alicePubkey = await alice.getPublicKey()
|
|
1058
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1059
|
-
const imkcPubkey = await imkc.getPublicKey()
|
|
1060
|
-
const original = eventFixture('private')
|
|
1061
|
-
NsecSigner.setContentSigners(alice, [imkc])
|
|
1062
|
-
const [wrapped] = await wrapEvent({ senderSigner: alice, imkcSigner: imkc, receivers: [bobPubkey], event: original, _getIykcProofs: noContentKeys })
|
|
1063
|
-
const router = await decryptPrivateBroadcast(alice, await alice.getPublicKey(), wrapped.content)
|
|
1064
|
-
const imkcTag = router.tags.find(t => t[0] === 'imkc')
|
|
1065
|
-
|
|
1066
|
-
assert.equal(router.tags.find(t => t[0] === 'f')?.[1], alicePubkey)
|
|
1067
|
-
assert.equal(imkcTag?.[1], imkcPubkey)
|
|
1068
|
-
assert.equal(verifyContentKeyProof({ ownerPubkey: alicePubkey, contentPubkey: imkcPubkey, proof: imkcTag?.[2] }), true)
|
|
1069
|
-
assert.deepEqual(
|
|
1070
|
-
await unwrapEvent({
|
|
1071
|
-
receiverSigner: bob,
|
|
1072
|
-
privateChannelSigner: alice,
|
|
1073
|
-
event: wrapped,
|
|
1074
|
-
receiverPubkey: bobPubkey
|
|
1075
|
-
}),
|
|
1076
|
-
unwrappedFixture(original, alicePubkey)
|
|
1077
|
-
)
|
|
1078
|
-
})
|
|
1079
|
-
|
|
1080
|
-
test('wrapEvent can add imkc from senderSigner Double-DH without direct content signer access', async () => {
|
|
1081
|
-
const alice = signer()
|
|
1082
|
-
const aliceContent = signer()
|
|
1083
|
-
const bob = signer()
|
|
1084
|
-
const aliceProxy = await signerWithInternalContentKey(alice, aliceContent)
|
|
1085
|
-
const alicePubkey = await alice.getPublicKey()
|
|
1086
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1087
|
-
const contentPubkey = await aliceContent.getPublicKey()
|
|
1088
|
-
const original = eventFixture('private')
|
|
1089
|
-
const [wrapped] = await wrapEvent({
|
|
1090
|
-
senderSigner: aliceProxy,
|
|
1091
|
-
privateChannelSigner: alice,
|
|
1092
|
-
receivers: [bobPubkey],
|
|
1093
|
-
event: original,
|
|
1094
|
-
_getIykcProofs: noContentKeys
|
|
1095
|
-
})
|
|
1096
|
-
const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
|
|
1097
|
-
const imkcTag = router.tags.find(t => t[0] === 'imkc')
|
|
1098
|
-
|
|
1099
|
-
assert.equal(imkcTag?.[1], contentPubkey)
|
|
1100
|
-
assert.equal(verifyContentKeyProof({ ownerPubkey: alicePubkey, contentPubkey, proof: imkcTag?.[2] }), true)
|
|
1101
|
-
assert.deepEqual(
|
|
1102
|
-
await unwrapEvent({
|
|
1103
|
-
receiverSigner: bob,
|
|
1104
|
-
privateChannelSigner: alice,
|
|
1105
|
-
event: wrapped,
|
|
1106
|
-
receiverPubkey: bobPubkey
|
|
1107
|
-
}),
|
|
1108
|
-
unwrappedFixture(original, alicePubkey)
|
|
1109
|
-
)
|
|
1110
|
-
})
|
|
1111
|
-
|
|
1112
|
-
test('wrapEvent uses Double-DH returned own content pubkey for imkc tag', async () => {
|
|
1113
|
-
const alice = signer()
|
|
1114
|
-
const oldContent = signer()
|
|
1115
|
-
const actualContent = signer()
|
|
1116
|
-
const bob = signer()
|
|
1117
|
-
const alicePubkey = await alice.getPublicKey()
|
|
1118
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1119
|
-
const actualContentPubkey = await actualContent.getPublicKey()
|
|
1120
|
-
const senderSigner = {
|
|
1121
|
-
getPublicKey: () => alice.getPublicKey(),
|
|
1122
|
-
signEvent: event => alice.signEvent(event),
|
|
1123
|
-
nip44Encrypt: (peerPubkey, plaintext) => alice.nip44Encrypt(peerPubkey, plaintext),
|
|
1124
|
-
nip44v3Encrypt: (peerPubkey, kind, scope, plaintextB64) => alice.nip44v3Encrypt(peerPubkey, kind, scope, plaintextB64),
|
|
1125
|
-
nip44EncryptDoubleDH: (...params) => {
|
|
1126
|
-
NsecSigner.setContentSigners(alice, [actualContent])
|
|
1127
|
-
return alice.nip44EncryptDoubleDH(...params)
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
const [wrapped] = await wrapEvent({
|
|
1131
|
-
senderSigner,
|
|
1132
|
-
imkcSigner: oldContent,
|
|
1133
|
-
privateChannelSigner: alice,
|
|
1134
|
-
receivers: [bobPubkey],
|
|
1135
|
-
event: eventFixture('private'),
|
|
1136
|
-
_getIykcProofs: noContentKeys
|
|
1137
|
-
})
|
|
1138
|
-
const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
|
|
1139
|
-
const imkcTag = router.tags.find(t => t[0] === 'imkc')
|
|
1140
|
-
|
|
1141
|
-
assert.equal(imkcTag?.[1], actualContentPubkey)
|
|
1142
|
-
assert.equal(verifyContentKeyProof({ ownerPubkey: alicePubkey, contentPubkey: actualContentPubkey, proof: imkcTag?.[2] }), true)
|
|
1143
|
-
})
|
|
1144
|
-
|
|
1145
|
-
test('wrapEvent retries once when sender content key rotates while writing chunks', async () => {
|
|
1146
|
-
const alice = signer()
|
|
1147
|
-
const oldContent = signer()
|
|
1148
|
-
const actualContent = signer()
|
|
1149
|
-
const bob = signer()
|
|
1150
|
-
const carol = signer()
|
|
1151
|
-
const alicePubkey = await alice.getPublicKey()
|
|
1152
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1153
|
-
const carolPubkey = await carol.getPublicKey()
|
|
1154
|
-
const actualContentPubkey = await actualContent.getPublicKey()
|
|
1155
|
-
let encryptCalls = 0
|
|
1156
|
-
const senderSigner = {
|
|
1157
|
-
getPublicKey: () => alice.getPublicKey(),
|
|
1158
|
-
signEvent: event => alice.signEvent(event),
|
|
1159
|
-
nip44Encrypt: (peerPubkey, plaintext) => alice.nip44Encrypt(peerPubkey, plaintext),
|
|
1160
|
-
nip44v3Encrypt: (peerPubkey, kind, scope, plaintextB64) => alice.nip44v3Encrypt(peerPubkey, kind, scope, plaintextB64),
|
|
1161
|
-
nip44EncryptDoubleDH: (...params) => {
|
|
1162
|
-
encryptCalls++
|
|
1163
|
-
NsecSigner.setContentSigners(alice, [encryptCalls === 1 ? oldContent : actualContent])
|
|
1164
|
-
return alice.nip44EncryptDoubleDH(...params)
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
const original = eventFixture('private after rotation')
|
|
1168
|
-
const [wrapped] = await wrapEvent({
|
|
1169
|
-
senderSigner,
|
|
1170
|
-
imkcSigner: oldContent,
|
|
1171
|
-
privateChannelSigner: alice,
|
|
1172
|
-
receivers: [bobPubkey, carolPubkey],
|
|
1173
|
-
event: original,
|
|
1174
|
-
_getIykcProofs: noContentKeys
|
|
1175
|
-
})
|
|
1176
|
-
const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
|
|
1177
|
-
const imkcTag = router.tags.find(t => t[0] === 'imkc')
|
|
1178
|
-
|
|
1179
|
-
assert.equal(encryptCalls, 4)
|
|
1180
|
-
assert.equal(imkcTag?.[1], actualContentPubkey)
|
|
1181
|
-
assert.deepEqual(
|
|
1182
|
-
await unwrapEvent({
|
|
1183
|
-
receiverSigner: bob,
|
|
1184
|
-
privateChannelSigner: alice,
|
|
1185
|
-
event: wrapped,
|
|
1186
|
-
receiverPubkey: bobPubkey
|
|
1187
|
-
}),
|
|
1188
|
-
unwrappedFixture(original, alicePubkey)
|
|
1189
|
-
)
|
|
1190
|
-
})
|
|
1191
|
-
|
|
1192
|
-
test('unwrapEvent rejects sender imkc keys that do not match the ciphertext', async () => {
|
|
1193
|
-
const alice = signer()
|
|
1194
|
-
const oldImkc = signer()
|
|
1195
|
-
const newImkc = signer()
|
|
1196
|
-
const bob = signer()
|
|
1197
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1198
|
-
NsecSigner.setContentSigners(alice, [oldImkc])
|
|
1199
|
-
const [wrapped] = await wrapEvent({
|
|
1200
|
-
senderSigner: alice,
|
|
1201
|
-
imkcSigner: oldImkc,
|
|
1202
|
-
receivers: [bobPubkey],
|
|
1203
|
-
event: eventFixture('private'),
|
|
1204
|
-
_getIykcProofs: noContentKeys
|
|
1205
|
-
})
|
|
1206
|
-
const channelPubkey = await alice.getPublicKey()
|
|
1207
|
-
const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
|
|
1208
|
-
const newImkcPubkey = await newImkc.getPublicKey()
|
|
1209
|
-
const newImkcProof = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: alice, contentKeySigner: newImkc, createdAt: 7 })).iykcProof
|
|
1210
|
-
router.tags = router.tags.map(tag => tag[0] === 'imkc' ? ['imkc', newImkcPubkey, newImkcProof] : tag)
|
|
1211
|
-
const tampered = await alice.signEvent({
|
|
1212
|
-
kind: PRIVATE_BROADCAST_KIND,
|
|
1213
|
-
created_at: wrapped.created_at,
|
|
1214
|
-
tags: wrapped.tags,
|
|
1215
|
-
content: await encryptPrivateBroadcast(alice, channelPubkey, router)
|
|
1216
|
-
})
|
|
1217
|
-
|
|
1218
|
-
await assert.rejects(
|
|
1219
|
-
() => unwrapEvent({
|
|
1220
|
-
receiverSigner: bob,
|
|
1221
|
-
privateChannelSigner: alice,
|
|
1222
|
-
event: tampered,
|
|
1223
|
-
receiverPubkey: bobPubkey
|
|
1224
|
-
}),
|
|
1225
|
-
/invalid MAC/
|
|
1226
|
-
)
|
|
1227
|
-
})
|
|
1228
|
-
|
|
1229
|
-
test('unwrapEvent rejects sender imkc rows without valid proofs', async () => {
|
|
1230
|
-
const alice = signer()
|
|
1231
|
-
const imkc = signer()
|
|
1232
|
-
const bob = signer()
|
|
1233
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1234
|
-
NsecSigner.setContentSigners(alice, [imkc])
|
|
1235
|
-
const [wrapped] = await wrapEvent({
|
|
1236
|
-
senderSigner: alice,
|
|
1237
|
-
imkcSigner: imkc,
|
|
1238
|
-
receivers: [bobPubkey],
|
|
1239
|
-
event: eventFixture('private'),
|
|
1240
|
-
_getIykcProofs: noContentKeys
|
|
1241
|
-
})
|
|
1242
|
-
const channelPubkey = await alice.getPublicKey()
|
|
1243
|
-
const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
|
|
1244
|
-
router.tags = router.tags.map(tag => tag[0] === 'imkc' ? ['imkc', tag[1]] : tag)
|
|
1245
|
-
const tampered = await alice.signEvent({
|
|
1246
|
-
kind: PRIVATE_BROADCAST_KIND,
|
|
1247
|
-
created_at: wrapped.created_at,
|
|
1248
|
-
tags: wrapped.tags,
|
|
1249
|
-
content: await encryptPrivateBroadcast(alice, channelPubkey, router)
|
|
1250
|
-
})
|
|
1251
|
-
|
|
1252
|
-
await assert.rejects(
|
|
1253
|
-
() => unwrapEvent({
|
|
1254
|
-
receiverSigner: bob,
|
|
1255
|
-
privateChannelSigner: alice,
|
|
1256
|
-
event: tampered,
|
|
1257
|
-
receiverPubkey: bobPubkey
|
|
1258
|
-
}),
|
|
1259
|
-
/INVALID_IMKC_PROOF/
|
|
1260
|
-
)
|
|
1261
|
-
})
|
|
1262
|
-
|
|
1263
|
-
test('subscribe emits content key usage for own sent direct messages', async () => {
|
|
1264
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
1265
|
-
let handlers = null
|
|
1266
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
1267
|
-
handlers = nextHandlers
|
|
1268
|
-
return { close: () => {} }
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
try {
|
|
1272
|
-
const alice = signer()
|
|
1273
|
-
const bob = signer()
|
|
1274
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1275
|
-
const [wrapped] = await wrapEvent({
|
|
1276
|
-
senderSigner: alice,
|
|
1277
|
-
receivers: [bobPubkey],
|
|
1278
|
-
receiverTag: bobPubkey,
|
|
1279
|
-
event: eventFixture('private'),
|
|
1280
|
-
_getIykcProofs: noContentKeys
|
|
1281
|
-
})
|
|
1282
|
-
const usages = []
|
|
1283
|
-
const delivered = []
|
|
1284
|
-
|
|
1285
|
-
subscribe({
|
|
1286
|
-
receiverSigner: alice,
|
|
1287
|
-
privateChannelSigner: alice,
|
|
1288
|
-
receiverPubkey: await alice.getPublicKey(),
|
|
1289
|
-
relays: ['wss://relay.example'],
|
|
1290
|
-
onContentKeyUsage: usage => usages.push(usage),
|
|
1291
|
-
onEvent: event => delivered.push(event)
|
|
1292
|
-
})
|
|
1293
|
-
await handlers.onevent(wrapped)
|
|
1294
|
-
|
|
1295
|
-
assert.equal(usages.length, 1)
|
|
1296
|
-
assert.equal(usages[0].direction, 'sent')
|
|
1297
|
-
assert.equal(usages[0].senderPubkey, await alice.getPublicKey())
|
|
1298
|
-
assert.equal(usages[0].receiverPubkey, bobPubkey)
|
|
1299
|
-
assert.equal(usages[0].contentKeyPubkey, '')
|
|
1300
|
-
assert.equal(delivered.length, 0)
|
|
1301
|
-
} finally {
|
|
1302
|
-
pool.subscribeMany = originalSubscribeMany
|
|
1303
|
-
}
|
|
1304
|
-
})
|
|
1305
|
-
|
|
1306
|
-
test('subscribe treats watchtower mode as recovery seed storage', async () => {
|
|
1307
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
1308
|
-
let handlers = null
|
|
1309
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
1310
|
-
handlers = nextHandlers
|
|
1311
|
-
return { close: () => {} }
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
try {
|
|
1315
|
-
const alice = signer()
|
|
1316
|
-
const bob = signer()
|
|
1317
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1318
|
-
const original = eventFixture('watchtower payload')
|
|
1319
|
-
const [wrapped] = await wrapEvent({
|
|
1320
|
-
senderSigner: alice,
|
|
1321
|
-
receivers: [bobPubkey],
|
|
1322
|
-
event: original,
|
|
1323
|
-
_getIykcProofs: noContentKeys
|
|
1324
|
-
})
|
|
1325
|
-
const events = []
|
|
1326
|
-
const seeds = []
|
|
1327
|
-
|
|
1328
|
-
subscribe({
|
|
1329
|
-
receiverSigner: bob,
|
|
1330
|
-
privateChannelSigner: alice,
|
|
1331
|
-
receiverPubkey: bobPubkey,
|
|
1332
|
-
relays: ['wss://relay.example'],
|
|
1333
|
-
mode: 'watchtower',
|
|
1334
|
-
onEvent: event => events.push(event),
|
|
1335
|
-
onSeedEvent: seed => seeds.push(seed)
|
|
1336
|
-
})
|
|
1337
|
-
await handlers.onevent(wrapped)
|
|
1338
|
-
|
|
1339
|
-
assert.deepEqual(events, [unwrappedFixture(original, await alice.getPublicKey())])
|
|
1340
|
-
assert.equal(seeds.length, 1)
|
|
1341
|
-
assert.equal(seeds[0].channelPubkey, await alice.getPublicKey())
|
|
1342
|
-
assert.ok(seeds[0].router.content)
|
|
1343
|
-
} finally {
|
|
1344
|
-
pool.subscribeMany = originalSubscribeMany
|
|
1345
|
-
}
|
|
1346
|
-
})
|
|
1347
|
-
|
|
1348
|
-
test('wrapEvent uses receiver content key rows when iykc is advertised', async () => {
|
|
1349
|
-
const alice = signer()
|
|
1350
|
-
const bob = signer()
|
|
1351
|
-
const bobContent = signer()
|
|
1352
|
-
const alicePubkey = await alice.getPublicKey()
|
|
1353
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1354
|
-
const contentKeyEvent = await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 })
|
|
1355
|
-
const contentKey = parseContentKeyEvent(contentKeyEvent)
|
|
1356
|
-
const original = eventFixture('private')
|
|
1357
|
-
const [wrapped] = await wrapEvent({
|
|
1358
|
-
senderSigner: alice,
|
|
1359
|
-
receivers: [bobPubkey],
|
|
1360
|
-
event: original,
|
|
1361
|
-
_getIykcProofs: async () => ({
|
|
1362
|
-
[bobPubkey]: contentKey
|
|
1363
|
-
})
|
|
1364
|
-
})
|
|
1365
|
-
const router = await decryptPrivateBroadcast(alice, alicePubkey, wrapped.content)
|
|
1366
|
-
const line = routerRecipientRows(router)[0]
|
|
1367
|
-
|
|
1368
|
-
assert.deepEqual(line.slice(0, 1), [bobPubkey])
|
|
1369
|
-
assert.deepEqual(line.slice(2), [contentKey.iykcPubkey, contentKey.iykcProof])
|
|
1370
|
-
await assert.rejects(
|
|
1371
|
-
() => unwrapEvent({ receiverSigner: bob, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }),
|
|
1372
|
-
/RECEIVER_CONTENT_KEY_REQUIRED/
|
|
1373
|
-
)
|
|
1374
|
-
NsecSigner.setContentSigners(bob, [bobContent])
|
|
1375
|
-
await assert.rejects(
|
|
1376
|
-
() => bob.nip44DecryptDoubleDH(alicePubkey, ROUTER_KIND, '', line[1], router.tags.find(t => t[0] === 'imkc')?.[1] || '', contentKey.iykcPubkey),
|
|
1377
|
-
/scope mismatch/
|
|
1378
|
-
)
|
|
1379
|
-
assert.deepEqual(await unwrapEvent({ receiverSigner: bob, iykcSigner: bobContent, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), unwrappedFixture(original, alicePubkey))
|
|
1380
|
-
})
|
|
1381
|
-
|
|
1382
|
-
test('unwrapEvent can decrypt iykc rows through receiverSigner without direct content signer access', async () => {
|
|
1383
|
-
const alice = signer()
|
|
1384
|
-
const bob = signer()
|
|
1385
|
-
const bobContent = signer()
|
|
1386
|
-
const bobProxy = await signerWithInternalContentKey(bob, bobContent)
|
|
1387
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1388
|
-
const contentKeyEvent = await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 })
|
|
1389
|
-
const contentKey = parseContentKeyEvent(contentKeyEvent)
|
|
1390
|
-
const original = eventFixture('private')
|
|
1391
|
-
const [wrapped] = await wrapEvent({
|
|
1392
|
-
senderSigner: alice,
|
|
1393
|
-
receivers: [bobPubkey],
|
|
1394
|
-
event: original,
|
|
1395
|
-
_getIykcProofs: async () => ({
|
|
1396
|
-
[bobPubkey]: contentKey
|
|
1397
|
-
})
|
|
1398
|
-
})
|
|
1399
|
-
|
|
1400
|
-
assert.deepEqual(
|
|
1401
|
-
await unwrapEvent({
|
|
1402
|
-
receiverSigner: bobProxy,
|
|
1403
|
-
privateChannelSigner: alice,
|
|
1404
|
-
event: wrapped,
|
|
1405
|
-
receiverPubkey: bobPubkey
|
|
1406
|
-
}),
|
|
1407
|
-
unwrappedFixture(original, await alice.getPublicKey())
|
|
1408
|
-
)
|
|
1409
|
-
})
|
|
1410
|
-
|
|
1411
|
-
test('unwrapEvent lets receiverSigner resolve older iykc when current content signer differs', async () => {
|
|
1412
|
-
const alice = signer()
|
|
1413
|
-
const bob = signer()
|
|
1414
|
-
const bobOldContent = signer()
|
|
1415
|
-
const bobLatestContent = signer()
|
|
1416
|
-
const bobProxy = await signerWithInternalContentKey(bob, bobOldContent)
|
|
1417
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1418
|
-
const oldContentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobOldContent, createdAt: 7 }))
|
|
1419
|
-
const original = eventFixture('older key private')
|
|
1420
|
-
const [wrapped] = await wrapEvent({
|
|
1421
|
-
senderSigner: alice,
|
|
1422
|
-
receivers: [bobPubkey],
|
|
1423
|
-
event: original,
|
|
1424
|
-
_getIykcProofs: async () => ({
|
|
1425
|
-
[bobPubkey]: oldContentKey
|
|
1426
|
-
})
|
|
1427
|
-
})
|
|
1428
|
-
|
|
1429
|
-
assert.deepEqual(
|
|
1430
|
-
await unwrapEvent({
|
|
1431
|
-
receiverSigner: bobProxy,
|
|
1432
|
-
iykcSigner: bobLatestContent,
|
|
1433
|
-
privateChannelSigner: alice,
|
|
1434
|
-
event: wrapped,
|
|
1435
|
-
receiverPubkey: bobPubkey
|
|
1436
|
-
}),
|
|
1437
|
-
unwrappedFixture(original, await alice.getPublicKey())
|
|
1438
|
-
)
|
|
1439
|
-
})
|
|
1440
|
-
|
|
1441
|
-
test('wrapEvent accepts explicit receiver content keys with valid proofs', async () => {
|
|
1442
|
-
const alice = signer()
|
|
1443
|
-
const bob = signer()
|
|
1444
|
-
const bobContent = signer()
|
|
1445
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1446
|
-
const contentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 }))
|
|
1447
|
-
const original = eventFixture('private')
|
|
1448
|
-
NsecSigner.setContentSigners(bob, [bobContent])
|
|
1449
|
-
const [wrapped] = await wrapEvent({
|
|
1450
|
-
senderSigner: alice,
|
|
1451
|
-
receivers: [[bobPubkey, contentKey.iykcPubkey, contentKey.iykcProof]],
|
|
1452
|
-
event: original,
|
|
1453
|
-
_getIykcProofs: noContentKeys
|
|
1454
|
-
})
|
|
1455
|
-
const router = await decryptPrivateBroadcast(alice, await alice.getPublicKey(), wrapped.content)
|
|
1456
|
-
const line = routerRecipientRows(router)[0]
|
|
1457
|
-
|
|
1458
|
-
assert.equal(line.length, 4)
|
|
1459
|
-
assert.deepEqual(await unwrapEvent({ receiverSigner: bob, iykcSigner: bobContent, privateChannelSigner: alice, event: wrapped, receiverPubkey: bobPubkey }), unwrappedFixture(original, await alice.getPublicKey()))
|
|
1460
|
-
})
|
|
1461
|
-
|
|
1462
|
-
test('wrapEvent rejects explicit receiver content keys without valid proofs', async () => {
|
|
1463
|
-
const alice = signer()
|
|
1464
|
-
const bob = signer()
|
|
1465
|
-
const bobContent = signer()
|
|
1466
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1467
|
-
const bobContentPubkey = await bobContent.getPublicKey()
|
|
1468
|
-
|
|
1469
|
-
await assert.rejects(
|
|
1470
|
-
() => wrapEvent({
|
|
1471
|
-
senderSigner: alice,
|
|
1472
|
-
receivers: [[bobPubkey, bobContentPubkey]],
|
|
1473
|
-
event: eventFixture('private'),
|
|
1474
|
-
_getIykcProofs: noContentKeys
|
|
1475
|
-
}),
|
|
1476
|
-
/INVALID_IYKC_PROOF/
|
|
1477
|
-
)
|
|
1478
|
-
})
|
|
1479
|
-
|
|
1480
|
-
test('unwrapEvent rejects receiver iykc rows without valid proofs', async () => {
|
|
1481
|
-
const alice = signer()
|
|
1482
|
-
const bob = signer()
|
|
1483
|
-
const bobContent = signer()
|
|
1484
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1485
|
-
const contentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 }))
|
|
1486
|
-
const original = eventFixture('private')
|
|
1487
|
-
const [wrapped] = await wrapEvent({
|
|
1488
|
-
senderSigner: alice,
|
|
1489
|
-
receivers: [[bobPubkey, contentKey.iykcPubkey, contentKey.iykcProof]],
|
|
1490
|
-
event: original,
|
|
1491
|
-
_getIykcProofs: noContentKeys
|
|
1492
|
-
})
|
|
1493
|
-
const channelPubkey = await alice.getPublicKey()
|
|
1494
|
-
const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
|
|
1495
|
-
const rows = routerJsonlRows(router)
|
|
1496
|
-
const [receiverPubkey, ciphertext, iykcPubkey] = rows.find(row => row.length !== 1)
|
|
1497
|
-
router.content = Buffer.from(`${JSON.stringify(rows[0])}\n${JSON.stringify([receiverPubkey, ciphertext, iykcPubkey, '7:bad'])}\n`).toString('base64')
|
|
1498
|
-
const tampered = await alice.signEvent({
|
|
1499
|
-
kind: PRIVATE_BROADCAST_KIND,
|
|
1500
|
-
created_at: wrapped.created_at,
|
|
1501
|
-
tags: wrapped.tags,
|
|
1502
|
-
content: await encryptPrivateBroadcast(alice, channelPubkey, router)
|
|
1503
|
-
})
|
|
1504
|
-
|
|
1505
|
-
await assert.rejects(
|
|
1506
|
-
() => unwrapEvent({ receiverSigner: bob, iykcSigner: bobContent, privateChannelSigner: alice, event: tampered, receiverPubkey: bobPubkey }),
|
|
1507
|
-
/INVALID_IYKC_PROOF/
|
|
1508
|
-
)
|
|
1509
|
-
})
|
|
1510
|
-
|
|
1511
|
-
test('subscribe only emits receiver content key usage after iykc proof validation', async () => {
|
|
1512
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
1513
|
-
let handlers = null
|
|
1514
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
1515
|
-
handlers = nextHandlers
|
|
1516
|
-
return { close: () => {} }
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
try {
|
|
1520
|
-
const alice = signer()
|
|
1521
|
-
const bob = signer()
|
|
1522
|
-
const bobContent = signer()
|
|
1523
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1524
|
-
const contentKey = parseContentKeyEvent(await makeContentKeyEvent({ userSigner: bob, contentKeySigner: bobContent, createdAt: 7 }))
|
|
1525
|
-
const [wrapped] = await wrapEvent({
|
|
1526
|
-
senderSigner: alice,
|
|
1527
|
-
receivers: [[bobPubkey, contentKey.iykcPubkey, contentKey.iykcProof]],
|
|
1528
|
-
event: eventFixture('private'),
|
|
1529
|
-
_getIykcProofs: noContentKeys
|
|
1530
|
-
})
|
|
1531
|
-
const channelPubkey = await alice.getPublicKey()
|
|
1532
|
-
const router = await decryptPrivateBroadcast(alice, channelPubkey, wrapped.content)
|
|
1533
|
-
const rows = routerJsonlRows(router)
|
|
1534
|
-
const [receiverPubkey, ciphertext, iykcPubkey] = rows.find(row => row.length !== 1)
|
|
1535
|
-
router.content = Buffer.from(`${JSON.stringify(rows[0])}\n${JSON.stringify([receiverPubkey, ciphertext, iykcPubkey, '7:bad'])}\n`).toString('base64')
|
|
1536
|
-
const tampered = await alice.signEvent({
|
|
1537
|
-
kind: PRIVATE_BROADCAST_KIND,
|
|
1538
|
-
created_at: wrapped.created_at,
|
|
1539
|
-
tags: wrapped.tags,
|
|
1540
|
-
content: await encryptPrivateBroadcast(alice, channelPubkey, router)
|
|
1541
|
-
})
|
|
1542
|
-
const usages = []
|
|
1543
|
-
const errors = []
|
|
1544
|
-
|
|
1545
|
-
subscribe({
|
|
1546
|
-
privateChannelSigner: alice,
|
|
1547
|
-
receiverPubkey: bobPubkey,
|
|
1548
|
-
relays: ['wss://relay.example'],
|
|
1549
|
-
onContentKeyUsage: usage => usages.push(usage),
|
|
1550
|
-
onError: err => errors.push(err)
|
|
1551
|
-
})
|
|
1552
|
-
|
|
1553
|
-
await handlers.onevent(tampered)
|
|
1554
|
-
|
|
1555
|
-
assert.deepEqual(usages, [])
|
|
1556
|
-
assert.equal(errors.length, 1)
|
|
1557
|
-
assert.equal(errors[0].message, 'INVALID_IYKC_PROOF')
|
|
1558
|
-
} finally {
|
|
1559
|
-
pool.subscribeMany = originalSubscribeMany
|
|
1560
|
-
}
|
|
1561
|
-
})
|
|
1562
|
-
|
|
1563
|
-
test('wrapEvent chunks large jsonl without oversize events and unwraps reassembled bytes', async () => {
|
|
1564
|
-
const alice = signer()
|
|
1565
|
-
const imkc = signer()
|
|
1566
|
-
const bob = signer()
|
|
1567
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1568
|
-
const imkcPubkey = await imkc.getPublicKey()
|
|
1569
|
-
const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
|
|
1570
|
-
NsecSigner.setContentSigners(alice, [imkc])
|
|
1571
|
-
const wrapped = await wrapEvent({
|
|
1572
|
-
senderSigner: alice,
|
|
1573
|
-
imkcSigner: imkc,
|
|
1574
|
-
receivers: [bobPubkey],
|
|
1575
|
-
deletionPubkey: 'd'.repeat(64),
|
|
1576
|
-
event: original,
|
|
1577
|
-
_getIykcProofs: noContentKeys
|
|
1578
|
-
})
|
|
1579
|
-
|
|
1580
|
-
assert.ok(wrapped.length > 1)
|
|
1581
|
-
const deletionPubkeys = new Set()
|
|
1582
|
-
for (const event of wrapped) {
|
|
1583
|
-
assert.ok(new TextEncoder().encode(JSON.stringify(event)).length <= MAX_EVENT_BYTES)
|
|
1584
|
-
deletionPubkeys.add(event.tags.find(tag => tag[0] === 's')?.[1])
|
|
1585
|
-
}
|
|
1586
|
-
assert.deepEqual(deletionPubkeys, new Set(['d'.repeat(64)]))
|
|
1587
|
-
|
|
1588
|
-
const routers = []
|
|
1589
|
-
for (const event of wrapped) {
|
|
1590
|
-
routers.push(await decryptPrivateBroadcast(alice, await alice.getPublicKey(), event.content))
|
|
1591
|
-
}
|
|
1592
|
-
assert.equal(routers[0].kind, ROUTER_KIND)
|
|
1593
|
-
assert.equal(routers[0].tags.find(t => t[0] === 'r')?.[1], bobPubkey)
|
|
1594
|
-
assert.equal(routers[0].tags.find(t => t[0] === 'imkc')?.[1], imkcPubkey)
|
|
1595
|
-
assert.ok(routers[0].tags.find(t => t[0] === 'imkc')?.[2])
|
|
1596
|
-
assert.equal(routers.length, Number(routers[0].tags.find(t => t[0] === 'c')[2]))
|
|
1597
|
-
})
|
|
1598
|
-
|
|
1599
|
-
test('wrapEvents cleans temporary chunks when the stream is stopped early', async () => {
|
|
1600
|
-
const alice = signer()
|
|
1601
|
-
const bob = signer()
|
|
1602
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1603
|
-
const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
|
|
1604
|
-
const stream = wrapEvents({ senderSigner: alice, receivers: [bobPubkey], event: original, _getIykcProofs: noContentKeys })
|
|
1605
|
-
|
|
1606
|
-
const first = await stream.next()
|
|
1607
|
-
assert.equal(first.done, false)
|
|
1608
|
-
assert.notEqual(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
1609
|
-
|
|
1610
|
-
await stream.return()
|
|
1611
|
-
|
|
1612
|
-
assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
1613
|
-
})
|
|
1614
|
-
|
|
1615
|
-
test('wrapEvents uses a caller-supplied temporary storage area', async () => {
|
|
1616
|
-
const alice = signer()
|
|
1617
|
-
const bob = signer()
|
|
1618
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1619
|
-
const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
|
|
1620
|
-
const stream = wrapEvents({
|
|
1621
|
-
senderSigner: alice,
|
|
1622
|
-
receivers: [bobPubkey],
|
|
1623
|
-
event: original,
|
|
1624
|
-
temporaryStorageArea: globalThis.localStorage,
|
|
1625
|
-
_getIykcProofs: noContentKeys
|
|
1626
|
-
})
|
|
1627
|
-
|
|
1628
|
-
await stream.next()
|
|
1629
|
-
|
|
1630
|
-
assert.notEqual(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
1631
|
-
assert.equal(globalThis.sessionStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
1632
|
-
|
|
1633
|
-
await stream.return()
|
|
1634
|
-
|
|
1635
|
-
assert.equal(globalThis.localStorage.getItem(TEMPORARY_STORAGE_KEYS_KEY), null)
|
|
1636
|
-
})
|
|
1637
|
-
|
|
1638
|
-
test('subscribe buffers out-of-order chunks and unwraps when the missing chunk arrives', async () => {
|
|
1639
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
1640
|
-
let handlers = null
|
|
1641
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
1642
|
-
handlers = nextHandlers
|
|
1643
|
-
return { close: () => {} }
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
try {
|
|
1647
|
-
const alice = signer()
|
|
1648
|
-
const bob = signer()
|
|
1649
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1650
|
-
const original = eventFixture('x'.repeat(getJsonlChunkByteSize()))
|
|
1651
|
-
const wrapped = await wrapEvent({ senderSigner: alice, receivers: [bobPubkey], event: original, _getIykcProofs: noContentKeys })
|
|
1652
|
-
const events = []
|
|
1653
|
-
const chunks = []
|
|
1654
|
-
|
|
1655
|
-
assert.ok(wrapped.length > 1)
|
|
1656
|
-
subscribe({
|
|
1657
|
-
receiverSigner: bob,
|
|
1658
|
-
privateChannelSigner: alice,
|
|
1659
|
-
receiverPubkey: bobPubkey,
|
|
1660
|
-
relays: ['wss://relay.example'],
|
|
1661
|
-
onChunk: chunk => chunks.push(chunk),
|
|
1662
|
-
onEvent: event => events.push(event),
|
|
1663
|
-
_getIykcProofs: noContentKeys
|
|
1664
|
-
})
|
|
1665
|
-
|
|
1666
|
-
for (const event of wrapped.slice(1)) await handlers.onevent(event)
|
|
1667
|
-
|
|
1668
|
-
assert.equal(events.length, 0)
|
|
1669
|
-
assert.ok(chunks.at(-1).missing.includes(0))
|
|
1670
|
-
|
|
1671
|
-
await handlers.onevent(wrapped[0])
|
|
1672
|
-
|
|
1673
|
-
assert.deepEqual(events, [unwrappedFixture(original, await alice.getPublicKey())])
|
|
1674
|
-
} finally {
|
|
1675
|
-
pool.subscribeMany = originalSubscribeMany
|
|
1676
|
-
}
|
|
1677
|
-
})
|
|
1678
|
-
|
|
1679
|
-
test('subscribe drops invalid signed inner events and emits an error', async () => {
|
|
1680
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
1681
|
-
let handlers = null
|
|
1682
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
1683
|
-
handlers = nextHandlers
|
|
1684
|
-
return { close: () => {} }
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
try {
|
|
1688
|
-
const alice = signer()
|
|
1689
|
-
const bob = signer()
|
|
1690
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1691
|
-
const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'signed' }, generateSecretKey())
|
|
1692
|
-
const [wrapped] = await wrapEvent({
|
|
1693
|
-
senderSigner: alice,
|
|
1694
|
-
receivers: [bobPubkey],
|
|
1695
|
-
event: { ...signed, content: 'tampered' },
|
|
1696
|
-
_getIykcProofs: noContentKeys
|
|
1697
|
-
})
|
|
1698
|
-
const events = []
|
|
1699
|
-
const errors = []
|
|
1700
|
-
|
|
1701
|
-
subscribe({
|
|
1702
|
-
receiverSigner: bob,
|
|
1703
|
-
privateChannelSigner: alice,
|
|
1704
|
-
receiverPubkey: bobPubkey,
|
|
1705
|
-
relays: ['wss://relay.example'],
|
|
1706
|
-
onEvent: event => events.push(event),
|
|
1707
|
-
onError: err => errors.push(err),
|
|
1708
|
-
_getIykcProofs: noContentKeys
|
|
1709
|
-
})
|
|
1710
|
-
|
|
1711
|
-
await handlers.onevent(wrapped)
|
|
1712
|
-
await handlers.onevent(wrapped)
|
|
1713
|
-
|
|
1714
|
-
assert.equal(events.length, 0)
|
|
1715
|
-
assert.equal(errors.length, 1)
|
|
1716
|
-
assert.equal(errors[0].message, 'INVALID_SIGNED_INNER_EVENT')
|
|
1717
|
-
} finally {
|
|
1718
|
-
pool.subscribeMany = originalSubscribeMany
|
|
1719
|
-
}
|
|
1720
|
-
})
|
|
1721
|
-
|
|
1722
|
-
test('subscribe forgets ignored groups after their ttl', async () => {
|
|
1723
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
1724
|
-
const originalNow = Date.now
|
|
1725
|
-
let handlers = null
|
|
1726
|
-
let now = 1000
|
|
1727
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
1728
|
-
handlers = nextHandlers
|
|
1729
|
-
return { close: () => {} }
|
|
1730
|
-
}
|
|
1731
|
-
Date.now = () => now
|
|
1732
|
-
|
|
1733
|
-
try {
|
|
1734
|
-
const alice = signer()
|
|
1735
|
-
const bob = signer()
|
|
1736
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1737
|
-
const signed = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'signed' }, generateSecretKey())
|
|
1738
|
-
const [wrapped] = await wrapEvent({
|
|
1739
|
-
senderSigner: alice,
|
|
1740
|
-
receivers: [bobPubkey],
|
|
1741
|
-
event: { ...signed, content: 'tampered' },
|
|
1742
|
-
_getIykcProofs: noContentKeys
|
|
1743
|
-
})
|
|
1744
|
-
const errors = []
|
|
1745
|
-
|
|
1746
|
-
subscribe({
|
|
1747
|
-
receiverSigner: bob,
|
|
1748
|
-
privateChannelSigner: alice,
|
|
1749
|
-
receiverPubkey: bobPubkey,
|
|
1750
|
-
relays: ['wss://relay.example'],
|
|
1751
|
-
ignoredGroupTtlMs: 5,
|
|
1752
|
-
onError: err => errors.push(err),
|
|
1753
|
-
_getIykcProofs: noContentKeys
|
|
1754
|
-
})
|
|
1755
|
-
|
|
1756
|
-
await handlers.onevent(wrapped)
|
|
1757
|
-
now += 4
|
|
1758
|
-
await handlers.onevent(wrapped)
|
|
1759
|
-
now += 2
|
|
1760
|
-
await handlers.onevent(wrapped)
|
|
1761
|
-
|
|
1762
|
-
assert.equal(errors.length, 2)
|
|
1763
|
-
assert.equal(errors[0].message, 'INVALID_SIGNED_INNER_EVENT')
|
|
1764
|
-
assert.equal(errors[1].message, 'INVALID_SIGNED_INNER_EVENT')
|
|
1765
|
-
} finally {
|
|
1766
|
-
Date.now = originalNow
|
|
1767
|
-
pool.subscribeMany = originalSubscribeMany
|
|
1768
|
-
}
|
|
1769
|
-
})
|
|
1770
|
-
|
|
1771
|
-
test('subscribe evicts old ignored groups when the tombstone cache is full', async () => {
|
|
1772
|
-
const originalSubscribeMany = pool.subscribeMany
|
|
1773
|
-
let handlers = null
|
|
1774
|
-
pool.subscribeMany = (_relays, _filter, nextHandlers) => {
|
|
1775
|
-
handlers = nextHandlers
|
|
1776
|
-
return { close: () => {} }
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
try {
|
|
1780
|
-
const alice = signer()
|
|
1781
|
-
const bob = signer()
|
|
1782
|
-
const bobPubkey = await bob.getPublicKey()
|
|
1783
|
-
const signedA = finalizeEvent({ kind: 9002, created_at: 2, tags: [], content: 'a' }, generateSecretKey())
|
|
1784
|
-
const signedB = finalizeEvent({ kind: 9002, created_at: 3, tags: [], content: 'b' }, generateSecretKey())
|
|
1785
|
-
const [wrappedA] = await wrapEvent({
|
|
1786
|
-
senderSigner: alice,
|
|
1787
|
-
receivers: [bobPubkey],
|
|
1788
|
-
event: { ...signedA, content: 'tampered-a' },
|
|
1789
|
-
_getIykcProofs: noContentKeys
|
|
1790
|
-
})
|
|
1791
|
-
const [wrappedB] = await wrapEvent({
|
|
1792
|
-
senderSigner: alice,
|
|
1793
|
-
receivers: [bobPubkey],
|
|
1794
|
-
event: { ...signedB, content: 'tampered-b' },
|
|
1795
|
-
_getIykcProofs: noContentKeys
|
|
1796
|
-
})
|
|
1797
|
-
const errors = []
|
|
1798
|
-
|
|
1799
|
-
subscribe({
|
|
1800
|
-
receiverSigner: bob,
|
|
1801
|
-
privateChannelSigner: alice,
|
|
1802
|
-
receiverPubkey: bobPubkey,
|
|
1803
|
-
relays: ['wss://relay.example'],
|
|
1804
|
-
ignoredGroupMaxEntries: 1,
|
|
1805
|
-
onError: err => errors.push(err),
|
|
1806
|
-
_getIykcProofs: noContentKeys
|
|
1807
|
-
})
|
|
1808
|
-
|
|
1809
|
-
await handlers.onevent(wrappedA)
|
|
1810
|
-
await handlers.onevent(wrappedA)
|
|
1811
|
-
await handlers.onevent(wrappedB)
|
|
1812
|
-
await handlers.onevent(wrappedA)
|
|
1813
|
-
|
|
1814
|
-
assert.equal(errors.length, 3)
|
|
1815
|
-
assert.ok(errors.every(err => err.message === 'INVALID_SIGNED_INNER_EVENT'))
|
|
1816
|
-
} finally {
|
|
1817
|
-
pool.subscribeMany = originalSubscribeMany
|
|
1818
|
-
}
|
|
1819
|
-
})
|
|
1820
|
-
|
|
1821
|
-
test('received chunk store purges stale incomplete groups', () => {
|
|
1822
|
-
const originalNow = Date.now
|
|
1823
|
-
const storage = (() => {
|
|
1824
|
-
const data = new Map()
|
|
1825
|
-
return {
|
|
1826
|
-
clear: () => data.clear(),
|
|
1827
|
-
getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
|
|
1828
|
-
removeItem: key => { data.delete(String(key)) },
|
|
1829
|
-
setItem: (key, value) => { data.set(String(key), String(value)) },
|
|
1830
|
-
get length () { return data.size }
|
|
1831
|
-
}
|
|
1832
|
-
})()
|
|
1833
|
-
let now = 1000
|
|
1834
|
-
Date.now = () => now
|
|
1835
|
-
|
|
1836
|
-
try {
|
|
1837
|
-
const store = createReceivedChunkStore({
|
|
1838
|
-
prefix: 'test:received-chunks',
|
|
1839
|
-
storageArea: storage,
|
|
1840
|
-
ttlMs: 5
|
|
1841
|
-
})
|
|
1842
|
-
store.put({
|
|
1843
|
-
channelPubkey: 'channel',
|
|
1844
|
-
routerPubkey: 'router',
|
|
1845
|
-
index: 1,
|
|
1846
|
-
total: 2,
|
|
1847
|
-
content: 'abc'
|
|
1848
|
-
})
|
|
1849
|
-
|
|
1850
|
-
assert.ok(storage.length > 0)
|
|
1851
|
-
|
|
1852
|
-
now += 6
|
|
1853
|
-
store.cleanupStale()
|
|
1854
|
-
|
|
1855
|
-
assert.equal(storage.length, 0)
|
|
1856
|
-
} finally {
|
|
1857
|
-
Date.now = originalNow
|
|
1858
|
-
}
|
|
1859
|
-
})
|
|
1860
|
-
|
|
1861
|
-
test('received chunk store resumes incremental parsing after reload', async () => {
|
|
1862
|
-
const storage = (() => {
|
|
1863
|
-
const data = new Map()
|
|
1864
|
-
return {
|
|
1865
|
-
clear: () => data.clear(),
|
|
1866
|
-
getItem: key => data.has(String(key)) ? data.get(String(key)) : null,
|
|
1867
|
-
removeItem: key => { data.delete(String(key)) },
|
|
1868
|
-
setItem: (key, value) => { data.set(String(key), String(value)) },
|
|
1869
|
-
get length () { return data.size }
|
|
1870
|
-
}
|
|
1871
|
-
})()
|
|
1872
|
-
const line = `${JSON.stringify(['alice', 'ciphertext'])}\n`
|
|
1873
|
-
const first = line.slice(0, 12)
|
|
1874
|
-
const second = line.slice(12)
|
|
1875
|
-
const firstStore = createReceivedChunkStore({
|
|
1876
|
-
prefix: 'test:received-chunks:reload',
|
|
1877
|
-
storageArea: storage
|
|
1878
|
-
})
|
|
1879
|
-
const lines = []
|
|
1880
|
-
|
|
1881
|
-
firstStore.put({
|
|
1882
|
-
channelPubkey: 'channel',
|
|
1883
|
-
routerPubkey: 'router',
|
|
1884
|
-
index: 0,
|
|
1885
|
-
total: 2,
|
|
1886
|
-
content: Buffer.from(first).toString('base64')
|
|
1887
|
-
})
|
|
1888
|
-
|
|
1889
|
-
const firstDrain = await firstStore.drainAvailable('channel:router', { onLine: line => lines.push(line) })
|
|
1890
|
-
|
|
1891
|
-
assert.equal(firstDrain.complete, false)
|
|
1892
|
-
assert.equal(firstDrain.meta.nextIndex, 1)
|
|
1893
|
-
assert.equal(firstDrain.meta.carry, first)
|
|
1894
|
-
assert.deepEqual(lines, [])
|
|
1895
|
-
|
|
1896
|
-
const secondStore = createReceivedChunkStore({
|
|
1897
|
-
prefix: 'test:received-chunks:reload',
|
|
1898
|
-
storageArea: storage
|
|
1899
|
-
})
|
|
1900
|
-
secondStore.put({
|
|
1901
|
-
channelPubkey: 'channel',
|
|
1902
|
-
routerPubkey: 'router',
|
|
1903
|
-
index: 1,
|
|
1904
|
-
total: 2,
|
|
1905
|
-
content: Buffer.from(second).toString('base64')
|
|
1906
|
-
})
|
|
1907
|
-
|
|
1908
|
-
const secondDrain = await secondStore.drainAvailable('channel:router', { onLine: line => lines.push(line) })
|
|
1909
|
-
|
|
1910
|
-
assert.equal(secondDrain.complete, true)
|
|
1911
|
-
assert.deepEqual(lines, [line.trim()])
|
|
1912
|
-
})
|