pakstr 0.20.0 → 0.21.1

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 +130 -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 +1 -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,513 @@
1
+ package com.pakstr.app.crypto
2
+
3
+ import org.bouncycastle.crypto.engines.ChaCha7539Engine
4
+ import org.bouncycastle.crypto.params.KeyParameter
5
+ import org.bouncycastle.crypto.params.ParametersWithIV
6
+ import java.math.BigInteger
7
+ import java.nio.ByteBuffer
8
+ import java.nio.charset.CodingErrorAction
9
+ import java.security.MessageDigest
10
+ import java.security.SecureRandom
11
+ import java.util.Arrays
12
+ import java.util.Base64
13
+ import javax.crypto.Mac
14
+ import javax.crypto.spec.SecretKeySpec
15
+
16
+ object Nip44Crypto : NostrEncryption {
17
+ private const val VERSION = 2
18
+ private const val MIN_PLAINTEXT_SIZE = 1L
19
+ private const val MAX_PLAINTEXT_SIZE = 16L * 1024L * 1024L
20
+ private const val MIN_ENCODED_PAYLOAD_SIZE = 132
21
+ private const val HKDF_OUTPUT_SIZE = 76
22
+ private val SECURE_RANDOM = SecureRandom()
23
+ private val HKDF_SALT = "nip44-v2".toByteArray(Charsets.UTF_8)
24
+
25
+ override fun encrypt(
26
+ plaintext: String,
27
+ privateKey: ByteArray,
28
+ publicKey: ByteArray
29
+ ): String {
30
+ val conversationKey =
31
+ deriveConversationKey(
32
+ privateKey,
33
+ publicKey
34
+ )
35
+ val nonce = ByteArray(32)
36
+ SECURE_RANDOM.nextBytes(nonce)
37
+
38
+ return try {
39
+ encryptWithNonce(
40
+ plaintext,
41
+ conversationKey,
42
+ nonce
43
+ )
44
+ } finally {
45
+ Arrays.fill(conversationKey, 0)
46
+ Arrays.fill(nonce, 0)
47
+ }
48
+ }
49
+
50
+ override fun decrypt(
51
+ payload: String,
52
+ privateKey: ByteArray,
53
+ publicKey: ByteArray
54
+ ): String {
55
+ val conversationKey =
56
+ deriveConversationKey(
57
+ privateKey,
58
+ publicKey
59
+ )
60
+
61
+ return try {
62
+ decryptWithConversationKey(
63
+ payload,
64
+ conversationKey
65
+ )
66
+ } finally {
67
+ Arrays.fill(conversationKey, 0)
68
+ }
69
+ }
70
+
71
+ internal fun deriveConversationKey(
72
+ privateKey: ByteArray,
73
+ publicKey: ByteArray
74
+ ): ByteArray {
75
+ val scalar = Secp256k1.privateScalar(privateKey)
76
+ val peer = Secp256k1.liftX(publicKey)
77
+ val sharedPoint = peer.multiply(scalar).normalize()
78
+ require(!sharedPoint.isInfinity && sharedPoint.isValid) {
79
+ "Invalid shared secret"
80
+ }
81
+
82
+ val sharedX =
83
+ Secp256k1.fixedSizeBytes(
84
+ sharedPoint.affineXCoord.toBigInteger()
85
+ )
86
+
87
+ return try {
88
+ hmacSha256(
89
+ HKDF_SALT,
90
+ sharedX
91
+ )
92
+ } finally {
93
+ Arrays.fill(sharedX, 0)
94
+ }
95
+ }
96
+
97
+ internal fun deriveMessageKeys(
98
+ conversationKey: ByteArray,
99
+ nonce: ByteArray
100
+ ): MessageKeys {
101
+ require(conversationKey.size == 32) {
102
+ "Conversation key must be 32 bytes"
103
+ }
104
+ require(nonce.size == 32) {
105
+ "Nonce must be 32 bytes"
106
+ }
107
+
108
+ val output = ByteArray(HKDF_OUTPUT_SIZE)
109
+ var previous = ByteArray(0)
110
+ var offset = 0
111
+ var counter = 1
112
+
113
+ try {
114
+ while (offset < output.size) {
115
+ val mac = newHmac(conversationKey)
116
+ mac.update(previous)
117
+ mac.update(nonce)
118
+ mac.update(counter.toByte())
119
+ val block = mac.doFinal()
120
+ Arrays.fill(previous, 0)
121
+ previous = block
122
+
123
+ val count = minOf(block.size, output.size - offset)
124
+ block.copyInto(
125
+ output,
126
+ destinationOffset = offset,
127
+ endIndex = count
128
+ )
129
+ offset += count
130
+ counter++
131
+ }
132
+
133
+ return MessageKeys(
134
+ chachaKey = output.copyOfRange(0, 32),
135
+ chachaNonce = output.copyOfRange(32, 44),
136
+ hmacKey = output.copyOfRange(44, 76)
137
+ )
138
+ } finally {
139
+ Arrays.fill(previous, 0)
140
+ Arrays.fill(output, 0)
141
+ }
142
+ }
143
+
144
+ internal fun calculatePaddedLength(
145
+ plaintextLength: Long
146
+ ): Long {
147
+ require(
148
+ plaintextLength in
149
+ MIN_PLAINTEXT_SIZE..0xffffffffL
150
+ ) {
151
+ "Invalid plaintext size"
152
+ }
153
+
154
+ if (plaintextLength <= 32L) {
155
+ return 32L
156
+ }
157
+
158
+ val nextPower =
159
+ java.lang.Long.highestOneBit(
160
+ plaintextLength - 1L
161
+ ) shl 1
162
+ val chunk =
163
+ if (nextPower <= 256L) 32L
164
+ else nextPower / 8L
165
+
166
+ return chunk *
167
+ ((plaintextLength - 1L) / chunk + 1L)
168
+ }
169
+
170
+ internal fun encryptWithNonce(
171
+ plaintext: String,
172
+ conversationKey: ByteArray,
173
+ nonce: ByteArray
174
+ ): String {
175
+ val plaintextBytes =
176
+ plaintext.toByteArray(Charsets.UTF_8)
177
+ require(
178
+ plaintextBytes.size.toLong() <=
179
+ MAX_PLAINTEXT_SIZE
180
+ ) {
181
+ "Plaintext exceeds implementation limit"
182
+ }
183
+
184
+ val padded = pad(plaintextBytes)
185
+ val keys =
186
+ deriveMessageKeys(
187
+ conversationKey,
188
+ nonce
189
+ )
190
+
191
+ return try {
192
+ val ciphertext =
193
+ chacha20(
194
+ keys.chachaKey,
195
+ keys.chachaNonce,
196
+ padded
197
+ )
198
+ val mac =
199
+ hmacSha256(
200
+ keys.hmacKey,
201
+ nonce,
202
+ ciphertext
203
+ )
204
+ val payload =
205
+ ByteArray(
206
+ 1 + nonce.size +
207
+ ciphertext.size + mac.size
208
+ )
209
+
210
+ try {
211
+ payload[0] = VERSION.toByte()
212
+ nonce.copyInto(payload, destinationOffset = 1)
213
+ ciphertext.copyInto(
214
+ payload,
215
+ destinationOffset = 33
216
+ )
217
+ mac.copyInto(
218
+ payload,
219
+ destinationOffset =
220
+ 33 + ciphertext.size
221
+ )
222
+
223
+ Base64.getEncoder().encodeToString(payload)
224
+ } finally {
225
+ Arrays.fill(ciphertext, 0)
226
+ Arrays.fill(mac, 0)
227
+ Arrays.fill(payload, 0)
228
+ }
229
+ } finally {
230
+ keys.close()
231
+ Arrays.fill(plaintextBytes, 0)
232
+ Arrays.fill(padded, 0)
233
+ }
234
+ }
235
+
236
+ internal fun decryptWithConversationKey(
237
+ payload: String,
238
+ conversationKey: ByteArray
239
+ ): String {
240
+ require(!payload.startsWith("#")) {
241
+ "Unsupported encryption version"
242
+ }
243
+ require(payload.length >= MIN_ENCODED_PAYLOAD_SIZE) {
244
+ "Invalid encrypted payload"
245
+ }
246
+ require(payload.length <= maximumEncodedPayloadSize()) {
247
+ "Encrypted payload exceeds implementation limit"
248
+ }
249
+
250
+ require(payload.length % 4 == 0) {
251
+ "Invalid encrypted payload"
252
+ }
253
+
254
+ val decoded =
255
+ try {
256
+ Base64.getDecoder().decode(payload)
257
+ } catch (_: IllegalArgumentException) {
258
+ throw IllegalArgumentException(
259
+ "Invalid encrypted payload"
260
+ )
261
+ }
262
+
263
+ try {
264
+ require(
265
+ Base64.getEncoder()
266
+ .encodeToString(decoded) == payload
267
+ ) {
268
+ "Invalid encrypted payload"
269
+ }
270
+ require(decoded.size >= 99) {
271
+ "Invalid encrypted payload"
272
+ }
273
+ require(decoded[0].toInt() == VERSION) {
274
+ "Unsupported encryption version"
275
+ }
276
+
277
+ val nonce = decoded.copyOfRange(1, 33)
278
+ val ciphertext =
279
+ decoded.copyOfRange(
280
+ 33,
281
+ decoded.size - 32
282
+ )
283
+ val receivedMac =
284
+ decoded.copyOfRange(
285
+ decoded.size - 32,
286
+ decoded.size
287
+ )
288
+ val keys =
289
+ deriveMessageKeys(
290
+ conversationKey,
291
+ nonce
292
+ )
293
+
294
+ try {
295
+ val calculatedMac =
296
+ hmacSha256(
297
+ keys.hmacKey,
298
+ nonce,
299
+ ciphertext
300
+ )
301
+
302
+ try {
303
+ require(
304
+ MessageDigest.isEqual(
305
+ calculatedMac,
306
+ receivedMac
307
+ )
308
+ ) {
309
+ "Invalid encrypted payload"
310
+ }
311
+ } finally {
312
+ Arrays.fill(calculatedMac, 0)
313
+ }
314
+
315
+ val padded =
316
+ chacha20(
317
+ keys.chachaKey,
318
+ keys.chachaNonce,
319
+ ciphertext
320
+ )
321
+
322
+ val plaintext = unpad(padded)
323
+
324
+ return try {
325
+ decodeUtf8(plaintext)
326
+ } finally {
327
+ Arrays.fill(plaintext, 0)
328
+ Arrays.fill(padded, 0)
329
+ }
330
+ } finally {
331
+ keys.close()
332
+ Arrays.fill(nonce, 0)
333
+ Arrays.fill(ciphertext, 0)
334
+ Arrays.fill(receivedMac, 0)
335
+ }
336
+ } finally {
337
+ Arrays.fill(decoded, 0)
338
+ }
339
+ }
340
+
341
+ private fun pad(plaintext: ByteArray): ByteArray {
342
+ val length = plaintext.size.toLong()
343
+ require(length >= MIN_PLAINTEXT_SIZE) {
344
+ "Plaintext must not be empty"
345
+ }
346
+
347
+ val paddedLength = calculatePaddedLength(length)
348
+ val prefixLength = if (length < 65536L) 2 else 6
349
+ require(
350
+ paddedLength + prefixLength <= Int.MAX_VALUE
351
+ ) {
352
+ "Plaintext exceeds implementation limit"
353
+ }
354
+
355
+ return ByteArray(
356
+ prefixLength + paddedLength.toInt()
357
+ ).also { output ->
358
+ if (prefixLength == 2) {
359
+ output[0] = (length ushr 8).toByte()
360
+ output[1] = length.toByte()
361
+ } else {
362
+ output[2] = (length ushr 24).toByte()
363
+ output[3] = (length ushr 16).toByte()
364
+ output[4] = (length ushr 8).toByte()
365
+ output[5] = length.toByte()
366
+ }
367
+
368
+ plaintext.copyInto(
369
+ output,
370
+ destinationOffset = prefixLength
371
+ )
372
+ }
373
+ }
374
+
375
+ private fun unpad(padded: ByteArray): ByteArray {
376
+ require(padded.size >= 34) {
377
+ "Invalid padding"
378
+ }
379
+
380
+ val shortLength =
381
+ ((padded[0].toInt() and 0xff) shl 8) or
382
+ (padded[1].toInt() and 0xff)
383
+ val prefixLength: Int
384
+ val plaintextLength: Long
385
+
386
+ if (shortLength == 0) {
387
+ require(padded.size >= 38) {
388
+ "Invalid padding"
389
+ }
390
+ prefixLength = 6
391
+ plaintextLength =
392
+ ((padded[2].toLong() and 0xff) shl 24) or
393
+ ((padded[3].toLong() and 0xff) shl 16) or
394
+ ((padded[4].toLong() and 0xff) shl 8) or
395
+ (padded[5].toLong() and 0xff)
396
+ require(plaintextLength >= 65536L) {
397
+ "Invalid padding"
398
+ }
399
+ } else {
400
+ prefixLength = 2
401
+ plaintextLength = shortLength.toLong()
402
+ }
403
+
404
+ require(
405
+ plaintextLength <= MAX_PLAINTEXT_SIZE
406
+ ) {
407
+ "Plaintext exceeds implementation limit"
408
+ }
409
+ val paddedLength =
410
+ calculatePaddedLength(plaintextLength)
411
+ require(
412
+ padded.size.toLong() ==
413
+ prefixLength + paddedLength
414
+ ) {
415
+ "Invalid padding"
416
+ }
417
+
418
+ val plaintextEnd =
419
+ prefixLength + plaintextLength.toInt()
420
+ require(
421
+ (plaintextEnd until padded.size)
422
+ .all { index ->
423
+ padded[index] == 0.toByte()
424
+ }
425
+ ) {
426
+ "Invalid padding"
427
+ }
428
+
429
+ return padded.copyOfRange(
430
+ prefixLength,
431
+ plaintextEnd
432
+ )
433
+ }
434
+
435
+ private fun decodeUtf8(value: ByteArray): String {
436
+ return Charsets.UTF_8
437
+ .newDecoder()
438
+ .onMalformedInput(CodingErrorAction.REPORT)
439
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
440
+ .decode(ByteBuffer.wrap(value))
441
+ .toString()
442
+ }
443
+
444
+ private fun chacha20(
445
+ key: ByteArray,
446
+ nonce: ByteArray,
447
+ input: ByteArray
448
+ ): ByteArray {
449
+ val engine = ChaCha7539Engine()
450
+ engine.init(
451
+ true,
452
+ ParametersWithIV(
453
+ KeyParameter(key),
454
+ nonce
455
+ )
456
+ )
457
+
458
+ return ByteArray(input.size).also { output ->
459
+ engine.processBytes(
460
+ input,
461
+ 0,
462
+ input.size,
463
+ output,
464
+ 0
465
+ )
466
+ }
467
+ }
468
+
469
+ private fun hmacSha256(
470
+ key: ByteArray,
471
+ vararg parts: ByteArray
472
+ ): ByteArray {
473
+ val mac = newHmac(key)
474
+ parts.forEach(mac::update)
475
+ return mac.doFinal()
476
+ }
477
+
478
+ private fun newHmac(key: ByteArray): Mac {
479
+ return Mac.getInstance("HmacSHA256")
480
+ .apply {
481
+ init(
482
+ SecretKeySpec(
483
+ key,
484
+ "HmacSHA256"
485
+ )
486
+ )
487
+ }
488
+ }
489
+
490
+ private fun maximumEncodedPayloadSize(): Int {
491
+ val paddedLength =
492
+ calculatePaddedLength(MAX_PLAINTEXT_SIZE)
493
+ val decodedLength =
494
+ 1L + 32L + 6L +
495
+ paddedLength + 32L
496
+
497
+ return (
498
+ (decodedLength + 2L) / 3L * 4L
499
+ ).toInt()
500
+ }
501
+
502
+ internal class MessageKeys(
503
+ val chachaKey: ByteArray,
504
+ val chachaNonce: ByteArray,
505
+ val hmacKey: ByteArray
506
+ ) : AutoCloseable {
507
+ override fun close() {
508
+ Arrays.fill(chachaKey, 0)
509
+ Arrays.fill(chachaNonce, 0)
510
+ Arrays.fill(hmacKey, 0)
511
+ }
512
+ }
513
+ }
@@ -0,0 +1,140 @@
1
+ package com.pakstr.app.crypto
2
+
3
+ import java.math.BigInteger
4
+ import java.security.SecureRandom
5
+ import java.util.Arrays
6
+
7
+ class Nip46ClientKey private constructor(
8
+ private var privateKey: ByteArray?
9
+ ) : AutoCloseable {
10
+ private val publicKeyBytes =
11
+ Secp256k1.xOnlyPublicKey(
12
+ requireNotNull(privateKey)
13
+ )
14
+
15
+ val publicKey: ByteArray
16
+ get() = publicKeyBytes.copyOf()
17
+
18
+ val publicKeyHex: String
19
+ get() = Bip340Hash.bytesToHex(publicKeyBytes)
20
+
21
+ fun sign(message: ByteArray): ByteArray {
22
+ val key = requireOpenKey()
23
+ val auxiliaryRandom = ByteArray(32)
24
+ SECURE_RANDOM.nextBytes(auxiliaryRandom)
25
+
26
+ return try {
27
+ Bip340Signer.sign(
28
+ key,
29
+ message,
30
+ auxiliaryRandom
31
+ )
32
+ } finally {
33
+ Arrays.fill(auxiliaryRandom, 0)
34
+ }
35
+ }
36
+
37
+ internal fun encryptNip44(
38
+ plaintext: String,
39
+ peerPublicKeyHex: String
40
+ ): String {
41
+ val key = requireOpenKey()
42
+ val peerPublicKey = decodePeerPublicKey(peerPublicKeyHex)
43
+
44
+ return try {
45
+ Nip44Crypto.encrypt(
46
+ plaintext,
47
+ key,
48
+ peerPublicKey
49
+ )
50
+ } finally {
51
+ Arrays.fill(peerPublicKey, 0)
52
+ }
53
+ }
54
+
55
+ internal fun decryptNip44(
56
+ payload: String,
57
+ peerPublicKeyHex: String
58
+ ): String {
59
+ val key = requireOpenKey()
60
+ val peerPublicKey = decodePeerPublicKey(peerPublicKeyHex)
61
+
62
+ return try {
63
+ Nip44Crypto.decrypt(
64
+ payload,
65
+ key,
66
+ peerPublicKey
67
+ )
68
+ } finally {
69
+ Arrays.fill(peerPublicKey, 0)
70
+ }
71
+ }
72
+
73
+ internal fun <T> withPrivateKey(
74
+ block: (ByteArray) -> T
75
+ ): T {
76
+ val copy = requireOpenKey().copyOf()
77
+ return try {
78
+ block(copy)
79
+ } finally {
80
+ Arrays.fill(copy, 0)
81
+ }
82
+ }
83
+
84
+ override fun close() {
85
+ privateKey?.let { key ->
86
+ Arrays.fill(key, 0)
87
+ }
88
+ privateKey = null
89
+ }
90
+
91
+ private fun requireOpenKey(): ByteArray {
92
+ return privateKey
93
+ ?: throw IllegalStateException(
94
+ "Client key is closed"
95
+ )
96
+ }
97
+
98
+ private fun decodePeerPublicKey(
99
+ value: String
100
+ ): ByteArray {
101
+ require(
102
+ value.matches(Regex("[0-9a-f]{64}")) &&
103
+ Bip340Verifier.isValidXOnlyPubkey(value)
104
+ ) {
105
+ "Invalid peer public key"
106
+ }
107
+
108
+ return Bip340Hash.hexToBytes(value)
109
+ }
110
+
111
+ companion object {
112
+ private val SECURE_RANDOM = SecureRandom()
113
+
114
+ fun generate(): Nip46ClientKey {
115
+ while (true) {
116
+ val candidate = ByteArray(32)
117
+ SECURE_RANDOM.nextBytes(candidate)
118
+ val scalar = BigInteger(1, candidate)
119
+
120
+ if (
121
+ scalar.signum() > 0 &&
122
+ scalar < Secp256k1.N
123
+ ) {
124
+ return Nip46ClientKey(candidate)
125
+ }
126
+
127
+ Arrays.fill(candidate, 0)
128
+ }
129
+ }
130
+
131
+ internal fun fromPrivateKey(
132
+ privateKey: ByteArray
133
+ ): Nip46ClientKey {
134
+ Secp256k1.privateScalar(privateKey)
135
+ return Nip46ClientKey(
136
+ privateKey.copyOf()
137
+ )
138
+ }
139
+ }
140
+ }
@@ -1,6 +1,5 @@
1
1
  package com.pakstr.app.crypto
2
2
 
3
- import org.json.JSONArray
4
3
  import org.json.JSONObject
5
4
  import java.security.MessageDigest
6
5
 
@@ -38,39 +37,15 @@ object NostrEventHasher {
38
37
  event.optString("pubkey")
39
38
 
40
39
  require(pubkey.isNotBlank()) {
41
- "Missing pubkey in nostr event: $event"
40
+ "Missing pubkey in nostr event"
42
41
  }
43
42
 
44
43
  val serialized =
45
- JSONArray()
46
- .apply {
47
-
48
- put(0)
49
-
50
- put(pubkey)
51
-
52
- put(
53
- event.getLong("created_at")
54
- )
55
-
56
- put(
57
- event.getInt("kind")
58
- )
59
-
60
- put(
61
- event.getJSONArray("tags")
62
- )
63
-
64
- put(
65
- event.getString("content")
66
- )
67
-
68
- }
69
- .toString()
44
+ NostrEventSerializer.serialize(event)
70
45
 
71
46
 
72
47
  return sha256Hex(
73
- serialized.toByteArray()
48
+ serialized.toByteArray(Charsets.UTF_8)
74
49
  )
75
50
  }
76
51