pakstr 0.27.0 → 0.29.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 (20) hide show
  1. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +266 -0
  2. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/local/LocalAccountStoreTest.kt +272 -0
  3. package/android-template/app/src/androidTest/java/com/pakstr/app/utils/SignerPickerBottomSheetTest.kt +165 -0
  4. package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +211 -11
  5. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip19Codec.kt +181 -0
  6. package/android-template/app/src/main/java/com/pakstr/app/signer/LocalSigner.kt +100 -0
  7. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +6 -6
  8. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +135 -3
  9. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerType.kt +1 -0
  10. package/android-template/app/src/main/java/com/pakstr/app/signer/local/LocalAccountImportCandidate.kt +42 -0
  11. package/android-template/app/src/main/java/com/pakstr/app/signer/local/LocalAccountStore.kt +260 -0
  12. package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +98 -12
  13. package/android-template/app/src/main/res/layout/activity_developer_tools.xml +15 -0
  14. package/android-template/app/src/main/res/layout/dialog_picker.xml +80 -0
  15. package/android-template/app/src/main/res/values/strings.xml +16 -1
  16. package/android-template/app/src/main/res/xml/backup_rules.xml +4 -11
  17. package/android-template/app/src/main/res/xml/data_extraction_rules.xml +7 -13
  18. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip19CodecTest.kt +178 -0
  19. package/android-template/app/src/test/java/com/pakstr/app/signer/LocalSignerTest.kt +110 -0
  20. package/package.json +3 -3
