pakstr 0.0.4 → 0.2.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/android-template/app/build.gradle.kts +3 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/ExampleInstrumentedTest.kt +8 -3
- package/android-template/app/src/androidTest/java/com/pakstr/app/signer/NostrEventBuilderTest.kt +67 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/signer/NostrSignatureVerifierTest.kt +43 -0
- package/android-template/app/src/main/AndroidManifest.xml +15 -3
- package/android-template/app/src/main/assets/nip07.js +85 -4
- package/android-template/app/src/main/assets/www/index.html +215 -0
- package/android-template/app/src/main/java/com/pakstr/app/ApplicationClass.kt +2 -0
- package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +35 -10
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +121 -101
- package/android-template/app/src/main/java/com/pakstr/app/ShellRuntime.kt +5 -6
- package/android-template/app/src/main/java/com/pakstr/app/WebViewController.kt +69 -20
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Verifier.kt +318 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/CryptoInitializer.kt +34 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip04Crypto.kt +33 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEncryption.kt +16 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +100 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventVerifier.kt +54 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrSignatureVerifier.kt +43 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +63 -0
- package/android-template/app/src/main/java/com/pakstr/app/debug/AppDebugLogger.kt +21 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/AmberSigner.kt +427 -74
- package/android-template/app/src/main/java/com/pakstr/app/signer/BunkerSigner.kt +14 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/FakeSigner.kt +14 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +297 -32
- package/android-template/app/src/main/java/com/pakstr/app/signer/NostrEventBuilder.kt +39 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +103 -73
- package/android-template/app/src/main/java/com/pakstr/app/signer/SignerType.kt +0 -1
- package/android-template/app/src/main/java/com/pakstr/app/signer/exceptions/SignerUnavailableException.kt +5 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/interfaces/NostrSigner.kt +10 -0
- package/android-template/app/src/main/res/values/strings.xml +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
import com.pakstr.app.debug.AppDebugLogger
|
|
4
|
+
import org.bouncycastle.math.ec.ECPoint
|
|
5
|
+
import java.math.BigInteger
|
|
6
|
+
import java.security.MessageDigest
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Verifies BIP-340 Schnorr signatures over the secp256k1 curve.
|
|
10
|
+
*
|
|
11
|
+
* This implementation follows the BIP-340 verification algorithm:
|
|
12
|
+
*
|
|
13
|
+
* 1. Validate public key, message and signature sizes.
|
|
14
|
+
* 2. Lift the x-only public key to a curve point.
|
|
15
|
+
* 3. Compute the tagged challenge hash.
|
|
16
|
+
* 4. Reconstruct R = sG - eP.
|
|
17
|
+
* 5. Verify that R has an even Y coordinate and that its X coordinate
|
|
18
|
+
* matches the signature's R value.
|
|
19
|
+
*
|
|
20
|
+
* Returns true only if the signature is fully valid.
|
|
21
|
+
*/
|
|
22
|
+
object Bip340Verifier {
|
|
23
|
+
|
|
24
|
+
private const val CHALLENGE_TAG =
|
|
25
|
+
"BIP0340/challenge"
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Verifies a BIP-340 Schnorr signature.
|
|
29
|
+
*
|
|
30
|
+
* @param pubkeyHex x-only public key in hexadecimal format.
|
|
31
|
+
* @param messageHex 32-byte message hash in hexadecimal format.
|
|
32
|
+
* @param signatureHex Schnorr signature (r || s) in hexadecimal format.
|
|
33
|
+
*
|
|
34
|
+
* @return true if the signature is valid.
|
|
35
|
+
*/
|
|
36
|
+
fun verify(
|
|
37
|
+
pubkeyHex: String,
|
|
38
|
+
messageHex: String,
|
|
39
|
+
signatureHex: String
|
|
40
|
+
): Boolean {
|
|
41
|
+
|
|
42
|
+
return try {
|
|
43
|
+
|
|
44
|
+
val pubkey =
|
|
45
|
+
hexToBytes(pubkeyHex)
|
|
46
|
+
|
|
47
|
+
val message =
|
|
48
|
+
hexToBytes(messageHex)
|
|
49
|
+
|
|
50
|
+
val signature =
|
|
51
|
+
hexToBytes(signatureHex)
|
|
52
|
+
|
|
53
|
+
// BIP340 sizes
|
|
54
|
+
if (pubkey.size != 32)
|
|
55
|
+
return false
|
|
56
|
+
|
|
57
|
+
if (message.size != 32)
|
|
58
|
+
return false
|
|
59
|
+
|
|
60
|
+
if (signature.size != 64)
|
|
61
|
+
return false
|
|
62
|
+
|
|
63
|
+
// Extract r and s values from the 64-byte signature.
|
|
64
|
+
val r =
|
|
65
|
+
BigInteger(
|
|
66
|
+
1,
|
|
67
|
+
signature.copyOfRange(
|
|
68
|
+
0,
|
|
69
|
+
32
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
val s =
|
|
74
|
+
BigInteger(
|
|
75
|
+
1,
|
|
76
|
+
signature.copyOfRange(
|
|
77
|
+
32,
|
|
78
|
+
64
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// BIP340 range checks
|
|
83
|
+
if (r >= Secp256k1.P)
|
|
84
|
+
return false
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if (s >= Secp256k1.N)
|
|
88
|
+
return false
|
|
89
|
+
|
|
90
|
+
// Lift the x-only public key into a full secp256k1 point.
|
|
91
|
+
val P =
|
|
92
|
+
liftX(pubkey)
|
|
93
|
+
|
|
94
|
+
// Compute the BIP-340 tagged challenge hash.
|
|
95
|
+
val eBytes =
|
|
96
|
+
taggedHash(
|
|
97
|
+
CHALLENGE_TAG,
|
|
98
|
+
signature.copyOfRange(
|
|
99
|
+
0,
|
|
100
|
+
32
|
|
101
|
+
) +
|
|
102
|
+
pubkey +
|
|
103
|
+
message
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
val e =
|
|
107
|
+
BigInteger(
|
|
108
|
+
1,
|
|
109
|
+
eBytes
|
|
110
|
+
)
|
|
111
|
+
.mod(
|
|
112
|
+
Secp256k1.N
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// Reconstruct R = sG - eP as defined by BIP-340.
|
|
116
|
+
val R =
|
|
117
|
+
Secp256k1.G
|
|
118
|
+
.multiply(s)
|
|
119
|
+
.subtract(
|
|
120
|
+
P.multiply(e)
|
|
121
|
+
)
|
|
122
|
+
.normalize()
|
|
123
|
+
|
|
124
|
+
if (R.isInfinity)
|
|
125
|
+
return false
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if (!R.isValid)
|
|
129
|
+
return false
|
|
130
|
+
|
|
131
|
+
// BIP-340 requires the reconstructed point to have an even Y coordinate.
|
|
132
|
+
if (
|
|
133
|
+
R.affineYCoord
|
|
134
|
+
.toBigInteger()
|
|
135
|
+
.testBit(0)
|
|
136
|
+
) {
|
|
137
|
+
|
|
138
|
+
return false
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Signature is valid only if the reconstructed X coordinate equals r.
|
|
143
|
+
val x = R.affineXCoord.toBigInteger()
|
|
144
|
+
|
|
145
|
+
return x == r
|
|
146
|
+
|
|
147
|
+
} catch (e: Exception) {
|
|
148
|
+
|
|
149
|
+
AppDebugLogger.log(
|
|
150
|
+
"BIP340",
|
|
151
|
+
"Verification failed: ${e.message}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
false
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Converts a BIP-340 x-only public key into a full secp256k1 curve point.
|
|
161
|
+
*
|
|
162
|
+
* BIP-340 uses x-only public keys, so the corresponding curve point is
|
|
163
|
+
* reconstructed by selecting the even Y coordinate.
|
|
164
|
+
*
|
|
165
|
+
* @throws IllegalArgumentException if the provided x coordinate does not
|
|
166
|
+
* represent a valid point on the secp256k1 curve.
|
|
167
|
+
*/
|
|
168
|
+
private fun liftX(
|
|
169
|
+
xBytes: ByteArray
|
|
170
|
+
): ECPoint {
|
|
171
|
+
|
|
172
|
+
val x =
|
|
173
|
+
BigInteger(
|
|
174
|
+
1,
|
|
175
|
+
xBytes
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if (x >= Secp256k1.P) {
|
|
179
|
+
|
|
180
|
+
throw IllegalArgumentException(
|
|
181
|
+
"Invalid x coordinate"
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
val curve =
|
|
186
|
+
Secp256k1.CURVE.curve
|
|
187
|
+
|
|
188
|
+
val alpha =
|
|
189
|
+
x.modPow(
|
|
190
|
+
BigInteger.valueOf(3),
|
|
191
|
+
Secp256k1.P
|
|
192
|
+
)
|
|
193
|
+
.add(
|
|
194
|
+
curve.b.toBigInteger()
|
|
195
|
+
)
|
|
196
|
+
.mod(
|
|
197
|
+
Secp256k1.P
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
var y =
|
|
201
|
+
sqrtMod(
|
|
202
|
+
alpha,
|
|
203
|
+
Secp256k1.P
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// verify square root exists
|
|
207
|
+
if (
|
|
208
|
+
y.multiply(y)
|
|
209
|
+
.mod(Secp256k1.P) != alpha
|
|
210
|
+
) {
|
|
211
|
+
|
|
212
|
+
throw IllegalArgumentException(
|
|
213
|
+
"Invalid public key"
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (y.testBit(0)) {
|
|
218
|
+
|
|
219
|
+
y = Secp256k1.P.subtract(y)
|
|
220
|
+
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
val point =
|
|
224
|
+
curve.createPoint(
|
|
225
|
+
x,
|
|
226
|
+
y
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if (point.isInfinity) {
|
|
230
|
+
|
|
231
|
+
throw IllegalArgumentException(
|
|
232
|
+
"Point at infinity"
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!point.isValid) {
|
|
237
|
+
|
|
238
|
+
throw IllegalArgumentException(
|
|
239
|
+
"Invalid curve point"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
}
|
|
243
|
+
return point
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Calculates modular square root for secp256k1.
|
|
248
|
+
*
|
|
249
|
+
* Since secp256k1 prime satisfies p ≡ 3 (mod 4),
|
|
250
|
+
* the square root can be calculated as a^((p+1)/4) mod p.
|
|
251
|
+
*/
|
|
252
|
+
private fun sqrtMod(
|
|
253
|
+
a: BigInteger,
|
|
254
|
+
p: BigInteger
|
|
255
|
+
): BigInteger {
|
|
256
|
+
|
|
257
|
+
return a.modPow(
|
|
258
|
+
p.add(BigInteger.ONE)
|
|
259
|
+
.divide(
|
|
260
|
+
BigInteger.valueOf(4)
|
|
261
|
+
),
|
|
262
|
+
p
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private fun taggedHash(
|
|
267
|
+
tag: String,
|
|
268
|
+
msg: ByteArray
|
|
269
|
+
): ByteArray {
|
|
270
|
+
|
|
271
|
+
val tagHash =
|
|
272
|
+
sha256(
|
|
273
|
+
tag.toByteArray()
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return sha256(
|
|
277
|
+
tagHash +
|
|
278
|
+
tagHash +
|
|
279
|
+
msg
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private fun sha256(
|
|
285
|
+
data: ByteArray
|
|
286
|
+
): ByteArray {
|
|
287
|
+
|
|
288
|
+
return MessageDigest
|
|
289
|
+
.getInstance(
|
|
290
|
+
"SHA-256"
|
|
291
|
+
)
|
|
292
|
+
.digest(
|
|
293
|
+
data
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private fun hexToBytes(
|
|
298
|
+
hex: String
|
|
299
|
+
): ByteArray {
|
|
300
|
+
|
|
301
|
+
require(
|
|
302
|
+
hex.length % 2 == 0
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return ByteArray(
|
|
306
|
+
hex.length / 2
|
|
307
|
+
) { index ->
|
|
308
|
+
|
|
309
|
+
hex.substring(
|
|
310
|
+
index * 2,
|
|
311
|
+
index * 2 + 2
|
|
312
|
+
)
|
|
313
|
+
.toInt(16)
|
|
314
|
+
.toByte()
|
|
315
|
+
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
|
4
|
+
import java.security.Security
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Initializes the Bouncy Castle cryptographic provider.
|
|
8
|
+
*
|
|
9
|
+
* Bouncy Castle provides additional cryptographic algorithms and elliptic
|
|
10
|
+
* curve support required by the Nostr crypto layer, including secp256k1
|
|
11
|
+
* operations used for BIP-340 Schnorr signature verification.
|
|
12
|
+
*
|
|
13
|
+
* The provider is registered only once to avoid duplicate registrations.
|
|
14
|
+
*/
|
|
15
|
+
object CryptoInitializer {
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Registers Bouncy Castle as a security provider if it is not already
|
|
19
|
+
* available.
|
|
20
|
+
*
|
|
21
|
+
* This method should be called during application startup before any
|
|
22
|
+
* cryptographic operations are performed.
|
|
23
|
+
*/
|
|
24
|
+
fun init() {
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
Security.getProvider("BC") == null
|
|
28
|
+
) {
|
|
29
|
+
Security.addProvider(
|
|
30
|
+
BouncyCastleProvider()
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
class Nip04Crypto : NostrEncryption {
|
|
4
|
+
|
|
5
|
+
override fun encrypt(
|
|
6
|
+
|
|
7
|
+
plaintext: String,
|
|
8
|
+
|
|
9
|
+
privateKey: ByteArray,
|
|
10
|
+
|
|
11
|
+
publicKey: ByteArray
|
|
12
|
+
|
|
13
|
+
): String {
|
|
14
|
+
|
|
15
|
+
TODO()
|
|
16
|
+
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun decrypt(
|
|
20
|
+
|
|
21
|
+
payload: String,
|
|
22
|
+
|
|
23
|
+
privateKey: ByteArray,
|
|
24
|
+
|
|
25
|
+
publicKey: ByteArray
|
|
26
|
+
|
|
27
|
+
): String {
|
|
28
|
+
|
|
29
|
+
TODO()
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
interface NostrEncryption {
|
|
4
|
+
|
|
5
|
+
fun encrypt(
|
|
6
|
+
plaintext: String,
|
|
7
|
+
privateKey: ByteArray,
|
|
8
|
+
publicKey: ByteArray
|
|
9
|
+
): String
|
|
10
|
+
|
|
11
|
+
fun decrypt(
|
|
12
|
+
payload: String,
|
|
13
|
+
privateKey: ByteArray,
|
|
14
|
+
publicKey: ByteArray
|
|
15
|
+
): String
|
|
16
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
import org.json.JSONArray
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
import java.security.MessageDigest
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calculates Nostr event IDs according to the Nostr event hashing specification.
|
|
9
|
+
*
|
|
10
|
+
* The event ID is the SHA-256 hash of the serialized event array:
|
|
11
|
+
*
|
|
12
|
+
* [
|
|
13
|
+
* 0,
|
|
14
|
+
* pubkey,
|
|
15
|
+
* created_at,
|
|
16
|
+
* kind,
|
|
17
|
+
* tags,
|
|
18
|
+
* content
|
|
19
|
+
* ]
|
|
20
|
+
*
|
|
21
|
+
* The resulting hash is used as the event identifier and is later signed
|
|
22
|
+
* using a Schnorr signature (BIP-340).
|
|
23
|
+
*/
|
|
24
|
+
object NostrEventHasher {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Calculates the SHA-256 event ID for a Nostr event.
|
|
28
|
+
*
|
|
29
|
+
* @param event unsigned Nostr event JSON object.
|
|
30
|
+
*
|
|
31
|
+
* @return hexadecimal SHA-256 hash of the serialized event.
|
|
32
|
+
*/
|
|
33
|
+
fun hashEvent(
|
|
34
|
+
event: JSONObject
|
|
35
|
+
): String {
|
|
36
|
+
|
|
37
|
+
val serialized =
|
|
38
|
+
JSONArray()
|
|
39
|
+
.apply {
|
|
40
|
+
|
|
41
|
+
put(0)
|
|
42
|
+
|
|
43
|
+
put(
|
|
44
|
+
event.getString("pubkey")
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
put(
|
|
48
|
+
event.getLong("created_at")
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
put(
|
|
52
|
+
event.getInt("kind")
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
put(
|
|
56
|
+
event.getJSONArray("tags")
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
put(
|
|
60
|
+
event.getString("content")
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
.toString()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
return sha256Hex(
|
|
68
|
+
serialized.toByteArray()
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Calculates SHA-256 digest.
|
|
75
|
+
*/
|
|
76
|
+
private fun sha256(
|
|
77
|
+
data: ByteArray
|
|
78
|
+
): ByteArray {
|
|
79
|
+
|
|
80
|
+
return MessageDigest
|
|
81
|
+
.getInstance("SHA-256")
|
|
82
|
+
.digest(data)
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Calculates SHA-256 digest and returns it as lowercase hexadecimal string.
|
|
88
|
+
*/
|
|
89
|
+
private fun sha256Hex(
|
|
90
|
+
data: ByteArray
|
|
91
|
+
): String {
|
|
92
|
+
|
|
93
|
+
return sha256(data)
|
|
94
|
+
.joinToString("") {
|
|
95
|
+
|
|
96
|
+
"%02x".format(it)
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates a complete signed Nostr event.
|
|
7
|
+
*
|
|
8
|
+
* A Nostr event is considered valid only when:
|
|
9
|
+
*
|
|
10
|
+
* 1. The event ID matches the SHA-256 hash of the canonical event data.
|
|
11
|
+
* 2. The Schnorr signature is valid for the event ID and public key.
|
|
12
|
+
*
|
|
13
|
+
* This ensures that the event was not modified after signing
|
|
14
|
+
* and that the signature belongs to the provided public key.
|
|
15
|
+
*/
|
|
16
|
+
object NostrEventVerifier {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Verifies the integrity and signature of a signed Nostr event.
|
|
20
|
+
*
|
|
21
|
+
* @param event Signed Nostr event containing:
|
|
22
|
+
* id, pubkey, created_at, kind, tags, content and sig.
|
|
23
|
+
*
|
|
24
|
+
* @return true if both event hash and signature are valid.
|
|
25
|
+
*/
|
|
26
|
+
fun verify(
|
|
27
|
+
event: JSONObject
|
|
28
|
+
): Boolean {
|
|
29
|
+
|
|
30
|
+
val eventId =
|
|
31
|
+
event.getString("id")
|
|
32
|
+
|
|
33
|
+
val calculatedId =
|
|
34
|
+
NostrEventHasher.hashEvent(
|
|
35
|
+
event
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
eventId != calculatedId
|
|
40
|
+
) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return NostrSignatureVerifier.verify(
|
|
45
|
+
|
|
46
|
+
eventId,
|
|
47
|
+
|
|
48
|
+
event.getString("pubkey"),
|
|
49
|
+
|
|
50
|
+
event.getString("sig")
|
|
51
|
+
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies Nostr Schnorr signatures.
|
|
5
|
+
*
|
|
6
|
+
* This class acts as a bridge between the Nostr event validation layer
|
|
7
|
+
* and the underlying BIP-340 Schnorr signature verification.
|
|
8
|
+
*
|
|
9
|
+
* The signature is verified against:
|
|
10
|
+
*
|
|
11
|
+
* - event ID (32-byte message hash)
|
|
12
|
+
* - x-only public key
|
|
13
|
+
* - Schnorr signature (r || s)
|
|
14
|
+
*
|
|
15
|
+
* A valid signature proves that the event was signed by the owner
|
|
16
|
+
* of the corresponding private key.
|
|
17
|
+
*/
|
|
18
|
+
object NostrSignatureVerifier {
|
|
19
|
+
/**
|
|
20
|
+
* Verifies the signature of a Nostr event.
|
|
21
|
+
*
|
|
22
|
+
* @param eventId SHA-256 event ID used as the signed message.
|
|
23
|
+
* @param pubkey x-only secp256k1 public key.
|
|
24
|
+
* @param signature BIP-340 Schnorr signature.
|
|
25
|
+
*
|
|
26
|
+
* @return true if the signature is valid, otherwise false.
|
|
27
|
+
*/
|
|
28
|
+
fun verify(
|
|
29
|
+
eventId: String,
|
|
30
|
+
pubkey: String,
|
|
31
|
+
signature: String
|
|
32
|
+
): Boolean {
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
return Bip340Verifier.verify(
|
|
36
|
+
pubkeyHex = pubkey,
|
|
37
|
+
messageHex = eventId,
|
|
38
|
+
signatureHex = signature
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
import org.bouncycastle.asn1.x9.X9ECParameters
|
|
4
|
+
import org.bouncycastle.crypto.ec.CustomNamedCurves
|
|
5
|
+
import org.bouncycastle.math.ec.ECPoint
|
|
6
|
+
import java.math.BigInteger
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Provides secp256k1 curve parameters used by BIP-340 Schnorr verification.
|
|
10
|
+
*
|
|
11
|
+
* secp256k1 is the elliptic curve used by Bitcoin and Nostr Schnorr signatures.
|
|
12
|
+
*
|
|
13
|
+
* This object exposes the curve constants required for elliptic curve
|
|
14
|
+
* operations:
|
|
15
|
+
*
|
|
16
|
+
* - G: Generator point of the curve.
|
|
17
|
+
* - N: Order of the generator point.
|
|
18
|
+
* - P: Field prime defining the finite field.
|
|
19
|
+
*
|
|
20
|
+
* These values are used by Bip340Verifier when reconstructing and validating
|
|
21
|
+
* Schnorr signature points.
|
|
22
|
+
*/
|
|
23
|
+
object Secp256k1 {
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* secp256k1 curve parameters provided by Bouncy Castle.
|
|
27
|
+
*/
|
|
28
|
+
val CURVE: X9ECParameters =
|
|
29
|
+
CustomNamedCurves.getByName(
|
|
30
|
+
"secp256k1"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generator point (G) of the secp256k1 curve.
|
|
35
|
+
*
|
|
36
|
+
* All public keys are derived as:
|
|
37
|
+
*
|
|
38
|
+
* publicKey = privateKey * G
|
|
39
|
+
*/
|
|
40
|
+
val G: ECPoint =
|
|
41
|
+
CURVE.g
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Order of the generator point.
|
|
45
|
+
*
|
|
46
|
+
* Used for scalar multiplication limits in Schnorr verification.
|
|
47
|
+
*/
|
|
48
|
+
val N: BigInteger =
|
|
49
|
+
CURVE.n
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Field prime (p) defining the secp256k1 finite field.
|
|
53
|
+
*
|
|
54
|
+
* The curve equation is:
|
|
55
|
+
*
|
|
56
|
+
* y² = x³ + 7 mod p
|
|
57
|
+
*/
|
|
58
|
+
val P: BigInteger =
|
|
59
|
+
CURVE.curve
|
|
60
|
+
.field
|
|
61
|
+
.characteristic
|
|
62
|
+
|
|
63
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package com.pakstr.app.debug
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.pakstr.app.BuildConfig
|
|
5
|
+
|
|
6
|
+
object AppDebugLogger {
|
|
7
|
+
|
|
8
|
+
fun log(
|
|
9
|
+
tag: String,
|
|
10
|
+
message: String
|
|
11
|
+
) {
|
|
12
|
+
|
|
13
|
+
if (BuildConfig.DEBUG) {
|
|
14
|
+
|
|
15
|
+
Log.d(
|
|
16
|
+
tag,
|
|
17
|
+
message
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|