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
@@ -1,167 +1,209 @@
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
18
45
 
19
- private var pendingPublicKey:
20
- CancellableContinuation<String>? = null
21
-
22
-
23
-
24
- override suspend fun getPublicKey(): String {
25
-
26
- return suspendCancellableCoroutine { cont ->
46
+ override suspend fun getPublicKey(): String = withTimeout(30_000) {
27
47
 
28
- pendingPublicKey = cont
48
+ suspendCancellableCoroutine { continuation ->
29
49
 
30
- val intent = Intent().apply {
31
-
32
- action =
33
-
34
- "com.greenart7c3.nostrsigner.GET_PUBLIC_KEY"
50
+ pendingCallback = { pubkey ->
35
51
 
52
+ if (continuation.isActive) {
53
+ continuation.resume(pubkey)
54
+ }
36
55
  }
37
56
 
38
- val activities =
39
-
40
- context.packageManager.queryIntentActivities(
57
+ val intent = Intent(
58
+ Intent.ACTION_VIEW
59
+ ).apply {
41
60
 
42
- intent,
43
-
44
- 0
61
+ data = Uri.parse(
62
+ "nostrsigner:?type=get_public_key"
63
+ )
45
64
 
65
+ putExtra(
66
+ "type", "get_public_key"
46
67
  )
47
68
 
48
- if (activities.isEmpty()) {
49
- activities.forEach {
69
+ }
50
70
 
51
- println(
71
+ try {
52
72
 
53
- "Amber activity: ${it.activityInfo.packageName}/${it.activityInfo.name}"
73
+ launcher.launch(intent)
54
74
 
55
- )
75
+ } catch (_: ActivityNotFoundException) {
56
76
 
77
+ if (continuation.isActive) {
78
+ continuation.resume("")
57
79
  }
58
- pendingPublicKey = null
59
-
60
- cont.resumeWith(
61
-
62
- Result.failure(
63
-
64
- Exception(
65
-
66
- "Amber signer is not installed. Please install Amber to continue."
67
-
68
- )
69
-
70
- )
71
-
72
- )
73
-
74
- return@suspendCancellableCoroutine
75
-
76
80
  }
77
81
 
78
- launchIntent(intent)
79
-
82
+ continuation.invokeOnCancellation {
83
+ pendingCallback = null
84
+ }
80
85
  }
81
-
82
86
  }
83
87
 
88
+ override suspend fun signEvent(
89
+ eventJson: String
90
+ ): String =
84
91
 
92
+ withTimeout(30_000) {
85
93
 
86
- fun handleResult(
87
- result: ActivityResult
88
- ) {
89
-
90
- if (result.resultCode != Activity.RESULT_OK) {
94
+ suspendCancellableCoroutine { continuation ->
91
95
 
92
- pendingPublicKey?.resumeWith(
93
- Result.failure(
94
- Exception(
95
- "Amber cancelled"
96
- )
97
- )
98
- )
96
+ val event = JSONObject(eventJson)
99
97
 
100
- pendingPublicKey = null
98
+ pendingCallback = { signature ->
101
99
 
102
- return
100
+ if (signature.isEmpty()) {
103
101
 
104
- }
102
+ if (continuation.isActive) {
105
103
 
104
+ continuation.resume("")
106
105
 
107
- val pubkey =
108
- result.data
109
- ?.getStringExtra("pubkey")
106
+ }
110
107
 
108
+ } else {
111
109
 
112
- if (pubkey != null) {
110
+ val signedEvent =
113
111
 
114
- pendingPublicKey?.resume(
115
- pubkey
116
- )
112
+ NostrEventBuilder.addId(event)
113
+ .apply {
117
114
 
118
- } else {
115
+ put(
116
+ "sig", signature
117
+ )
119
118
 
120
- pendingPublicKey?.resumeWith(
121
- Result.failure(
122
- Exception(
123
- "No public key returned"
124
- )
125
- )
126
- )
119
+ }
120
+ // Verify the signed event locally before returning it.
121
+ // This ensures that the received signature matches the event data
122
+ // and protects against invalid signer responses.
123
+ val verified = NostrEventVerifier.verify(signedEvent)
127
124
 
128
- }
125
+ AppDebugLogger.log(
126
+ "NOSTR_VERIFY", "Event verified=$verified"
127
+ )
129
128
 
129
+ if (continuation.isActive) {
130
+ if (verified) {
131
+ continuation.resume(
132
+ signedEvent.toString()
133
+ )
134
+
135
+ } else {
136
+ continuation.resume("")
137
+ }
138
+ }
139
+ }
140
+ }
130
141
 
131
- pendingPublicKey = null
142
+ val intent = Intent(
143
+ Intent.ACTION_VIEW, Uri.parse(
144
+ "nostrsigner:${Uri.encode(event.toString())}"
145
+ )
146
+ ).apply {
147
+ putExtra(
148
+ "type", "sign_event"
149
+ )
150
+ }
151
+ try {
152
+ launcher.launch(intent)
132
153
 
133
- }
154
+ } catch (_: ActivityNotFoundException) {
134
155
 
156
+ if (continuation.isActive) {
157
+ continuation.resume("")
158
+ }
159
+ }
135
160
 
161
+ continuation.invokeOnCancellation {
162
+ pendingCallback = null
163
+ }
164
+ }
165
+ }
136
166
 
137
- override suspend fun signEvent(
138
- eventJson: String
167
+ override suspend fun nip04Encrypt(
168
+ pubkey: String, plaintext: String
139
169
  ): String {
140
170
 
141
- TODO()
142
-
171
+ throw NotImplementedError(
172
+ "NIP04 not implemented"
173
+ )
143
174
  }
144
175
 
176
+ override suspend fun nip04Decrypt(
177
+ pubkey: String, ciphertext: String
178
+ ): String {
145
179
 
180
+ throw NotImplementedError(
181
+ "NIP04 not implemented"
182
+ )
183
+ }
146
184
 
147
- override suspend fun nip04Encrypt(
148
- pubkey: String,
149
- plaintext: String
150
- ): String {
185
+ fun onResult(
186
+ result: ActivityResult
187
+ ) {
151
188
 
152
- TODO()
189
+ val callback = pendingCallback
153
190
 
154
- }
191
+ pendingCallback = null
155
192
 
193
+ if (result.resultCode != Activity.RESULT_OK) {
194
+ callback?.let {
195
+ it("")
196
+ }
197
+ return
156
198
 
199
+ }
157
200
 
158
- override suspend fun nip04Decrypt(
159
- pubkey: String,
160
- ciphertext: String
161
- ): String {
201
+ val value =
162
202
 
163
- TODO()
203
+ result.data?.getStringExtra("result") ?: result.data?.getStringExtra("signature") ?: ""
164
204
 
205
+ callback?.invoke(
206
+ value
207
+ )
165
208
  }
166
-
167
209
  }
@@ -4,93 +4,169 @@ 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(
50
-
51
107
  callbackId,
52
-
53
- ""
54
-
108
+ errorResponse(
109
+ "SIGNER_ERROR",
110
+ e.message ?: "Unknown signer error"
111
+ )
55
112
  )
56
-
57
113
  }
58
-
59
114
  }
60
-
61
115
  }
62
116
 
63
- "signEvent" -> {
64
- CoroutineScope(Dispatchers.IO).launch {
65
- val result = signerManager.getSigner().signEvent(payload ?: "")
66
- resolve(callbackId, result)
67
- }
117
+ else -> {
118
+
119
+ resolve(
120
+ callbackId,
121
+ ""
122
+ )
68
123
  }
69
124
  }
70
125
  }
71
126
 
72
- private fun resolve(callbackId: String, result: String) {
73
- mainHandler.post {
74
- val js = "window.__nostrResolve('$callbackId', '$result')"
75
- webView.evaluateJavascript(js, null)
76
- }
77
- }
127
+ private fun resolve(
128
+ callbackId: String,
129
+ result: String
130
+ ) {
78
131
 
79
- private fun showToast(message: String) {
132
+ AppDebugLogger.log(
133
+ "NOSTR_BRIDGE",
134
+ "Bridge response received"
135
+ )
80
136
 
81
137
  mainHandler.post {
82
138
 
83
- Toast.makeText(
139
+ val js =
140
+ """
141
+ window.__nostrResolve(
142
+ ${JSONObject.quote(callbackId)},
143
+ ${JSONObject.quote(result)}
144
+ );
145
+ """.trimIndent()
84
146
 
85
- webView.context,
147
+ webView.evaluateJavascript(js, null)
148
+ }
149
+ }
86
150
 
87
- message,
151
+ private fun errorResponse(
152
+ code: String,
153
+ message: String
154
+ ): String {
88
155
 
89
- Toast.LENGTH_LONG
156
+ return JSONObject()
157
+ .apply {
90
158
 
91
- ).show()
159
+ put(
160
+ "error",
161
+ code
162
+ )
92
163
 
93
- }
164
+ put(
165
+ "message",
166
+ message
167
+ )
94
168
 
169
+ }
170
+ .toString()
95
171
  }
96
- }
172
+ }
@@ -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
+ }