@@ -0,0 +1,165 @@
1
+ package com.pakstr.app.utils
2
+
3
+ import android.content.Context
4
+ import androidx.test.core.app.ActivityScenario
5
+ import androidx.test.core.app.ApplicationProvider
6
+ import androidx.test.espresso.Espresso.onView
7
+ import androidx.test.espresso.action.ViewActions.click
8
+ import androidx.test.espresso.action.ViewActions.replaceText
9
+ import androidx.test.espresso.assertion.ViewAssertions.matches
10
+ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
11
+ import androidx.test.espresso.matcher.ViewMatchers.isChecked
12
+ import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
13
+ import androidx.test.espresso.matcher.ViewMatchers.withId
14
+ import androidx.test.espresso.matcher.ViewMatchers.Visibility.GONE
15
+ import androidx.test.ext.junit.runners.AndroidJUnit4
16
+ import com.pakstr.app.MainActivity
17
+ import com.pakstr.app.R
18
+ import com.pakstr.app.signer.SignerManager
19
+ import com.pakstr.app.signer.SignerType
20
+ import com.pakstr.app.signer.local.LocalAccountStore
21
+ import org.junit.After
22
+ import org.junit.Assert.assertEquals
23
+ import org.junit.Assert.assertNull
24
+ import org.junit.Assert.assertTrue
25
+ import org.junit.Before
26
+ import org.junit.Test
27
+ import org.junit.runner.RunWith
28
+ import java.security.KeyStore
29
+
30
+ @RunWith(AndroidJUnit4::class)
31
+ class SignerPickerBottomSheetTest {
32
+ private lateinit var context: Context
33
+
34
+ @Before
35
+ fun setup() {
36
+ context = ApplicationProvider.getApplicationContext()
37
+ AppPreferences.init(context)
38
+ AppPreferences.saveBoolean("isFirstRun", false)
39
+ clearLocalAccount()
40
+ }
41
+
42
+ @After
43
+ fun teardown() {
44
+ clearLocalAccount()
45
+ context.getSharedPreferences("signer", Context.MODE_PRIVATE)
46
+ .edit()
47
+ .clear()
48
+ .commit()
49
+ AppPreferences.saveBoolean("isFirstRun", true)
50
+ }
51
+
52
+ @Test
53
+ fun cold_start_does_not_show_signer_picker() {
54
+ AppPreferences.saveBoolean("isFirstRun", true)
55
+
56
+ ActivityScenario.launch(MainActivity::class.java).use { scenario ->
57
+ scenario.onActivity { activity ->
58
+ assertNull(
59
+ activity.supportFragmentManager
60
+ .findFragmentByTag("SignerPicker")
61
+ )
62
+ }
63
+ }
64
+ }
65
+
66
+ @Test
67
+ fun completed_legacy_setup_migrates_signer_configuration() {
68
+ AppPreferences.saveBoolean("isFirstRun", false)
69
+
70
+ ActivityScenario.launch(MainActivity::class.java).use { scenario ->
71
+ scenario.onActivity { activity ->
72
+ assertTrue(signerManager(activity).isSignerConfigured())
73
+ }
74
+ }
75
+ }
76
+
77
+ @Test
78
+ fun local_option_creates_and_selects_local_signer() {
79
+ ActivityScenario.launch(MainActivity::class.java).use { scenario ->
80
+ showPicker(scenario)
81
+
82
+ onView(withId(R.id.radio_option_three))
83
+ .check(matches(isDisplayed()))
84
+ .perform(click())
85
+ onView(withId(R.id.radio_local_create))
86
+ .check(matches(isChecked()))
87
+ onView(withId(R.id.bunker_uri_container))
88
+ .check(matches(withEffectiveVisibility(GONE)))
89
+ onView(withId(R.id.button_share_pairing))
90
+ .check(matches(withEffectiveVisibility(GONE)))
91
+ onView(withId(R.id.button_confirm)).perform(click())
92
+
93
+ waitForLocalSelection(scenario)
94
+ }
95
+ }
96
+
97
+ @Test
98
+ fun local_import_can_be_cancelled_before_persistence() {
99
+ ActivityScenario.launch(MainActivity::class.java).use { scenario ->
100
+ showPicker(scenario)
101
+
102
+ onView(withId(R.id.radio_option_three)).perform(click())
103
+ onView(withId(R.id.radio_local_import)).perform(click())
104
+ onView(withId(R.id.local_nsec_container))
105
+ .check(matches(isDisplayed()))
106
+ onView(withId(R.id.local_nsec)).perform(replaceText(NSEC_ONE))
107
+ onView(withId(R.id.button_confirm)).perform(click())
108
+ onView(withId(android.R.id.button2)).perform(click())
109
+
110
+ scenario.onActivity { activity ->
111
+ assertTrue(!signerManager(activity).hasLocalAccount())
112
+ }
113
+ }
114
+ }
115
+
116
+ private fun showPicker(
117
+ scenario: ActivityScenario<MainActivity>
118
+ ) {
119
+ scenario.onActivity { activity ->
120
+ MainActivity::class.java
121
+ .getDeclaredMethod("showSignerPicker")
122
+ .apply { isAccessible = true }
123
+ .invoke(activity)
124
+ }
125
+ }
126
+
127
+ private fun waitForLocalSelection(
128
+ scenario: ActivityScenario<MainActivity>
129
+ ) {
130
+ repeat(50) {
131
+ var activeType: SignerType? = null
132
+ scenario.onActivity { activity ->
133
+ activeType = signerManager(activity).getActiveType()
134
+ }
135
+ if (activeType == SignerType.LOCAL) {
136
+ assertEquals(SignerType.LOCAL, activeType)
137
+ return
138
+ }
139
+ Thread.sleep(100)
140
+ }
141
+ throw AssertionError("LOCAL signer was not selected")
142
+ }
143
+
144
+ private fun signerManager(activity: MainActivity): SignerManager {
145
+ return MainActivity::class.java
146
+ .getDeclaredField("signerManager")
147
+ .apply { isAccessible = true }
148
+ .get(activity) as SignerManager
149
+ }
150
+
151
+ private fun clearLocalAccount() {
152
+ context.getSharedPreferences(
153
+ LocalAccountStore.PREFERENCES_NAME,
154
+ Context.MODE_PRIVATE
155
+ ).edit().clear().commit()
156
+ KeyStore.getInstance("AndroidKeyStore")
157
+ .apply { load(null) }
158
+ .deleteEntry(LocalAccountStore.KEY_ALIAS)
159
+ }
160
+
161
+ private companion object {
162
+ const val NSEC_ONE =
163
+ "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsmhltgl"
164
+ }
165
+ }
@@ -9,6 +9,7 @@ import android.os.Handler
9
9
  import android.os.Looper
10
10
 
11
11
  import android.view.View
12
+ import android.view.WindowManager
12
13
  import android.webkit.PermissionRequest
13
14
  import android.webkit.WebView
14
15
  import android.widget.FrameLayout
@@ -41,8 +42,11 @@ import com.pakstr.app.permissions.interfaces.PermissionHandler
41
42
 
42
43
  import com.pakstr.app.signer.SignerManager
