Address book encryption (#1643)
* Add Tink as a dependency * Serialize AddressBook to any OutputStream * Extract address book format parser * Address book key storage provider * Address book encryption finalisation * Implement AddressBook encryption * Address book encryption code cleanup * Address book reset hotfix * SDK snapshot * Documentation update * Code cleanup * Test hotfix * Error handling * Code cleanup * Unencrypted address book removed after successful encrypted file read * Code cleanup * Code cleanup * Test hotfix --------- Co-authored-by: Milan Cerovsky <milan@z.cash> Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
parent
f59add8e3b
commit
6aee0e2469
|
@ -7,6 +7,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
|
|||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Address book encryption
|
||||
- The device authentication feature on the Zashi app launch has been added
|
||||
- Zashi app now supports Spanish language
|
||||
- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI
|
||||
|
|
|
@ -10,6 +10,7 @@ directly impact users rather than highlighting other key architectural updates.*
|
|||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Address book encryption
|
||||
- The device authentication feature on the Zashi app launch has been added
|
||||
- Zashi app now supports Spanish language
|
||||
- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI
|
||||
|
|
|
@ -10,6 +10,7 @@ directly impact users rather than highlighting other key architectural updates.*
|
|||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Address book encryption
|
||||
- The device authentication feature on the Zashi app launch has been added
|
||||
- Zashi app now supports Spanish language
|
||||
- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI
|
||||
|
|
|
@ -211,6 +211,7 @@ PLAY_APP_UPDATE_VERSION=2.1.0
|
|||
PLAY_APP_UPDATE_KTX_VERSION=2.1.0
|
||||
PLAY_PUBLISHER_API_VERSION=v3-rev20231030-2.0.0
|
||||
PLAY_SERVICES_AUTH_VERSION=21.2.0
|
||||
TINK_VERSION=1.15.0
|
||||
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
|
||||
ZXING_VERSION=3.5.3
|
||||
ZIP_321_VERSION = 0.0.6
|
||||
|
|
|
@ -182,6 +182,7 @@ dependencyResolutionManagement {
|
|||
val markdownVersion = extra["MARKDOWN_VERSION"].toString()
|
||||
val playAppUpdateVersion = extra["PLAY_APP_UPDATE_VERSION"].toString()
|
||||
val playAppUpdateKtxVersion = extra["PLAY_APP_UPDATE_KTX_VERSION"].toString()
|
||||
val tinkVersion = extra["TINK_VERSION"].toString()
|
||||
val zcashBip39Version = extra["ZCASH_BIP39_VERSION"].toString()
|
||||
val zcashSdkVersion = extra["ZCASH_SDK_VERSION"].toString()
|
||||
val zip321Version = extra["ZIP_321_VERSION"].toString()
|
||||
|
@ -254,6 +255,7 @@ dependencyResolutionManagement {
|
|||
library("markdown", "org.jetbrains:markdown:$markdownVersion")
|
||||
library("play-update", "com.google.android.play:app-update:$playAppUpdateVersion")
|
||||
library("play-update-ktx", "com.google.android.play:app-update-ktx:$playAppUpdateKtxVersion")
|
||||
library("tink", "com.google.crypto.tink:tink-android:$tinkVersion")
|
||||
library("zcash-sdk", "cash.z.ecc.android:zcash-android-sdk:$zcashSdkVersion")
|
||||
library("zcash-sdk-incubator", "cash.z.ecc.android:zcash-android-sdk-incubator:$zcashSdkVersion")
|
||||
library("zcash-bip39", "cash.z.ecc.android:kotlin-bip39:$zcashBip39Version")
|
||||
|
|
|
@ -138,6 +138,7 @@ dependencies {
|
|||
implementation(libs.zcash.sdk)
|
||||
implementation(libs.zcash.sdk.incubator)
|
||||
implementation(libs.zcash.bip39)
|
||||
implementation(libs.tink)
|
||||
implementation(libs.zxing)
|
||||
|
||||
api(libs.flexa.core)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package co.electriccoin.zcash.di
|
||||
|
||||
import co.electriccoin.zcash.ui.common.provider.AddressBookKeyStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.AddressBookKeyStorageProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.AddressBookProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.AddressBookProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.AddressBookStorageProvider
|
||||
|
@ -20,4 +22,5 @@ val providerModule =
|
|||
factoryOf(::GetMonetarySeparatorProvider)
|
||||
factoryOf(::AddressBookStorageProviderImpl) bind AddressBookStorageProvider::class
|
||||
factoryOf(::AddressBookProviderImpl) bind AddressBookProvider::class
|
||||
factoryOf(::AddressBookKeyStorageProviderImpl) bind AddressBookKeyStorageProvider::class
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package co.electriccoin.zcash.di
|
||||
|
||||
import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeleteAddressBookUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetBackupPersistableWalletUseCase
|
||||
|
@ -28,6 +27,7 @@ import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase
|
|||
import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ResetAddressBookUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.SaveContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.SendEmailUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.SendSupportEmailUseCase
|
||||
|
@ -59,7 +59,7 @@ val useCaseModule =
|
|||
factoryOf(::RescanBlockchainUseCase)
|
||||
factoryOf(::GetTransparentAddressUseCase)
|
||||
factoryOf(::ObserveAddressBookContactsUseCase)
|
||||
factoryOf(::DeleteAddressBookUseCase)
|
||||
factoryOf(::ResetAddressBookUseCase)
|
||||
factoryOf(::ValidateContactAddressUseCase)
|
||||
factoryOf(::ValidateContactNameUseCase)
|
||||
factoryOf(::SaveContactUseCase)
|
||||
|
|
|
@ -1,33 +1,46 @@
|
|||
package co.electriccoin.zcash.ui.common.datasource
|
||||
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.spackle.io.deleteSuspend
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBook
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import co.electriccoin.zcash.ui.common.provider.AddressBookProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.AddressBookStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.serialization.addressbook.ADDRESS_BOOK_SERIALIZATION_V1
|
||||
import co.electriccoin.zcash.ui.common.serialization.addressbook.AddressBookKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.Clock
|
||||
import java.io.IOException
|
||||
import java.security.GeneralSecurityException
|
||||
|
||||
interface LocalAddressBookDataSource {
|
||||
suspend fun getContacts(): AddressBook
|
||||
suspend fun getContacts(addressBookKey: AddressBookKey): AddressBook
|
||||
|
||||
suspend fun saveContact(
|
||||
name: String,
|
||||
address: String
|
||||
address: String,
|
||||
addressBookKey: AddressBookKey
|
||||
): AddressBook
|
||||
|
||||
suspend fun updateContact(
|
||||
contact: AddressBookContact,
|
||||
name: String,
|
||||
address: String
|
||||
address: String,
|
||||
addressBookKey: AddressBookKey
|
||||
): AddressBook
|
||||
|
||||
suspend fun deleteContact(addressBookContact: AddressBookContact): AddressBook
|
||||
suspend fun deleteContact(
|
||||
addressBookContact: AddressBookContact,
|
||||
addressBookKey: AddressBookKey
|
||||
): AddressBook
|
||||
|
||||
suspend fun saveContacts(contacts: AddressBook)
|
||||
suspend fun saveAddressBook(
|
||||
addressBook: AddressBook,
|
||||
addressBookKey: AddressBookKey
|
||||
)
|
||||
|
||||
suspend fun deleteAddressBook()
|
||||
suspend fun resetAddressBook()
|
||||
}
|
||||
|
||||
class LocalAddressBookDataSourceImpl(
|
||||
|
@ -36,20 +49,20 @@ class LocalAddressBookDataSourceImpl(
|
|||
) : LocalAddressBookDataSource {
|
||||
private var addressBook: AddressBook? = null
|
||||
|
||||
override suspend fun getContacts(): AddressBook =
|
||||
override suspend fun getContacts(addressBookKey: AddressBookKey): AddressBook =
|
||||
withContext(Dispatchers.IO) {
|
||||
val addressBook = this@LocalAddressBookDataSourceImpl.addressBook
|
||||
|
||||
if (addressBook == null) {
|
||||
var newAddressBook: AddressBook? = readLocalFileToAddressBook()
|
||||
var newAddressBook: AddressBook? = readLocalFileToAddressBook(addressBookKey)
|
||||
if (newAddressBook == null) {
|
||||
newAddressBook =
|
||||
AddressBook(
|
||||
lastUpdated = Clock.System.now(),
|
||||
version = 1,
|
||||
version = ADDRESS_BOOK_SERIALIZATION_V1,
|
||||
contacts = emptyList(),
|
||||
)
|
||||
writeAddressBookToLocalStorage(newAddressBook)
|
||||
writeAddressBookToLocalStorage(newAddressBook, addressBookKey)
|
||||
}
|
||||
newAddressBook
|
||||
} else {
|
||||
|
@ -59,14 +72,15 @@ class LocalAddressBookDataSourceImpl(
|
|||
|
||||
override suspend fun saveContact(
|
||||
name: String,
|
||||
address: String
|
||||
address: String,
|
||||
addressBookKey: AddressBookKey
|
||||
): AddressBook =
|
||||
withContext(Dispatchers.IO) {
|
||||
val lastUpdated = Clock.System.now()
|
||||
addressBook =
|
||||
val newAddressBook =
|
||||
AddressBook(
|
||||
lastUpdated = lastUpdated,
|
||||
version = 1,
|
||||
version = ADDRESS_BOOK_SERIALIZATION_V1,
|
||||
contacts =
|
||||
addressBook?.contacts.orEmpty() +
|
||||
AddressBookContact(
|
||||
|
@ -75,21 +89,22 @@ class LocalAddressBookDataSourceImpl(
|
|||
lastUpdated = lastUpdated,
|
||||
),
|
||||
)
|
||||
writeAddressBookToLocalStorage(addressBook!!)
|
||||
addressBook!!
|
||||
writeAddressBookToLocalStorage(newAddressBook, addressBookKey)
|
||||
newAddressBook
|
||||
}
|
||||
|
||||
override suspend fun updateContact(
|
||||
contact: AddressBookContact,
|
||||
name: String,
|
||||
address: String
|
||||
address: String,
|
||||
addressBookKey: AddressBookKey
|
||||
): AddressBook =
|
||||
withContext(Dispatchers.IO) {
|
||||
val lastUpdated = Clock.System.now()
|
||||
addressBook =
|
||||
val newAddressBook =
|
||||
AddressBook(
|
||||
lastUpdated = lastUpdated,
|
||||
version = 1,
|
||||
version = ADDRESS_BOOK_SERIALIZATION_V1,
|
||||
contacts =
|
||||
addressBook?.contacts.orEmpty().toMutableList()
|
||||
.apply {
|
||||
|
@ -104,17 +119,20 @@ class LocalAddressBookDataSourceImpl(
|
|||
}
|
||||
.toList(),
|
||||
)
|
||||
writeAddressBookToLocalStorage(addressBook!!)
|
||||
addressBook!!
|
||||
writeAddressBookToLocalStorage(newAddressBook, addressBookKey)
|
||||
newAddressBook
|
||||
}
|
||||
|
||||
override suspend fun deleteContact(addressBookContact: AddressBookContact): AddressBook =
|
||||
override suspend fun deleteContact(
|
||||
addressBookContact: AddressBookContact,
|
||||
addressBookKey: AddressBookKey
|
||||
): AddressBook =
|
||||
withContext(Dispatchers.IO) {
|
||||
val lastUpdated = Clock.System.now()
|
||||
addressBook =
|
||||
val newAddressBook =
|
||||
AddressBook(
|
||||
lastUpdated = lastUpdated,
|
||||
version = 1,
|
||||
version = ADDRESS_BOOK_SERIALIZATION_V1,
|
||||
contacts =
|
||||
addressBook?.contacts.orEmpty().toMutableList()
|
||||
.apply {
|
||||
|
@ -122,27 +140,58 @@ class LocalAddressBookDataSourceImpl(
|
|||
}
|
||||
.toList(),
|
||||
)
|
||||
writeAddressBookToLocalStorage(addressBook!!)
|
||||
addressBook!!
|
||||
writeAddressBookToLocalStorage(newAddressBook, addressBookKey)
|
||||
newAddressBook
|
||||
}
|
||||
|
||||
override suspend fun saveContacts(contacts: AddressBook) {
|
||||
writeAddressBookToLocalStorage(contacts)
|
||||
this@LocalAddressBookDataSourceImpl.addressBook = contacts
|
||||
override suspend fun saveAddressBook(
|
||||
addressBook: AddressBook,
|
||||
addressBookKey: AddressBookKey
|
||||
) {
|
||||
writeAddressBookToLocalStorage(addressBook, addressBookKey)
|
||||
this.addressBook = addressBook
|
||||
}
|
||||
|
||||
override suspend fun deleteAddressBook() {
|
||||
addressBookStorageProvider.getStorageFile()?.deleteSuspend()
|
||||
override suspend fun resetAddressBook() {
|
||||
addressBook = null
|
||||
}
|
||||
|
||||
private fun readLocalFileToAddressBook(): AddressBook? {
|
||||
val file = addressBookStorageProvider.getStorageFile() ?: return null
|
||||
return addressBookProvider.readAddressBookFromFile(file)
|
||||
@Suppress("ReturnCount")
|
||||
private suspend fun readLocalFileToAddressBook(addressBookKey: AddressBookKey): AddressBook? {
|
||||
val encryptedFile = addressBookStorageProvider.getStorageFile(addressBookKey)
|
||||
val unencryptedFile = addressBookStorageProvider.getLegacyUnencryptedStorageFile()
|
||||
|
||||
if (encryptedFile != null) {
|
||||
return try {
|
||||
addressBookProvider.readAddressBookFromFile(encryptedFile, addressBookKey)
|
||||
.also {
|
||||
unencryptedFile?.deleteSuspend()
|
||||
}
|
||||
} catch (e: GeneralSecurityException) {
|
||||
Twig.warn(e) { "Failed to decrypt address book" }
|
||||
null
|
||||
} catch (e: IOException) {
|
||||
Twig.warn(e) { "Failed to decrypt address book" }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return if (unencryptedFile != null) {
|
||||
addressBookProvider.readLegacyUnencryptedAddressBookFromFile(unencryptedFile)
|
||||
.also { unencryptedAddressBook ->
|
||||
writeAddressBookToLocalStorage(unencryptedAddressBook, addressBookKey)
|
||||
unencryptedFile.deleteSuspend()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeAddressBookToLocalStorage(addressBook: AddressBook) {
|
||||
val file = addressBookStorageProvider.getOrCreateStorageFile()
|
||||
addressBookProvider.writeAddressBookToFile(file, addressBook)
|
||||
private fun writeAddressBookToLocalStorage(
|
||||
addressBook: AddressBook,
|
||||
addressBookKey: AddressBookKey
|
||||
) {
|
||||
val file = addressBookStorageProvider.getOrCreateStorageFile(addressBookKey)
|
||||
addressBookProvider.writeAddressBookToFile(file, addressBook, addressBookKey)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package co.electriccoin.zcash.ui.common.provider
|
||||
|
||||
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.api.PreferenceProvider
|
||||
import co.electriccoin.zcash.preference.model.entry.PreferenceDefault
|
||||
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
|
||||
import co.electriccoin.zcash.ui.common.serialization.addressbook.AddressBookKey
|
||||
import com.google.crypto.tink.InsecureSecretKeyAccess
|
||||
import com.google.crypto.tink.SecretKeyAccess
|
||||
import com.google.crypto.tink.util.SecretBytes
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
interface AddressBookKeyStorageProvider {
|
||||
suspend fun getAddressBookKey(): AddressBookKey?
|
||||
|
||||
suspend fun storeAddressBookKey(addressBookKey: AddressBookKey)
|
||||
}
|
||||
|
||||
class AddressBookKeyStorageProviderImpl(
|
||||
private val encryptedPreferenceProvider: EncryptedPreferenceProvider
|
||||
) : AddressBookKeyStorageProvider {
|
||||
private val default = AddressBookKeyPreferenceDefault()
|
||||
|
||||
override suspend fun getAddressBookKey(): AddressBookKey? {
|
||||
return default.getValue(encryptedPreferenceProvider())
|
||||
}
|
||||
|
||||
override suspend fun storeAddressBookKey(addressBookKey: AddressBookKey) {
|
||||
default.putValue(encryptedPreferenceProvider(), addressBookKey)
|
||||
}
|
||||
}
|
||||
|
||||
private class AddressBookKeyPreferenceDefault : PreferenceDefault<AddressBookKey?> {
|
||||
private val secretKeyAccess: SecretKeyAccess?
|
||||
get() = InsecureSecretKeyAccess.get()
|
||||
|
||||
override val key: PreferenceKey = PreferenceKey("address_book_key")
|
||||
|
||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.decode()
|
||||
|
||||
override suspend fun putValue(
|
||||
preferenceProvider: PreferenceProvider,
|
||||
newValue: AddressBookKey?
|
||||
) = preferenceProvider.putString(key, newValue?.encode())
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
private fun AddressBookKey?.encode() =
|
||||
if (this != null) {
|
||||
Base64.encode(this.key.toByteArray(secretKeyAccess))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
private fun String?.decode() =
|
||||
if (this != null) {
|
||||
AddressBookKey(SecretBytes.copyFrom(Base64.decode(this), secretKeyAccess))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
|
@ -1,100 +1,63 @@
|
|||
package co.electriccoin.zcash.ui.common.provider
|
||||
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBook
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import kotlinx.datetime.Instant
|
||||
import co.electriccoin.zcash.ui.common.serialization.addressbook.AddressBookEncryptor
|
||||
import co.electriccoin.zcash.ui.common.serialization.addressbook.AddressBookKey
|
||||
import co.electriccoin.zcash.ui.common.serialization.addressbook.AddressBookSerializer
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import kotlin.LazyThreadSafetyMode.NONE
|
||||
|
||||
interface AddressBookProvider {
|
||||
fun writeAddressBookToFile(
|
||||
file: File,
|
||||
addressBook: AddressBook
|
||||
addressBook: AddressBook,
|
||||
addressBookKey: AddressBookKey
|
||||
)
|
||||
|
||||
fun readAddressBookFromFile(file: File): AddressBook
|
||||
fun readAddressBookFromFile(
|
||||
file: File,
|
||||
addressBookKey: AddressBookKey
|
||||
): AddressBook
|
||||
|
||||
fun readLegacyUnencryptedAddressBookFromFile(file: File): AddressBook
|
||||
}
|
||||
|
||||
class AddressBookProviderImpl : AddressBookProvider {
|
||||
private val addressBookSerializer by lazy(NONE) { AddressBookSerializer() }
|
||||
private val addressBookEncryptor by lazy(NONE) { AddressBookEncryptor() }
|
||||
|
||||
override fun writeAddressBookToFile(
|
||||
file: File,
|
||||
addressBook: AddressBook
|
||||
addressBook: AddressBook,
|
||||
addressBookKey: AddressBookKey
|
||||
) {
|
||||
file.outputStream().use {
|
||||
serializeAddressBookToByteArrayFile(it, addressBook)
|
||||
file.outputStream().buffered().use { stream ->
|
||||
addressBookEncryptor.encryptAddressBook(
|
||||
addressBookKey,
|
||||
addressBookSerializer,
|
||||
stream,
|
||||
addressBook
|
||||
)
|
||||
stream.flush()
|
||||
}
|
||||
}
|
||||
|
||||
override fun readAddressBookFromFile(file: File): AddressBook {
|
||||
return file.inputStream().use {
|
||||
deserializeByteArrayFileToAddressBook(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun serializeAddressBookToByteArrayFile(
|
||||
outputStream: FileOutputStream,
|
||||
addressBook: AddressBook
|
||||
) {
|
||||
outputStream.buffered().use {
|
||||
it.write(addressBook.version.createByteArray())
|
||||
it.write(addressBook.lastUpdated.toEpochMilliseconds().createByteArray())
|
||||
it.write(addressBook.contacts.size.createByteArray())
|
||||
|
||||
addressBook.contacts.forEach { contact ->
|
||||
it.write(contact.lastUpdated.toEpochMilliseconds().createByteArray())
|
||||
it.write(contact.address.createByteArray())
|
||||
it.write(contact.name.createByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deserializeByteArrayFileToAddressBook(inputStream: InputStream): AddressBook {
|
||||
return inputStream.buffered().use { stream ->
|
||||
AddressBook(
|
||||
version = stream.readInt(),
|
||||
lastUpdated = stream.readLong().let { Instant.fromEpochMilliseconds(it) },
|
||||
contacts =
|
||||
stream.readInt().let { contactsSize ->
|
||||
(0 until contactsSize).map { _ ->
|
||||
AddressBookContact(
|
||||
lastUpdated = stream.readLong().let { Instant.fromEpochMilliseconds(it) },
|
||||
address = stream.readString(),
|
||||
name = stream.readString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
override fun readAddressBookFromFile(
|
||||
file: File,
|
||||
addressBookKey: AddressBookKey
|
||||
): AddressBook {
|
||||
return file.inputStream().use { stream ->
|
||||
addressBookEncryptor.decryptAddressBook(
|
||||
addressBookKey,
|
||||
addressBookSerializer,
|
||||
stream
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.createByteArray(): ByteArray = this.toLong().createByteArray()
|
||||
|
||||
private fun Long.createByteArray(): ByteArray =
|
||||
ByteBuffer
|
||||
.allocate(Long.SIZE_BYTES).order(BYTE_ORDER).putLong(this).array()
|
||||
|
||||
private fun String.createByteArray(): ByteArray {
|
||||
val byteArray = this.toByteArray()
|
||||
return byteArray.size.createByteArray() + byteArray
|
||||
}
|
||||
|
||||
private fun InputStream.readInt(): Int = readLong().toInt()
|
||||
|
||||
private fun InputStream.readLong(): Long {
|
||||
val buffer = ByteArray(Long.SIZE_BYTES)
|
||||
this.read(buffer)
|
||||
return ByteBuffer.wrap(buffer).order(BYTE_ORDER).getLong()
|
||||
}
|
||||
|
||||
private fun InputStream.readString(): String {
|
||||
val size = this.readInt()
|
||||
val buffer = ByteArray(size)
|
||||
this.read(buffer)
|
||||
return String(buffer)
|
||||
override fun readLegacyUnencryptedAddressBookFromFile(file: File): AddressBook {
|
||||
return file.inputStream().use { stream ->
|
||||
addressBookSerializer.deserializeAddressBook(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val BYTE_ORDER = ByteOrder.BIG_ENDIAN
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
package co.electriccoin.zcash.ui.common.provider
|
||||
|
||||
import android.content.Context
|
||||
import co.electriccoin.zcash.ui.common.serialization.addressbook.AddressBookKey
|
||||
import java.io.File
|
||||
|
||||
interface AddressBookStorageProvider {
|
||||
fun getStorageFile(): File?
|
||||
fun getStorageFile(addressBookKey: AddressBookKey): File?
|
||||
|
||||
fun getOrCreateStorageFile(): File
|
||||
fun getLegacyUnencryptedStorageFile(): File?
|
||||
|
||||
fun getOrCreateStorageFile(addressBookKey: AddressBookKey): File
|
||||
|
||||
/**
|
||||
* Create a temporary file into which data from remote is written. This file is removed after usage.
|
||||
|
@ -17,12 +20,19 @@ interface AddressBookStorageProvider {
|
|||
class AddressBookStorageProviderImpl(
|
||||
private val context: Context
|
||||
) : AddressBookStorageProvider {
|
||||
override fun getStorageFile(): File? {
|
||||
return File(context.noBackupFilesDir, LOCAL_ADDRESS_BOOK_FILE_NAME)
|
||||
override fun getStorageFile(addressBookKey: AddressBookKey): File? {
|
||||
return File(context.noBackupFilesDir, addressBookKey.fileIdentifier())
|
||||
.takeIf { it.exists() && it.isFile }
|
||||
}
|
||||
|
||||
override fun getOrCreateStorageFile(): File = getOrCreateFile(LOCAL_ADDRESS_BOOK_FILE_NAME)
|
||||
override fun getLegacyUnencryptedStorageFile(): File? {
|
||||
return File(context.noBackupFilesDir, LEGACY_UNENCRYPTED_ADDRESS_BOOK_FILE_NAME)
|
||||
.takeIf { it.exists() && it.isFile }
|
||||
}
|
||||
|
||||
override fun getOrCreateStorageFile(addressBookKey: AddressBookKey): File {
|
||||
return getOrCreateFile(addressBookKey.fileIdentifier())
|
||||
}
|
||||
|
||||
override fun getOrCreateTempStorageFile(): File = getOrCreateFile(REMOTE_ADDRESS_BOOK_FILE_NAME_LOCAL_COPY)
|
||||
|
||||
|
@ -35,5 +45,5 @@ class AddressBookStorageProviderImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private const val LOCAL_ADDRESS_BOOK_FILE_NAME = "address_book"
|
||||
private const val LEGACY_UNENCRYPTED_ADDRESS_BOOK_FILE_NAME = "address_book"
|
||||
private const val REMOTE_ADDRESS_BOOK_FILE_NAME_LOCAL_COPY = "address_book_temp"
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package co.electriccoin.zcash.ui.common.repository
|
||||
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.datasource.LocalAddressBookDataSource
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBook
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import co.electriccoin.zcash.ui.common.provider.AddressBookKeyStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.serialization.addressbook.AddressBookKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -38,7 +39,7 @@ interface AddressBookRepository {
|
|||
|
||||
suspend fun deleteContact(contact: AddressBookContact)
|
||||
|
||||
suspend fun deleteAddressBook()
|
||||
suspend fun resetAddressBook()
|
||||
|
||||
// fun onGoogleSignInSuccess()
|
||||
//
|
||||
|
@ -50,6 +51,8 @@ interface AddressBookRepository {
|
|||
@Suppress("TooManyFunctions")
|
||||
class AddressBookRepositoryImpl(
|
||||
private val localAddressBookDataSource: LocalAddressBookDataSource,
|
||||
private val addressBookKeyStorageProvider: AddressBookKeyStorageProvider,
|
||||
private val walletRepository: WalletRepository,
|
||||
// private val remoteAddressBookDataSource: RemoteAddressBookDataSource,
|
||||
// private val context: Context
|
||||
) : AddressBookRepository {
|
||||
|
@ -92,9 +95,9 @@ class AddressBookRepositoryImpl(
|
|||
InternalOperation.Delete(contact = contact)
|
||||
)
|
||||
|
||||
override suspend fun deleteAddressBook() =
|
||||
override suspend fun resetAddressBook() =
|
||||
withNonCancellableSemaphore {
|
||||
localAddressBookDataSource.deleteAddressBook()
|
||||
localAddressBookDataSource.resetAddressBook()
|
||||
addressBookCache.update { null }
|
||||
}
|
||||
|
||||
|
@ -150,12 +153,18 @@ class AddressBookRepositoryImpl(
|
|||
// }
|
||||
val merged =
|
||||
mergeContacts(
|
||||
local = localAddressBookDataSource.getContacts(),
|
||||
local =
|
||||
localAddressBookDataSource.getContacts(
|
||||
addressBookKey = getAddressBookKey()
|
||||
),
|
||||
// remote = remote,
|
||||
remote = null,
|
||||
fromOperation = operation
|
||||
)
|
||||
localAddressBookDataSource.saveContacts(merged)
|
||||
localAddressBookDataSource.saveAddressBook(
|
||||
addressBook = merged,
|
||||
addressBookKey = getAddressBookKey()
|
||||
)
|
||||
// executeRemoteAddressBookSafe {
|
||||
// remoteAddressBookDataSource.uploadContacts()
|
||||
// Twig.info { "Address Book: ensureSynchronization - remote address book uploaded" }
|
||||
|
@ -232,12 +241,19 @@ class AddressBookRepositoryImpl(
|
|||
when (operation) {
|
||||
is InternalOperation.Delete -> {
|
||||
Twig.info { "Address Book: executeInternalOperation - delete" }
|
||||
localAddressBookDataSource.deleteContact(addressBookContact = operation.contact)
|
||||
localAddressBookDataSource.deleteContact(
|
||||
addressBookContact = operation.contact,
|
||||
addressBookKey = getAddressBookKey()
|
||||
)
|
||||
}
|
||||
|
||||
is InternalOperation.Save -> {
|
||||
Twig.info { "Address Book: executeInternalOperation - save" }
|
||||
localAddressBookDataSource.saveContact(name = operation.name, address = operation.address)
|
||||
localAddressBookDataSource.saveContact(
|
||||
name = operation.name,
|
||||
address = operation.address,
|
||||
addressBookKey = getAddressBookKey()
|
||||
)
|
||||
}
|
||||
|
||||
is InternalOperation.Update -> {
|
||||
|
@ -245,7 +261,8 @@ class AddressBookRepositoryImpl(
|
|||
localAddressBookDataSource.updateContact(
|
||||
contact = operation.contact,
|
||||
name = operation.name,
|
||||
address = operation.address
|
||||
address = operation.address,
|
||||
addressBookKey = getAddressBookKey()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -263,6 +280,24 @@ class AddressBookRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getAddressBookKey(account: Account = Account.DEFAULT): AddressBookKey {
|
||||
val key = addressBookKeyStorageProvider.getAddressBookKey()
|
||||
|
||||
return if (key != null) {
|
||||
key
|
||||
} else {
|
||||
val wallet = walletRepository.getPersistableWallet()
|
||||
val newKey =
|
||||
AddressBookKey.derive(
|
||||
seedPhrase = wallet.seedPhrase,
|
||||
network = wallet.network,
|
||||
account = account
|
||||
)
|
||||
addressBookKeyStorageProvider.storeAddressBookKey(newKey)
|
||||
newKey
|
||||
}
|
||||
}
|
||||
|
||||
// @Suppress("TooGenericExceptionCaught")
|
||||
// private suspend fun <T> executeRemoteAddressBookSafe(block: suspend () -> T): T? {
|
||||
// if (hasGoogleDrivePermission().not()) {
|
||||
|
|
|
@ -99,6 +99,8 @@ interface WalletRepository {
|
|||
suspend fun getAllServers(): List<LightWalletEndpoint>
|
||||
|
||||
suspend fun getSynchronizer(): Synchronizer
|
||||
|
||||
suspend fun getPersistableWallet(): PersistableWallet
|
||||
}
|
||||
|
||||
class WalletRepositoryImpl(
|
||||
|
@ -355,6 +357,8 @@ class WalletRepositoryImpl(
|
|||
}
|
||||
|
||||
override suspend fun getSynchronizer(): Synchronizer = synchronizer.filterNotNull().first()
|
||||
|
||||
override suspend fun getPersistableWallet(): PersistableWallet = persistableWallet.filterNotNull().first()
|
||||
}
|
||||
|
||||
private fun Synchronizer.toCommonError(): Flow<SynchronizerError?> =
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
package co.electriccoin.zcash.ui.common.serialization.addressbook
|
||||
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBook
|
||||
import com.google.crypto.tink.subtle.ChaCha20Poly1305
|
||||
import com.google.crypto.tink.subtle.Random
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
internal class AddressBookEncryptor : BaseAddressBookSerializer() {
|
||||
fun encryptAddressBook(
|
||||
addressBookKey: AddressBookKey,
|
||||
serializer: AddressBookSerializer,
|
||||
outputStream: OutputStream,
|
||||
addressBook: AddressBook
|
||||
) {
|
||||
// Generate a fresh one-time key for this ciphertext.
|
||||
val salt = Random.randBytes(ADDRESS_BOOK_SALT_SIZE)
|
||||
val cipherText =
|
||||
ByteArrayOutputStream()
|
||||
.use { stream ->
|
||||
serializer.serializeAddressBook(stream, addressBook)
|
||||
stream.toByteArray()
|
||||
}.let {
|
||||
val key = addressBookKey.deriveEncryptionKey(salt)
|
||||
// Tink encodes the ciphertext as `nonce || ciphertext || tag`.
|
||||
val cipher = ChaCha20Poly1305.create(key)
|
||||
cipher.encrypt(it, null)
|
||||
}
|
||||
|
||||
outputStream.write(ADDRESS_BOOK_ENCRYPTION_V1.createByteArray())
|
||||
outputStream.write(salt)
|
||||
outputStream.write(cipherText)
|
||||
}
|
||||
|
||||
fun decryptAddressBook(
|
||||
addressBookKey: AddressBookKey,
|
||||
serializer: AddressBookSerializer,
|
||||
inputStream: InputStream
|
||||
): AddressBook {
|
||||
val version = inputStream.readInt()
|
||||
if (version != ADDRESS_BOOK_ENCRYPTION_V1) {
|
||||
throw UnknownAddressBookEncryptionVersionException()
|
||||
}
|
||||
|
||||
val salt = ByteArray(ADDRESS_BOOK_SALT_SIZE)
|
||||
require(inputStream.read(salt) == salt.size) { "Input is too short" }
|
||||
|
||||
val ciphertext = inputStream.readBytes()
|
||||
|
||||
val key = addressBookKey.deriveEncryptionKey(salt)
|
||||
val cipher = ChaCha20Poly1305.create(key)
|
||||
val plaintext = cipher.decrypt(ciphertext, null)
|
||||
|
||||
return plaintext.inputStream().use { stream ->
|
||||
serializer.deserializeAddressBook(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UnknownAddressBookEncryptionVersionException : RuntimeException("Unknown address book encryption version")
|
|
@ -0,0 +1,77 @@
|
|||
package co.electriccoin.zcash.ui.common.serialization.addressbook
|
||||
|
||||
import cash.z.ecc.android.sdk.model.Account
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import com.google.crypto.tink.InsecureSecretKeyAccess
|
||||
import com.google.crypto.tink.aead.ChaCha20Poly1305Key
|
||||
import com.google.crypto.tink.subtle.Hkdf
|
||||
import com.google.crypto.tink.util.SecretBytes
|
||||
|
||||
/**
|
||||
* The long-term key that can decrypt an account's encrypted address book.
|
||||
*/
|
||||
class AddressBookKey(val key: SecretBytes) {
|
||||
/**
|
||||
* Derives the filename that this key is able to decrypt.
|
||||
*/
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun fileIdentifier(): String {
|
||||
val access = InsecureSecretKeyAccess.get()
|
||||
val fileIdentifier =
|
||||
Hkdf.computeHkdf(
|
||||
"HMACSHA256",
|
||||
key.toByteArray(access),
|
||||
null,
|
||||
"file_identifier".toByteArray(),
|
||||
ADDRESS_BOOK_FILE_IDENTIFIER_SIZE
|
||||
)
|
||||
return "zashi-address-book-" + fileIdentifier.toHexString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a one-time address book encryption key.
|
||||
*
|
||||
* At encryption time, the one-time property MUST be ensured by generating a
|
||||
* random 32-byte salt.
|
||||
*/
|
||||
fun deriveEncryptionKey(salt: ByteArray): ChaCha20Poly1305Key {
|
||||
assert(salt.size == ADDRESS_BOOK_SALT_SIZE)
|
||||
val access = InsecureSecretKeyAccess.get()
|
||||
val subKey =
|
||||
Hkdf.computeHkdf(
|
||||
"HMACSHA256",
|
||||
key.toByteArray(access),
|
||||
null,
|
||||
salt + "encryption_key".toByteArray(),
|
||||
ADDRESS_BOOK_ENCRYPTION_KEY_SIZE
|
||||
)
|
||||
return ChaCha20Poly1305Key.create(SecretBytes.copyFrom(subKey, access))
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Derives the long-term key that can decrypt the given account's encrypted
|
||||
* address book.
|
||||
*
|
||||
* This requires access to the seed phrase. If the app has separate access
|
||||
* control requirements for the seed phrase and the address book, this key
|
||||
* should be cached in the app's keystore.
|
||||
*/
|
||||
suspend fun derive(
|
||||
seedPhrase: SeedPhrase,
|
||||
network: ZcashNetwork,
|
||||
account: Account
|
||||
): AddressBookKey {
|
||||
val key =
|
||||
DerivationTool.getInstance().deriveArbitraryAccountKey(
|
||||
"ZashiAddressBookEncryptionV1".toByteArray(),
|
||||
seedPhrase.toByteArray(),
|
||||
network,
|
||||
account,
|
||||
)
|
||||
return AddressBookKey(SecretBytes.copyFrom(key, InsecureSecretKeyAccess.get()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package co.electriccoin.zcash.ui.common.serialization.addressbook
|
||||
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBook
|
||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||
import kotlinx.datetime.Instant
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
internal class AddressBookSerializer : BaseAddressBookSerializer() {
|
||||
fun serializeAddressBook(
|
||||
outputStream: OutputStream,
|
||||
addressBook: AddressBook
|
||||
) {
|
||||
outputStream.write(addressBook.version.createByteArray())
|
||||
outputStream.write(addressBook.lastUpdated.toEpochMilliseconds().createByteArray())
|
||||
outputStream.write(addressBook.contacts.size.createByteArray())
|
||||
|
||||
addressBook.contacts.forEach { contact ->
|
||||
outputStream.write(contact.lastUpdated.toEpochMilliseconds().createByteArray())
|
||||
outputStream.write(contact.address.createByteArray())
|
||||
outputStream.write(contact.name.createByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun deserializeAddressBook(inputStream: InputStream): AddressBook {
|
||||
return AddressBook(
|
||||
version = inputStream.readInt(),
|
||||
lastUpdated = inputStream.readLong().let { Instant.fromEpochMilliseconds(it) },
|
||||
contacts =
|
||||
inputStream.readInt().let { contactsSize ->
|
||||
(0 until contactsSize).map { _ ->
|
||||
AddressBookContact(
|
||||
lastUpdated = inputStream.readLong().let { Instant.fromEpochMilliseconds(it) },
|
||||
address = inputStream.readString(),
|
||||
name = inputStream.readString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package co.electriccoin.zcash.ui.common.serialization.addressbook
|
||||
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
internal abstract class BaseAddressBookSerializer {
|
||||
protected fun Int.createByteArray(): ByteArray {
|
||||
return this.toLong().createByteArray()
|
||||
}
|
||||
|
||||
protected fun Long.createByteArray(): ByteArray {
|
||||
return ByteBuffer.allocate(Long.SIZE_BYTES).order(ADDRESS_BOOK_BYTE_ORDER).putLong(this).array()
|
||||
}
|
||||
|
||||
protected fun String.createByteArray(): ByteArray {
|
||||
val byteArray = this.toByteArray()
|
||||
return byteArray.size.createByteArray() + byteArray
|
||||
}
|
||||
|
||||
protected fun InputStream.readInt(): Int {
|
||||
return readLong().toInt()
|
||||
}
|
||||
|
||||
protected fun InputStream.readLong(): Long {
|
||||
val buffer = ByteArray(Long.SIZE_BYTES)
|
||||
require(this.read(buffer) == buffer.size)
|
||||
return ByteBuffer.wrap(buffer).order(ADDRESS_BOOK_BYTE_ORDER).getLong()
|
||||
}
|
||||
|
||||
protected fun InputStream.readString(): String {
|
||||
val size = this.readInt()
|
||||
val buffer = ByteArray(size)
|
||||
require(this.read(buffer) == buffer.size)
|
||||
return String(buffer)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package co.electriccoin.zcash.ui.common.serialization.addressbook
|
||||
|
||||
import java.nio.ByteOrder
|
||||
|
||||
internal const val ADDRESS_BOOK_SERIALIZATION_V1 = 1
|
||||
internal const val ADDRESS_BOOK_ENCRYPTION_V1 = 1
|
||||
internal const val ADDRESS_BOOK_ENCRYPTION_KEY_SIZE = 32
|
||||
internal const val ADDRESS_BOOK_FILE_IDENTIFIER_SIZE = 32
|
||||
internal const val ADDRESS_BOOK_SALT_SIZE = 32
|
||||
internal val ADDRESS_BOOK_BYTE_ORDER = ByteOrder.BIG_ENDIAN
|
|
@ -1,14 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DeleteAddressBookUseCase(
|
||||
private val addressBookRepository: AddressBookRepository
|
||||
) {
|
||||
suspend operator fun invoke() =
|
||||
withContext(Dispatchers.IO) {
|
||||
addressBookRepository.deleteAddressBook()
|
||||
}
|
||||
}
|
|
@ -1,14 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class GetPersistableWalletUseCase(
|
||||
private val walletRepository: WalletRepository
|
||||
) {
|
||||
suspend operator fun invoke() =
|
||||
walletRepository.persistableWallet
|
||||
.filterNotNull()
|
||||
.first()
|
||||
suspend operator fun invoke() = walletRepository.getPersistableWallet()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
|
||||
class ResetAddressBookUseCase(
|
||||
private val addressBookRepository: AddressBookRepository
|
||||
) {
|
||||
suspend operator fun invoke() = addressBookRepository.resetAddressBook()
|
||||
}
|
|
@ -24,9 +24,9 @@ import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
|
|||
import co.electriccoin.zcash.ui.common.repository.BalanceRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeleteAddressBookUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ResetAddressBookUseCase
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
|
||||
import co.electriccoin.zcash.ui.screen.account.ext.getSortHeight
|
||||
|
@ -64,7 +64,7 @@ class WalletViewModel(
|
|||
private val encryptedPreferenceProvider: EncryptedPreferenceProvider,
|
||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||
private val getAvailableServers: GetDefaultServersProvider,
|
||||
private val deleteAddressBookUseCase: DeleteAddressBookUseCase,
|
||||
private val resetAddressBook: ResetAddressBookUseCase,
|
||||
private val isFlexaAvailable: IsFlexaAvailableUseCase,
|
||||
private val getSynchronizer: GetSynchronizerUseCase
|
||||
) : AndroidViewModel(application) {
|
||||
|
@ -227,7 +227,7 @@ class WalletViewModel(
|
|||
val encryptedPrefsCleared =
|
||||
encryptedPreferenceProvider()
|
||||
.clearPreferences()
|
||||
deleteAddressBookUseCase()
|
||||
resetAddressBook()
|
||||
|
||||
Twig.info { "Both preferences cleared: ${standardPrefsCleared && encryptedPrefsCleared}" }
|
||||
|
||||
|
|
Loading…
Reference in New Issue