pakstr 0.0.3 → 0.1.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 (27) hide show
  1. package/android-template/app/build.gradle.kts +3 -0
  2. package/android-template/app/src/androidTest/java/com/pakstr/app/ExampleInstrumentedTest.kt +8 -3
  3. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/NostrEventBuilderTest.kt +67 -0
  4. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/NostrSignatureVerifierTest.kt +43 -0
  5. package/android-template/app/src/main/AndroidManifest.xml +15 -3
  6. package/android-template/app/src/main/assets/nip07.js +47 -7
  7. package/android-template/app/src/main/assets/www/index.html +220 -0
  8. package/android-template/app/src/main/java/com/pakstr/app/ApplicationClass.kt +2 -0
  9. package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +35 -10
  10. package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +102 -103
  11. package/android-template/app/src/main/java/com/pakstr/app/ShellRuntime.kt +5 -6
  12. package/android-template/app/src/main/java/com/pakstr/app/WebViewController.kt +65 -18
  13. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Verifier.kt +318 -0
  14. package/android-template/app/src/main/java/com/pakstr/app/crypto/CryptoInitializer.kt +34 -0
  15. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +100 -0
  16. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventVerifier.kt +54 -0
  17. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrSignatureVerifier.kt +43 -0
  18. package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +63 -0
  19. package/android-template/app/src/main/java/com/pakstr/app/debug/AppDebugLogger.kt +21 -0
  20. package/android-template/app/src/main/java/com/pakstr/app/signer/AmberSigner.kt +142 -100
  21. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +115 -39
  22. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrEventBuilder.kt +39 -0
  23. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +83 -75
  24. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerType.kt +0 -1
  25. package/android-template/app/src/main/java/com/pakstr/app/signer/exceptions/SignerUnavailableException.kt +5 -0
  26. package/android-template/app/src/main/res/values/strings.xml +1 -1
  27. 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,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
+ }