43
44
  import com.pakstr.app.signer.SignerType
45
+ import com.pakstr.app.signer.local.LocalAccountImportCandidate
44
46
  import kotlinx.coroutines.CancellationException
47
+ import kotlinx.coroutines.Dispatchers
45
48
  import kotlinx.coroutines.launch
49
+ import kotlinx.coroutines.withContext
46
50
  import com.pakstr.app.utils.AppPreferences
47
51
  import com.pakstr.app.utils.BunkerPairingDialog
48
52
  import com.pakstr.app.utils.SignerPickerBottomSheet
@@ -61,6 +65,9 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
61
65
  private lateinit var fab: FloatingActionButton
62
66
  private lateinit var supportUnlockGestureLayout: SupportUnlockGestureLayout
63
67
  private var bunkerPairingDialog: AlertDialog? = null
68
+ private var localImportDialog: AlertDialog? = null
69
+ private var localImportCandidate: LocalAccountImportCandidate? = null
70
+ private var signerSetupInProgress = false
64
71
 
65
72
  private val diagnosticsHandler = Handler(Looper.getMainLooper())
66
73
  private val diagnosticsExpiryCheck = Runnable {
@@ -128,12 +135,6 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
128
135
  }
129
136
 
130
137
  initSigner()
131
- if (
132
- savedInstanceState == null &&
133
- AppPreferences.getBoolean("isFirstRun", true)
134
- ) {
135
- container.post(::showSignerPicker)
136
- }
137
138
 
138
139
  ViewCompat.setOnApplyWindowInsetsListener(
139
140
 
@@ -269,8 +270,26 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
269
270
  SignerManager(
270
271
  this,
271
272
  amberLauncher,
272
- bunkerAuthChallengeHandler = ::openBunkerAuthChallenge
273
+ bunkerAuthChallengeHandler = ::openBunkerAuthChallenge,
274
+ signerPickerRequestHandler = ::requestSignerPicker
273
275
  )
276
+ signerManager.migrateLegacySignerConfiguration(
277
+ setupCompleted = !AppPreferences.getBoolean("isFirstRun", true)
278
+ )
279
+ }
280
+
281
+ private fun requestSignerPicker() {
282
+ runOnUiThread {
283
+ if (
284
+ isFinishing ||
285
+ isDestroyed ||
286
+ supportFragmentManager.isStateSaved
287
+ ) {
288
+ signerManager.cancelSignerSelection()
289
+ return@runOnUiThread
290
+ }
291
+ showSignerPicker()
292
+ }
274
293
  }
275
294
 
276
295
  private fun openBunkerAuthChallenge(value: String) {
@@ -360,6 +379,11 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
360
379
 
361
380
  bunkerPairingDialog?.dismiss()
362
381
  bunkerPairingDialog = null
382
+ localImportDialog?.setOnDismissListener(null)
383
+ localImportDialog?.dismiss()
384
+ localImportDialog = null
385
+ localImportCandidate?.close()
386
+ localImportCandidate = null
363
387
 
364
388
  if (::signerManager.isInitialized) {
365
389
  signerManager.close()
@@ -408,8 +432,26 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
408
432
  }
409
433
 
410
434
  private fun showSignerPicker() {
435
+ if (
436
+ supportFragmentManager.findFragmentByTag(SIGNER_PICKER_TAG) != null
437
+ ) {
438
+ return
439
+ }
440
+ val hasLocalAccount =
441
+ try {
442
+ signerManager.hasLocalAccount()
443
+ } catch (_: Exception) {
444
+ Toast.makeText(
445
+ this,
446
+ R.string.signer_picker_local_status_failed,
447
+ Toast.LENGTH_LONG
448
+ ).show()
449
+ signerManager.cancelSignerSelection()
450
+ return
451
+ }
411
452
  SignerPickerBottomSheet(
412
453
  hasBunkerSession = signerManager.hasBunkerSession(),
454
+ hasLocalAccount = hasLocalAccount,
413
455
  onAmberSelected = {
414
456
  signerManager.setActiveType(SignerType.AMBER)
415
457
  handleSignerSetup()
@@ -423,7 +465,31 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
423
465
  false
424
466
  }
425
467
  },
426
- onSharePairing = ::shareBunkerPairing,
468
+ onLocalSelected = {
469
+ signerSetupInProgress = true
470
+ createLocalAccount()
471
+ },
472
+ onLocalImportSelected = { nsec ->
473
+ val candidate =
474
+ try {
475
+ signerManager.prepareLocalAccountImport(nsec)
476
+ } catch (_: Exception) {
477
+ null
478
+ }
479
+ if (candidate == null) {
480
+ false
481
+ } else {
482
+ signerSetupInProgress = true
483
+ localImportCandidate?.close()
484
+ localImportCandidate = candidate
485
+ showLocalImportConfirmation(candidate)
486
+ true
487
+ }
488
+ },
489
+ onSharePairing = { relayUrls ->
490
+ signerSetupInProgress = true
491
+ shareBunkerPairing(relayUrls)
492
+ },
427
493
  onBunkerLogout = {
428
494
  signerManager.logoutBunker()
429
495
  Toast.makeText(
@@ -431,13 +497,130 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
431
497
  R.string.signer_picker_disconnected,
432
498
  Toast.LENGTH_SHORT
433
499
  ).show()
500
+ },
501
+ onDismissed = {
502
+ if (!signerSetupInProgress) {
503
+ signerManager.cancelSignerSelection()
504
+ }
434
505
  }
435
506
  ).show(
436
507
  supportFragmentManager,
437
- "SignerPicker"
508
+ SIGNER_PICKER_TAG
438
509
  )
439
510
  }
