pakstr 0.20.0 → 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 (50) 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/package.json +1 -1
@@ -0,0 +1,695 @@
1
+ package com.pakstr.app.signer.bunker
2
+
3
+ import com.pakstr.app.crypto.Bip340Verifier
4
+ import com.pakstr.app.crypto.Nip46ClientKey
5
+ import com.pakstr.app.crypto.NostrEventVerifier
6
+ import com.pakstr.app.signer.NostrEventBuilder
7
+ import org.json.JSONArray
8
+ import org.json.JSONObject
9
+ import java.net.URI
10
+ import java.security.MessageDigest
11
+
12
+ internal class Nip46Protocol(
13
+ private val clientKey: Nip46ClientKey,
14
+ private val config: BunkerConfig,
15
+ private val requestIdGenerator: Nip46RequestIdGenerator =
16
+ SecureNip46RequestIdGenerator,
17
+ private val clockSeconds: () -> Long = {
18
+ System.currentTimeMillis() / 1000L
19
+ },
20
+ private val limits: Nip46ProtocolLimits =
21
+ Nip46ProtocolLimits()
22
+ ) {
23
+ fun prepareConnect(
24
+ requestedPermissions: String? = null,
25
+ clientMetadataJson: String? = null
26
+ ): Nip46OutboundRequest {
27
+ clientMetadataJson?.let(::validateMetadata)
28
+
29
+ val pairingSecret = config.pairingSecret
30
+ val params = mutableListOf(config.remoteSignerPubkey)
31
+ if (
32
+ pairingSecret != null ||
33
+ requestedPermissions != null ||
34
+ clientMetadataJson != null
35
+ ) {
36
+ params += pairingSecret.orEmpty()
37
+ }
38
+ if (
39
+ requestedPermissions != null ||
40
+ clientMetadataJson != null
41
+ ) {
42
+ params += requestedPermissions.orEmpty()
43
+ }
44
+ if (clientMetadataJson != null) {
45
+ params += clientMetadataJson
46
+ }
47
+
48
+ return prepare(
49
+ Nip46Method.CONNECT,
50
+ params,
51
+ expectedConnectResult = pairingSecret
52
+ )
53
+ }
54
+
55
+ fun clearPairingSecret() {
56
+ config.clearPairingSecret()
57
+ }
58
+
59
+ fun prepareDisconnect(): Nip46OutboundRequest {
60
+ return prepare(
61
+ Nip46Method.DISCONNECT,
62
+ emptyList()
63
+ )
64
+ }
65
+
66
+ fun prepareGetPublicKey(): Nip46OutboundRequest {
67
+ return prepare(
68
+ Nip46Method.GET_PUBLIC_KEY,
69
+ emptyList()
70
+ )
71
+ }
72
+
73
+ fun prepareSignEvent(
74
+ unsignedEventJson: String,
75
+ expectedUserPubkey: String
76
+ ): Nip46OutboundRequest {
77
+ validatePublicKey(expectedUserPubkey)
78
+ val normalized =
79
+ normalizeUnsignedEvent(unsignedEventJson)
80
+ val fingerprint =
81
+ MessageDigest.getInstance("SHA-256")
82
+ .digest(
83
+ normalized.toByteArray(Charsets.UTF_8)
84
+ )
85
+
86
+ return prepare(
87
+ Nip46Method.SIGN_EVENT,
88
+ listOf(normalized),
89
+ fingerprint,
90
+ expectedUserPubkey
91
+ )
92
+ }
93
+
94
+ fun prepareNip04Encrypt(
95
+ pubkey: String,
96
+ plaintext: String
97
+ ): Nip46OutboundRequest {
98
+ validatePublicKey(pubkey)
99
+ return prepare(
100
+ Nip46Method.NIP04_ENCRYPT,
101
+ listOf(pubkey, plaintext)
102
+ )
103
+ }
104
+
105
+ fun prepareNip04Decrypt(
106
+ pubkey: String,
107
+ ciphertext: String
108
+ ): Nip46OutboundRequest {
109
+ validatePublicKey(pubkey)
110
+ return prepare(
111
+ Nip46Method.NIP04_DECRYPT,
112
+ listOf(pubkey, ciphertext)
113
+ )
114
+ }
115
+
116
+ fun prepareNip44Encrypt(
117
+ pubkey: String,
118
+ plaintext: String
119
+ ): Nip46OutboundRequest {
120
+ validatePublicKey(pubkey)
121
+ return prepare(
122
+ Nip46Method.NIP44_ENCRYPT,
123
+ listOf(pubkey, plaintext)
124
+ )
125
+ }
126
+
127
+ fun prepareNip44Decrypt(
128
+ pubkey: String,
129
+ ciphertext: String
130
+ ): Nip46OutboundRequest {
131
+ validatePublicKey(pubkey)
132
+ return prepare(
133
+ Nip46Method.NIP44_DECRYPT,
134
+ listOf(pubkey, ciphertext)
135
+ )
136
+ }
137
+
138
+ fun decodeResponseEvent(
139
+ eventJson: String
140
+ ): Nip46VerifiedResponse {
141
+ requireSize(
142
+ eventJson,
143
+ limits.maximumEventBytes,
144
+ "NIP-46 response event is too large"
145
+ )
146
+
147
+ val event = parseObject(
148
+ eventJson,
149
+ "Invalid NIP-46 response event"
150
+ )
151
+ validateEventShape(event)
152
+
153
+ if (event.getInt("kind") != EVENT_KIND) {
154
+ throw Nip46ProtocolException(
155
+ "Invalid NIP-46 response event"
156
+ )
157
+ }
158
+ if (
159
+ event.getString("pubkey") !=
160
+ config.remoteSignerPubkey
161
+ ) {
162
+ throw Nip46ProtocolException(
163
+ "Unexpected NIP-46 response author"
164
+ )
165
+ }
166
+ validateRecipientTags(
167
+ event.getJSONArray("tags")
168
+ )
169
+
170
+ val verified =
171
+ try {
172
+ NostrEventVerifier.verify(event)
173
+ } catch (_: Exception) {
174
+ false
175
+ }
176
+ if (!verified) {
177
+ throw Nip46ProtocolException(
178
+ "NIP-46 response verification failed"
179
+ )
180
+ }
181
+
182
+ val encryptedContent = event.getString("content")
183
+ if (
184
+ encryptedContent.length >
185
+ limits.maximumEncryptedContentLength
186
+ ) {
187
+ throw Nip46ProtocolException(
188
+ "NIP-46 response payload is too large"
189
+ )
190
+ }
191
+
192
+ val decrypted =
193
+ try {
194
+ clientKey.decryptNip44(
195
+ encryptedContent,
196
+ config.remoteSignerPubkey
197
+ )
198
+ } catch (_: Exception) {
199
+ throw Nip46ProtocolException(
200
+ "Invalid NIP-46 response payload"
201
+ )
202
+ }
203
+ requireSize(
204
+ decrypted,
205
+ limits.maximumPayloadBytes,
206
+ "NIP-46 response payload is too large"
207
+ )
208
+
209
+ val response =
210
+ try {
211
+ Nip46Response.parse(decrypted)
212
+ } catch (_: Exception) {
213
+ throw Nip46ProtocolException(
214
+ "Invalid NIP-46 response payload"
215
+ )
216
+ }
217
+
218
+ return Nip46VerifiedResponse(response)
219
+ }
220
+
221
+ fun correlateResponse(
222
+ verified: Nip46VerifiedResponse,
223
+ context: Nip46RequestContext
224
+ ): Nip46ResponseOutcome {
225
+ val response = verified.response
226
+ if (response.id != context.id) {
227
+ throw Nip46ProtocolException(
228
+ "NIP-46 response does not match request"
229
+ )
230
+ }
231
+
232
+ if (
233
+ response.result == AUTH_URL_RESULT &&
234
+ response.error != null
235
+ ) {
236
+ validateAuthUrl(response.error)
237
+ context.beginResponse(authChallenge = true)
238
+ return Nip46ResponseOutcome.AuthChallenge(
239
+ response.error
240
+ )
241
+ }
242
+
243
+ if (response.error != null) {
244
+ context.beginResponse(authChallenge = false)
245
+ return Nip46ResponseOutcome.RemoteError()
246
+ }
247
+
248
+ val result =
249
+ response.result
250
+ ?: throw Nip46ProtocolException(
251
+ "Invalid NIP-46 response"
252
+ )
253
+ validateResult(
254
+ context,
255
+ result
256
+ )
257
+ context.beginResponse(authChallenge = false)
258
+
259
+ return Nip46ResponseOutcome.Success(result)
260
+ }
261
+
262
+ private fun validateAuthUrl(value: String) {
263
+ if (value.length > limits.maximumAuthUrlLength) {
264
+ throw Nip46ProtocolException(
265
+ "Invalid NIP-46 auth URL"
266
+ )
267
+ }
268
+ val uri =
269
+ try {
270
+ URI(value)
271
+ } catch (_: Exception) {
272
+ throw Nip46ProtocolException(
273
+ "Invalid NIP-46 auth URL"
274
+ )
275
+ }
276
+ if (
277
+ uri.scheme != "https" ||
278
+ uri.host.isNullOrBlank() ||
279
+ uri.rawUserInfo != null ||
280
+ uri.rawFragment != null
281
+ ) {
282
+ throw Nip46ProtocolException(
283
+ "Invalid NIP-46 auth URL"
284
+ )
285
+ }
286
+ }
287
+
288
+ private fun prepare(
289
+ method: Nip46Method,
290
+ params: List<String>,
291
+ fingerprint: ByteArray? = null,
292
+ expectedUserPubkey: String? = null,
293
+ expectedConnectResult: String? = null
294
+ ): Nip46OutboundRequest {
295
+ val id = requestIdGenerator.nextId()
296
+ validateNip46RequestId(id)
297
+ val request = Nip46Request(id, method, params)
298
+ val requestJson = request.toJson()
299
+ requireSize(
300
+ requestJson,
301
+ limits.maximumPayloadBytes,
302
+ "NIP-46 request payload is too large"
303
+ )
304
+
305
+ val encrypted =
306
+ try {
307
+ clientKey.encryptNip44(
308
+ requestJson,
309
+ config.remoteSignerPubkey
310
+ )
311
+ } catch (_: Exception) {
312
+ throw Nip46ProtocolException(
313
+ "Unable to encrypt NIP-46 request"
314
+ )
315
+ }
316
+ if (
317
+ encrypted.length >
318
+ limits.maximumEncryptedContentLength
319
+ ) {
320
+ throw Nip46ProtocolException(
321
+ "NIP-46 request payload is too large"
322
+ )
323
+ }
324
+
325
+ val event =
326
+ JSONObject()
327
+ .put("pubkey", clientKey.publicKeyHex)
328
+ .put("created_at", clockSeconds())
329
+ .put("kind", EVENT_KIND)
330
+ .put(
331
+ "tags",
332
+ JSONArray()
333
+ .put(
334
+ JSONArray()
335
+ .put("p")
336
+ .put(config.remoteSignerPubkey)
337
+ )
338
+ )
339
+ .put("content", encrypted)
340
+ NostrEventBuilder.addId(event)
341
+
342
+ val eventId = hexToBytes(event.getString("id"))
343
+ val signature =
344
+ try {
345
+ clientKey.sign(eventId)
346
+ } finally {
347
+ eventId.fill(0)
348
+ }
349
+ event.put("sig", bytesToHex(signature))
350
+ signature.fill(0)
351
+
352
+ if (!NostrEventVerifier.verify(event)) {
353
+ throw Nip46ProtocolException(
354
+ "Unable to verify NIP-46 request event"
355
+ )
356
+ }
357
+
358
+ val eventJson = event.toString()
359
+ requireSize(
360
+ eventJson,
361
+ limits.maximumEventBytes,
362
+ "NIP-46 request event is too large"
363
+ )
364
+
365
+ return Nip46OutboundRequest(
366
+ eventJson,
367
+ Nip46RequestContext(
368
+ id = id,
369
+ method = method,
370
+ expectedUserPubkey = expectedUserPubkey,
371
+ expectedConnectResult = expectedConnectResult,
372
+ unsignedEventFingerprint = fingerprint
373
+ )
374
+ )
375
+ }
376
+
377
+ private fun validateResult(
378
+ context: Nip46RequestContext,
379
+ result: String
380
+ ) {
381
+ when (context.method) {
382
+ Nip46Method.CONNECT -> {
383
+ if (
384
+ result != "ack" &&
385
+ result != context.expectedConnectResult
386
+ ) {
387
+ throw Nip46ProtocolException(
388
+ "Invalid NIP-46 connect response"
389
+ )
390
+ }
391
+ }
392
+ Nip46Method.DISCONNECT -> Unit
393
+ Nip46Method.GET_PUBLIC_KEY ->
394
+ validatePublicKey(result)
395
+ Nip46Method.SIGN_EVENT ->
396
+ validateSignedEvent(result, context)
397
+ Nip46Method.NIP04_ENCRYPT,
398
+ Nip46Method.NIP44_ENCRYPT -> {
399
+ if (result.isEmpty()) {
400
+ throw Nip46ProtocolException(
401
+ "Invalid NIP-46 encryption response"
402
+ )
403
+ }
404
+ }
405
+ Nip46Method.NIP04_DECRYPT,
406
+ Nip46Method.NIP44_DECRYPT -> Unit
407
+ }
408
+ }
409
+
410
+ private fun validateSignedEvent(
411
+ value: String,
412
+ context: Nip46RequestContext
413
+ ) {
414
+ val event = parseObject(
415
+ value,
416
+ "Invalid signed event response"
417
+ )
418
+ validateEventShape(event)
419
+ val verified =
420
+ try {
421
+ NostrEventVerifier.verify(event)
422
+ } catch (_: Exception) {
423
+ false
424
+ }
425
+ if (!verified) {
426
+ throw Nip46ProtocolException(
427
+ "Invalid signed event response"
428
+ )
429
+ }
430
+ if (
431
+ event.getString("pubkey") !=
432
+ context.expectedUserPubkey
433
+ ) {
434
+ throw Nip46ProtocolException(
435
+ "Unexpected signed event author"
436
+ )
437
+ }
438
+
439
+ val normalized = normalizeSignedEvent(event)
440
+ val fingerprint =
441
+ MessageDigest.getInstance("SHA-256")
442
+ .digest(
443
+ normalized.toByteArray(Charsets.UTF_8)
444
+ )
445
+ try {
446
+ if (!context.matchesUnsignedEvent(fingerprint)) {
447
+ throw Nip46ProtocolException(
448
+ "Signed event does not match request"
449
+ )
450
+ }
451
+ } finally {
452
+ fingerprint.fill(0)
453
+ }
454
+ }
455
+
456
+ private fun normalizeUnsignedEvent(value: String): String {
457
+ val json = parseObject(
458
+ value,
459
+ "Invalid unsigned event"
460
+ )
461
+ val forbidden = listOf("id", "pubkey", "sig")
462
+ if (forbidden.any(json::has)) {
463
+ throw Nip46ProtocolException(
464
+ "Unsigned event contains signer-owned fields"
465
+ )
466
+ }
467
+ val allowed = setOf(
468
+ "kind",
469
+ "created_at",
470
+ "tags",
471
+ "content"
472
+ )
473
+ if (json.keys().asSequence().any { it !in allowed }) {
474
+ throw Nip46ProtocolException(
475
+ "Unsigned event contains unsupported fields"
476
+ )
477
+ }
478
+
479
+ return normalizedUnsignedFields(json).toString()
480
+ }
481
+
482
+ private fun normalizeSignedEvent(
483
+ event: JSONObject
484
+ ): String {
485
+ return normalizedUnsignedFields(event).toString()
486
+ }
487
+
488
+ private fun normalizedUnsignedFields(
489
+ json: JSONObject
490
+ ): JSONObject {
491
+ val kind = requireKind(json)
492
+ requireIntegral(json, "created_at")
493
+ val tags = requireTags(json)
494
+ requireString(json, "content")
495
+
496
+ return JSONObject()
497
+ .put("kind", kind)
498
+ .put("created_at", json.getLong("created_at"))
499
+ .put("tags", tags)
500
+ .put("content", json.getString("content"))
501
+ }
502
+
503
+ private fun validateEventShape(event: JSONObject) {
504
+ requireString(event, "id")
505
+ requireString(event, "pubkey")
506
+ requireIntegral(event, "created_at")
507
+ requireKind(event)
508
+ requireTags(event)
509
+ requireString(event, "content")
510
+ requireString(event, "sig")
511
+ validatePublicKey(event.getString("pubkey"))
512
+ if (!event.getString("id").matches(Regex("[0-9a-f]{64}"))) {
513
+ throw Nip46ProtocolException(
514
+ "Invalid NIP-46 event ID"
515
+ )
516
+ }
517
+ if (!event.getString("sig").matches(Regex("[0-9a-f]{128}"))) {
518
+ throw Nip46ProtocolException(
519
+ "Invalid NIP-46 event signature"
520
+ )
521
+ }
522
+ }
523
+
524
+ private fun validateRecipientTags(tags: JSONArray) {
525
+ var recipients = 0
526
+ for (index in 0 until tags.length()) {
527
+ val tag = tags.getJSONArray(index)
528
+ if (
529
+ tag.length() > 0 &&
530
+ tag.getString(0) == "p"
531
+ ) {
532
+ recipients++
533
+ if (
534
+ tag.length() < 2 ||
535
+ tag.getString(1) != clientKey.publicKeyHex
536
+ ) {
537
+ throw Nip46ProtocolException(
538
+ "Unexpected NIP-46 response recipient"
539
+ )
540
+ }
541
+ }
542
+ }
543
+ if (recipients != 1) {
544
+ throw Nip46ProtocolException(
545
+ "Invalid NIP-46 response recipient"
546
+ )
547
+ }
548
+ }
549
+
550
+ private fun validateMetadata(value: String) {
551
+ requireSize(
552
+ value,
553
+ limits.maximumPayloadBytes,
554
+ "NIP-46 client metadata is too large"
555
+ )
556
+ parseObject(
557
+ value,
558
+ "Invalid NIP-46 client metadata"
559
+ )
560
+ }
561
+
562
+ private fun validatePublicKey(value: String) {
563
+ if (
564
+ !value.matches(Regex("[0-9a-f]{64}")) ||
565
+ !Bip340Verifier.isValidXOnlyPubkey(value)
566
+ ) {
567
+ throw Nip46ProtocolException(
568
+ "Invalid Nostr public key"
569
+ )
570
+ }
571
+ }
572
+
573
+ private fun requireTags(json: JSONObject): JSONArray {
574
+ val tags = json.optJSONArray("tags")
575
+ ?: throw Nip46ProtocolException(
576
+ "Invalid Nostr event"
577
+ )
578
+ for (index in 0 until tags.length()) {
579
+ val tag = tags.optJSONArray(index)
580
+ ?: throw Nip46ProtocolException(
581
+ "Invalid Nostr event"
582
+ )
583
+ if (tag.length() == 0) {
584
+ throw Nip46ProtocolException(
585
+ "Invalid Nostr event"
586
+ )
587
+ }
588
+ for (valueIndex in 0 until tag.length()) {
589
+ if (tag.get(valueIndex) !is String) {
590
+ throw Nip46ProtocolException(
591
+ "Invalid Nostr event"
592
+ )
593
+ }
594
+ }
595
+ }
596
+ return tags
597
+ }
598
+
599
+ private fun requireString(
600
+ json: JSONObject,
601
+ key: String
602
+ ) {
603
+ if (
604
+ !json.has(key) ||
605
+ json.isNull(key) ||
606
+ json.get(key) !is String
607
+ ) {
608
+ throw Nip46ProtocolException(
609
+ "Invalid Nostr event"
610
+ )
611
+ }
612
+ }
613
+
614
+ private fun requireKind(json: JSONObject): Int {
615
+ requireIntegral(json, "kind")
616
+ val kind = json.getLong("kind")
617
+ if (kind !in 0L..65535L) {
618
+ throw Nip46ProtocolException(
619
+ "Invalid Nostr event kind"
620
+ )
621
+ }
622
+ return kind.toInt()
623
+ }
624
+
625
+ private fun requireIntegral(
626
+ json: JSONObject,
627
+ key: String
628
+ ) {
629
+ if (!json.has(key) || json.isNull(key)) {
630
+ throw Nip46ProtocolException(
631
+ "Invalid Nostr event"
632
+ )
633
+ }
634
+ val value = json.get(key)
635
+ if (
636
+ value !is Byte &&
637
+ value !is Short &&
638
+ value !is Int &&
639
+ value !is Long
640
+ ) {
641
+ throw Nip46ProtocolException(
642
+ "Invalid Nostr event"
643
+ )
644
+ }
645
+ }
646
+
647
+ private fun parseObject(
648
+ value: String,
649
+ message: String
650
+ ): JSONObject {
651
+ return try {
652
+ JSONObject(value)
653
+ } catch (_: Exception) {
654
+ throw Nip46ProtocolException(message)
655
+ }
656
+ }
657
+
658
+ private fun requireSize(
659
+ value: String,
660
+ maximumBytes: Int,
661
+ message: String
662
+ ) {
663
+ if (
664
+ value.toByteArray(Charsets.UTF_8).size >
665
+ maximumBytes
666
+ ) {
667
+ throw Nip46ProtocolException(message)
668
+ }
669
+ }
670
+
671
+ private fun hexToBytes(value: String): ByteArray {
672
+ if (!value.matches(Regex("[0-9a-f]{64}"))) {
673
+ throw Nip46ProtocolException(
674
+ "Invalid NIP-46 event ID"
675
+ )
676
+ }
677
+ return ByteArray(32) { index ->
678
+ value.substring(
679
+ index * 2,
680
+ index * 2 + 2
681
+ ).toInt(16).toByte()
682
+ }
683
+ }
684
+
685
+ private fun bytesToHex(value: ByteArray): String {
686
+ return value.joinToString("") { byte ->
687
+ "%02x".format(byte.toInt() and 0xff)
688
+ }
689
+ }
690
+
691
+ companion object {
692
+ private const val EVENT_KIND = 24133
693
+ private const val AUTH_URL_RESULT = "auth_url"
694
+ }
695
+ }