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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/android-template/app/build.gradle.kts +4 -0
  3. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +273 -0
  4. package/android-template/app/src/main/assets/www/index.html +0 -6
  5. package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +20 -4
  6. package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +136 -12
  7. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Hash.kt +56 -0
  8. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Signer.kt +104 -0
  9. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Verifier.kt +20 -170
  10. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip44Crypto.kt +513 -0
  11. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip46ClientKey.kt +140 -0
  12. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +3 -28
  13. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventSerializer.kt +74 -0
  14. package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +100 -0
  15. package/android-template/app/src/main/java/com/pakstr/app/signer/BunkerSigner.kt +32 -7
  16. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +36 -9
  17. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +240 -163
  18. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConfig.kt +175 -0
  19. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConnectionStore.kt +144 -0
  20. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Client.kt +79 -0
  21. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Models.kt +319 -0
  22. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingClient.kt +159 -0
  23. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingQrCode.kt +35 -0
  24. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingUri.kt +104 -0
  25. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Protocol.kt +695 -0
  26. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46RelayTransport.kt +489 -0
  27. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/RelayNip46Client.kt +403 -0
  28. package/android-template/app/src/main/java/com/pakstr/app/utils/BunkerPairingDialog.kt +82 -0
  29. package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +78 -17
  30. package/android-template/app/src/main/res/layout/activity_layout.xml +11 -0
  31. package/android-template/app/src/main/res/layout/dialog_bunker_pairing.xml +75 -0
  32. package/android-template/app/src/main/res/layout/dialog_picker.xml +60 -31
  33. package/android-template/app/src/main/res/values/strings.xml +27 -0
  34. package/android-template/app/src/test/java/com/pakstr/app/LocalServerQueryKeepAliveTest.kt +118 -0
  35. package/android-template/app/src/test/java/com/pakstr/app/crypto/Bip340SignerTest.kt +133 -0
  36. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip44CryptoTest.kt +199 -0
  37. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyProtocolTest.kt +82 -0
  38. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyTest.kt +52 -0
  39. package/android-template/app/src/test/java/com/pakstr/app/crypto/NostrEventSerializerTest.kt +42 -0
  40. package/android-template/app/src/test/java/com/pakstr/app/signer/NostrBridgeErrorTest.kt +32 -0
  41. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/BunkerConfigTest.kt +120 -0
  42. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ModelsTest.kt +111 -0
  43. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingClientTest.kt +271 -0
  44. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingQrCodeTest.kt +54 -0
  45. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingUriTest.kt +80 -0
  46. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ProtocolTest.kt +578 -0
  47. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46RelayTransportTest.kt +508 -0
  48. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/RelayNip46ClientTest.kt +678 -0
  49. package/android-template/gradle/libs.versions.toml +8 -0
  50. package/dist/cli.js +8 -0
  51. package/dist/commands/init.js +16 -0
  52. package/dist/commands/nsite.js +38 -0
  53. package/dist/commands/publish.js +50 -2
  54. package/dist/commands/run.js +5 -5
  55. package/dist/core/blossom.js +2 -2
  56. package/dist/core/nsite.js +176 -0
  57. package/dist/core/pakstrConfig.js +85 -6
  58. package/package.json +1 -1
package/README.md CHANGED
@@ -12,7 +12,7 @@ The same `nsec` serves two core purposes:
12
12
 
13
13
  ## Status
14
14
 
15
- Pakstr is currently in **early development (0.x)**.
15
+ Pakstr is currently in **early development (0.x)**.
16
16
  The CLI API and configuration format may change before 1.0.0.
17
17
 
18
18
  ---
@@ -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
+ }
@@ -140,14 +140,8 @@ Waiting...
140
140
  }
141
141
 
142
142
 
143
- const pubkey =
144
- await window.nostr.getPublicKey();
145
-
146
-
147
143
  const event = {
148
144
 
149
- pubkey,
150
-
151
145
  kind: 1,
152
146
 
153
147
  content:
@@ -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 query = session?.queryParameterString
398
- val localRequestUrl = URL(
399
- "http://127.0.0.1:${Config.PORT}$uri" +
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
- signerManager.setActiveType(
125
-
126
- SignerType.AMBER
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
- SignerPickerBottomSheet { selectedType ->
374
- signerManager.setActiveType(selectedType)
375
- handleSignerSetup()
376
- }.show(
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
+ }