232 lines
9.6 KiB
Kotlin
232 lines
9.6 KiB
Kotlin
package cash.z.ecc.android.ui.setup
|
|
|
|
import android.content.Context
|
|
import androidx.lifecycle.ViewModel
|
|
import cash.z.ecc.android.ZcashWalletApp
|
|
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.ext.twig
|
|
import cash.z.ecc.android.sdk.tool.DerivationTool
|
|
import cash.z.ecc.android.sdk.tool.WalletBirthdayTool
|
|
import cash.z.ecc.android.sdk.type.UnifiedViewingKey
|
|
import cash.z.ecc.android.sdk.type.WalletBirthday
|
|
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
|
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.NO_SEED
|
|
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITHOUT_BACKUP
|
|
import cash.z.ecc.android.ui.setup.WalletSetupViewModel.WalletSetupState.SEED_WITH_BACKUP
|
|
import cash.z.ecc.kotlin.mnemonic.Mnemonics
|
|
import com.bugsnag.android.Bugsnag
|
|
import kotlinx.coroutines.Dispatchers.IO
|
|
import kotlinx.coroutines.flow.Flow
|
|
import kotlinx.coroutines.flow.flow
|
|
import kotlinx.coroutines.withContext
|
|
import javax.inject.Inject
|
|
import javax.inject.Named
|
|
|
|
class WalletSetupViewModel @Inject constructor() : ViewModel() {
|
|
|
|
@Inject
|
|
lateinit var mnemonics: Mnemonics
|
|
|
|
@Inject
|
|
lateinit var lockBox: LockBox
|
|
|
|
@Inject
|
|
@Named(Const.Name.APP_PREFS)
|
|
lateinit var prefs: LockBox
|
|
|
|
@Inject
|
|
lateinit var feedback: 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(): Int? {
|
|
val h: Int? = lockBox[Const.Backup.BIRTHDAY_HEIGHT]
|
|
twig("Loaded birthday with key ${Const.Backup.BIRTHDAY_HEIGHT} and found $h")
|
|
return h
|
|
}
|
|
|
|
suspend fun newWallet(): Initializer {
|
|
val network = ZcashWalletApp.instance.defaultNetwork
|
|
twig("Initializing new ${network.networkName} wallet")
|
|
with(mnemonics) {
|
|
storeWallet(nextMnemonic(nextEntropy()), network, loadNearestBirthday(network))
|
|
}
|
|
return openStoredWallet()
|
|
}
|
|
|
|
suspend fun importWallet(seedPhrase: String, birthdayHeight: Int): Initializer {
|
|
val network = ZcashWalletApp.instance.defaultNetwork
|
|
twig("Importing ${network.networkName} wallet. Requested birthday: $birthdayHeight")
|
|
storeWallet(seedPhrase.toCharArray(), network, loadNearestBirthday(network, birthdayHeight))
|
|
return openStoredWallet()
|
|
}
|
|
|
|
suspend fun openStoredWallet(): Initializer {
|
|
val config = loadConfig()
|
|
return ZcashWalletApp.component.initializerSubcomponent().create(config).initializer()
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
|
|
twig("Done loading config variables")
|
|
return Initializer.Config {
|
|
it.importWallet(vk, birthdayHeight, network, host, port)
|
|
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?
|
|
Bugsnag.leaveBreadcrumb("One of $missingKeys not found in keySet: $keyNames")
|
|
// 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) {
|
|
Bugsnag.leaveBreadcrumb("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 fun onMissingBirthday(network: ZcashNetwork): Int = failWith(InitializerException.MissingBirthdayException) {
|
|
twig("Recover Birthday: falling back to sapling birthday")
|
|
loadNearestBirthday(network, network.saplingActivationHeight).height
|
|
}
|
|
|
|
private fun loadNearestBirthday(network: ZcashNetwork, birthdayHeight: Int? = null) =
|
|
WalletBirthdayTool.loadNearest(ZcashWalletApp.instance, network, birthdayHeight)
|
|
|
|
//
|
|
// 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: WalletBirthday
|
|
) {
|
|
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: WalletBirthday) = withContext(IO) {
|
|
twig("Storing birthday ${birthday.height} with and key ${Const.Backup.BIRTHDAY_HEIGHT}")
|
|
lockBox[Const.Backup.BIRTHDAY_HEIGHT] = birthday.height
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|