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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/android-template/app/build.gradle.kts +4 -0
  3. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +273 -0
  4. package/android-template/app/src/main/assets/www/index.html +0 -6
  5. package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +20 -4
  6. package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +136 -12
  7. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Hash.kt +56 -0
  8. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Signer.kt +104 -0
  9. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Verifier.kt +20 -170
  10. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip44Crypto.kt +513 -0
  11. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip46ClientKey.kt +140 -0
  12. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +3 -28
  13. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventSerializer.kt +74 -0
  14. package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +100 -0
  15. package/android-template/app/src/main/java/com/pakstr/app/signer/BunkerSigner.kt +32 -7
  16. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +36 -9
  17. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +240 -163
  18. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConfig.kt +175 -0
  19. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConnectionStore.kt +144 -0
  20. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Client.kt +79 -0
  21. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Models.kt +319 -0
  22. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingClient.kt +159 -0
  23. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingQrCode.kt +35 -0
  24. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingUri.kt +104 -0
  25. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Protocol.kt +695 -0
  26. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46RelayTransport.kt +489 -0
  27. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/RelayNip46Client.kt +403 -0
  28. package/android-template/app/src/main/java/com/pakstr/app/utils/BunkerPairingDialog.kt +82 -0
  29. package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +78 -17
  30. package/android-template/app/src/main/res/layout/activity_layout.xml +11 -0
  31. package/android-template/app/src/main/res/layout/dialog_bunker_pairing.xml +75 -0
  32. package/android-template/app/src/main/res/layout/dialog_picker.xml +60 -31
  33. package/android-template/app/src/main/res/values/strings.xml +27 -0
  34. package/android-template/app/src/test/java/com/pakstr/app/LocalServerQueryKeepAliveTest.kt +118 -0
  35. package/android-template/app/src/test/java/com/pakstr/app/crypto/Bip340SignerTest.kt +133 -0
  36. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip44CryptoTest.kt +199 -0
  37. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyProtocolTest.kt +82 -0
  38. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyTest.kt +52 -0
  39. package/android-template/app/src/test/java/com/pakstr/app/crypto/NostrEventSerializerTest.kt +42 -0
  40. package/android-template/app/src/test/java/com/pakstr/app/signer/NostrBridgeErrorTest.kt +32 -0
  41. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/BunkerConfigTest.kt +120 -0
  42. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ModelsTest.kt +111 -0
  43. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingClientTest.kt +271 -0
  44. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingQrCodeTest.kt +54 -0
  45. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingUriTest.kt +80 -0
  46. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ProtocolTest.kt +578 -0
  47. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46RelayTransportTest.kt +508 -0
  48. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/RelayNip46ClientTest.kt +678 -0
  49. package/android-template/gradle/libs.versions.toml +8 -0
  50. package/dist/cli.js +8 -0
  51. package/dist/commands/init.js +16 -0
  52. package/dist/commands/nsite.js +38 -0
  53. package/dist/commands/publish.js +50 -2
  54. package/dist/commands/run.js +5 -5
  55. package/dist/core/blossom.js +2 -2
  56. package/dist/core/nsite.js +176 -0
  57. package/dist/core/pakstrConfig.js +85 -6
  58. 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
+ }