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
@@ -4,93 +4,358 @@ import android.os.Handler
4
4
  import android.os.Looper
5
5
  import android.webkit.JavascriptInterface
6
6
  import android.webkit.WebView
7
- import android.widget.Toast
7
+
8
+ import com.pakstr.app.debug.AppDebugLogger
9
+ import com.pakstr.app.signer.exceptions.SignerUnavailableException
8
10
  import kotlinx.coroutines.CoroutineScope
9
11
  import kotlinx.coroutines.Dispatchers
12
+ import kotlinx.coroutines.SupervisorJob
10
13
  import kotlinx.coroutines.launch
14
+ import org.json.JSONObject
11
15
 
12
16
  class NostrBridge(
13
-
14
17
  private val webView: WebView,
15
-
16
18
  private val signerManager: SignerManager
17
-
18
19
  ) {
19
20
 
20
21
  private val mainHandler = Handler(Looper.getMainLooper())
21
22
 
23
+ private val scope = CoroutineScope(
24
+ SupervisorJob() + Dispatchers.IO
25
+ )
26
+
22
27
  @JavascriptInterface
23
- fun call(method: String, callbackId: String, payload: String?) {
28
+ fun call(
29
+ method: String,
30
+ callbackId: String,
31
+ payload: String?
32
+ ) {
24
33
 
25
34
  when (method) {
26
35
 
27
36
  "getPublicKey" -> {
28
37
 
29
- CoroutineScope(Dispatchers.IO).launch {
38
+ scope.launch {
30
39
 
31
40
  try {
41
+ val pk =
42
+ signerManager
43
+ .getSigner()
44
+ .getPublicKey()
45
+ resolve(
46
+ callbackId,
47
+ pk
48
+ )
49
+ } catch (e: SignerUnavailableException) {
50
+ resolve(
51
+ callbackId,
52
+ errorResponse(
53
+ "SIGNER_UNAVAILABLE",
54
+ e.message ?: "Signer unavailable"
55
+ )
56
+ )
32
57
 
33
- val pk = signerManager
58
+ } catch (e: Exception) {
59
+ resolve(
60
+ callbackId,
61
+ errorResponse(
62
+ "SIGNER_ERROR",
63
+ e.message ?: "Unknown signer error"
64
+ )
65
+ )
66
+ }
67
+ }
68
+ }
34
69
 
35
- .getSigner()
70
+ "signEvent" -> {
71
+ AppDebugLogger.log(
72
+ "NOSTR_SIGN",
73
+ "Signing event request"
74
+ )
36
75
 
37
- .getPublicKey()
76
+ scope.launch {
38
77
 
39
- resolve(callbackId, pk)
78
+ try {
79
+ require(!payload.isNullOrBlank()) {
80
+ "Event payload is empty"
81
+ }
40
82
 
41
- } catch (e: Exception) {
83
+ val result =
84
+ signerManager
85
+ .getSigner()
86
+ .signEvent(
87
+ payload
88
+ )
42
89
 
43
- showToast(
90
+ resolve(
91
+ callbackId,
92
+ result
93
+ )
44
94
 
45
- e.message ?: "Signer error"
95
+ } catch (e: SignerUnavailableException) {
46
96
 
97
+ resolve(
98
+ callbackId,
99
+ errorResponse(
100
+ "SIGNER_UNAVAILABLE",
101
+ e.message ?: "Signer unavailable"
102
+ )
47
103
  )
48
104
 
105
+ } catch (e: Exception) {
49
106
  resolve(
107
+ callbackId,
108
+ errorResponse(
109
+ "SIGNER_ERROR",
110
+ e.message ?: "Unknown signer error"
111
+ )
112
+ )
113
+ }
114
+ }
115
+ }
116
+
117
+ "nip04_encrypt" -> {
50
118
 
119
+ scope.launch {
120
+
121
+ try {
122
+
123
+ require(!payload.isNullOrBlank()) {
124
+ "NIP04 encrypt payload is empty"
125
+ }
126
+
127
+ val json =
128
+ JSONObject(payload)
129
+
130
+ val pubkey =
131
+ json.getString("pubkey")
132
+
133
+ val plaintext =
134
+ json.getString("plaintext")
135
+
136
+
137
+ val result =
138
+ signerManager
139
+ .getSigner()
140
+ .nip04Encrypt(
141
+ pubkey,
142
+ plaintext
143
+ )
144
+
145
+
146
+ resolve(
51
147
  callbackId,
148
+ result
149
+ )
52
150
 
53
- ""
151
+ } catch (e: SignerUnavailableException) {
54
152
 
153
+ resolve(
154
+ callbackId,
155
+ errorResponse(
156
+ "SIGNER_UNAVAILABLE",
157
+ e.message ?: "Signer unavailable"
158
+ )
55
159
  )
56
160
 
161
+ } catch (e: Exception) {
162
+
163
+ resolve(
164
+ callbackId,
165
+ errorResponse(
166
+ "SIGNER_ERROR",
167
+ e.message ?: "Unknown signer error"
168
+ )
169
+ )
57
170
  }
171
+ }
172
+ }
173
+
174
+
175
+ "nip04_decrypt" -> {
176
+
177
+ scope.launch {
178
+
179
+ try {
180
+
181
+ require(!payload.isNullOrBlank()) {
182
+ "NIP04 decrypt payload is empty"
183
+ }
184
+
185
+
186
+ val json =
187
+ JSONObject(payload)
188
+
189
+ val pubkey =
190
+ json.getString("pubkey")
191
+
192
+ val ciphertext =
193
+ json.getString("ciphertext")
194
+
195
+
196
+ val result =
197
+ signerManager
198
+ .getSigner()
199
+ .nip04Decrypt(
200
+ pubkey,
201
+ ciphertext
202
+ )
58
203
 
204
+
205
+ resolve(
206
+ callbackId,
207
+ result
208
+ )
209
+
210
+
211
+ } catch (e: SignerUnavailableException) {
212
+
213
+ resolve(
214
+ callbackId,
215
+ errorResponse(
216
+ "SIGNER_UNAVAILABLE",
217
+ e.message ?: "Signer unavailable"
218
+ )
219
+ )
220
+
221
+ } catch (e: Exception) {
222
+
223
+ resolve(
224
+ callbackId,
225
+ errorResponse(
226
+ "SIGNER_ERROR",
227
+ e.message ?: "Unknown signer error"
228
+ )
229
+ )
230
+ }
59
231
  }
232
+ }
233
+
234
+ "nip44_encrypt" -> {
235
+
236
+ scope.launch {
237
+
238
+ try {
239
+
240
+ val json =
241
+ JSONObject(payload ?: "")
242
+
243
+ val result =
244
+ signerManager
245
+ .getSigner()
246
+ .nip44Encrypt(
247
+ json.getString("pubkey"),
248
+ json.getString("plaintext")
249
+ )
250
+
251
+ resolve(
252
+ callbackId,
253
+ result
254
+ )
255
+
60
256
 
257
+ } catch (e: Exception) {
258
+
259
+ resolve(
260
+ callbackId,
261
+ errorResponse(
262
+ "SIGNER_ERROR",
263
+ e.message ?: ""
264
+ )
265
+ )
266
+ }
267
+ }
61
268
  }
62
269
 
63
- "signEvent" -> {
64
- CoroutineScope(Dispatchers.IO).launch {
65
- val result = signerManager.getSigner().signEvent(payload ?: "")
66
- resolve(callbackId, result)
270
+ "nip44_decrypt" -> {
271
+
272
+ scope.launch {
273
+
274
+ try {
275
+
276
+ val json =
277
+ JSONObject(payload ?: "")
278
+
279
+ val result =
280
+ signerManager
281
+ .getSigner()
282
+ .nip44Decrypt(
283
+ json.getString("pubkey"),
284
+ json.getString("ciphertext")
285
+ )
286
+
287
+ resolve(
288
+ callbackId,
289
+ result
290
+ )
291
+
292
+
293
+ } catch (e: Exception) {
294
+
295
+ resolve(
296
+ callbackId,
297
+ errorResponse(
298
+ "SIGNER_ERROR",
299
+ e.message ?: ""
300
+ )
301
+ )
302
+ }
67
303
  }
68
304
  }
69
- }
70
- }
71
305
 
72
- private fun resolve(callbackId: String, result: String) {
73
- mainHandler.post {
74
- val js = "window.__nostrResolve('$callbackId', '$result')"
75
- webView.evaluateJavascript(js, null)
306
+ else -> {
307
+
308
+ resolve(
309
+ callbackId,
310
+ ""
311
+ )
312
+ }
76
313
  }
77
314
  }
78
315
 
79
- private fun showToast(message: String) {
316
+ private fun resolve(
317
+ callbackId: String,
318
+ result: String
319
+ ) {
320
+
321
+ AppDebugLogger.log(
322
+ "NOSTR_BRIDGE",
323
+ "Bridge response received"
324
+ )
80
325
 
81
326
  mainHandler.post {
82
327
 
83
- Toast.makeText(
328
+ val js =
329
+ """
330
+ window.__nostrResolve(
331
+ ${JSONObject.quote(callbackId)},
332
+ ${JSONObject.quote(result)}
333
+ );
334
+ """.trimIndent()
84
335
 
85
- webView.context,
336
+ webView.evaluateJavascript(js, null)
337
+ }
338
+ }
86
339
 
87
- message,
340
+ private fun errorResponse(
341
+ code: String,
342
+ message: String
343
+ ): String {
88
344
 
89
- Toast.LENGTH_LONG
345
+ return JSONObject()
346
+ .apply {
90
347
 
91
- ).show()
348
+ put(
349
+ "error",
350
+ code
351
+ )
92
352
 
93
- }
353
+ put(
354
+ "message",
355
+ message
356
+ )
94
357
 
358
+ }
359
+ .toString()
95
360
  }
96
- }
361
+ }
@@ -0,0 +1,39 @@
1
+ package com.pakstr.app.signer
2
+
3
+ import com.pakstr.app.crypto.NostrEventHasher
4
+ import org.json.JSONObject
5
+
6
+ /**
7
+ * Builds Nostr events before signing.
8
+ *
9
+ * Generates the event ID according to Nostr event hashing rules
10
+ * and adds it to the event JSON.
11
+ *
12
+ * The generated ID is later used as the message that gets signed
13
+ * by the external signer (e.g. Amber).
14
+ */
15
+ object NostrEventBuilder {
16
+
17
+ /**
18
+ * Calculates and adds the Nostr event ID.
19
+ *
20
+ * @param event unsigned Nostr event JSON
21
+ * @return the same event containing the generated "id" field
22
+ */
23
+ fun addId(
24
+ event: JSONObject
25
+ ): JSONObject {
26
+
27
+ val id = NostrEventHasher.hashEvent(
28
+ event
29
+ )
30
+
31
+ event.put(
32
+ "id", id
33
+ )
34
+
35
+ return event
36
+
37
+ }
38
+
39
+ }
@@ -1,148 +1,178 @@
1
1
  package com.pakstr.app.signer
2
2
 
3
3
  import android.content.Context
4
+ import android.content.Intent
4
5
  import androidx.activity.result.ActivityResult
6
+ import androidx.activity.result.ActivityResultLauncher
5
7
  import com.pakstr.app.signer.interfaces.NostrSigner
6
-
8
+ import androidx.core.content.edit
9
+ import com.pakstr.app.signer.exceptions.SignerUnavailableException
10
+
11
+ /**
12
+ * Manages Nostr signer selection and lifecycle.
13
+ *
14
+ * Provides a single access point for obtaining the active NostrSigner
15
+ * implementation used by the application.
16
+ *
17
+ * Supported signer types:
18
+ *
19
+ * - Amber
20
+ * External Android signer app using the nostrsigner URI scheme.
21
+ *
22
+ * - Fake
23
+ * Test signer used for development and testing.
24
+ *
25
+ * - Bunker
26
+ * Reserved for NIP-46 remote signer implementation.
27
+ *
28
+ * The selected signer type is persisted using SharedPreferences.
29
+ * The signer instance is cached and recreated only when the user
30
+ * changes the active signer.
31
+ */
7
32
  class SignerManager(
8
- private val context: Context,
9
- private val launchAmber: (android.content.Intent) -> Unit
33
+ context: Context,
34
+ private val amberLauncher: ActivityResultLauncher<Intent>
10
35
  ) {
36
+ private val context =
37
+ context.applicationContext
11
38
 
12
39
  private val prefs =
13
- context.getSharedPreferences(
40
+ this.context.getSharedPreferences(
14
41
  "signer",
15
42
  Context.MODE_PRIVATE
16
43
  )
17
44
 
18
-
19
45
  private var activeSigner: NostrSigner? = null
20
46
 
47
+ private var currentUserPubkey: String? = null
21
48
 
22
49
  fun getActiveType(): SignerType? {
23
50
 
24
- return prefs.getString(
25
- "type",
26
- null
27
- )?.let {
51
+ val value =
52
+ prefs.getString(
53
+ "type",
54
+ SignerType.AMBER.name
55
+ )
28
56
 
29
- SignerType.valueOf(it)
57
+ return try {
58
+ SignerType.valueOf(value.toString())
30
59
 
60
+ } catch (_: IllegalArgumentException) {
61
+ SignerType.AMBER
31
62
  }
32
-
33
63
  }
34
64
 
65
+ suspend fun getCurrentUserPubkey(): String {
35
66
 
36
- fun setActiveType(type: SignerType) {
67
+ currentUserPubkey?.let {
37
68
 
38
- prefs.edit()
39
- .putString(
40
- "type",
41
- type.name
42
- )
43
- .apply()
69
+ return it
44
70
 
71
+ }
45
72
 
46
- activeSigner = null
73
+ val pk =
47
74
 
48
- }
75
+ getSigner()
49
76
 
77
+ .getPublicKey()
50
78
 
51
- fun getSigner(): NostrSigner {
79
+ currentUserPubkey = pk
80
+
81
+ return pk
82
+
83
+ }
52
84
 
85
+ fun setActiveType(
86
+ type: SignerType
87
+ ) {
53
88
 
54
- if (activeSigner != null) {
55
- return activeSigner!!
89
+ prefs.edit {
90
+ putString(
91
+ "type",
92
+ type.name
93
+ )
56
94
  }
57
95
 
96
+ activeSigner = null
97
+ }
98
+
99
+ /**
100
+ * Returns the currently selected signer implementation.
101
+ *
102
+ * Creates the signer lazily on first request and reuses the same
103
+ * instance until the signer type changes.
104
+ */
105
+ fun getSigner(): NostrSigner {
58
106
 
59
- activeSigner =
107
+ activeSigner?.let {
108
+ return it
109
+ }
60
110
 
61
- when(getActiveType()) {
111
+ val signer =
112
+ when (getActiveType()) {
62
113
 
63
114
  SignerType.AMBER -> {
64
115
 
65
116
  if (!isAmberInstalled()) {
66
-
117
+ throw SignerUnavailableException(
118
+ "Amber signer application is not installed"
119
+ )
67
120
  }
68
121
 
69
122
  AmberSigner(
70
-
71
- context,
72
-
73
- launchAmber
74
-
123
+ amberLauncher
75
124
  )
76
-
77
125
  }
78
126
 
79
127
  SignerType.BUNKER -> {
128
+ throw SignerUnavailableException(
129
+ "Bunker signer is not implemented"
130
+ )
131
+ }
80
132
 
133
+ SignerType.FAKE -> {
81
134
  FakeSigner()
82
-
83
135
  }
84
136
 
85
137
  null -> {
86
138
 
87
139
  throw IllegalStateException(
88
-
89
140
  "No signer selected"
90
-
91
141
  )
92
-
93
- }
94
-
95
- else -> {
96
-
97
- FakeSigner()
98
-
99
142
  }
100
-
101
143
  }
102
144
 
145
+ activeSigner = signer
103
146
 
104
- return activeSigner!!
105
-
106
- }
107
-
108
-
109
-
110
- fun handleAmberResult(
111
- result: ActivityResult
112
- ) {
113
-
114
- val signer =
115
- activeSigner
116
-
117
-
118
- if (signer is AmberSigner) {
119
-
120
- signer.handleResult(result)
121
-
122
- }
123
-
147
+ return signer
124
148
  }
125
149
 
126
150
  private fun isAmberInstalled(): Boolean {
127
151
 
128
152
  return try {
129
153
 
130
- context.packageManager.getPackageInfo(
131
-
132
- "com.greenart7c3.nostrsigner",
133
-
134
- 0
135
-
136
- )
154
+ context.packageManager
155
+ .getPackageInfo(
156
+ "com.greenart7c3.nostrsigner",
157
+ 0
158
+ )
137
159
 
138
160
  true
139
161
 
140
- } catch (e: Exception) {
162
+ } catch (_: Exception) {
141
163
 
142
164
  false
143
-
144
165
  }
145
-
146
166
  }
147
167
 
168
+ /**
169
+ * Forwards Android Activity results back to the active Amber signer.
170
+ *
171
+ * Required because Amber communication is based on the Android
172
+ * Activity Result API.
173
+ */
174
+ fun handleAmberResult(result: ActivityResult) {
175
+ (activeSigner as? AmberSigner)
176
+ ?.onResult(result)
177
+ }
148
178
  }
@@ -2,7 +2,6 @@ package com.pakstr.app.signer
2
2
 
3
3
  enum class SignerType {
4
4
  AMBER,
5
-
6
5
  BUNKER,
7
6
  FAKE
8
7
 
@@ -0,0 +1,5 @@
1
+ package com.pakstr.app.signer.exceptions
2
+
3
+ class SignerUnavailableException(
4
+ message: String
5
+ ) : Exception(message)
@@ -22,4 +22,14 @@ interface NostrSigner {
22
22
 
23
23
  ): String
24
24
 
25
+ suspend fun nip44Encrypt(
26
+ pubkey: String,
27
+ plaintext: String
28
+ ): String
29
+
30
+ suspend fun nip44Decrypt(
31
+ pubkey: String,
32
+ ciphertext: String
33
+ ): String
34
+
25
35
  }