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.
Files changed (32) 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 +85 -4
  7. package/android-template/app/src/main/assets/www/index.html +215 -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 +121 -101
  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 +69 -20
  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/Nip04Crypto.kt +33 -0
  16. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEncryption.kt +16 -0
  17. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +100 -0
  18. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventVerifier.kt +54 -0
  19. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrSignatureVerifier.kt +43 -0
  20. package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +63 -0
  21. package/android-template/app/src/main/java/com/pakstr/app/debug/AppDebugLogger.kt +21 -0
  22. package/android-template/app/src/main/java/com/pakstr/app/signer/AmberSigner.kt +427 -74
  23. package/android-template/app/src/main/java/com/pakstr/app/signer/BunkerSigner.kt +14 -0
  24. package/android-template/app/src/main/java/com/pakstr/app/signer/FakeSigner.kt +14 -0
  25. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +297 -32
  26. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrEventBuilder.kt +39 -0
  27. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +103 -73
  28. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerType.kt +0 -1
  29. package/android-template/app/src/main/java/com/pakstr/app/signer/exceptions/SignerUnavailableException.kt +5 -0
  30. package/android-template/app/src/main/java/com/pakstr/app/signer/interfaces/NostrSigner.kt +10 -0
  31. package/android-template/app/src/main/res/values/strings.xml +1 -1
  32. package/package.json +1 -1
@@ -1,167 +1,520 @@
1
1
  package com.pakstr.app.signer
2
2
 
3
3
  import android.app.Activity
4
- import android.content.Context
4
+ import android.content.ActivityNotFoundException
5
5
  import android.content.Intent
6
+ import android.net.Uri
6
7
  import androidx.activity.result.ActivityResult
8
+ import androidx.activity.result.ActivityResultLauncher
9
+ import com.pakstr.app.crypto.NostrEventVerifier
10
+ import com.pakstr.app.debug.AppDebugLogger
7
11
  import com.pakstr.app.signer.interfaces.NostrSigner
8
- import kotlinx.coroutines.CancellableContinuation
9
12
  import kotlinx.coroutines.suspendCancellableCoroutine
13
+ import kotlinx.coroutines.withTimeout
14
+ import org.json.JSONObject
10
15
  import kotlin.coroutines.resume
11
16
 