440
511
 
512
+ private fun createLocalAccount() {
513
+ lifecycleScope.launch {
514
+ val created =
515
+ withContext(Dispatchers.IO) {
516
+ try {
517
+ signerManager.createLocalAccount()
518
+ true
519
+ } catch (cancelled: CancellationException) {
520
+ throw cancelled
521
+ } catch (_: Exception) {
522
+ false
523
+ }
524
+ }
525
+ signerSetupInProgress = false
526
+ if (created) {
527
+ handleSignerSetup()
528
+ } else {
529
+ signerManager.cancelSignerSelection()
530
+ Toast.makeText(
531
+ this@MainActivity,
532
+ R.string.signer_picker_local_failed,
533
+ Toast.LENGTH_LONG
534
+ ).show()
535
+ }
536
+ }
537
+ }
538
+
539
+ private fun showLocalImportConfirmation(
540
+ candidate: LocalAccountImportCandidate
541
+ ) {
542
+ if (localImportCandidate !== candidate) {
543
+ return
544
+ }
545
+ if (isFinishing || isDestroyed) {
546
+ discardLocalAccountImport()
547
+ return
548
+ }
549
+ var confirmed = false
550
+ val dialog =
551
+ AlertDialog.Builder(this)
552
+ .setTitle(R.string.signer_picker_local_confirm_title)
553
+ .setMessage(
554
+ getString(
555
+ R.string.signer_picker_local_confirm_message,
556
+ candidate.npub
557
+ )
558
+ )
559
+ .setPositiveButton(
560
+ R.string.signer_picker_local_confirm
561
+ ) { _, _ ->
562
+ confirmed = true
563
+ confirmLocalAccountImport(candidate)
564
+ }
565
+ .setNegativeButton(
566
+ R.string.signer_picker_local_cancel,
567
+ null
568
+ )
569
+ .create()
570
+ dialog.setOnDismissListener {
571
+ if (!confirmed) {
572
+ discardLocalAccountImport()
573
+ }
574
+ if (localImportDialog === dialog) {
575
+ localImportDialog = null
576
+ }
577
+ }
578
+ localImportDialog = dialog
579
+ dialog.show()
580
+ dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
581
+ }
582
+
583
+ private fun confirmLocalAccountImport(
584
+ candidate: LocalAccountImportCandidate
585
+ ) {
586
+ if (localImportCandidate !== candidate) {
587
+ return
588
+ }
589
+ localImportCandidate = null
590
+ lifecycleScope.launch {
591
+ val imported =
592
+ withContext(Dispatchers.IO) {
593
+ try {
594
+ signerManager.importLocalAccount(candidate)
595
+ true
596
+ } catch (cancelled: CancellationException) {
597
+ candidate.close()
598
+ throw cancelled
599
+ } catch (_: Exception) {
600
+ false
601
+ }
602
+ }
603
+ signerSetupInProgress = false
604
+ if (imported) {
605
+ handleSignerSetup()
606
+ } else {
607
+ signerManager.cancelSignerSelection()
608
+ Toast.makeText(
609
+ this@MainActivity,
610
+ R.string.signer_picker_local_import_failed,
611
+ Toast.LENGTH_LONG
612
+ ).show()
613
+ }
614
+ }
615
+ }
616
+
617
+ private fun discardLocalAccountImport() {
618
+ localImportCandidate?.close()
619
+ localImportCandidate = null
620
+ signerSetupInProgress = false
621
+ signerManager.cancelSignerSelection()
622
+ }
623
+
441
624
  private fun shareBunkerPairing(relayUrls: List<String>) {
442
625
  lifecycleScope.launch {
443
626
  val pairingUri =
@@ -446,6 +629,8 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
446
629
  } catch (cancelled: CancellationException) {
447
630
  throw cancelled
448
631
  } catch (_: Exception) {
632
+ signerSetupInProgress = false
633
+ signerManager.cancelSignerSelection()
449
634
  Toast.makeText(
450
635
  this@MainActivity,
451
636
  R.string.signer_picker_connection_failed,
@@ -469,7 +654,18 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
469
654
  R.string.signer_picker_pairing_waiting,
470
655
  Toast.LENGTH_LONG
471
656
  ).show()
472
- val connected = signerManager.finishBunkerPairing()
657
+ val connected =
658
+ try {
659
+ signerManager.finishBunkerPairing()
660
+ } catch (cancelled: CancellationException) {
661
+ throw cancelled
662
+ } catch (_: Exception) {
663
+ false
664
+ }
665
+ signerSetupInProgress = false
666
+ if (!connected) {
667
+ signerManager.cancelSignerSelection()
668
+ }
473
669
  if (pairingDialog.isShowing) {
474
670
  pairingDialog.dismiss()
475
671
  }
@@ -510,4 +706,8 @@ class MainActivity : AppCompatActivity(), PermissionHandler {
510
706
  DebugReportShare.share(this)
511
707
 
512
708
  }
513
- }
709
+
710
+ private companion object {
711
+ const val SIGNER_PICKER_TAG = "SignerPicker"
712
+ }
713
+ }
@@ -0,0 +1,181 @@
1
+ package com.pakstr.app.crypto
2
+
3
+ internal class InvalidNsecException : IllegalArgumentException(
4
+ "Invalid nsec"
5
+ )
6
+
7
+ internal object Nip19Codec {
8
+ private const val NSEC_HRP = "nsec"
9
+ private const val NPUB_HRP = "npub"
10
+ private const val PAYLOAD_SIZE = 32
11
+ private const val CHECKSUM_SIZE = 6
12
+ private const val MAX_ENCODED_LENGTH = 90
13
+ private const val BECH32_CONSTANT = 1
14
+ private const val CHARSET =
15
+ "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
16
+ private val reverseCharset = IntArray(128) { -1 }.apply {
17
+ CHARSET.forEachIndexed { index, character ->
18
+ this[character.code] = index
19
+ }
20
+ }
21
+
22
+ fun decodeNsec(value: String): ByteArray {
23
+ if (
24
+ value.isEmpty() ||
25
+ value.length > MAX_ENCODED_LENGTH ||
26
+ value != value.lowercase()
27
+ ) {
28
+ throw InvalidNsecException()
29
+ }
30
+
31
+ val separator = value.lastIndexOf('1')
32
+ if (
33
+ separator < 1 ||
34
+ separator + CHECKSUM_SIZE + 1 > value.length
35
+ ) {
36
+ throw InvalidNsecException()
37
+ }
38
+
39
+ val hrp = value.substring(0, separator)
40
+ if (
41
+ hrp != NSEC_HRP ||
42
+ hrp.any { it.code !in 33..126 }
43
+ ) {
44
+ throw InvalidNsecException()
45
+ }
46
+
47
+ val data = IntArray(value.length - separator - 1)
48
+ value.substring(separator + 1).forEachIndexed { index, character ->
49
+ val decoded =
50
+ if (character.code < reverseCharset.size) {
51
+ reverseCharset[character.code]
52
+ } else {
53
+ -1
54
+ }
55
+ if (decoded < 0) {
56
+ throw InvalidNsecException()
57
+ }
58
+ data[index] = decoded
59
+ }
60
+ if (polymod(hrpExpand(hrp) + data) != BECH32_CONSTANT) {
61
+ throw InvalidNsecException()
62
+ }
63
+
64
+ val decoded = convertBits(
65
+ data.copyOf(data.size - CHECKSUM_SIZE),
66
+ fromBits = 5,
67
+ toBits = 8,
68
+ pad = false
69
+ )
70
+ if (decoded.size != PAYLOAD_SIZE) {
71
+ decoded.fill(0)
72
+ throw InvalidNsecException()
73
+ }
74
+ return ByteArray(decoded.size) { index ->
75
+ decoded[index].toByte()
76
+ }.also {
77
+ decoded.fill(0)
78
+ }
79
+ }
80
+
81
+ fun encodeNpub(publicKey: ByteArray): String {
82
+ require(publicKey.size == PAYLOAD_SIZE) {
83
+ "Public key must be exactly 32 bytes"
84
+ }
85
+ val data = convertBits(
86
+ IntArray(publicKey.size) { index ->
87
+ publicKey[index].toInt() and 0xff
88
+ },
89
+ fromBits = 8,
90
+ toBits = 5,
91
+ pad = true
92
+ )
93
+ val checksum = createChecksum(NPUB_HRP, data)
94
+ return buildString {
95
+ append(NPUB_HRP)
96
+ append('1')
97
+ (data + checksum).forEach { append(CHARSET[it]) }
98
+ }
99
+ }
100
+
101
+ private fun createChecksum(
102
+ hrp: String,
103
+ data: IntArray
104
+ ): IntArray {
105
+ val values = hrpExpand(hrp) + data + IntArray(CHECKSUM_SIZE)
106
+ val remainder = polymod(values) xor BECH32_CONSTANT
107
+ return IntArray(CHECKSUM_SIZE) { index ->
108
+ (remainder shr (5 * (CHECKSUM_SIZE - 1 - index))) and 31
109
+ }
110
+ }
111
+
112
+ private fun hrpExpand(hrp: String): IntArray {
113
+ val result = IntArray(hrp.length * 2 + 1)
114
+ hrp.forEachIndexed { index, character ->
115
+ result[index] = character.code shr 5
116
+ result[hrp.length + 1 + index] = character.code and 31
117
+ }
118
+ return result
119
+ }
120
+
121
+ private fun polymod(values: IntArray): Int {
122
+ var checksum = 1
123
+ val generators = intArrayOf(
124
+ 0x3b6a57b2,
125
+ 0x26508e6d,
126
+ 0x1ea119fa,
127
+ 0x3d4233dd,
128
+ 0x2a1462b3
129
+ )
130
+ values.forEach { value ->
131
+ val top = checksum ushr 25
132
+ checksum = (checksum and 0x1ffffff) shl 5 xor value
133
+ generators.forEachIndexed { index, generator ->
134
+ if ((top shr index) and 1 != 0) {
135
+ checksum = checksum xor generator
136
+ }
137
+ }
138
+ }
139
+ return checksum
140
+ }
141
+
142
+ private fun convertBits(
143
+ values: IntArray,
144
+ fromBits: Int,
145
+ toBits: Int,
146
+ pad: Boolean
147
+ ): IntArray {
148
+ var accumulator = 0
149
+ var bitCount = 0
150
+ val output = ArrayList<Int>()
151
+ val fromMask = (1 shl fromBits) - 1
152
+ val toMask = (1 shl toBits) - 1
153
+
154
+ values.forEach { value ->
155
+ if (value < 0 || value ushr fromBits != 0) {
156
+ throw InvalidNsecException()
157
+ }
158
+ accumulator =
159
+ ((accumulator and ((1 shl (fromBits + toBits - 1)) - 1))
160
+ shl fromBits) or (value and fromMask)
161
+ bitCount += fromBits
162
+ while (bitCount >= toBits) {
163
+ bitCount -= toBits
164
+ output += (accumulator ushr bitCount) and toMask
165
+ }
166
+ }
167
+
168
+ if (pad) {
169
+ if (bitCount > 0) {
170
+ output += (accumulator shl (toBits - bitCount)) and toMask
171
+ }
172
+ } else if (
173
+ bitCount >= fromBits ||
174
+ ((accumulator shl (toBits - bitCount)) and toMask) != 0
175
+ ) {
176
+ throw InvalidNsecException()
177
+ }
178
+
179
+ return output.toIntArray()
180
+ }
181
+ }