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.
- 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 +130 -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 +1 -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/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
|
|
40
|
+
"Missing pubkey in nostr event"
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
val serialized =
|
|
45
|
-
|
|
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
|
|