pakstr 0.19.3 → 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/dist/cli.js +8 -0
- package/dist/commands/init.js +16 -0
- package/dist/commands/nsite.js +38 -0
- package/dist/commands/publish.js +50 -2
- package/dist/commands/run.js +5 -5
- package/dist/core/blossom.js +2 -2
- package/dist/core/nsite.js +176 -0
- package/dist/core/pakstrConfig.js +85 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -132,6 +132,7 @@ dependencies {
|
|
|
132
132
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
|
133
133
|
implementation(libs.androidx.compose.material3)
|
|
134
134
|
testImplementation(libs.junit)
|
|
135
|
+
testImplementation(libs.json)
|
|
135
136
|
androidTestImplementation(libs.androidx.junit)
|
|
136
137
|
androidTestImplementation(libs.androidx.espresso.core)
|
|
137
138
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
|
@@ -142,5 +143,8 @@ dependencies {
|
|
|
142
143
|
implementation(libs.androidx.appcompat)
|
|
143
144
|
implementation(libs.material)
|
|
144
145
|
implementation(libs.androidx.core.splashscreen)
|
|
146
|
+
implementation(libs.okhttp)
|
|
147
|
+
implementation(libs.androidx.security.crypto)
|
|
148
|
+
implementation(libs.zxing.core)
|
|
145
149
|
implementation("org.bouncycastle:bcprov-jdk18on:1.78.1")
|
|
146
150
|
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
package com.pakstr.app.signer
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import androidx.activity.result.ActivityResultLauncher
|
|
6
|
+
import androidx.activity.result.contract.ActivityResultContract
|
|
7
|
+
import androidx.core.app.ActivityOptionsCompat
|
|
8
|
+
import androidx.test.core.app.ApplicationProvider
|
|
9
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
10
|
+
import com.pakstr.app.crypto.Nip46ClientKey
|
|
11
|
+
import com.pakstr.app.signer.bunker.BunkerConfig
|
|
12
|
+
import com.pakstr.app.signer.bunker.BunkerConnectionStore
|
|
13
|
+
import com.pakstr.app.signer.bunker.Nip46Client
|
|
14
|
+
import com.pakstr.app.signer.interfaces.NostrSigner
|
|
15
|
+
import kotlinx.coroutines.runBlocking
|
|
16
|
+
import org.junit.Assert.assertEquals
|
|
17
|
+
import org.junit.Assert.assertFalse
|
|
18
|
+
import org.junit.Assert.assertNull
|
|
19
|
+
import org.junit.Assert.assertTrue
|
|
20
|
+
import org.junit.Assert.fail
|
|
21
|
+
import org.junit.Test
|
|
22
|
+
import org.junit.runner.RunWith
|
|
23
|
+
|
|
24
|
+
@RunWith(AndroidJUnit4::class)
|
|
25
|
+
class SignerManagerTest {
|
|
26
|
+
|
|
27
|
+
@Test
|
|
28
|
+
fun signer_change_clears_cached_user_pubkey() = runBlocking {
|
|
29
|
+
val context =
|
|
30
|
+
ApplicationProvider.getApplicationContext<Context>()
|
|
31
|
+
val manager =
|
|
32
|
+
SignerManager(
|
|
33
|
+
context,
|
|
34
|
+
NoOpLauncher()
|
|
35
|
+
)
|
|
36
|
+
val amberSigner = CountingSigner("amber-pubkey")
|
|
37
|
+
val bunkerSigner = CountingSigner("bunker-pubkey")
|
|
38
|
+
|
|
39
|
+
manager.setActiveType(SignerType.AMBER)
|
|
40
|
+
setActiveSigner(manager, amberSigner)
|
|
41
|
+
|
|
42
|
+
assertEquals("amber-pubkey", manager.getCurrentUserPubkey())
|
|
43
|
+
assertEquals("amber-pubkey", manager.getCurrentUserPubkey())
|
|
44
|
+
assertEquals(1, amberSigner.publicKeyRequests)
|
|
45
|
+
|
|
46
|
+
manager.setActiveType(SignerType.BUNKER)
|
|
47
|
+
setActiveSigner(manager, bunkerSigner)
|
|
48
|
+
|
|
49
|
+
assertEquals("bunker-pubkey", manager.getCurrentUserPubkey())
|
|
50
|
+
assertEquals(1, bunkerSigner.publicKeyRequests)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Test
|
|
54
|
+
fun invalid_pairing_relay_does_not_leave_pending_pairing() = runBlocking {
|
|
55
|
+
val context =
|
|
56
|
+
ApplicationProvider.getApplicationContext<Context>()
|
|
57
|
+
val manager =
|
|
58
|
+
SignerManager(
|
|
59
|
+
context,
|
|
60
|
+
NoOpLauncher()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
manager.createBunkerPairingUri(listOf("wss://"))
|
|
65
|
+
fail("Expected invalid relay URL")
|
|
66
|
+
} catch (_: IllegalArgumentException) {
|
|
67
|
+
assertNull(getPendingPairing(manager))
|
|
68
|
+
} finally {
|
|
69
|
+
manager.close()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
fun established_bunker_session_survives_manager_recreation_and_logout() {
|
|
75
|
+
val context =
|
|
76
|
+
ApplicationProvider.getApplicationContext<Context>()
|
|
77
|
+
val store = BunkerConnectionStore(context)
|
|
78
|
+
val clientKey = Nip46ClientKey.generate()
|
|
79
|
+
val config =
|
|
80
|
+
BunkerConfig.parse(
|
|
81
|
+
"bunker://$VALID_PUBKEY?relay=wss://relay.example"
|
|
82
|
+
)
|
|
83
|
+
val first = SignerManager(context, NoOpLauncher())
|
|
84
|
+
val restored = SignerManager(context, NoOpLauncher())
|
|
85
|
+
store.clear()
|
|
86
|
+
try {
|
|
87
|
+
store.save(config, clientKey, VALID_PUBKEY)
|
|
88
|
+
first.setActiveType(SignerType.BUNKER)
|
|
89
|
+
|
|
90
|
+
assertTrue(restored.hasBunkerSession())
|
|
91
|
+
assertEquals(SignerType.BUNKER, restored.getActiveType())
|
|
92
|
+
|
|
93
|
+
restored.logoutBunker()
|
|
94
|
+
|
|
95
|
+
assertFalse(restored.hasBunkerSession())
|
|
96
|
+
assertEquals(SignerType.AMBER, restored.getActiveType())
|
|
97
|
+
} finally {
|
|
98
|
+
first.close()
|
|
99
|
+
restored.close()
|
|
100
|
+
clientKey.close()
|
|
101
|
+
store.clear()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
fun switching_from_bunker_disconnects_and_closes_client() {
|
|
107
|
+
val context =
|
|
108
|
+
ApplicationProvider.getApplicationContext<Context>()
|
|
109
|
+
val manager = SignerManager(context, NoOpLauncher())
|
|
110
|
+
val client = RecordingNip46Client()
|
|
111
|
+
setActiveSigner(manager, BunkerSigner(context, client))
|
|
112
|
+
|
|
113
|
+
manager.logoutBunker()
|
|
114
|
+
|
|
115
|
+
assertTrue(client.disconnected)
|
|
116
|
+
assertTrue(client.closed)
|
|
117
|
+
assertEquals(SignerType.AMBER, manager.getActiveType())
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Test
|
|
121
|
+
fun pending_bunker_secret_is_removed_from_established_storage() {
|
|
122
|
+
val context =
|
|
123
|
+
ApplicationProvider.getApplicationContext<Context>()
|
|
124
|
+
val store = BunkerConnectionStore(context)
|
|
125
|
+
val clientKey = Nip46ClientKey.generate()
|
|
126
|
+
val config =
|
|
127
|
+
BunkerConfig.parse(
|
|
128
|
+
"bunker://$VALID_PUBKEY" +
|
|
129
|
+
"?relay=wss://relay.example" +
|
|
130
|
+
"&secret=$PAIRING_SECRET"
|
|
131
|
+
)
|
|
132
|
+
store.clear()
|
|
133
|
+
try {
|
|
134
|
+
store.savePending(config, clientKey)
|
|
135
|
+
val pending = requireNotNull(store.restore())
|
|
136
|
+
assertEquals(PAIRING_SECRET, pending.config.pairingSecret)
|
|
137
|
+
assertFalse(pending.connected)
|
|
138
|
+
pending.clientKey.close()
|
|
139
|
+
|
|
140
|
+
store.save(config, clientKey, VALID_PUBKEY)
|
|
141
|
+
val established = requireNotNull(store.restore())
|
|
142
|
+
assertNull(established.config.pairingSecret)
|
|
143
|
+
assertEquals(VALID_PUBKEY, established.userPubkey)
|
|
144
|
+
assertTrue(established.connected)
|
|
145
|
+
established.clientKey.close()
|
|
146
|
+
} finally {
|
|
147
|
+
clientKey.close()
|
|
148
|
+
store.clear()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private fun getPendingPairing(manager: SignerManager): Any? {
|
|
153
|
+
return SignerManager::class.java
|
|
154
|
+
.getDeclaredField("pendingPairing")
|
|
155
|
+
.apply {
|
|
156
|
+
isAccessible = true
|
|
157
|
+
}
|
|
158
|
+
.get(manager)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private fun setActiveSigner(
|
|
162
|
+
manager: SignerManager,
|
|
163
|
+
signer: NostrSigner
|
|
164
|
+
) {
|
|
165
|
+
SignerManager::class.java
|
|
166
|
+
.getDeclaredField("activeSigner")
|
|
167
|
+
.apply {
|
|
168
|
+
isAccessible = true
|
|
169
|
+
}
|
|
170
|
+
.set(manager, signer)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private class RecordingNip46Client : Nip46Client {
|
|
174
|
+
var disconnected = false
|
|
175
|
+
private set
|
|
176
|
+
var closed = false
|
|
177
|
+
private set
|
|
178
|
+
|
|
179
|
+
override suspend fun getPublicKey(): String = VALID_PUBKEY
|
|
180
|
+
|
|
181
|
+
override suspend fun signEvent(eventJson: String): String = eventJson
|
|
182
|
+
|
|
183
|
+
override suspend fun nip04Encrypt(
|
|
184
|
+
pubkey: String,
|
|
185
|
+
plaintext: String
|
|
186
|
+
): String = plaintext
|
|
187
|
+
|
|
188
|
+
override suspend fun nip04Decrypt(
|
|
189
|
+
pubkey: String,
|
|
190
|
+
ciphertext: String
|
|
191
|
+
): String = ciphertext
|
|
192
|
+
|
|
193
|
+
override suspend fun nip44Encrypt(
|
|
194
|
+
pubkey: String,
|
|
195
|
+
plaintext: String
|
|
196
|
+
): String = plaintext
|
|
197
|
+
|
|
198
|
+
override suspend fun nip44Decrypt(
|
|
199
|
+
pubkey: String,
|
|
200
|
+
ciphertext: String
|
|
201
|
+
): String = ciphertext
|
|
202
|
+
|
|
203
|
+
override fun disconnect() {
|
|
204
|
+
disconnected = true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
override fun close() {
|
|
208
|
+
closed = true
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private class CountingSigner(
|
|
213
|
+
private val pubkey: String
|
|
214
|
+
) : NostrSigner {
|
|
215
|
+
var publicKeyRequests = 0
|
|
216
|
+
private set
|
|
217
|
+
|
|
218
|
+
override suspend fun getPublicKey(): String {
|
|
219
|
+
publicKeyRequests++
|
|
220
|
+
return pubkey
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
override suspend fun signEvent(eventJson: String): String {
|
|
224
|
+
throw UnsupportedOperationException()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
override suspend fun nip04Encrypt(
|
|
228
|
+
pubkey: String,
|
|
229
|
+
plaintext: String
|
|
230
|
+
): String {
|
|
231
|
+
throw UnsupportedOperationException()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
override suspend fun nip04Decrypt(
|
|
235
|
+
pubkey: String,
|
|
236
|
+
ciphertext: String
|
|
237
|
+
): String {
|
|
238
|
+
throw UnsupportedOperationException()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
override suspend fun nip44Encrypt(
|
|
242
|
+
pubkey: String,
|
|
243
|
+
plaintext: String
|
|
244
|
+
): String {
|
|
245
|
+
throw UnsupportedOperationException()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
override suspend fun nip44Decrypt(
|
|
249
|
+
pubkey: String,
|
|
250
|
+
ciphertext: String
|
|
251
|
+
): String {
|
|
252
|
+
throw UnsupportedOperationException()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private companion object {
|
|
257
|
+
const val VALID_PUBKEY =
|
|
258
|
+
"cb227f60cb2a6d982ecbfc51b9199d340afc00b1ef81b67d8e3778e097e70d2c"
|
|
259
|
+
const val PAIRING_SECRET = "pending-pairing-secret"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private class NoOpLauncher : ActivityResultLauncher<Intent>() {
|
|
263
|
+
override fun launch(
|
|
264
|
+
input: Intent,
|
|
265
|
+
options: ActivityOptionsCompat?
|
|
266
|
+
) = Unit
|
|
267
|
+
|
|
268
|
+
override fun unregister() = Unit
|
|
269
|
+
|
|
270
|
+
override val contract: ActivityResultContract<Intent, *>
|
|
271
|
+
get() = throw UnsupportedOperationException()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -63,6 +63,23 @@ internal fun shouldForwardProxyRequestHeader(
|
|
|
63
63
|
"x-forwarded-proto"
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
+
internal fun currentRawQueryString(
|
|
67
|
+
session: NanoHTTPD.IHTTPSession?
|
|
68
|
+
): String? {
|
|
69
|
+
val query = session?.queryParameterString
|
|
70
|
+
// NanoHTTPD resets parameters per request but can retain this raw value on keep-alive.
|
|
71
|
+
if (query.isNullOrEmpty() || session.parameters.isEmpty()) return null
|
|
72
|
+
return query
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
internal fun localRequestUrl(
|
|
76
|
+
uri: String,
|
|
77
|
+
query: String?
|
|
78
|
+
): URL = URL(
|
|
79
|
+
"http://127.0.0.1:${Config.PORT}$uri" +
|
|
80
|
+
if (query.isNullOrEmpty()) "" else "?$query"
|
|
81
|
+
)
|
|
82
|
+
|
|
66
83
|
internal data class ProxyHttpResponse(
|
|
67
84
|
val code: Int,
|
|
68
85
|
val contentType: String?,
|
|
@@ -394,10 +411,9 @@ class LocalServer(
|
|
|
394
411
|
val url = URL(finalUrl)
|
|
395
412
|
val method = session?.method?.name ?: "GET"
|
|
396
413
|
val requestHeaders = session?.headers ?: emptyMap()
|
|
397
|
-
val
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (query.isNullOrEmpty()) "" else "?$query"
|
|
414
|
+
val localRequestUrl = localRequestUrl(
|
|
415
|
+
uri,
|
|
416
|
+
currentRawQueryString(session)
|
|
401
417
|
)
|
|
402
418
|
val preparedHeaders = prepareProxyRequestHeaders(
|
|
403
419
|
headers = requestHeaders,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
package com.pakstr.app
|
|
2
2
|
|
|
3
3
|
import android.Manifest
|
|
4
|
+
import android.content.ActivityNotFoundException
|
|
4
5
|
import android.content.Intent
|
|
6
|
+
import android.net.Uri
|
|
5
7
|
import android.os.Bundle
|
|
6
8
|
import android.os.Handler
|
|
7
9
|
import android.os.Looper
|
|
@@ -15,8 +17,10 @@ import androidx.activity.OnBackPressedCallback
|
|
|
15
17
|
import androidx.activity.enableEdgeToEdge
|
|
16
18
|
import androidx.activity.result.ActivityResultLauncher
|
|
17
19
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
20
|
+
import androidx.appcompat.app.AlertDialog
|
|
18
21
|
import androidx.appcompat.app.AppCompatActivity
|
|
19
22
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
23
|
+
import androidx.lifecycle.lifecycleScope
|
|
20
24
|
import androidx.core.view.ViewCompat
|
|
21
25
|
import androidx.core.view.WindowInsetsCompat
|
|
22
26
|
import androidx.core.view.updatePadding
|
|
@@ -37,7 +41,10 @@ import com.pakstr.app.permissions.interfaces.PermissionHandler
|
|
|
37
41
|
|
|
38
42
|
import com.pakstr.app.signer.SignerManager
|
|
39
43
|
import com.pakstr.app.signer.SignerType
|
|
44
|
+
import kotlinx.coroutines.CancellationException
|
|
45
|
+
import kotlinx.coroutines.launch
|
|
40
46
|
import com.pakstr.app.utils.AppPreferences
|
|
47
|
+
import com.pakstr.app.utils.BunkerPairingDialog
|
|
41
48
|
import com.pakstr.app.utils.SignerPickerBottomSheet
|
|
42
49
|
|
|
43
50
|
class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
@@ -52,7 +59,9 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
52
59
|
ActivityResultLauncher<Intent>
|
|
53
60
|
|
|
54
61
|
private lateinit var fab: FloatingActionButton
|
|
62
|
+
private lateinit var signerSettingsFab: FloatingActionButton
|
|
55
63
|
private lateinit var supportUnlockGestureLayout: SupportUnlockGestureLayout
|
|
64
|
+
private var bunkerPairingDialog: AlertDialog? = null
|
|
56
65
|
|
|
57
66
|
private val diagnosticsHandler = Handler(Looper.getMainLooper())
|
|
58
67
|
private val diagnosticsExpiryCheck = Runnable {
|
|
@@ -120,12 +129,12 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
initSigner()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
if (
|
|
133
|
+
savedInstanceState == null &&
|
|
134
|
+
AppPreferences.getBoolean("isFirstRun", true)
|
|
135
|
+
) {
|
|
136
|
+
container.post(::showSignerPicker)
|
|
137
|
+
}
|
|
129
138
|
|
|
130
139
|
ViewCompat.setOnApplyWindowInsetsListener(
|
|
131
140
|
|
|
@@ -260,10 +269,34 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
260
269
|
signerManager =
|
|
261
270
|
SignerManager(
|
|
262
271
|
this,
|
|
263
|
-
amberLauncher
|
|
272
|
+
amberLauncher,
|
|
273
|
+
bunkerAuthChallengeHandler = ::openBunkerAuthChallenge
|
|
264
274
|
)
|
|
265
275
|
}
|
|
266
276
|
|
|
277
|
+
private fun openBunkerAuthChallenge(value: String) {
|
|
278
|
+
runOnUiThread {
|
|
279
|
+
val uri = Uri.parse(value)
|
|
280
|
+
if (
|
|
281
|
+
uri.scheme != "https" ||
|
|
282
|
+
uri.host.isNullOrBlank()
|
|
283
|
+
) {
|
|
284
|
+
return@runOnUiThread
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
startActivity(
|
|
288
|
+
Intent(Intent.ACTION_VIEW, uri)
|
|
289
|
+
)
|
|
290
|
+
} catch (_: ActivityNotFoundException) {
|
|
291
|
+
Toast.makeText(
|
|
292
|
+
this,
|
|
293
|
+
R.string.signer_auth_browser_unavailable,
|
|
294
|
+
Toast.LENGTH_LONG
|
|
295
|
+
).show()
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
267
300
|
private fun initViews() {
|
|
268
301
|
|
|
269
302
|
container =
|
|
@@ -286,6 +319,11 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
286
319
|
|
|
287
320
|
R.id.developerToolsFab)
|
|
288
321
|
|
|
322
|
+
signerSettingsFab = findViewById(R.id.signerSettingsFab)
|
|
323
|
+
signerSettingsFab.setOnClickListener {
|
|
324
|
+
showSignerPicker()
|
|
325
|
+
}
|
|
326
|
+
|
|
289
327
|
}
|
|
290
328
|
|
|
291
329
|
private fun handleSignerSetup() {
|
|
@@ -326,6 +364,13 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
326
364
|
|
|
327
365
|
}
|
|
328
366
|
|
|
367
|
+
bunkerPairingDialog?.dismiss()
|
|
368
|
+
bunkerPairingDialog = null
|
|
369
|
+
|
|
370
|
+
if (::signerManager.isInitialized) {
|
|
371
|
+
signerManager.close()
|
|
372
|
+
}
|
|
373
|
+
|
|
329
374
|
super.onDestroy()
|
|
330
375
|
|
|
331
376
|
}
|
|
@@ -369,16 +414,95 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
369
414
|
}
|
|
370
415
|
|
|
371
416
|
private fun showSignerPicker() {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
417
|
+
SignerPickerBottomSheet(
|
|
418
|
+
hasBunkerSession = signerManager.hasBunkerSession(),
|
|
419
|
+
onAmberSelected = {
|
|
420
|
+
signerManager.setActiveType(SignerType.AMBER)
|
|
421
|
+
handleSignerSetup()
|
|
422
|
+
},
|
|
423
|
+
onBunkerSelected = { value ->
|
|
424
|
+
try {
|
|
425
|
+
signerManager.connectBunker(value)
|
|
426
|
+
handleSignerSetup()
|
|
427
|
+
true
|
|
428
|
+
} catch (_: Exception) {
|
|
429
|
+
false
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
onSharePairing = ::shareBunkerPairing,
|
|
433
|
+
onBunkerLogout = {
|
|
434
|
+
signerManager.logoutBunker()
|
|
435
|
+
Toast.makeText(
|
|
436
|
+
this,
|
|
437
|
+
R.string.signer_picker_disconnected,
|
|
438
|
+
Toast.LENGTH_SHORT
|
|
439
|
+
).show()
|
|
440
|
+
}
|
|
441
|
+
).show(
|
|
377
442
|
supportFragmentManager,
|
|
378
443
|
"SignerPicker"
|
|
379
444
|
)
|
|
380
445
|
}
|
|
381
446
|
|
|
447
|
+
private fun shareBunkerPairing(relayUrls: List<String>) {
|
|
448
|
+
lifecycleScope.launch {
|
|
449
|
+
val pairingUri =
|
|
450
|
+
try {
|
|
451
|
+
signerManager.createBunkerPairingUri(relayUrls)
|
|
452
|
+
} catch (cancelled: CancellationException) {
|
|
453
|
+
throw cancelled
|
|
454
|
+
} catch (_: Exception) {
|
|
455
|
+
Toast.makeText(
|
|
456
|
+
this@MainActivity,
|
|
457
|
+
R.string.signer_picker_connection_failed,
|
|
458
|
+
Toast.LENGTH_LONG
|
|
459
|
+
).show()
|
|
460
|
+
return@launch
|
|
461
|
+
}
|
|
462
|
+
val pairingDialog = BunkerPairingDialog.show(
|
|
463
|
+
activity = this@MainActivity,
|
|
464
|
+
pairingUri = pairingUri,
|
|
465
|
+
onShare = ::shareBunkerPairingUri
|
|
466
|
+
)
|
|
467
|
+
bunkerPairingDialog = pairingDialog
|
|
468
|
+
pairingDialog.setOnDismissListener {
|
|
469
|
+
if (bunkerPairingDialog === pairingDialog) {
|
|
470
|
+
bunkerPairingDialog = null
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
Toast.makeText(
|
|
474
|
+
this@MainActivity,
|
|
475
|
+
R.string.signer_picker_pairing_waiting,
|
|
476
|
+
Toast.LENGTH_LONG
|
|
477
|
+
).show()
|
|
478
|
+
val connected = signerManager.finishBunkerPairing()
|
|
479
|
+
if (pairingDialog.isShowing) {
|
|
480
|
+
pairingDialog.dismiss()
|
|
481
|
+
}
|
|
482
|
+
Toast.makeText(
|
|
483
|
+
this@MainActivity,
|
|
484
|
+
if (connected) {
|
|
485
|
+
handleSignerSetup()
|
|
486
|
+
R.string.signer_picker_pairing_connected
|
|
487
|
+
} else {
|
|
488
|
+
R.string.signer_picker_pairing_failed
|
|
489
|
+
},
|
|
490
|
+
Toast.LENGTH_LONG
|
|
491
|
+
).show()
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private fun shareBunkerPairingUri(pairingUri: String) {
|
|
496
|
+
startActivity(
|
|
497
|
+
Intent.createChooser(
|
|
498
|
+
Intent(Intent.ACTION_SEND)
|
|
499
|
+
.setType("text/plain")
|
|
500
|
+
.putExtra(Intent.EXTRA_TEXT, pairingUri),
|
|
501
|
+
getString(R.string.signer_picker_pairing_share_title)
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
382
506
|
private fun showLoader(
|
|
383
507
|
show: Boolean
|
|
384
508
|
) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
package com.pakstr.app.crypto
|
|
2
|
+
|
|
3
|
+
import java.security.MessageDigest
|
|
4
|
+
|
|
5
|
+
internal object Bip340Hash {
|
|
6
|
+
fun sha256(data: ByteArray): ByteArray {
|
|
7
|
+
return MessageDigest
|
|
8
|
+
.getInstance("SHA-256")
|
|
9
|
+
.digest(data)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fun taggedHash(
|
|
13
|
+
tag: String,
|
|
14
|
+
vararg parts: ByteArray
|
|
15
|
+
): ByteArray {
|
|
16
|
+
val tagHash =
|
|
17
|
+
sha256(
|
|
18
|
+
tag.toByteArray(Charsets.UTF_8)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return try {
|
|
22
|
+
MessageDigest
|
|
23
|
+
.getInstance("SHA-256")
|
|
24
|
+
.apply {
|
|
25
|
+
update(tagHash)
|
|
26
|
+
update(tagHash)
|
|
27
|
+
parts.forEach(::update)
|
|
28
|
+
}
|
|
29
|
+
.digest()
|
|
30
|
+
} finally {
|
|
31
|
+
tagHash.fill(0)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fun hexToBytes(value: String): ByteArray {
|
|
36
|
+
require(value.length % 2 == 0) {
|
|
37
|
+
"Hex value must have an even length"
|
|
38
|
+
}
|
|
39
|
+
require(value.matches(Regex("[0-9a-fA-F]*"))) {
|
|
40
|
+
"Invalid hexadecimal value"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ByteArray(value.length / 2) { index ->
|
|
44
|
+
value.substring(
|
|
45
|
+
index * 2,
|
|
46
|
+
index * 2 + 2
|
|
47
|
+
).toInt(16).toByte()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fun bytesToHex(value: ByteArray): String {
|
|
52
|
+
return value.joinToString("") { byte ->
|
|
53
|
+
"%02x".format(byte.toInt() and 0xff)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|