pakstr 0.0.4 → 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.
- 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 +47 -7
- package/android-template/app/src/main/assets/www/index.html +220 -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 +102 -103
- 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 +65 -18
- 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/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 +142 -100
- package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +115 -39
- 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 +83 -75
- 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/res/values/strings.xml +1 -1
- 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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
suspendCancellableCoroutine { continuation ->
|
|
29
49
|
|
|
30
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
val intent = Intent(
|
|
58
|
+
Intent.ACTION_VIEW
|
|
59
|
+
).apply {
|
|
41
60
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
49
|
-
activities.forEach {
|
|
69
|
+
}
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
try {
|
|
52
72
|
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
result: ActivityResult
|
|
88
|
-
) {
|
|
89
|
-
|
|
90
|
-
if (result.resultCode != Activity.RESULT_OK) {
|
|
94
|
+
suspendCancellableCoroutine { continuation ->
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
Result.failure(
|
|
94
|
-
Exception(
|
|
95
|
-
"Amber cancelled"
|
|
96
|
-
)
|
|
97
|
-
)
|
|
98
|
-
)
|
|
96
|
+
val event = JSONObject(eventJson)
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
pendingCallback = { signature ->
|
|
101
99
|
|
|
102
|
-
|
|
100
|
+
if (signature.isEmpty()) {
|
|
103
101
|
|
|
104
|
-
|
|
102
|
+
if (continuation.isActive) {
|
|
105
103
|
|
|
104
|
+
continuation.resume("")
|
|
106
105
|
|
|
107
|
-
|
|
108
|
-
result.data
|
|
109
|
-
?.getStringExtra("pubkey")
|
|
106
|
+
}
|
|
110
107
|
|
|
108
|
+
} else {
|
|
111
109
|
|
|
112
|
-
|
|
110
|
+
val signedEvent =
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
112
|
+
NostrEventBuilder.addId(event)
|
|
113
|
+
.apply {
|
|
117
114
|
|
|
118
|
-
|
|
115
|
+
put(
|
|
116
|
+
"sig", signature
|
|
117
|
+
)
|
|
119
118
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
138
|
-
|
|
167
|
+
override suspend fun nip04Encrypt(
|
|
168
|
+
pubkey: String, plaintext: String
|
|
139
169
|
): String {
|
|
140
170
|
|
|
141
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
): String {
|
|
185
|
+
fun onResult(
|
|
186
|
+
result: ActivityResult
|
|
187
|
+
) {
|
|
151
188
|
|
|
152
|
-
|
|
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
|
-
|
|
159
|
-
pubkey: String,
|
|
160
|
-
ciphertext: String
|
|
161
|
-
): String {
|
|
201
|
+
val value =
|
|
162
202
|
|
|
163
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
"signEvent" -> {
|
|
71
|
+
AppDebugLogger.log(
|
|
72
|
+
"NOSTR_SIGN",
|
|
73
|
+
"Signing event request"
|
|
74
|
+
)
|
|
36
75
|
|
|
37
|
-
|
|
76
|
+
scope.launch {
|
|
38
77
|
|
|
39
|
-
|
|
78
|
+
try {
|
|
79
|
+
require(!payload.isNullOrBlank()) {
|
|
80
|
+
"Event payload is empty"
|
|
81
|
+
}
|
|
40
82
|
|
|
41
|
-
|
|
83
|
+
val result =
|
|
84
|
+
signerManager
|
|
85
|
+
.getSigner()
|
|
86
|
+
.signEvent(
|
|
87
|
+
payload
|
|
88
|
+
)
|
|
42
89
|
|
|
43
|
-
|
|
90
|
+
resolve(
|
|
91
|
+
callbackId,
|
|
92
|
+
result
|
|
93
|
+
)
|
|
44
94
|
|
|
45
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
117
|
+
else -> {
|
|
118
|
+
|
|
119
|
+
resolve(
|
|
120
|
+
callbackId,
|
|
121
|
+
""
|
|
122
|
+
)
|
|
68
123
|
}
|
|
69
124
|
}
|
|
70
125
|
}
|
|
71
126
|
|
|
72
|
-
private fun resolve(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
}
|
|
127
|
+
private fun resolve(
|
|
128
|
+
callbackId: String,
|
|
129
|
+
result: String
|
|
130
|
+
) {
|
|
78
131
|
|
|
79
|
-
|
|
132
|
+
AppDebugLogger.log(
|
|
133
|
+
"NOSTR_BRIDGE",
|
|
134
|
+
"Bridge response received"
|
|
135
|
+
)
|
|
80
136
|
|
|
81
137
|
mainHandler.post {
|
|
82
138
|
|
|
83
|
-
|
|
139
|
+
val js =
|
|
140
|
+
"""
|
|
141
|
+
window.__nostrResolve(
|
|
142
|
+
${JSONObject.quote(callbackId)},
|
|
143
|
+
${JSONObject.quote(result)}
|
|
144
|
+
);
|
|
145
|
+
""".trimIndent()
|
|
84
146
|
|
|
85
|
-
|
|
147
|
+
webView.evaluateJavascript(js, null)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
86
150
|
|
|
87
|
-
|
|
151
|
+
private fun errorResponse(
|
|
152
|
+
code: String,
|
|
153
|
+
message: String
|
|
154
|
+
): String {
|
|
88
155
|
|
|
89
|
-
|
|
156
|
+
return JSONObject()
|
|
157
|
+
.apply {
|
|
90
158
|
|
|
91
|
-
|
|
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
|
+
}
|