You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

239 lines
9.5 KiB

package cash.z.ecc.android.ui.setup
import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.ZcashWalletApp
import cash.z.ecc.android.di.DependenciesHolder
import cash.z.ecc.android.ext.Const
import cash.z.ecc.android.ext.failWith
import cash.z.ecc.android.feedback.Feedback
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.lockbox.LockBox
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.exception.InitializerException
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.LightWalletEndpoint
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.*
import cash.z.ecc.android.util.twig
import cash.z.ecc.kotlin.mnemonic.Mnemonics
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
class WalletSetupViewModel : ViewModel() {
private val mnemonics: Mnemonics = DependenciesHolder.mnemonics
private val lockBox: LockBox = DependenciesHolder.lockBox
private val prefs: LockBox = DependenciesHolder.prefs
private val feedback: Feedback = DependenciesHolder.feedback
enum class WalletSetupState {
SEED_WITH_BACKUP, SEED_WITHOUT_BACKUP, NO_SEED
}
fun checkSeed(): Flow<WalletSetupState> = flow {
when {
lockBox.getBoolean(Const.Backup.HAS_BACKUP) -> emit(SEED_WITH_BACKUP)
lockBox.getBoolean(Const.Backup.HAS_SEED) -> emit(SEED_WITHOUT_BACKUP)
else -> emit(NO_SEED)
}
}
/**
* Throw an exception if the seed phrase is bad.
*/
fun validatePhrase(seedPhrase: String) {
mnemonics.validate(seedPhrase.toCharArray())
}
fun loadBirthdayHeight(): BlockHeight? {
val h: Int? = lockBox[Const.Backup.BIRTHDAY_HEIGHT]
twig("Loaded birthday with key ${Const.Backup.BIRTHDAY_HEIGHT} and found $h")
h?.let {
return BlockHeight.new(ZcashWalletApp.instance.defaultNetwork, it.toLong())
}
return null
}
suspend fun newWallet() {
val network = ZcashWalletApp.instance.defaultNetwork
twig("Initializing new ${network.networkName} wallet")
with(mnemonics) {
storeWallet(nextMnemonic(nextEntropy()), network, loadNearestBirthday(network))
}
openStoredWallet()
}
suspend fun importWallet(seedPhrase: String, birthdayHeight: BlockHeight?) {
val network = ZcashWalletApp.instance.defaultNetwork
twig("Importing ${network.networkName} wallet. Requested birthday: $birthdayHeight")
storeWallet(
seedPhrase.toCharArray(),
network,
birthdayHeight ?: loadNearestBirthday(network)
)
openStoredWallet()
}
suspend fun openStoredWallet() {
DependenciesHolder.initializerComponent.createInitializer(loadConfig())
}
/**
* Build a config object by loading in the viewingKey, birthday and server info which is already
* known by this point.
*/
private suspend fun loadConfig(): Initializer.Config {
twig("Loading config variables")
var overwriteVks = false
val network = ZcashWalletApp.instance.defaultNetwork
val vk =
loadUnifiedViewingKey() ?: onMissingViewingKey(network).also { overwriteVks = true }
val birthdayHeight = loadBirthdayHeight() ?: onMissingBirthday(network)
val host = prefs[Const.Pref.SERVER_HOST] ?: Const.Default.Server.HOST
val port = prefs[Const.Pref.SERVER_PORT] ?: Const.Default.Server.PORT
Log.d("SilentDragon", "host: $host")
// TODO: Maybe check server availability here
twig("Done loading config variables")
return Initializer.Config {
it.importWallet(vk, birthdayHeight, network, LightWalletEndpoint(host, port, true))
it.setOverwriteKeys(overwriteVks)
}
}
private fun loadUnifiedViewingKey(): UnifiedViewingKey? {
val extfvk = lockBox.getCharsUtf8(Const.Backup.VIEWING_KEY)
val extpub = lockBox.getCharsUtf8(Const.Backup.PUBLIC_KEY)
return when {
extfvk == null || extpub == null -> {
if (extfvk == null) {
twig("Warning: Shielded key was missing")
}
if (extpub == null) {
twig("Warning: Transparent key was missing")
}
null
}
else -> UnifiedViewingKey(extfvk = String(extfvk), extpub = String(extpub))
}
}
private suspend fun onMissingViewingKey(network: ZcashNetwork): UnifiedViewingKey {
twig("Recover VK: Viewing key was missing")
// add some temporary logic to help us troubleshoot this problem.
ZcashWalletApp.instance.getSharedPreferences("SecurePreferences", Context.MODE_PRIVATE)
.all.map { it.key }.joinToString().let { keyNames ->
"${Const.Backup.VIEWING_KEY}, ${Const.Backup.PUBLIC_KEY}".let { missingKeys ->
// is there a typo or change in how the value is labelled?
// for troubleshooting purposes, let's see if we CAN derive the vk from the seed in these situations
var recoveryViewingKey: UnifiedViewingKey? = null
var ableToLoadSeed = false
try {
val seed = lockBox.getBytes(Const.Backup.SEED)!!
ableToLoadSeed = true
twig("Recover UVK: Seed found")
recoveryViewingKey =
DerivationTool.deriveUnifiedViewingKeys(seed, network)[0]
twig("Recover UVK: successfully derived UVK from seed")
} catch (t: Throwable) {
twig("Failed while trying to recover UVK due to: $t")
}
// this will happen during rare upgrade scenarios when the user migrates from a seed-only wallet to this vk-based version
// or during more common scenarios where the user migrates from a vk only wallet to a unified vk wallet
if (recoveryViewingKey != null) {
storeUnifiedViewingKey(recoveryViewingKey)
return recoveryViewingKey
} else {
feedback.report(
Report.Issue.MissingViewkey(
ableToLoadSeed,
missingKeys,
keyNames,
lockBox.getCharsUtf8(Const.Backup.VIEWING_KEY) != null
)
)
}
throw InitializerException.MissingViewingKeyException
}
}
}
private suspend fun onMissingBirthday(network: ZcashNetwork): BlockHeight =
failWith(InitializerException.MissingBirthdayException) {
twig("Recover Birthday: falling back to sapling birthday")
loadNearestBirthday(network)
}
private suspend fun loadNearestBirthday(network: ZcashNetwork) =
BlockHeight.ofLatestCheckpoint(
ZcashWalletApp.instance,
network,
)
//
// Storage Helpers
//
/**
* Entry point for all storage. Takes a seed phrase and stores all the parts so that we can
* selectively use them, the next time the app is opened. Although we store everything, we
* primarily only work with the viewing key and spending key. The seed is only accessed when
* presenting backup information to the user.
*/
private suspend fun storeWallet(
seedPhraseChars: CharArray,
network: ZcashNetwork,
birthday: BlockHeight
) {
check(!lockBox.getBoolean(Const.Backup.HAS_SEED)) {
"Error! Cannot store 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!"
}
storeBirthday(birthday)
mnemonics.toSeed(seedPhraseChars).let { bip39Seed ->
DerivationTool.deriveUnifiedViewingKeys(bip39Seed, network)[0].let { viewingKey ->
storeSeedPhrase(seedPhraseChars)
storeSeed(bip39Seed)
storeUnifiedViewingKey(viewingKey)
}
}
}
private suspend fun storeBirthday(birthday: BlockHeight) = withContext(IO) {
twig("Storing birthday ${birthday.value} with and key ${Const.Backup.BIRTHDAY_HEIGHT}")
lockBox[Const.Backup.BIRTHDAY_HEIGHT] = birthday.value
}
private suspend fun storeSeedPhrase(seedPhrase: CharArray) = withContext(IO) {
twig("Storing seedphrase: ${seedPhrase.size}")
lockBox[Const.Backup.SEED_PHRASE] = seedPhrase
lockBox[Const.Backup.HAS_SEED_PHRASE] = true
}
private suspend fun storeSeed(bip39Seed: ByteArray) = withContext(IO) {
twig("Storing seed: ${bip39Seed.size}")
lockBox.setBytes(Const.Backup.SEED, bip39Seed)
lockBox[Const.Backup.HAS_SEED] = true
}
private suspend fun storeUnifiedViewingKey(vk: UnifiedViewingKey) = withContext(IO) {
twig("storeViewingKey vk: ${vk.extfvk.length}")
lockBox[Const.Backup.VIEWING_KEY] = vk.extfvk
lockBox[Const.Backup.PUBLIC_KEY] = vk.extpub
}
}