pakstr 0.20.0 → 0.21.1
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 +130 -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 +1 -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
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 {
|
|
@@ -53,6 +60,7 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
53
60
|
|
|
54
61
|
private lateinit var fab: FloatingActionButton
|
|
55
62
|
private lateinit var supportUnlockGestureLayout: SupportUnlockGestureLayout
|
|
63
|
+
private var bunkerPairingDialog: AlertDialog? = null
|
|
56
64
|
|
|
57
65
|
private val diagnosticsHandler = Handler(Looper.getMainLooper())
|
|
58
66
|
private val diagnosticsExpiryCheck = Runnable {
|
|
@@ -120,12 +128,12 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
initSigner()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
if (
|
|
132
|
+
savedInstanceState == null &&
|
|
133
|
+
AppPreferences.getBoolean("isFirstRun", true)
|
|
134
|
+
) {
|
|
135
|
+
container.post(::showSignerPicker)
|
|
136
|
+
}
|
|
129
137
|
|
|
130
138
|
ViewCompat.setOnApplyWindowInsetsListener(
|
|
131
139
|
|
|
@@ -260,10 +268,34 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
260
268
|
signerManager =
|
|
261
269
|
SignerManager(
|
|
262
270
|
this,
|
|
263
|
-
amberLauncher
|
|
271
|
+
amberLauncher,
|
|
272
|
+
bunkerAuthChallengeHandler = ::openBunkerAuthChallenge
|
|
264
273
|
)
|
|
265
274
|
}
|
|
266
275
|
|
|
276
|
+
private fun openBunkerAuthChallenge(value: String) {
|
|
277
|
+
runOnUiThread {
|
|
278
|
+
val uri = Uri.parse(value)
|
|
279
|
+
if (
|
|
280
|
+
uri.scheme != "https" ||
|
|
281
|
+
uri.host.isNullOrBlank()
|
|
282
|
+
) {
|
|
283
|
+
return@runOnUiThread
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
startActivity(
|
|
287
|
+
Intent(Intent.ACTION_VIEW, uri)
|
|
288
|
+
)
|
|
289
|
+
} catch (_: ActivityNotFoundException) {
|
|
290
|
+
Toast.makeText(
|
|
291
|
+
this,
|
|
292
|
+
R.string.signer_auth_browser_unavailable,
|
|
293
|
+
Toast.LENGTH_LONG
|
|
294
|
+
).show()
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
267
299
|
private fun initViews() {
|
|
268
300
|
|
|
269
301
|
container =
|
|
@@ -326,6 +358,13 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
326
358
|
|
|
327
359
|
}
|
|
328
360
|
|
|
361
|
+
bunkerPairingDialog?.dismiss()
|
|
362
|
+
bunkerPairingDialog = null
|
|
363
|
+
|
|
364
|
+
if (::signerManager.isInitialized) {
|
|
365
|
+
signerManager.close()
|
|
366
|
+
}
|
|
367
|
+
|
|
329
368
|
super.onDestroy()
|
|
330
369
|
|
|
331
370
|
}
|
|
@@ -369,16 +408,95 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
|
|
|
369
408
|
}
|
|
370
409
|
|
|
371
410
|
private fun showSignerPicker() {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
411
|
+
SignerPickerBottomSheet(
|
|
412
|
+
hasBunkerSession = signerManager.hasBunkerSession(),
|
|
413
|
+
onAmberSelected = {
|
|
414
|
+
signerManager.setActiveType(SignerType.AMBER)
|
|
415
|
+
handleSignerSetup()
|
|
416
|
+
},
|
|
417
|
+
onBunkerSelected = { value ->
|
|
418
|
+
try {
|
|
419
|
+
signerManager.connectBunker(value)
|
|
420
|
+
handleSignerSetup()
|
|
421
|
+
true
|
|
422
|
+
} catch (_: Exception) {
|
|
423
|
+
false
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
onSharePairing = ::shareBunkerPairing,
|
|
427
|
+
onBunkerLogout = {
|
|
428
|
+
signerManager.logoutBunker()
|
|
429
|
+
Toast.makeText(
|
|
430
|
+
this,
|
|
431
|
+
R.string.signer_picker_disconnected,
|
|
432
|
+
Toast.LENGTH_SHORT
|
|
433
|
+
).show()
|
|
434
|
+
}
|
|
435
|
+
).show(
|
|
377
436
|
supportFragmentManager,
|
|
378
437
|
"SignerPicker"
|
|
379
438
|
)
|
|
380
439
|
}
|
|
381
440
|
|
|
441
|
+
private fun shareBunkerPairing(relayUrls: List<String>) {
|
|
442
|
+
lifecycleScope.launch {
|
|
443
|
+
val pairingUri =
|
|
444
|
+
try {
|
|
445
|
+
signerManager.createBunkerPairingUri(relayUrls)
|
|
446
|
+
} catch (cancelled: CancellationException) {
|
|
447
|
+
throw cancelled
|
|
448
|
+
} catch (_: Exception) {
|
|
449
|
+
Toast.makeText(
|
|
450
|
+
this@MainActivity,
|
|
451
|
+
R.string.signer_picker_connection_failed,
|
|
452
|
+
Toast.LENGTH_LONG
|
|
453
|
+
).show()
|
|
454
|
+
return@launch
|
|
455
|
+
}
|
|
456
|
+
val pairingDialog = BunkerPairingDialog.show(
|
|
457
|
+
activity = this@MainActivity,
|
|
458
|
+
pairingUri = pairingUri,
|
|
459
|
+
onShare = ::shareBunkerPairingUri
|
|
460
|
+
)
|
|
461
|
+
bunkerPairingDialog = pairingDialog
|
|
462
|
+
pairingDialog.setOnDismissListener {
|
|
463
|
+
if (bunkerPairingDialog === pairingDialog) {
|
|
464
|
+
bunkerPairingDialog = null
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
Toast.makeText(
|
|
468
|
+
this@MainActivity,
|
|
469
|
+
R.string.signer_picker_pairing_waiting,
|
|
470
|
+
Toast.LENGTH_LONG
|
|
471
|
+
).show()
|
|
472
|
+
val connected = signerManager.finishBunkerPairing()
|
|
473
|
+
if (pairingDialog.isShowing) {
|
|
474
|
+
pairingDialog.dismiss()
|
|
475
|
+
}
|
|
476
|
+
Toast.makeText(
|
|
477
|
+
this@MainActivity,
|
|
478
|
+
if (connected) {
|
|
479
|
+
handleSignerSetup()
|
|
480
|
+
R.string.signer_picker_pairing_connected
|
|
481
|
+
} else {
|
|
482
|
+
R.string.signer_picker_pairing_failed
|
|
483
|
+
},
|
|
484
|
+
Toast.LENGTH_LONG
|
|
485
|
+
).show()
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private fun shareBunkerPairingUri(pairingUri: String) {
|
|
490
|
+
startActivity(
|
|
491
|
+
Intent.createChooser(
|
|
492
|
+
Intent(Intent.ACTION_SEND)
|
|
493
|
+
.setType("text/plain")
|
|
494
|
+
.putExtra(Intent.EXTRA_TEXT, pairingUri),
|
|
495
|
+
getString(R.string.signer_picker_pairing_share_title)
|
|
496
|
+
)
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
382
500
|
private fun showLoader(
|
|
383
501
|
show: Boolean
|
|
384
502
|
) {
|
|
@@ -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
|
+
}
|