pakstr 0.20.0 → 0.21.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/README.md +1 -1
- package/android-template/app/build.gradle.kts +4 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +273 -0
- package/android-template/app/src/main/assets/www/index.html +0 -6
- package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +20 -4
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +136 -12
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Hash.kt +56 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Signer.kt +104 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Verifier.kt +20 -170
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip44Crypto.kt +513 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip46ClientKey.kt +140 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +3 -28
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventSerializer.kt +74 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +100 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/BunkerSigner.kt +32 -7
- package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +36 -9
- package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +240 -163
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConfig.kt +175 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConnectionStore.kt +144 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Client.kt +79 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Models.kt +319 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingClient.kt +159 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingQrCode.kt +35 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingUri.kt +104 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Protocol.kt +695 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46RelayTransport.kt +489 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/RelayNip46Client.kt +403 -0
- package/android-template/app/src/main/java/com/pakstr/app/utils/BunkerPairingDialog.kt +82 -0
- package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +78 -17
- package/android-template/app/src/main/res/layout/activity_layout.xml +11 -0
- package/android-template/app/src/main/res/layout/dialog_bunker_pairing.xml +75 -0
- package/android-template/app/src/main/res/layout/dialog_picker.xml +60 -31
- package/android-template/app/src/main/res/values/strings.xml +27 -0
- package/android-template/app/src/test/java/com/pakstr/app/LocalServerQueryKeepAliveTest.kt +118 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Bip340SignerTest.kt +133 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip44CryptoTest.kt +199 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyProtocolTest.kt +82 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyTest.kt +52 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/NostrEventSerializerTest.kt +42 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/NostrBridgeErrorTest.kt +32 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/BunkerConfigTest.kt +120 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ModelsTest.kt +111 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingClientTest.kt +271 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingQrCodeTest.kt +54 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingUriTest.kt +80 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ProtocolTest.kt +578 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46RelayTransportTest.kt +508 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/RelayNip46ClientTest.kt +678 -0
- package/android-template/gradle/libs.versions.toml +8 -0
- package/package.json +1 -1
|
@@ -4,246 +4,323 @@ import android.content.Context
|
|
|
4
4
|
import android.content.Intent
|
|
5
5
|
import androidx.activity.result.ActivityResult
|
|
6
6
|
import androidx.activity.result.ActivityResultLauncher
|
|
7
|
-
import com.pakstr.app.signer.interfaces.NostrSigner
|
|
8
7
|
import androidx.core.content.edit
|
|
8
|
+
import com.pakstr.app.crypto.Nip46ClientKey
|
|
9
9
|
import com.pakstr.app.debug.AppDebugLogger
|
|
10
|
+
import com.pakstr.app.signer.bunker.BunkerConfig
|
|
11
|
+
import com.pakstr.app.signer.bunker.BunkerConnectionStore
|
|
12
|
+
import com.pakstr.app.signer.bunker.Nip46PairingClient
|
|
13
|
+
import com.pakstr.app.signer.bunker.Nip46PairingUri
|
|
14
|
+
import com.pakstr.app.signer.bunker.RelayNip46Client
|
|
10
15
|
import com.pakstr.app.signer.exceptions.SignerUnavailableException
|
|
16
|
+
import com.pakstr.app.signer.interfaces.NostrSigner
|
|
17
|
+
import org.json.JSONObject
|
|
11
18
|
|
|
12
|
-
/**
|
|
13
|
-
* Manages Nostr signer selection and lifecycle.
|
|
14
|
-
*
|
|
15
|
-
* Provides a single access point for obtaining the active NostrSigner
|
|
16
|
-
* implementation used by the application.
|
|
17
|
-
*
|
|
18
|
-
* Supported signer types:
|
|
19
|
-
*
|
|
20
|
-
* - Amber
|
|
21
|
-
* External Android signer app using the nostrsigner URI scheme.
|
|
22
|
-
*
|
|
23
|
-
* - Fake
|
|
24
|
-
* Test signer used for development and testing.
|
|
25
|
-
*
|
|
26
|
-
* - Bunker
|
|
27
|
-
* Reserved for NIP-46 remote signer implementation.
|
|
28
|
-
*
|
|
29
|
-
* The selected signer type is persisted using SharedPreferences.
|
|
30
|
-
* The signer instance is cached and recreated only when the user
|
|
31
|
-
* changes the active signer.
|
|
32
|
-
*/
|
|
33
19
|
class SignerManager(
|
|
34
20
|
context: Context,
|
|
35
|
-
private val amberLauncher: ActivityResultLauncher<Intent
|
|
21
|
+
private val amberLauncher: ActivityResultLauncher<Intent>,
|
|
22
|
+
private val bunkerAuthChallengeHandler: ((String) -> Unit)? = null
|
|
36
23
|
) {
|
|
37
|
-
private val context =
|
|
38
|
-
context.applicationContext
|
|
39
|
-
|
|
24
|
+
private val context = context.applicationContext
|
|
40
25
|
private val prefs =
|
|
41
26
|
this.context.getSharedPreferences(
|
|
42
27
|
"signer",
|
|
43
28
|
Context.MODE_PRIVATE
|
|
44
29
|
)
|
|
30
|
+
private val bunkerStore by lazy {
|
|
31
|
+
BunkerConnectionStore(this.context)
|
|
32
|
+
}
|
|
45
33
|
|
|
46
34
|
private var activeSigner: NostrSigner? = null
|
|
47
|
-
|
|
48
35
|
private var currentUserPubkey: String? = null
|
|
36
|
+
private var pendingPairing: PendingPairing? = null
|
|
49
37
|
|
|
50
38
|
init {
|
|
51
|
-
|
|
52
39
|
AppDebugLogger.log(
|
|
53
|
-
|
|
54
40
|
context,
|
|
55
|
-
|
|
56
41
|
"SIGNER",
|
|
57
|
-
|
|
58
42
|
"SignerManager initialized. Active type: ${getActiveType()}"
|
|
59
|
-
|
|
60
43
|
)
|
|
61
|
-
|
|
62
44
|
}
|
|
63
45
|
|
|
64
46
|
fun getActiveType(): SignerType? {
|
|
65
|
-
|
|
66
|
-
val value =
|
|
67
|
-
prefs.getString(
|
|
68
|
-
"type",
|
|
69
|
-
SignerType.AMBER.name
|
|
70
|
-
)
|
|
71
|
-
|
|
47
|
+
val value = prefs.getString("type", SignerType.AMBER.name)
|
|
72
48
|
return try {
|
|
73
49
|
SignerType.valueOf(value.toString())
|
|
74
|
-
|
|
75
50
|
} catch (_: IllegalArgumentException) {
|
|
76
51
|
SignerType.AMBER
|
|
77
52
|
}
|
|
78
53
|
}
|
|
79
54
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
55
|
+
fun hasBunkerSession(): Boolean {
|
|
56
|
+
return try {
|
|
57
|
+
val connection = bunkerStore.restore() ?: return false
|
|
58
|
+
connection.clientKey.close()
|
|
59
|
+
true
|
|
60
|
+
} catch (_: Exception) {
|
|
61
|
+
false
|
|
86
62
|
}
|
|
63
|
+
}
|
|
87
64
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.getPublicKey()
|
|
93
|
-
|
|
94
|
-
currentUserPubkey = pk
|
|
95
|
-
|
|
65
|
+
suspend fun getCurrentUserPubkey(): String {
|
|
66
|
+
currentUserPubkey?.let { return it }
|
|
67
|
+
val publicKey = getSigner().getPublicKey()
|
|
68
|
+
currentUserPubkey = publicKey
|
|
96
69
|
AppDebugLogger.log(
|
|
97
|
-
|
|
98
70
|
context,
|
|
99
|
-
|
|
100
71
|
"SIGNER",
|
|
72
|
+
"Public key received: ${publicKey.take(12)}..."
|
|
73
|
+
)
|
|
74
|
+
return publicKey
|
|
75
|
+
}
|
|
101
76
|
|
|
102
|
-
|
|
77
|
+
suspend fun createBunkerPairingUri(
|
|
78
|
+
relayUrls: List<String>,
|
|
79
|
+
permissions: String? = null
|
|
80
|
+
): String {
|
|
81
|
+
cancelBunkerPairing()
|
|
82
|
+
val clientKey = Nip46ClientKey.generate()
|
|
83
|
+
val secret = Nip46PairingClient.generateSecret()
|
|
84
|
+
var client: Nip46PairingClient? = null
|
|
85
|
+
var exposed = false
|
|
86
|
+
try {
|
|
87
|
+
val pairingUri =
|
|
88
|
+
Nip46PairingUri.create(
|
|
89
|
+
clientKey.publicKeyHex,
|
|
90
|
+
relayUrls,
|
|
91
|
+
secret,
|
|
92
|
+
permissions,
|
|
93
|
+
context.getString(com.pakstr.app.R.string.app_name)
|
|
94
|
+
).encode()
|
|
95
|
+
client =
|
|
96
|
+
Nip46PairingClient(
|
|
97
|
+
context,
|
|
98
|
+
relayUrls,
|
|
99
|
+
clientKey,
|
|
100
|
+
secret
|
|
101
|
+
)
|
|
102
|
+
if (!client.awaitRelayReady()) {
|
|
103
|
+
throw SignerUnavailableException(
|
|
104
|
+
"Unable to subscribe for NIP-46 pairing responses"
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
synchronized(this) {
|
|
108
|
+
check(pendingPairing == null) {
|
|
109
|
+
"Another NIP-46 pairing attempt is active"
|
|
110
|
+
}
|
|
111
|
+
pendingPairing = PendingPairing(clientKey, client)
|
|
112
|
+
}
|
|
113
|
+
exposed = true
|
|
114
|
+
return pairingUri
|
|
115
|
+
} finally {
|
|
116
|
+
if (!exposed) {
|
|
117
|
+
client?.close()
|
|
118
|
+
clientKey.close()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
103
122
|
|
|
104
|
-
|
|
123
|
+
suspend fun finishBunkerPairing(): Boolean {
|
|
124
|
+
val pairing = pendingPairing ?: return false
|
|
125
|
+
try {
|
|
126
|
+
val config = pairing.client.awaitConnection()
|
|
127
|
+
?: return false
|
|
128
|
+
bunkerStore.save(config, pairing.clientKey, null)
|
|
129
|
+
closeActiveSigner()
|
|
130
|
+
setActiveTypeWithoutClosing(SignerType.BUNKER)
|
|
131
|
+
return true
|
|
132
|
+
} finally {
|
|
133
|
+
pendingPairing = null
|
|
134
|
+
pairing.client.close()
|
|
135
|
+
pairing.clientKey.close()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
105
138
|
|
|
106
|
-
|
|
139
|
+
fun cancelBunkerPairing() {
|
|
140
|
+
val pairing = pendingPairing ?: return
|
|
141
|
+
pendingPairing = null
|
|
142
|
+
pairing.client.close()
|
|
143
|
+
pairing.clientKey.close()
|
|
144
|
+
}
|
|
107
145
|
|
|
146
|
+
fun connectBunker(connectionUri: String) {
|
|
147
|
+
val config = BunkerConfig.parse(connectionUri)
|
|
148
|
+
val clientKey = Nip46ClientKey.generate()
|
|
149
|
+
try {
|
|
150
|
+
bunkerStore.savePending(config, clientKey)
|
|
151
|
+
} catch (_: Exception) {
|
|
152
|
+
throw SignerUnavailableException(
|
|
153
|
+
"Unable to store bunker connection securely"
|
|
154
|
+
)
|
|
155
|
+
} finally {
|
|
156
|
+
clientKey.close()
|
|
157
|
+
}
|
|
158
|
+
closeActiveSigner()
|
|
159
|
+
setActiveTypeWithoutClosing(SignerType.BUNKER)
|
|
108
160
|
}
|
|
109
161
|
|
|
110
|
-
fun setActiveType(
|
|
111
|
-
type: SignerType
|
|
112
|
-
) {
|
|
162
|
+
fun setActiveType(type: SignerType) {
|
|
113
163
|
AppDebugLogger.log(
|
|
114
|
-
|
|
115
164
|
context,
|
|
116
|
-
|
|
117
165
|
"SIGNER",
|
|
118
|
-
|
|
119
166
|
"Changing active signer to: ${type.name}"
|
|
120
|
-
|
|
121
167
|
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
"type",
|
|
125
|
-
type.name
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
activeSigner = null
|
|
168
|
+
closeActiveSigner()
|
|
169
|
+
setActiveTypeWithoutClosing(type)
|
|
130
170
|
}
|
|
131
171
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
*/
|
|
138
|
-
fun getSigner(): NostrSigner {
|
|
139
|
-
|
|
140
|
-
activeSigner?.let {
|
|
141
|
-
return it
|
|
172
|
+
fun logoutBunker() {
|
|
173
|
+
try {
|
|
174
|
+
(activeSigner as? BunkerSigner)?.disconnect()
|
|
175
|
+
} finally {
|
|
176
|
+
closeActiveSigner()
|
|
142
177
|
}
|
|
178
|
+
try {
|
|
179
|
+
bunkerStore.clear()
|
|
180
|
+
} finally {
|
|
181
|
+
setActiveTypeWithoutClosing(SignerType.AMBER)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
143
184
|
|
|
185
|
+
fun getSigner(): NostrSigner {
|
|
186
|
+
activeSigner?.let { return it }
|
|
144
187
|
val activeType = getActiveType()
|
|
145
|
-
|
|
146
188
|
AppDebugLogger.log(
|
|
147
|
-
|
|
148
189
|
context,
|
|
149
|
-
|
|
150
190
|
"SIGNER",
|
|
151
|
-
|
|
152
191
|
"Creating signer instance: $activeType"
|
|
153
|
-
|
|
154
192
|
)
|
|
155
193
|
|
|
156
194
|
val signer =
|
|
157
|
-
when (
|
|
158
|
-
|
|
159
|
-
SignerType.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"Creating Amber signer"
|
|
167
|
-
|
|
168
|
-
)
|
|
169
|
-
if (!isAmberInstalled()) {
|
|
170
|
-
AppDebugLogger.error(
|
|
195
|
+
when (activeType) {
|
|
196
|
+
SignerType.AMBER -> createAmberSigner()
|
|
197
|
+
SignerType.BUNKER -> createBunkerSigner()
|
|
198
|
+
SignerType.FAKE -> FakeSigner()
|
|
199
|
+
null -> throw IllegalStateException("No signer selected")
|
|
200
|
+
}
|
|
201
|
+
activeSigner = signer
|
|
202
|
+
return signer
|
|
203
|
+
}
|
|
171
204
|
|
|
172
|
-
|
|
205
|
+
fun close() {
|
|
206
|
+
cancelBunkerPairing()
|
|
207
|
+
closeActiveSigner()
|
|
208
|
+
}
|
|
173
209
|
|
|
174
|
-
|
|
210
|
+
fun handleAmberResult(result: ActivityResult) {
|
|
211
|
+
AppDebugLogger.log(
|
|
212
|
+
context,
|
|
213
|
+
"AMBER",
|
|
214
|
+
"Activity result received: ${result.resultCode}"
|
|
215
|
+
)
|
|
216
|
+
(activeSigner as? AmberSigner)?.onResult(result)
|
|
217
|
+
}
|
|
175
218
|
|
|
176
|
-
|
|
219
|
+
private fun createAmberSigner(): AmberSigner {
|
|
220
|
+
AppDebugLogger.log(
|
|
221
|
+
context,
|
|
222
|
+
"AMBER",
|
|
223
|
+
"Creating Amber signer"
|
|
224
|
+
)
|
|
225
|
+
if (!isAmberInstalled()) {
|
|
226
|
+
AppDebugLogger.error(
|
|
227
|
+
context,
|
|
228
|
+
"AMBER",
|
|
229
|
+
"Amber signer app is not installed"
|
|
230
|
+
)
|
|
231
|
+
throw SignerUnavailableException(
|
|
232
|
+
"Amber signer application is not installed"
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
return AmberSigner(amberLauncher)
|
|
236
|
+
}
|
|
177
237
|
|
|
238
|
+
private fun createBunkerSigner(): BunkerSigner {
|
|
239
|
+
val connection =
|
|
240
|
+
try {
|
|
241
|
+
bunkerStore.restore()
|
|
242
|
+
} catch (_: Exception) {
|
|
243
|
+
null
|
|
244
|
+
} ?: throw SignerUnavailableException(
|
|
245
|
+
"Bunker connection is not configured"
|
|
246
|
+
)
|
|
247
|
+
currentUserPubkey = connection.userPubkey
|
|
248
|
+
|
|
249
|
+
val client =
|
|
250
|
+
try {
|
|
251
|
+
RelayNip46Client(
|
|
252
|
+
config = connection.config,
|
|
253
|
+
clientKey = connection.clientKey,
|
|
254
|
+
authChallengeHandler = bunkerAuthChallengeHandler,
|
|
255
|
+
initialUserPubkey = connection.userPubkey,
|
|
256
|
+
activationHandler = {
|
|
257
|
+
bunkerStore.save(
|
|
258
|
+
connection.config.withoutPairingSecret(),
|
|
259
|
+
connection.clientKey,
|
|
260
|
+
connection.userPubkey
|
|
178
261
|
)
|
|
179
|
-
|
|
180
|
-
|
|
262
|
+
},
|
|
263
|
+
userPubkeyHandler = { userPubkey ->
|
|
264
|
+
bunkerStore.save(
|
|
265
|
+
connection.config.withoutPairingSecret(),
|
|
266
|
+
connection.clientKey,
|
|
267
|
+
userPubkey
|
|
181
268
|
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
throw IllegalStateException(
|
|
202
|
-
"No signer selected"
|
|
203
|
-
)
|
|
204
|
-
}
|
|
269
|
+
currentUserPubkey = userPubkey
|
|
270
|
+
},
|
|
271
|
+
requestedPermissions = BUNKER_PERMISSIONS,
|
|
272
|
+
clientMetadataJson =
|
|
273
|
+
JSONObject()
|
|
274
|
+
.put(
|
|
275
|
+
"name",
|
|
276
|
+
context.getString(
|
|
277
|
+
com.pakstr.app.R.string.app_name
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
.toString(),
|
|
281
|
+
connected = connection.connected
|
|
282
|
+
)
|
|
283
|
+
} catch (_: Exception) {
|
|
284
|
+
connection.clientKey.close()
|
|
285
|
+
throw SignerUnavailableException(
|
|
286
|
+
"Unable to restore bunker connection"
|
|
287
|
+
)
|
|
205
288
|
}
|
|
289
|
+
return BunkerSigner(context, client)
|
|
290
|
+
}
|
|
206
291
|
|
|
207
|
-
|
|
292
|
+
private fun setActiveTypeWithoutClosing(type: SignerType) {
|
|
293
|
+
prefs.edit {
|
|
294
|
+
putString("type", type.name)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
208
297
|
|
|
209
|
-
|
|
298
|
+
private fun closeActiveSigner() {
|
|
299
|
+
(activeSigner as? BunkerSigner)?.close()
|
|
300
|
+
activeSigner = null
|
|
301
|
+
currentUserPubkey = null
|
|
210
302
|
}
|
|
211
303
|
|
|
212
|
-
private
|
|
304
|
+
private class PendingPairing(
|
|
305
|
+
val clientKey: Nip46ClientKey,
|
|
306
|
+
val client: Nip46PairingClient
|
|
307
|
+
)
|
|
213
308
|
|
|
309
|
+
private fun isAmberInstalled(): Boolean {
|
|
214
310
|
return try {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
0
|
|
220
|
-
)
|
|
221
|
-
|
|
311
|
+
context.packageManager.getPackageInfo(
|
|
312
|
+
"com.greenart7c3.nostrsigner",
|
|
313
|
+
0
|
|
314
|
+
)
|
|
222
315
|
true
|
|
223
|
-
|
|
224
316
|
} catch (_: Exception) {
|
|
225
|
-
|
|
226
317
|
false
|
|
227
318
|
}
|
|
228
319
|
}
|
|
229
320
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
* Activity Result API.
|
|
235
|
-
*/
|
|
236
|
-
fun handleAmberResult(result: ActivityResult) {
|
|
237
|
-
AppDebugLogger.log(
|
|
238
|
-
|
|
239
|
-
context,
|
|
240
|
-
|
|
241
|
-
"AMBER",
|
|
242
|
-
|
|
243
|
-
"Activity result received: ${result.resultCode}"
|
|
244
|
-
|
|
245
|
-
)
|
|
246
|
-
(activeSigner as? AmberSigner)
|
|
247
|
-
?.onResult(result)
|
|
321
|
+
private companion object {
|
|
322
|
+
const val BUNKER_PERMISSIONS =
|
|
323
|
+
"sign_event,nip04_encrypt,nip04_decrypt," +
|
|
324
|
+
"nip44_encrypt,nip44_decrypt"
|
|
248
325
|
}
|
|
249
|
-
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
package com.pakstr.app.signer.bunker
|
|
2
|
+
|
|
3
|
+
import com.pakstr.app.crypto.Bip340Verifier
|
|
4
|
+
import java.net.URI
|
|
5
|
+
import java.net.URLDecoder
|
|
6
|
+
import java.net.URLEncoder
|
|
7
|
+
import java.nio.charset.StandardCharsets
|
|
8
|
+
|
|
9
|
+
class BunkerConfig private constructor(
|
|
10
|
+
val remoteSignerPubkey: String,
|
|
11
|
+
relayUrls: List<String>,
|
|
12
|
+
pairingSecret: String?
|
|
13
|
+
) {
|
|
14
|
+
val relayUrls: List<String> = relayUrls.toList()
|
|
15
|
+
private var pairingSecretValue: String? = pairingSecret
|
|
16
|
+
val pairingSecret: String?
|
|
17
|
+
get() = pairingSecretValue
|
|
18
|
+
|
|
19
|
+
internal fun clearPairingSecret() {
|
|
20
|
+
pairingSecretValue = null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
internal fun withoutPairingSecret(): BunkerConfig {
|
|
24
|
+
return BunkerConfig(
|
|
25
|
+
remoteSignerPubkey,
|
|
26
|
+
relayUrls,
|
|
27
|
+
null
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
internal fun encode(): String {
|
|
32
|
+
val parameters = relayUrls.map { "relay" to it }.toMutableList()
|
|
33
|
+
pairingSecret?.let { parameters += "secret" to it }
|
|
34
|
+
return "bunker://$remoteSignerPubkey?" +
|
|
35
|
+
parameters.joinToString("&") { (name, value) ->
|
|
36
|
+
"${encodeParameter(name)}=${encodeParameter(value)}"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
override fun toString(): String {
|
|
41
|
+
return "BunkerConfig(" +
|
|
42
|
+
"remoteSignerPubkey=${remoteSignerPubkey.take(12)}..., " +
|
|
43
|
+
"relayCount=${relayUrls.size}, " +
|
|
44
|
+
"hasPairingSecret=${pairingSecret != null}" +
|
|
45
|
+
")"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
companion object {
|
|
49
|
+
fun parse(value: String): BunkerConfig {
|
|
50
|
+
val uri = parseUri(value)
|
|
51
|
+
|
|
52
|
+
require(uri.scheme == "bunker") {
|
|
53
|
+
"Invalid bunker connection scheme"
|
|
54
|
+
}
|
|
55
|
+
require(uri.rawAuthority != null && uri.rawUserInfo == null) {
|
|
56
|
+
"Invalid bunker connection authority"
|
|
57
|
+
}
|
|
58
|
+
require(uri.rawPath.isNullOrEmpty()) {
|
|
59
|
+
"Invalid bunker connection path"
|
|
60
|
+
}
|
|
61
|
+
require(uri.rawFragment == null) {
|
|
62
|
+
"Invalid bunker connection fragment"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
val remoteSignerPubkey = uri.host
|
|
66
|
+
require(
|
|
67
|
+
remoteSignerPubkey != null &&
|
|
68
|
+
remoteSignerPubkey.matches(Regex("[0-9a-f]{64}")) &&
|
|
69
|
+
Bip340Verifier.isValidXOnlyPubkey(remoteSignerPubkey)
|
|
70
|
+
) {
|
|
71
|
+
"Invalid remote signer public key"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
val parameters = parseQuery(uri.rawQuery)
|
|
75
|
+
val relayUrls =
|
|
76
|
+
parameters
|
|
77
|
+
.filter { it.first == "relay" }
|
|
78
|
+
.map { validateRelay(it.second) }
|
|
79
|
+
|
|
80
|
+
require(relayUrls.isNotEmpty()) {
|
|
81
|
+
"At least one relay is required"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
val secrets =
|
|
85
|
+
parameters
|
|
86
|
+
.filter { it.first == "secret" }
|
|
87
|
+
.map { it.second }
|
|
88
|
+
|
|
89
|
+
require(secrets.size <= 1) {
|
|
90
|
+
"Multiple pairing secrets are not supported"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return BunkerConfig(
|
|
94
|
+
remoteSignerPubkey = remoteSignerPubkey,
|
|
95
|
+
relayUrls = relayUrls,
|
|
96
|
+
pairingSecret = secrets.singleOrNull()
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private fun parseUri(value: String): URI {
|
|
101
|
+
require(value.isNotBlank() && value == value.trim()) {
|
|
102
|
+
"Invalid bunker connection"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return try {
|
|
106
|
+
URI(value)
|
|
107
|
+
} catch (_: Exception) {
|
|
108
|
+
throw IllegalArgumentException(
|
|
109
|
+
"Invalid bunker connection"
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private fun parseQuery(
|
|
115
|
+
rawQuery: String?
|
|
116
|
+
): List<Pair<String, String>> {
|
|
117
|
+
if (rawQuery.isNullOrEmpty()) {
|
|
118
|
+
return emptyList()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return rawQuery
|
|
122
|
+
.split("&")
|
|
123
|
+
.map { parameter ->
|
|
124
|
+
val parts = parameter.split("=", limit = 2)
|
|
125
|
+
require(parts.size == 2) {
|
|
126
|
+
"Invalid bunker connection parameter"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
decode(parts[0]) to decode(parts[1])
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private fun validateRelay(value: String): String {
|
|
134
|
+
val relay =
|
|
135
|
+
try {
|
|
136
|
+
URI(value)
|
|
137
|
+
} catch (_: Exception) {
|
|
138
|
+
throw IllegalArgumentException(
|
|
139
|
+
"Invalid relay URL"
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
require(
|
|
144
|
+
relay.scheme == "wss" &&
|
|
145
|
+
!relay.host.isNullOrBlank() &&
|
|
146
|
+
relay.rawUserInfo == null &&
|
|
147
|
+
relay.rawFragment == null
|
|
148
|
+
) {
|
|
149
|
+
"Invalid relay URL"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return relay.toASCIIString()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private fun encodeParameter(value: String): String {
|
|
156
|
+
return URLEncoder.encode(
|
|
157
|
+
value,
|
|
158
|
+
StandardCharsets.UTF_8.name()
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private fun decode(value: String): String {
|
|
163
|
+
return try {
|
|
164
|
+
URLDecoder.decode(
|
|
165
|
+
value,
|
|
166
|
+
StandardCharsets.UTF_8.name()
|
|
167
|
+
)
|
|
168
|
+
} catch (_: Exception) {
|
|
169
|
+
throw IllegalArgumentException(
|
|
170
|
+
"Invalid bunker connection parameter"
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|