12
-
17
+ /**
18
+ * NIP-07 signer implementation using Amber as an external signer.
19
+ *
20
+ * This class communicates with the Amber app through Android intents.
21
+ * The private key never leaves Amber. This app only sends signing requests
22
+ * and receives public keys or signatures back.
23
+ *
24
+ * Supported operations:
25
+ *
26
+ * - getPublicKey()
27
+ * Requests the user's public key from Amber.
28
+ *
29
+ * - signEvent(eventJson)
30
+ * Sends an unsigned Nostr event to Amber for signing.
31
+ * After receiving the signature, the event ID is generated and the
32
+ * signature is locally verified before returning the signed event.
33
+ *
34
+ * The class uses suspendCancellableCoroutine to bridge the asynchronous
35
+ * Android Activity Result API with Kotlin coroutines.
36
+ */
13
37
  class AmberSigner(
14
- private val context: Context,
15
- private val launchIntent: (Intent) -> Unit
38
+ private val launcher: ActivityResultLauncher<Intent>
16
39
  ) : NostrSigner {
17
40
 
41
+ // Stores the current pending signer request.
42
+ // Android intents return results asynchronously, so the callback is used
43
+ // to resume the suspended coroutine when Amber responds.
44
+ private var pendingCallback: ((String) -> Unit)? = null
45
+ private var cachedPubkey: String? = null
46
+ override suspend fun getPublicKey(): String {
47
+
48
+ cachedPubkey?.let {
18
49
 
19
- private var pendingPublicKey:
20
- CancellableContinuation<String>? = null
50
+ return it
21
51
 
52
+ }
22
53
 
54
+ return withTimeout(30_000) {
23
55
 
24
- override suspend fun getPublicKey(): String {
56
+ suspendCancellableCoroutine { continuation ->
25
57
 
26
- return suspendCancellableCoroutine { cont ->
58
+ pendingCallback = { pubkey ->
27
59
 
28
- pendingPublicKey = cont
60
+ cachedPubkey = pubkey
29
61
 
30
- val intent = Intent().apply {
62
+ if (continuation.isActive) {
31
63
 
32
- action =
64
+ continuation.resume(pubkey)
33
65
 
34
- "com.greenart7c3.nostrsigner.GET_PUBLIC_KEY"
66
+ }
35
67
 
36
- }
68
+ }
37
69
 
38
- val activities =
70
+ val intent = Intent(
39
71
 
40
- context.packageManager.queryIntentActivities(
72
+ Intent.ACTION_VIEW
41
73
 
42
- intent,
74
+ ).apply {
43
75
 
44
- 0
76
+ data = Uri.parse(
45
77
 
46
- )
78
+ "nostrsigner:?type=get_public_key"
79
+
80
+ )
47
81
 
48
- if (activities.isEmpty()) {
49
- activities.forEach {
82
+ putExtra(
50
83
 
51
- println(
84
+ "type",
52
85
 
53
- "Amber activity: ${it.activityInfo.packageName}/${it.activityInfo.name}"
86
+ "get_public_key"
54
87
 
55
88
  )
56
89
 
57
90
  }
58
- pendingPublicKey = null
59
91
 
60
- cont.resumeWith(
92
+ try {
61
93
 
62
- Result.failure(
94
+ launcher.launch(intent)
63
95
 
64
- Exception(
96
+ } catch (_: ActivityNotFoundException) {
65
97
 
66
- "Amber signer is not installed. Please install Amber to continue."
98
+ if (continuation.isActive) {
67
99
 
68
- )
100
+ continuation.resume("")
69
101
 
70
- )
102
+ }
71
103
 
72
- )
104
+ }
73
105
 
74
- return@suspendCancellableCoroutine
106
+ continuation.invokeOnCancellation {
75
107
 
76
- }
108
+ pendingCallback = null
77
109
 
78
- launchIntent(intent)
110
+ }
111
+
112
+ }
79
113
 
80
114
  }
81
115
 
82
116
  }
83
117
 
118
+ override suspend fun signEvent(
119
+ eventJson: String
120
+ ): String =
84
121
 
122
+ withTimeout(30_000) {
85
123
 
86
- fun handleResult(
87
- result: ActivityResult
88
- ) {
124
+ suspendCancellableCoroutine { continuation ->
89
125
 
90
- if (result.resultCode != Activity.RESULT_OK) {
126
+ val event = JSONObject(eventJson)
127
+
128
+ pendingCallback = { signature ->
91
129
 
92
- pendingPublicKey?.resumeWith(
93
- Result.failure(
94
- Exception(
95
- "Amber cancelled"
130
+ if (signature.isEmpty()) {
131
+
132
+ if (continuation.isActive) {
133
+
134
+ continuation.resume("")
135
+
136
+ }
137
+
138
+ } else {
139
+
140
+ val signedEvent =
141
+
142
+ NostrEventBuilder.addId(event)
143
+ .apply {
144
+
145
+ put(
146
+ "sig", signature
147
+ )
148
+
149
+ }
150
+ // Verify the signed event locally before returning it.
151
+ // This ensures that the received signature matches the event data
152
+ // and protects against invalid signer responses.
153
+ val verified = NostrEventVerifier.verify(signedEvent)
154
+
155
+ AppDebugLogger.log(
156
+ "NOSTR_VERIFY", "Event verified=$verified"
157
+ )
158
+
159
+ if (continuation.isActive) {
160
+ if (verified) {
161
+ continuation.resume(
162
+ signedEvent.toString()
163
+ )
164
+
165
+ } else {
166
+ continuation.resume("")
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ val intent = Intent(
173
+ Intent.ACTION_VIEW, Uri.parse(
174
+ "nostrsigner:${Uri.encode(event.toString())}"
96
175
  )
97
- )
98
- )
176
+ ).apply {
177
+ putExtra(
178
+ "type", "sign_event"
179
+ )
180
+ }
181
+ try {
182
+ launcher.launch(intent)
99
183
 
100
- pendingPublicKey = null
184
+ } catch (_: ActivityNotFoundException) {
101
185
 
102
- return
186
+ if (continuation.isActive) {
187
+ continuation.resume("")
188
+ }
189
+ }
103
190
 
191
+ continuation.invokeOnCancellation {
192
+ pendingCallback = null
193
+ }
194
+ }
104
195
  }
105
196
 
197
+ override suspend fun nip04Encrypt(
198
+ pubkey: String,
199
+ plaintext: String
200
+ ): String =
201
+ withTimeout(30_000) {
202
+
203
+ suspendCancellableCoroutine { continuation ->
106
204
 
107
- val pubkey =
108
- result.data
109
- ?.getStringExtra("pubkey")
205
+ pendingCallback = { encrypted ->
110
206
 
207
+ if (continuation.isActive) {
208
+ continuation.resume(encrypted)
209
+ }
210
+ }
211
+
212
+ val intent = Intent(
213
+ Intent.ACTION_VIEW
214
+ ).apply {
111
215
 
112
- if (pubkey != null) {
216
+ data = Uri.parse(
217
+ "nostrsigner:?type=nip04_encrypt" +
218
+ "&pubkey=$pubkey" +
219
+ "&plaintext=${Uri.encode(plaintext)}"
220
+ )
113
221
 
114
- pendingPublicKey?.resume(
115
- pubkey
116
- )
222
+ putExtra(
223
+ "type",
224
+ "nip04_encrypt"
225
+ )
117
226
 
118
- } else {
227
+ putExtra(
228
+ "pubkey",
229
+ pubkey
230
+ )
119
231
 
120
- pendingPublicKey?.resumeWith(
121
- Result.failure(
122
- Exception(
123
- "No public key returned"
232
+ putExtra(
233
+ "plaintext",
234
+ plaintext
124
235
  )
125
- )
126
- )
236
+ }
237
+
238
+ try {
239
+
240
+ launcher.launch(intent)
241
+
242
+ } catch (_: ActivityNotFoundException) {
243
+
244
+ if (continuation.isActive) {
245
+ continuation.resume("")
246
+ }
247
+ }
127
248
 
249
+ continuation.invokeOnCancellation {
250
+ pendingCallback = null
251
+ }
252
+ }
128
253
  }
129
254
 
255
+ override suspend fun nip04Decrypt(
256
+ pubkey: String,
257
+ ciphertext: String
258
+ ): String = withTimeout(30_000) {
130
259
 
131
- pendingPublicKey = null
260
+ suspendCancellableCoroutine { continuation ->
132
261
 
133
- }
262
+ pendingCallback = { result ->
134
263
 
264
+ if (continuation.isActive) {
265
+ continuation.resume(result)
266
+ }
267
+ }
135
268
 
269
+ val intent = Intent(
270
+ Intent.ACTION_VIEW
271
+ ).apply {
136
272
 
137
- override suspend fun signEvent(
138
- eventJson: String
139
- ): String {
273
+ data = Uri.parse(
274
+ "nostrsigner:?type=nip04_decrypt" +
275
+ "&pubkey=$pubkey" +
276
+ "&message=${Uri.encode(ciphertext)}"
277
+ )
278
+
279
+ putExtra(
280
+ "type",
281
+ "nip04_decrypt"
282
+ )
140
283
 
141
- TODO()
284
+ putExtra(
285
+ "pubkey",
286
+ pubkey
287
+ )
142
288
 
143
- }
289
+ putExtra(
290
+ "message",
291
+ ciphertext
292
+ )
293
+ }
144
294
 
295
+ try {
145
296
 
297
+ launcher.launch(intent)
298
+
299
+ } catch (_: ActivityNotFoundException) {
300
+
301
+ if (continuation.isActive) {
302
+ continuation.resume("")
303
+ }
304
+ }
305
+
306
+ continuation.invokeOnCancellation {
307
+ pendingCallback = null
308
+ }
309
+ }
310
+ }
311
+
312
+ override suspend fun nip44Encrypt(
146
313
 
147
- override suspend fun nip04Encrypt(
148
314
  pubkey: String,
315
+
149
316
  plaintext: String
150
- ): String {
151
317
 
152
- TODO()
318
+ ): String =
153
319
 
154
- }
320
+ withTimeout(30_000) {
155
321
 
322
+ val currentUser = getCachedPublicKey()
156
323
 
324
+ suspendCancellableCoroutine { continuation ->
157
325
 
158
- override suspend fun nip04Decrypt(
326
+ pendingCallback = { encrypted ->
327
+
328
+ if (continuation.isActive) {
329
+
330
+ continuation.resume(encrypted)
331
+
332
+ }
333
+
334
+ }
335
+
336
+ val intent = Intent(
337
+
338
+ Intent.ACTION_VIEW
339
+
340
+ ).apply {
341
+
342
+ data = Uri.parse(
343
+
344
+ "nostrsigner:${Uri.encode(plaintext)}"
345
+
346
+ )
347
+
348
+ putExtra(
349
+
350
+ "type",
351
+
352
+ "nip44_encrypt"
353
+
354
+ )
355
+
356
+ putExtra(
357
+
358
+ "pubkey",
359
+
360
+ pubkey
361
+
362
+ )
363
+
364
+ putExtra(
365
+
366
+ "current_user",
367
+
368
+ currentUser
369
+
370
+ )
371
+
372
+ }
373
+
374
+ try {
375
+
376
+ launcher.launch(intent)
377
+
378
+ } catch (_: ActivityNotFoundException) {
379
+
380
+ if (continuation.isActive) {
381
+
382
+ continuation.resume("")
383
+
384
+ }
385
+
386
+ }
387
+
388
+ continuation.invokeOnCancellation {
389
+
390
+ pendingCallback = null
391
+
392
+ }
393
+
394
+ }
395
+
396
+ }
397
+
398
+ override suspend fun nip44Decrypt(
159
399
  pubkey: String,
160
400
  ciphertext: String
161
- ): String {
401
+ ): String =
402
+ withTimeout(30_000) {
403
+ val currentUser = getCachedPublicKey()
404
+ suspendCancellableCoroutine { continuation ->
405
+
406
+ pendingCallback = { result ->
407
+
408
+ if (continuation.isActive) {
409
+ continuation.resume(result)
410
+ }
411
+ }
412
+
413
+ val intent = Intent(
414
+ Intent.ACTION_VIEW
415
+ ).apply {
416
+
417
+ data = Uri.parse(
418
+ "nostrsigner:${Uri.encode(ciphertext)}"
419
+ )
162
420
 
163
- TODO()
421
+ putExtra(
422
+ "type",
423
+ "nip44_decrypt"
424
+ )
164
425
 
426
+ putExtra(
427
+ "pubkey",
428
+ pubkey
429
+ )
430
+
431
+ putExtra(
432
+ "current_user",
433
+ currentUser
434
+ )
435
+ }
436
+
437
+
438
+ AppDebugLogger.log(
439
+ "NIP44",
440
+ "decrypt sender=$pubkey current=$pubkey length=${ciphertext.length}"
441
+ )
442
+
443
+
444
+ try {
445
+
446
+ launcher.launch(intent)
447
+
448
+ } catch (_: ActivityNotFoundException) {
449
+
450
+ if (continuation.isActive) {
451
+ continuation.resume("")
452
+ }
453
+ }
454
+
455
+
456
+ continuation.invokeOnCancellation {
457
+ pendingCallback = null
458
+ }
459
+ }
460
+ }
461
+
462
+ fun onResult(
463
+ result: ActivityResult
464
+ ) {
465
+
466
+ val callback = pendingCallback
467
+
468
+ pendingCallback = null
469
+
470
+ if (result.resultCode != Activity.RESULT_OK) {
471
+ callback?.let {
472
+ it("")
473
+ }
474
+ return
475
+
476
+ }
477
+
478
+ val value =
479
+
480
+ result.data?.getStringExtra("result")
481
+
482
+ ?: result.data?.getStringExtra("event")
483
+
484
+ ?: result.data?.getStringExtra("signature")
485
+
486
+ ?: result.data?.getStringExtra("plaintext")
487
+
488
+ ?: result.data?.getStringExtra("message")
489
+ ?: result.data?.getStringExtra("content")
490
+
491
+ ?: ""
492
+
493
+ AppDebugLogger.log(
494
+
495
+ "AMBER_RESULT",
496
+
497
+ value
498
+
499
+ )
500
+ AppDebugLogger.log(
501
+
502
+ "AMBER_RESULT_KEYS",
503
+
504
+ result.data?.extras?.keySet()?.joinToString()
505
+
506
+ ?: "NO_KEYS"
507
+
508
+ )
509
+
510
+ callback?.invoke(
511
+ value
512
+ )
165
513
  }
166
514
 
515
+ private suspend fun getCachedPublicKey(): String {
516
+
517
+ return cachedPubkey ?: getPublicKey()
518
+
519
+ }
167
520
  }
@@ -25,4 +25,18 @@ class BunkerSigner(context: Context) : NostrSigner {
25
25
  ): String {
26
26
  TODO("Not yet implemented")
27
27
  }
28
+
29
+ override suspend fun nip44Encrypt(
30
+ pubkey: String,
31
+ plaintext: String
32
+ ): String {
33
+ TODO("Not yet implemented")
34
+ }
35
+
36
+ override suspend fun nip44Decrypt(
37
+ pubkey: String,
38
+ ciphertext: String
39
+ ): String {
40
+ TODO("Not yet implemented")
41
+ }
28
42
  }
@@ -28,4 +28,18 @@ class FakeSigner : NostrSigner {
28
28
 
29
29
  }
30
30
 
31
+ override suspend fun nip44Encrypt(
32
+ pubkey: String,
33
+ plaintext: String
34
+ ): String {
35
+ TODO("Not yet implemented")
36
+ }
37
+
38
+ override suspend fun nip44Decrypt(
39
+ pubkey: String,
40
+ ciphertext: String
41
+ ): String {
42
+ TODO("Not yet implemented")
43
+ }
44
+
31
45
  }