pakstr 0.19.3 → 0.21.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/README.md +1 -1
- package/android-template/app/build.gradle.kts +4 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +273 -0
- package/android-template/app/src/main/assets/www/index.html +0 -6
- package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +20 -4
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +136 -12
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Hash.kt +56 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Signer.kt +104 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Verifier.kt +20 -170
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip44Crypto.kt +513 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip46ClientKey.kt +140 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +3 -28
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventSerializer.kt +74 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +100 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/BunkerSigner.kt +32 -7
- package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +36 -9
- package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +240 -163
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConfig.kt +175 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConnectionStore.kt +144 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Client.kt +79 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Models.kt +319 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingClient.kt +159 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingQrCode.kt +35 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingUri.kt +104 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Protocol.kt +695 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46RelayTransport.kt +489 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/RelayNip46Client.kt +403 -0
- package/android-template/app/src/main/java/com/pakstr/app/utils/BunkerPairingDialog.kt +82 -0
- package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +78 -17
- package/android-template/app/src/main/res/layout/activity_layout.xml +11 -0
- package/android-template/app/src/main/res/layout/dialog_bunker_pairing.xml +75 -0
- package/android-template/app/src/main/res/layout/dialog_picker.xml +60 -31
- package/android-template/app/src/main/res/values/strings.xml +27 -0
- package/android-template/app/src/test/java/com/pakstr/app/LocalServerQueryKeepAliveTest.kt +118 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Bip340SignerTest.kt +133 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip44CryptoTest.kt +199 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyProtocolTest.kt +82 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyTest.kt +52 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/NostrEventSerializerTest.kt +42 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/NostrBridgeErrorTest.kt +32 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/BunkerConfigTest.kt +120 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ModelsTest.kt +111 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingClientTest.kt +271 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingQrCodeTest.kt +54 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingUriTest.kt +80 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ProtocolTest.kt +578 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46RelayTransportTest.kt +508 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/RelayNip46ClientTest.kt +678 -0
- package/android-template/gradle/libs.versions.toml +8 -0
- package/dist/cli.js +8 -0
- package/dist/commands/init.js +16 -0
- package/dist/commands/nsite.js +38 -0
- package/dist/commands/publish.js +50 -2
- package/dist/commands/run.js +5 -5
- package/dist/core/blossom.js +2 -2
- package/dist/core/nsite.js +176 -0
- package/dist/core/pakstrConfig.js +85 -6
- package/package.json +1 -1
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
package com.pakstr.app.signer.bunker
|
|
2
|
+
|
|
3
|
+
import com.pakstr.app.crypto.Bip340Hash
|
|
4
|
+
import com.pakstr.app.crypto.Nip44Crypto
|
|
5
|
+
import com.pakstr.app.crypto.Nip46ClientKey
|
|
6
|
+
import com.pakstr.app.crypto.NostrEventVerifier
|
|
7
|
+
import com.pakstr.app.signer.NostrEventBuilder
|
|
8
|
+
import org.json.JSONArray
|
|
9
|
+
import org.json.JSONObject
|
|
10
|
+
import org.junit.After
|
|
11
|
+
import org.junit.Assert.assertEquals
|
|
12
|
+
import org.junit.Assert.assertFalse
|
|
13
|
+
import org.junit.Assert.assertThrows
|
|
14
|
+
import org.junit.Assert.assertTrue
|
|
15
|
+
import org.junit.Before
|
|
16
|
+
import org.junit.Test
|
|
17
|
+
|
|
18
|
+
class Nip46ProtocolTest {
|
|
19
|
+
private lateinit var clientKey: Nip46ClientKey
|
|
20
|
+
private lateinit var signerKey: Nip46ClientKey
|
|
21
|
+
private lateinit var config: BunkerConfig
|
|
22
|
+
private lateinit var protocol: Nip46Protocol
|
|
23
|
+
|
|
24
|
+
@Before
|
|
25
|
+
fun setup() {
|
|
26
|
+
clientKey = Nip46ClientKey.fromPrivateKey(privateKey(1))
|
|
27
|
+
signerKey = Nip46ClientKey.fromPrivateKey(privateKey(2))
|
|
28
|
+
config =
|
|
29
|
+
BunkerConfig.parse(
|
|
30
|
+
"bunker://${signerKey.publicKeyHex}" +
|
|
31
|
+
"?relay=wss://relay.example" +
|
|
32
|
+
"&secret=$PAIRING_SECRET"
|
|
33
|
+
)
|
|
34
|
+
protocol =
|
|
35
|
+
Nip46Protocol(
|
|
36
|
+
clientKey,
|
|
37
|
+
config,
|
|
38
|
+
Nip46RequestIdGenerator { REQUEST_ID },
|
|
39
|
+
clockSeconds = { CREATED_AT }
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@After
|
|
44
|
+
fun teardown() {
|
|
45
|
+
clientKey.close()
|
|
46
|
+
signerKey.close()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Test
|
|
50
|
+
fun connect_request_builds_signed_encrypted_kind_24133_event() {
|
|
51
|
+
val outbound =
|
|
52
|
+
protocol.prepareConnect(
|
|
53
|
+
clientMetadataJson =
|
|
54
|
+
JSONObject()
|
|
55
|
+
.put("name", "Pakstr")
|
|
56
|
+
.toString()
|
|
57
|
+
)
|
|
58
|
+
val event = JSONObject(outbound.eventJson)
|
|
59
|
+
|
|
60
|
+
assertEquals(24133, event.getInt("kind"))
|
|
61
|
+
assertEquals(clientKey.publicKeyHex, event.getString("pubkey"))
|
|
62
|
+
assertEquals(CREATED_AT, event.getLong("created_at"))
|
|
63
|
+
assertEquals(
|
|
64
|
+
signerKey.publicKeyHex,
|
|
65
|
+
event.getJSONArray("tags")
|
|
66
|
+
.getJSONArray(0)
|
|
67
|
+
.getString(1)
|
|
68
|
+
)
|
|
69
|
+
assertTrue(NostrEventVerifier.verify(event))
|
|
70
|
+
assertFalse(outbound.eventJson.contains(PAIRING_SECRET))
|
|
71
|
+
|
|
72
|
+
val request =
|
|
73
|
+
decryptRequest(
|
|
74
|
+
event.getString("content")
|
|
75
|
+
)
|
|
76
|
+
assertEquals(Nip46Method.CONNECT, request.method)
|
|
77
|
+
assertEquals(signerKey.publicKeyHex, request.params[0])
|
|
78
|
+
assertEquals(PAIRING_SECRET, request.params[1])
|
|
79
|
+
assertEquals("", request.params[2])
|
|
80
|
+
assertEquals(4, request.params.size)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Test
|
|
84
|
+
fun disconnect_request_builds_signed_encrypted_event() {
|
|
85
|
+
val outbound = protocol.prepareDisconnect()
|
|
86
|
+
val event = JSONObject(outbound.eventJson)
|
|
87
|
+
val request = decryptRequest(event.getString("content"))
|
|
88
|
+
|
|
89
|
+
assertEquals(24133, event.getInt("kind"))
|
|
90
|
+
assertTrue(NostrEventVerifier.verify(event))
|
|
91
|
+
assertEquals(Nip46Method.DISCONNECT, request.method)
|
|
92
|
+
assertTrue(request.params.isEmpty())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Test
|
|
96
|
+
fun connect_accepts_ack_or_matching_pairing_secret() {
|
|
97
|
+
listOf("ack", PAIRING_SECRET).forEach { result ->
|
|
98
|
+
val outbound = protocol.prepareConnect()
|
|
99
|
+
val outcome =
|
|
100
|
+
protocol.correlateResponse(
|
|
101
|
+
protocol.decodeResponseEvent(
|
|
102
|
+
createResponseEvent(
|
|
103
|
+
REQUEST_ID,
|
|
104
|
+
result = result
|
|
105
|
+
)
|
|
106
|
+
),
|
|
107
|
+
outbound.context
|
|
108
|
+
)
|
|
109
|
+
assertEquals(
|
|
110
|
+
result,
|
|
111
|
+
(outcome as Nip46ResponseOutcome.Success).result
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Test
|
|
117
|
+
fun connect_rejects_wrong_pairing_secret() {
|
|
118
|
+
val outbound = protocol.prepareConnect()
|
|
119
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
120
|
+
protocol.correlateResponse(
|
|
121
|
+
protocol.decodeResponseEvent(
|
|
122
|
+
createResponseEvent(
|
|
123
|
+
REQUEST_ID,
|
|
124
|
+
result = "wrong-pairing-secret"
|
|
125
|
+
)
|
|
126
|
+
),
|
|
127
|
+
outbound.context
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Test
|
|
133
|
+
fun sign_event_uses_exact_unsigned_fields_and_rejects_signer_fields() {
|
|
134
|
+
val unsigned =
|
|
135
|
+
JSONObject()
|
|
136
|
+
.put("kind", 1)
|
|
137
|
+
.put("created_at", CREATED_AT)
|
|
138
|
+
.put("tags", JSONArray().put(JSONArray().put("t").put("test")))
|
|
139
|
+
.put("content", "private event content")
|
|
140
|
+
val outbound = protocol.prepareSignEvent(
|
|
141
|
+
unsigned.toString(),
|
|
142
|
+
signerKey.publicKeyHex
|
|
143
|
+
)
|
|
144
|
+
val request =
|
|
145
|
+
decryptRequest(
|
|
146
|
+
JSONObject(outbound.eventJson)
|
|
147
|
+
.getString("content")
|
|
148
|
+
)
|
|
149
|
+
val parameter = JSONObject(request.params.single())
|
|
150
|
+
|
|
151
|
+
assertEquals(4, parameter.length())
|
|
152
|
+
assertFalse(parameter.has("id"))
|
|
153
|
+
assertFalse(parameter.has("pubkey"))
|
|
154
|
+
assertFalse(parameter.has("sig"))
|
|
155
|
+
|
|
156
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
157
|
+
protocol.prepareSignEvent(
|
|
158
|
+
JSONObject(unsigned.toString())
|
|
159
|
+
.put("pubkey", signerKey.publicKeyHex)
|
|
160
|
+
.toString(),
|
|
161
|
+
signerKey.publicKeyHex
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@Test
|
|
167
|
+
fun verified_response_is_decrypted_and_correlated() {
|
|
168
|
+
val outbound = protocol.prepareGetPublicKey()
|
|
169
|
+
val responseEvent =
|
|
170
|
+
createResponseEvent(
|
|
171
|
+
REQUEST_ID,
|
|
172
|
+
result = signerKey.publicKeyHex
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
val verified =
|
|
176
|
+
protocol.decodeResponseEvent(responseEvent)
|
|
177
|
+
val outcome =
|
|
178
|
+
protocol.correlateResponse(
|
|
179
|
+
verified,
|
|
180
|
+
outbound.context
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
assertEquals(
|
|
184
|
+
signerKey.publicKeyHex,
|
|
185
|
+
(outcome as Nip46ResponseOutcome.Success).result
|
|
186
|
+
)
|
|
187
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
188
|
+
protocol.correlateResponse(
|
|
189
|
+
verified,
|
|
190
|
+
outbound.context
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Test
|
|
196
|
+
fun auth_url_requires_absolute_https_and_allows_followup_response() {
|
|
197
|
+
val outbound = protocol.prepareGetPublicKey()
|
|
198
|
+
val authUrl = "https://signer.example/approve/request"
|
|
199
|
+
val verified =
|
|
200
|
+
protocol.decodeResponseEvent(
|
|
201
|
+
createResponseEvent(
|
|
202
|
+
REQUEST_ID,
|
|
203
|
+
result = "auth_url",
|
|
204
|
+
error = authUrl
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
val outcome =
|
|
209
|
+
protocol.correlateResponse(
|
|
210
|
+
verified,
|
|
211
|
+
outbound.context
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
assertEquals(
|
|
215
|
+
authUrl,
|
|
216
|
+
(outcome as Nip46ResponseOutcome.AuthChallenge).url
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
val success =
|
|
220
|
+
protocol.correlateResponse(
|
|
221
|
+
protocol.decodeResponseEvent(
|
|
222
|
+
createResponseEvent(
|
|
223
|
+
REQUEST_ID,
|
|
224
|
+
result = signerKey.publicKeyHex
|
|
225
|
+
)
|
|
226
|
+
),
|
|
227
|
+
outbound.context
|
|
228
|
+
)
|
|
229
|
+
assertTrue(success is Nip46ResponseOutcome.Success)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@Test
|
|
233
|
+
fun non_https_auth_urls_are_rejected() {
|
|
234
|
+
listOf(
|
|
235
|
+
"http://signer.example/approve",
|
|
236
|
+
"nostr-auth://approve/request",
|
|
237
|
+
"javascript:alert(1)",
|
|
238
|
+
"/relative/approve",
|
|
239
|
+
"https://user@signer.example/approve",
|
|
240
|
+
"https://signer.example/approve#fragment"
|
|
241
|
+
).forEach { authUrl ->
|
|
242
|
+
val outbound = protocol.prepareGetPublicKey()
|
|
243
|
+
val verified =
|
|
244
|
+
protocol.decodeResponseEvent(
|
|
245
|
+
createResponseEvent(
|
|
246
|
+
REQUEST_ID,
|
|
247
|
+
result = "auth_url",
|
|
248
|
+
error = authUrl
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
252
|
+
protocol.correlateResponse(
|
|
253
|
+
verified,
|
|
254
|
+
outbound.context
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@Test
|
|
261
|
+
fun invalid_author_recipient_signature_payload_and_id_are_rejected() {
|
|
262
|
+
val outbound = protocol.prepareGetPublicKey()
|
|
263
|
+
val valid = JSONObject(
|
|
264
|
+
createResponseEvent(
|
|
265
|
+
REQUEST_ID,
|
|
266
|
+
result = signerKey.publicKeyHex
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
val wrongAuthor = JSONObject(valid.toString())
|
|
271
|
+
.put("pubkey", clientKey.publicKeyHex)
|
|
272
|
+
assertProtocolFailureDoesNotExpose(
|
|
273
|
+
wrongAuthor.toString(),
|
|
274
|
+
signerKey.publicKeyHex
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
val wrongRecipient =
|
|
278
|
+
resignResponse(
|
|
279
|
+
JSONObject(valid.toString())
|
|
280
|
+
.put(
|
|
281
|
+
"tags",
|
|
282
|
+
JSONArray()
|
|
283
|
+
.put(
|
|
284
|
+
JSONArray()
|
|
285
|
+
.put("p")
|
|
286
|
+
.put(signerKey.publicKeyHex)
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
assertProtocolFailureDoesNotExpose(
|
|
291
|
+
wrongRecipient,
|
|
292
|
+
signerKey.publicKeyHex
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
val badSignature = JSONObject(valid.toString())
|
|
296
|
+
.put("sig", "0".repeat(128))
|
|
297
|
+
assertProtocolFailureDoesNotExpose(
|
|
298
|
+
badSignature.toString(),
|
|
299
|
+
PAIRING_SECRET
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
val tamperedPayload = JSONObject(valid.toString())
|
|
303
|
+
.put("content", valid.getString("content") + "A")
|
|
304
|
+
assertProtocolFailureDoesNotExpose(
|
|
305
|
+
tamperedPayload.toString(),
|
|
306
|
+
PAIRING_SECRET
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
val mismatched =
|
|
310
|
+
protocol.decodeResponseEvent(
|
|
311
|
+
createResponseEvent(
|
|
312
|
+
"b".repeat(64),
|
|
313
|
+
result = signerKey.publicKeyHex
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
val error =
|
|
317
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
318
|
+
protocol.correlateResponse(
|
|
319
|
+
mismatched,
|
|
320
|
+
outbound.context
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
assertFalse(error.message.orEmpty().contains(REQUEST_ID))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@Test
|
|
327
|
+
fun response_requires_kind_and_exactly_one_client_recipient_tag() {
|
|
328
|
+
val valid = JSONObject(
|
|
329
|
+
createResponseEvent(
|
|
330
|
+
REQUEST_ID,
|
|
331
|
+
result = signerKey.publicKeyHex
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
val invalidEvents =
|
|
335
|
+
listOf(
|
|
336
|
+
JSONObject(valid.toString()).put("kind", 24132),
|
|
337
|
+
JSONObject(valid.toString()).put("tags", JSONArray()),
|
|
338
|
+
JSONObject(valid.toString())
|
|
339
|
+
.put(
|
|
340
|
+
"tags",
|
|
341
|
+
JSONArray()
|
|
342
|
+
.put(JSONArray().put("p").put(clientKey.publicKeyHex))
|
|
343
|
+
.put(JSONArray().put("p").put(clientKey.publicKeyHex))
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
invalidEvents.forEach { event ->
|
|
348
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
349
|
+
protocol.decodeResponseEvent(resignResponse(event))
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
@Test
|
|
355
|
+
fun nip44_authenticated_payload_and_result_boundaries_are_enforced() {
|
|
356
|
+
val outbound = protocol.prepareNip44Encrypt(
|
|
357
|
+
signerKey.publicKeyHex,
|
|
358
|
+
"message"
|
|
359
|
+
)
|
|
360
|
+
val tampered = JSONObject(
|
|
361
|
+
createResponseEvent(
|
|
362
|
+
REQUEST_ID,
|
|
363
|
+
result = "ciphertext"
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
val content = tampered.getString("content")
|
|
367
|
+
tampered.put(
|
|
368
|
+
"content",
|
|
369
|
+
content.dropLast(1) + if (content.last() == 'A') "B" else "A"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
373
|
+
protocol.decodeResponseEvent(resignResponse(tampered))
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
377
|
+
protocol.correlateResponse(
|
|
378
|
+
protocol.decodeResponseEvent(
|
|
379
|
+
createResponseEvent(REQUEST_ID, result = "")
|
|
380
|
+
),
|
|
381
|
+
outbound.context
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
val decryptOutbound = protocol.prepareNip44Decrypt(
|
|
386
|
+
signerKey.publicKeyHex,
|
|
387
|
+
"ciphertext"
|
|
388
|
+
)
|
|
389
|
+
val emptyPlaintext = protocol.correlateResponse(
|
|
390
|
+
protocol.decodeResponseEvent(
|
|
391
|
+
createResponseEvent(REQUEST_ID, result = "")
|
|
392
|
+
),
|
|
393
|
+
decryptOutbound.context
|
|
394
|
+
)
|
|
395
|
+
assertEquals(
|
|
396
|
+
"",
|
|
397
|
+
(emptyPlaintext as Nip46ResponseOutcome.Success).result
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
@Test
|
|
402
|
+
fun sign_event_rejects_out_of_range_kinds_and_wrong_user_key() {
|
|
403
|
+
listOf(-1L, 65536L, 4_294_967_297L).forEach { kind ->
|
|
404
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
405
|
+
protocol.prepareSignEvent(
|
|
406
|
+
JSONObject()
|
|
407
|
+
.put("kind", kind)
|
|
408
|
+
.put("created_at", CREATED_AT)
|
|
409
|
+
.put("tags", JSONArray())
|
|
410
|
+
.put("content", "content")
|
|
411
|
+
.toString(),
|
|
412
|
+
signerKey.publicKeyHex
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
val unsigned =
|
|
418
|
+
JSONObject()
|
|
419
|
+
.put("kind", 1)
|
|
420
|
+
.put("created_at", CREATED_AT)
|
|
421
|
+
.put("tags", JSONArray())
|
|
422
|
+
.put("content", "expected")
|
|
423
|
+
val outbound = protocol.prepareSignEvent(
|
|
424
|
+
unsigned.toString(),
|
|
425
|
+
clientKey.publicKeyHex
|
|
426
|
+
)
|
|
427
|
+
val signedByWrongKey = signUserEvent(unsigned)
|
|
428
|
+
|
|
429
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
430
|
+
protocol.correlateResponse(
|
|
431
|
+
protocol.decodeResponseEvent(
|
|
432
|
+
createResponseEvent(
|
|
433
|
+
REQUEST_ID,
|
|
434
|
+
result = signedByWrongKey
|
|
435
|
+
)
|
|
436
|
+
),
|
|
437
|
+
outbound.context
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
@Test
|
|
443
|
+
fun signed_event_response_must_match_requested_unsigned_fields() {
|
|
444
|
+
val unsigned =
|
|
445
|
+
JSONObject()
|
|
446
|
+
.put("kind", 1)
|
|
447
|
+
.put("created_at", CREATED_AT)
|
|
448
|
+
.put("tags", JSONArray())
|
|
449
|
+
.put("content", "expected")
|
|
450
|
+
val outbound = protocol.prepareSignEvent(
|
|
451
|
+
unsigned.toString(),
|
|
452
|
+
signerKey.publicKeyHex
|
|
453
|
+
)
|
|
454
|
+
val signed = signUserEvent(unsigned)
|
|
455
|
+
|
|
456
|
+
val success =
|
|
457
|
+
protocol.correlateResponse(
|
|
458
|
+
protocol.decodeResponseEvent(
|
|
459
|
+
createResponseEvent(
|
|
460
|
+
REQUEST_ID,
|
|
461
|
+
result = signed
|
|
462
|
+
)
|
|
463
|
+
),
|
|
464
|
+
outbound.context
|
|
465
|
+
)
|
|
466
|
+
assertTrue(success is Nip46ResponseOutcome.Success)
|
|
467
|
+
|
|
468
|
+
val changed =
|
|
469
|
+
signUserEvent(
|
|
470
|
+
JSONObject(unsigned.toString())
|
|
471
|
+
.put("content", "changed")
|
|
472
|
+
)
|
|
473
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
474
|
+
protocol.correlateResponse(
|
|
475
|
+
protocol.decodeResponseEvent(
|
|
476
|
+
createResponseEvent(
|
|
477
|
+
REQUEST_ID,
|
|
478
|
+
result = changed
|
|
479
|
+
)
|
|
480
|
+
),
|
|
481
|
+
outbound.context
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private fun decryptRequest(payload: String): Nip46Request {
|
|
487
|
+
return Nip46Request.parse(
|
|
488
|
+
signerKey.decryptNip44(
|
|
489
|
+
payload,
|
|
490
|
+
clientKey.publicKeyHex
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private fun createResponseEvent(
|
|
496
|
+
id: String,
|
|
497
|
+
result: String? = null,
|
|
498
|
+
error: String? = null
|
|
499
|
+
): String {
|
|
500
|
+
val response =
|
|
501
|
+
JSONObject()
|
|
502
|
+
.put("id", id)
|
|
503
|
+
result?.let { response.put("result", it) }
|
|
504
|
+
error?.let { response.put("error", it) }
|
|
505
|
+
|
|
506
|
+
val encrypted =
|
|
507
|
+
signerKey.encryptNip44(
|
|
508
|
+
response.toString(),
|
|
509
|
+
clientKey.publicKeyHex
|
|
510
|
+
)
|
|
511
|
+
val event =
|
|
512
|
+
JSONObject()
|
|
513
|
+
.put("pubkey", signerKey.publicKeyHex)
|
|
514
|
+
.put("created_at", CREATED_AT)
|
|
515
|
+
.put("kind", 24133)
|
|
516
|
+
.put(
|
|
517
|
+
"tags",
|
|
518
|
+
JSONArray()
|
|
519
|
+
.put(
|
|
520
|
+
JSONArray()
|
|
521
|
+
.put("p")
|
|
522
|
+
.put(clientKey.publicKeyHex)
|
|
523
|
+
)
|
|
524
|
+
)
|
|
525
|
+
.put("content", encrypted)
|
|
526
|
+
|
|
527
|
+
return resignResponse(event)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private fun resignResponse(event: JSONObject): String {
|
|
531
|
+
NostrEventBuilder.addId(event)
|
|
532
|
+
val id = Bip340Hash.hexToBytes(event.getString("id"))
|
|
533
|
+
val signature = signerKey.sign(id)
|
|
534
|
+
event.put("sig", Bip340Hash.bytesToHex(signature))
|
|
535
|
+
return event.toString()
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private fun signUserEvent(unsigned: JSONObject): String {
|
|
539
|
+
val event =
|
|
540
|
+
JSONObject(unsigned.toString())
|
|
541
|
+
.put("pubkey", signerKey.publicKeyHex)
|
|
542
|
+
NostrEventBuilder.addId(event)
|
|
543
|
+
val signature =
|
|
544
|
+
signerKey.sign(
|
|
545
|
+
Bip340Hash.hexToBytes(
|
|
546
|
+
event.getString("id")
|
|
547
|
+
)
|
|
548
|
+
)
|
|
549
|
+
event.put("sig", Bip340Hash.bytesToHex(signature))
|
|
550
|
+
return event.toString()
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private fun assertProtocolFailureDoesNotExpose(
|
|
554
|
+
event: String,
|
|
555
|
+
sensitiveValue: String
|
|
556
|
+
) {
|
|
557
|
+
val error =
|
|
558
|
+
assertThrows(Nip46ProtocolException::class.java) {
|
|
559
|
+
protocol.decodeResponseEvent(event)
|
|
560
|
+
}
|
|
561
|
+
assertFalse(
|
|
562
|
+
error.message.orEmpty().contains(sensitiveValue)
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private fun privateKey(value: Int): ByteArray {
|
|
567
|
+
return ByteArray(32).also {
|
|
568
|
+
it[31] = value.toByte()
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
companion object {
|
|
573
|
+
private const val REQUEST_ID =
|
|
574
|
+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
575
|
+
private const val CREATED_AT = 1_700_000_000L
|
|
576
|
+
private const val PAIRING_SECRET = "temporary-pairing-secret"
|
|
577
|
+
}
|
|
578
|
+
}
|