Incorporate secure seed storage and mneumonic phrase support.

Also added the related UI to display the seed phrase.
This commit is contained in:
Kevin Gorham 2019-12-17 16:20:23 -05:00
parent eb4c3323df
commit 76b94f184c
No known key found for this signature in database
GPG Key ID: CCA55602DF49FC38
14 changed files with 481 additions and 78 deletions

View File

@ -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
}
}

View File

@ -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>)

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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

View File

@ -37,6 +37,7 @@ android {
}
dependencies {
implementation Deps.JavaX.INJECT
implementation Deps.Kotlin.STDLIB
implementation Deps.AndroidX.APPCOMPAT
implementation Deps.AndroidX.CORE_KTX

View File

@ -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()
}

View File

@ -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
}

View File

@ -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'

View File

@ -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>
}

View File

@ -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)

View File

@ -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 ->