Incorporate secure seed storage and mneumonic phrase support.
Also added the related UI to display the seed phrase.
This commit is contained in:
parent
eb4c3323df
commit
76b94f184c
|
@ -0,0 +1,40 @@
|
|||
package cash.z.ecc.android.di
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import cash.z.ecc.android.di.annotation.ViewModelKey
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
abstract class ViewModelModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindViewModelFactory(implementation: ViewModelFactory): ViewModelProvider.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(WalletSetupViewModel::class)
|
||||
abstract fun bindWalletSetupViewModel(implementation: WalletSetupViewModel): ViewModel
|
||||
}
|
||||
|
||||
@Singleton
|
||||
class ViewModelFactory @Inject constructor(
|
||||
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
val creator = creators[modelClass] ?: creators.entries.firstOrNull {
|
||||
modelClass.isAssignableFrom(it.key)
|
||||
}?.value ?: throw IllegalArgumentException(
|
||||
"No map entry found for ${modelClass.canonicalName}." +
|
||||
" Verify that this ViewModel has been added to the ViewModelModule."
|
||||
)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return creator.get() as T
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package cash.z.ecc.android.di.annotation
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.MapKey
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@Target(
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER
|
||||
)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MapKey
|
||||
annotation class ViewModelKey(val value: KClass<out ViewModel>)
|
|
@ -8,10 +8,14 @@ import android.view.View
|
|||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.ZcashWalletApp
|
||||
import cash.z.ecc.android.databinding.FragmentBackupBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey
|
||||
import cash.z.ecc.android.ui.util.AddressPartNumberSpan
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
|
@ -24,14 +28,14 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
with(binding) {
|
||||
applySpan(
|
||||
textAddressPart1,
|
||||
textAddressPart2,
|
||||
textAddressPart3,
|
||||
textAddressPart4,
|
||||
textAddressPart5,
|
||||
textAddressPart6,
|
||||
textAddressPart7,
|
||||
textAddressPart8
|
||||
textAddressPart1, textAddressPart2, textAddressPart3,
|
||||
textAddressPart4, textAddressPart5, textAddressPart6,
|
||||
textAddressPart7, textAddressPart8, textAddressPart9,
|
||||
textAddressPart10, textAddressPart11, textAddressPart12,
|
||||
textAddressPart13, textAddressPart14, textAddressPart15,
|
||||
textAddressPart16, textAddressPart17, textAddressPart18,
|
||||
textAddressPart19, textAddressPart20, textAddressPart21,
|
||||
textAddressPart22, textAddressPart23, textAddressPart24
|
||||
)
|
||||
}
|
||||
binding.buttonPositive.setOnClickListener {
|
||||
|
@ -41,17 +45,28 @@ class BackupFragment : BaseFragment<FragmentBackupBinding>() {
|
|||
|
||||
private fun onEnterWallet() {
|
||||
Toast.makeText(activity, "Backup verification coming soon! For now, enjoy your new wallet!", Toast.LENGTH_LONG).show()
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_backup_to_nav_home)
|
||||
mainActivity?.navController?.popBackStack(R.id.wallet_setup_navigation, true)
|
||||
}
|
||||
|
||||
private fun applySpan(vararg textViews: TextView) {
|
||||
val words = loadSeedWords()
|
||||
val thinSpace = "\u2005" // 0.25 em space
|
||||
textViews.forEachIndexed { index, textView ->
|
||||
textView.text = SpannableString("${index + 1}$thinSpace${textView.text}").apply {
|
||||
setSpan(AddressPartNumberSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
val numLength = "$index".length
|
||||
val word = words[index]
|
||||
// TODO: work with a charsequence here, rather than constructing a String
|
||||
textView.text = SpannableString("${index + 1}$thinSpace${String(word)}").apply {
|
||||
setSpan(AddressPartNumberSpan(), 0, 1 + numLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSeedWords(): List<CharArray> {
|
||||
val lockBox = LockBox(ZcashWalletApp.instance)
|
||||
val mnemonics = Mnemonics()
|
||||
val seed = lockBox.getBytes(LockBoxKey.SEED)!!
|
||||
return mnemonics.nextMnemonicList(seed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,16 +4,29 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import cash.z.ecc.android.R
|
||||
import cash.z.ecc.android.ZcashWalletApp
|
||||
import cash.z.ecc.android.databinding.FragmentLandingBinding
|
||||
import cash.z.ecc.android.di.annotation.FragmentScope
|
||||
import cash.z.ecc.android.feedback.MetricType.SEED_CREATION
|
||||
import cash.z.ecc.android.feedback.measure
|
||||
import cash.z.ecc.android.isEmulator
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.base.BaseFragment
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
import javax.inject.Inject
|
||||
|
||||
class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelProvider.Factory
|
||||
|
||||
private val walletSetup: WalletSetupViewModel by activityViewModels { viewModelFactory }
|
||||
private var skipCount: Int = 0
|
||||
|
||||
override fun inflate(inflater: LayoutInflater): FragmentLandingBinding =
|
||||
|
@ -62,6 +75,10 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
|||
}
|
||||
|
||||
private fun onNewWallet() {
|
||||
mainActivity?.feedback?.measure(SEED_CREATION) {
|
||||
walletSetup.createSeed()
|
||||
}
|
||||
|
||||
binding.textMessage.text = "Wallet created! Congratulations!"
|
||||
binding.buttonNegative.text = "Skip"
|
||||
binding.buttonPositive.text = "Backup"
|
||||
|
@ -75,9 +92,8 @@ class LandingFragment : BaseFragment<FragmentLandingBinding>() {
|
|||
}
|
||||
|
||||
private fun onEnterWallet() {
|
||||
|
||||
skipCount = 0
|
||||
mainActivity?.navController?.navigate(R.id.action_nav_landing_to_nav_home)
|
||||
mainActivity?.navController?.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package cash.z.ecc.android.ui.setup
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import cash.z.ecc.android.lockbox.LockBox
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.HAS_BACKUP
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.HAS_SEED
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.LockBoxKey.SEED
|
||||
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
|
||||
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class WalletSetupViewModel @Inject constructor(val mnemonics: Mnemonics, val lockBox: LockBox) :
|
||||
ViewModel() {
|
||||
|
||||
enum class WalletSetupState {
|
||||
UNKNOWN, SEED_WITH_BACKUP, SEED_WITHOUT_BACKUP, NO_SEED
|
||||
}
|
||||
|
||||
fun checkSeed(): Flow<WalletSetupState> = flow {
|
||||
when {
|
||||
lockBox.getBoolean(HAS_BACKUP) -> emit(SEED_WITH_BACKUP)
|
||||
lockBox.getBoolean(HAS_SEED) -> emit(SEED_WITHOUT_BACKUP)
|
||||
else -> emit(NO_SEED)
|
||||
}
|
||||
}
|
||||
|
||||
fun createSeed() {
|
||||
check(!lockBox.getBoolean(HAS_SEED)) {
|
||||
"Error! Cannot create a seed when one already exists! This would overwrite the" +
|
||||
" existing seed and could lead to a loss of funds if the user has no backup!"
|
||||
}
|
||||
|
||||
mnemonics.apply {
|
||||
lockBox.setBytes(SEED, nextSeed())
|
||||
lockBox.setBoolean(HAS_SEED, true)
|
||||
}
|
||||
}
|
||||
|
||||
object LockBoxKey {
|
||||
const val SEED = "cash.z.ecc.android.SEED1"
|
||||
const val HAS_SEED = "cash.z.ecc.android.HAS_SEED1"
|
||||
const val HAS_BACKUP = "cash.z.ecc.android.HAS_BACKUP1"
|
||||
}
|
||||
}
|
|
@ -3,5 +3,5 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="10dp" />
|
||||
<stroke android:width="1dp" android:color="#282828"/>
|
||||
<solid android:color="#171717"/>
|
||||
<solid android:color="@color/background_banner"/>
|
||||
</shape>
|
|
@ -17,6 +17,7 @@
|
|||
<!-- Address parts -->
|
||||
|
||||
<!-- Someday, there will be an advanced VirtualLayout that helps us do this without nesting but for now, this seems to be the only clean way to center all the fields -->
|
||||
<!-- its tempting to do this programmatically but for now, it's always 24 words so I'll do it statically. If this ever changes, we'll probably be using Jetpack Compose by then so it will be easier to do in code -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/receive_address_parts"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -28,50 +29,95 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_message"
|
||||
>
|
||||
|
||||
<!-- -->
|
||||
<!-- Column 1 -->
|
||||
<!-- -->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_1"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="drum"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_3"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_4"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_3"
|
||||
android:id="@+id/text_address_part_4"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="inject"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_5"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_5"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="plate"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_7"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_3" />
|
||||
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_1"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_7" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_7"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="talk"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_5" />
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_4"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_10" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_10"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_7"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_13" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_13"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_10"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_16" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_16"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_13"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_19" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_19"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_16"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_22" />
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_22"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_1"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_19" />
|
||||
|
||||
<!-- -->
|
||||
<!-- Column 2 -->
|
||||
<!-- -->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_2"
|
||||
|
@ -81,52 +127,193 @@
|
|||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:text="fitness"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_4"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_5"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_1"
|
||||
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_4"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:id="@+id/text_address_part_5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="pool"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_6"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_8"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_6"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:id="@+id/text_address_part_8"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="inform"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_8"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_11"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_4" />
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_5" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_8"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:id="@+id/text_address_part_11"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="helmet"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_14"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_6" />
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_8" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_left_address_column"
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_14"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_17"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_11" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_17"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_20"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_14" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_20"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_23"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_17" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_23"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_2"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_20" />
|
||||
|
||||
<!-- -->
|
||||
<!-- Column 3 -->
|
||||
<!-- -->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_3"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:text="goals"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_6"
|
||||
app:layout_constraintStart_toEndOf="@id/barrier_left_address_column_2"
|
||||
app:layout_constraintTop_toTopOf="@id/text_address_part_1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_6"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_9"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_3" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_9"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_12"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_6" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_12"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_15"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_9" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_15"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_18"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_12" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_18"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_21"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_15" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_21"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_address_part_24"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_18" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_part_24"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Zcash.TextAppearance.AddressPart"
|
||||
android:text="word"
|
||||
app:layout_constraintStart_toStartOf="@id/text_address_part_3"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_address_part_21" />
|
||||
<!--
|
||||
text_address_part_3, text_address_part_6, text_address_part_9, text_address_part_12, text_address_part_15, text_address_part_18, text_address_part_21, text_address_part_24
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_left_address_column_1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="150dp"
|
||||
android:layout_marginRight="150dp"
|
||||
android:padding="150dp"
|
||||
app:barrierDirection="end"
|
||||
app:constraint_referenced_ids="text_address_part_1,text_address_part_3,text_address_part_5,text_address_part_7" />
|
||||
app:constraint_referenced_ids="text_address_part_1, text_address_part_4, text_address_part_7, text_address_part_10, text_address_part_13, text_address_part_16, text_address_part_19, text_address_part_22" />
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_left_address_column_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="end"
|
||||
app:constraint_referenced_ids="text_address_part_2, text_address_part_5, text_address_part_8, text_address_part_11, text_address_part_14, text_address_part_17, text_address_part_20, text_address_part_23" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
|
|
|
@ -37,6 +37,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation Deps.JavaX.INJECT
|
||||
implementation Deps.Kotlin.STDLIB
|
||||
implementation Deps.AndroidX.APPCOMPAT
|
||||
implementation Deps.AndroidX.CORE_KTX
|
||||
|
|
|
@ -6,24 +6,32 @@ import java.nio.ByteBuffer
|
|||
import java.nio.CharBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class LockBox @Inject constructor(private val appContext: Context) : LockBoxProvider {
|
||||
|
||||
class LockBox(private val appContext: Context) : LockBoxProvider {
|
||||
override fun setBoolean(key: String, value: Boolean) {
|
||||
SecurePreferences.setValue(appContext, key, value)
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String): Boolean {
|
||||
return SecurePreferences.getBooleanValue(appContext, key, false)
|
||||
}
|
||||
|
||||
override fun setBytes(key: String, value: ByteArray) {
|
||||
SecurePreferences.setValue(appContext, key, value.toHex())
|
||||
}
|
||||
|
||||
override fun getBytes(key: String): ByteArray {
|
||||
return SecurePreferences.getStringValue(appContext, key, null)!!.fromHex()
|
||||
override fun getBytes(key: String): ByteArray? {
|
||||
return SecurePreferences.getStringValue(appContext, key, null)?.fromHex()
|
||||
}
|
||||
|
||||
override fun setCharsUtf8(key: String, value: CharArray) {
|
||||
setBytes(key, value.toBytes())
|
||||
}
|
||||
|
||||
override fun getCharsUtf8(key: String): CharArray {
|
||||
return getBytes(key).fromBytes()
|
||||
override fun getCharsUtf8(key: String): CharArray? {
|
||||
return getBytes(key)?.fromBytes()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -6,8 +6,11 @@ package cash.z.ecc.android.lockbox
|
|||
*/
|
||||
interface LockBoxProvider {
|
||||
fun setBytes(key: String, value: ByteArray)
|
||||
fun getBytes(key: String): ByteArray
|
||||
fun getBytes(key: String): ByteArray?
|
||||
|
||||
fun setCharsUtf8(key: String, value: CharArray)
|
||||
fun getCharsUtf8(key: String): CharArray
|
||||
fun getCharsUtf8(key: String): CharArray?
|
||||
|
||||
fun setBoolean(key: String, value: Boolean)
|
||||
fun getBoolean(key: String): Boolean
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import cash.z.ecc.android.Deps
|
|||
apply plugin: 'kotlin'
|
||||
|
||||
dependencies {
|
||||
implementation Deps.JavaX.INJECT
|
||||
implementation Deps.Kotlin.STDLIB
|
||||
|
||||
implementation 'com.madgag.spongycastle:core:1.58.0.0'
|
||||
|
|
|
@ -5,18 +5,40 @@ package cash.z.ecc.kotlin.mnemonic
|
|||
* interacts with it.
|
||||
*/
|
||||
interface MnemonicProvider {
|
||||
|
||||
/**
|
||||
* Generate a random 24-word mnemonic phrase
|
||||
* Generate a random seed.
|
||||
*/
|
||||
fun nextSeed(): ByteArray
|
||||
|
||||
/**
|
||||
* Generate a random 24-word mnemonic phrase.
|
||||
*/
|
||||
fun nextMnemonic(): CharArray
|
||||
|
||||
/**
|
||||
* Generate the 24-word mnemonic phrase corresponding to the given seed.
|
||||
*/
|
||||
fun nextMnemonic(seed: ByteArray): CharArray
|
||||
|
||||
/**
|
||||
* Generate a random 24-word mnemonic phrase, represented as a list of words.
|
||||
*/
|
||||
fun nextMnemonicList(): List<CharArray>
|
||||
|
||||
/**
|
||||
* Generate a 64-byte seed from the 24-word mnemonic phrase
|
||||
* Generate the 24-word mnemonic phrase corresponding to the given seed, represented as a list.
|
||||
*/
|
||||
fun nextMnemonicList(seed: ByteArray): List<CharArray>
|
||||
|
||||
/**
|
||||
* Generate a 64-byte seed from the 24-word mnemonic phrase.
|
||||
*/
|
||||
fun toSeed(mnemonic: CharArray): ByteArray
|
||||
|
||||
/**
|
||||
* Split the given mnemonic around spaces.
|
||||
*/
|
||||
fun toWordList(mnemonic: CharArray): List<CharArray>
|
||||
|
||||
}
|
||||
|
|
|
@ -5,28 +5,40 @@ import io.github.novacrypto.bip39.SeedCalculator
|
|||
import io.github.novacrypto.bip39.Words
|
||||
import io.github.novacrypto.bip39.wordlists.English
|
||||
import java.security.SecureRandom
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO: either find another library that allows for doing this without strings or modify this code
|
||||
// to leverage SecureCharBuffer (which doesn't work well with SeedCalculator.calculateSeed,
|
||||
// which expects a string so for that reason, we just use Strings here)
|
||||
class Mnemonics @Inject constructor(): MnemonicProvider {
|
||||
|
||||
override fun nextSeed(): ByteArray {
|
||||
return ByteArray(Words.TWENTY_FOUR.byteLength()).apply {
|
||||
SecureRandom().nextBytes(this)
|
||||
}
|
||||
}
|
||||
|
||||
class Mnemonics : MnemonicProvider {
|
||||
override fun nextMnemonic(): CharArray {
|
||||
// TODO: either find another library that allows for doing this without strings or modify this code to leverage SecureCharBuffer (which doesn't work well with SeedCalculator.calculateSeed, which expects a string so for that reason, we just use Strings here)
|
||||
return nextMnemonic(nextSeed())
|
||||
}
|
||||
|
||||
override fun nextMnemonic(seed: ByteArray): CharArray {
|
||||
return StringBuilder().let { builder ->
|
||||
ByteArray(Words.TWENTY_FOUR.byteLength()).also {
|
||||
SecureRandom().nextBytes(it)
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(it) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(seed) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
builder.toString().toCharArray()
|
||||
}
|
||||
}
|
||||
|
||||
override fun nextMnemonicList(): List<CharArray> {
|
||||
return nextMnemonicList(nextSeed())
|
||||
}
|
||||
|
||||
override fun nextMnemonicList(seed: ByteArray): List<CharArray> {
|
||||
return WordListBuilder().let { builder ->
|
||||
ByteArray(Words.TWENTY_FOUR.byteLength()).also {
|
||||
SecureRandom().nextBytes(it)
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(it) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
MnemonicGenerator(English.INSTANCE).createMnemonic(seed) { c ->
|
||||
builder.append(c)
|
||||
}
|
||||
builder.wordList
|
||||
}
|
||||
|
@ -37,11 +49,30 @@ class Mnemonics : MnemonicProvider {
|
|||
return SeedCalculator().calculateSeed(mnemonic.toString(), "")
|
||||
}
|
||||
|
||||
override fun toWordList(mnemonic: CharArray): List<CharArray> {
|
||||
val wordList = mutableListOf<CharArray>()
|
||||
var cursor = 0
|
||||
repeat(mnemonic.size) { i ->
|
||||
val isSpace = mnemonic[i] == ' '
|
||||
if (isSpace || i == (mnemonic.size - 1)) {
|
||||
val wordSize = i - cursor + if (isSpace) 0 else 1
|
||||
wordList.add(CharArray(wordSize).apply {
|
||||
repeat(wordSize) {
|
||||
this[it] = mnemonic[cursor + it]
|
||||
}
|
||||
})
|
||||
cursor = i + 1
|
||||
}
|
||||
}
|
||||
return wordList
|
||||
}
|
||||
|
||||
class WordListBuilder {
|
||||
val wordList = mutableListOf<CharArray>()
|
||||
fun append(c: CharSequence) {
|
||||
if (c[0] != English.INSTANCE.space) addWord(c)
|
||||
}
|
||||
|
||||
private fun addWord(c: CharSequence) {
|
||||
c.length.let { size ->
|
||||
val word = CharArray(size)
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.junit.Assert.assertEquals
|
|||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.lang.Math.max
|
||||
import java.security.SecureRandom
|
||||
|
||||
|
||||
|
@ -45,6 +46,24 @@ class MnemonicTest {
|
|||
validate(words.map { String(it) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMnemonic_toList() {
|
||||
val words = mnemonics.run {
|
||||
toWordList(nextMnemonic())
|
||||
}
|
||||
assertEquals(24, words.size)
|
||||
validate(words.map { String(it) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMnemonic_longestWord() {
|
||||
var max = 0
|
||||
repeat(2048) {
|
||||
max = max(max, English.INSTANCE.getWord(it).length)
|
||||
}
|
||||
assertEquals(8, max)
|
||||
}
|
||||
|
||||
private fun validate(words: List<String>) {
|
||||
// return or crash!
|
||||
words.forEach { word ->
|
||||
|
|
Loading…
Reference in New Issue