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]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Address book encryption
|
||||||
- The device authentication feature on the Zashi app launch has been added
|
- The device authentication feature on the Zashi app launch has been added
|
||||||
- Zashi app now supports Spanish language
|
- Zashi app now supports Spanish language
|
||||||
- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI
|
- 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]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Address book encryption
|
||||||
- The device authentication feature on the Zashi app launch has been added
|
- The device authentication feature on the Zashi app launch has been added
|
||||||
- Zashi app now supports Spanish language
|
- Zashi app now supports Spanish language
|
||||||
- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI
|
- 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]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Address book encryption
|
||||||
- The device authentication feature on the Zashi app launch has been added
|
- The device authentication feature on the Zashi app launch has been added
|
||||||
- Zashi app now supports Spanish language
|
- Zashi app now supports Spanish language
|
||||||
- The Flexa SDK has been adopted to enable payments using the embedded Flexa UI
|
- 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_APP_UPDATE_KTX_VERSION=2.1.0
|
||||||
PLAY_PUBLISHER_API_VERSION=v3-rev20231030-2.0.0
|
PLAY_PUBLISHER_API_VERSION=v3-rev20231030-2.0.0
|
||||||
PLAY_SERVICES_AUTH_VERSION=21.2.0
|
PLAY_SERVICES_AUTH_VERSION=21.2.0
|
||||||
|
TINK_VERSION=1.15.0
|
||||||
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
|
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
|
||||||
ZXING_VERSION=3.5.3
|
ZXING_VERSION=3.5.3
|
||||||
ZIP_321_VERSION = 0.0.6
|
ZIP_321_VERSION = 0.0.6
|
||||||
|
|
|
@ -182,6 +182,7 @@ dependencyResolutionManagement {
|
||||||
val markdownVersion = extra["MARKDOWN_VERSION"].toString()
|
val markdownVersion = extra["MARKDOWN_VERSION"].toString()
|
||||||
val playAppUpdateVersion = extra["PLAY_APP_UPDATE_VERSION"].toString()
|
val playAppUpdateVersion = extra["PLAY_APP_UPDATE_VERSION"].toString()
|
||||||
val playAppUpdateKtxVersion = extra["PLAY_APP_UPDATE_KTX_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 zcashBip39Version = extra["ZCASH_BIP39_VERSION"].toString()
|
||||||
val zcashSdkVersion = extra["ZCASH_SDK_VERSION"].toString()
|
val zcashSdkVersion = extra["ZCASH_SDK_VERSION"].toString()
|
||||||
val zip321Version = extra["ZIP_321_VERSION"].toString()
|
val zip321Version = extra["ZIP_321_VERSION"].toString()
|
||||||
|
@ -254,6 +255,7 @@ dependencyResolutionManagement {
|
||||||
library("markdown", "org.jetbrains:markdown:$markdownVersion")
|
library("markdown", "org.jetbrains:markdown:$markdownVersion")
|
||||||
library("play-update", "com.google.android.play:app-update:$playAppUpdateVersion")
|
library("play-update", "com.google.android.play:app-update:$playAppUpdateVersion")
|
||||||
library("play-update-ktx", "com.google.android.play:app-update-ktx:$playAppUpdateKtxVersion")
|
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", "cash.z.ecc.android:zcash-android-sdk:$zcashSdkVersion")
|
||||||
library("zcash-sdk-incubator", "cash.z.ecc.android:zcash-android-sdk-incubator:$zcashSdkVersion")
|
library("zcash-sdk-incubator", "cash.z.ecc.android:zcash-android-sdk-incubator:$zcashSdkVersion")
|
||||||
library("zcash-bip39", "cash.z.ecc.android:kotlin-bip39:$zcashBip39Version")
|
library("zcash-bip39", "cash.z.ecc.android:kotlin-bip39:$zcashBip39Version")
|
||||||
|
|
|
@ -138,6 +138,7 @@ dependencies {
|
||||||
implementation(libs.zcash.sdk)
|
implementation(libs.zcash.sdk)
|
||||||
implementation(libs.zcash.sdk.incubator)
|
implementation(libs.zcash.sdk.incubator)
|
||||||
implementation(libs.zcash.bip39)
|
implementation(libs.zcash.bip39)
|
||||||
|
implementation(libs.tink)
|
||||||
implementation(libs.zxing)
|
implementation(libs.zxing)
|
||||||
|
|
||||||
api(libs.flexa.core)
|
api(libs.flexa.core)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package co.electriccoin.zcash.di
|
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.AddressBookProvider
|
||||||
import co.electriccoin.zcash.ui.common.provider.AddressBookProviderImpl
|
import co.electriccoin.zcash.ui.common.provider.AddressBookProviderImpl
|
||||||
import co.electriccoin.zcash.ui.common.provider.AddressBookStorageProvider
|
import co.electriccoin.zcash.ui.common.provider.AddressBookStorageProvider
|
||||||
|
@ -20,4 +22,5 @@ val providerModule =
|
||||||
factoryOf(::GetMonetarySeparatorProvider)
|
factoryOf(::GetMonetarySeparatorProvider)
|
||||||
factoryOf(::AddressBookStorageProviderImpl) bind AddressBookStorageProvider::class
|
factoryOf(::AddressBookStorageProviderImpl) bind AddressBookStorageProvider::class
|
||||||
factoryOf(::AddressBookProviderImpl) bind AddressBookProvider::class
|
factoryOf(::AddressBookProviderImpl) bind AddressBookProvider::class
|
||||||
|
factoryOf(::AddressBookKeyStorageProviderImpl) bind AddressBookKeyStorageProvider::class
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package co.electriccoin.zcash.di
|
package co.electriccoin.zcash.di
|
||||||
|
|
||||||
import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase
|
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.DeleteContactUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetBackupPersistableWalletUseCase
|
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.PersistEndpointUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase
|
import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
|
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.SaveContactUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.SendEmailUseCase
|
import co.electriccoin.zcash.ui.common.usecase.SendEmailUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.SendSupportEmailUseCase
|
import co.electriccoin.zcash.ui.common.usecase.SendSupportEmailUseCase
|
||||||
|
@ -59,7 +59,7 @@ val useCaseModule =
|
||||||
factoryOf(::RescanBlockchainUseCase)
|
factoryOf(::RescanBlockchainUseCase)
|
||||||
factoryOf(::GetTransparentAddressUseCase)
|
factoryOf(::GetTransparentAddressUseCase)
|
||||||
factoryOf(::ObserveAddressBookContactsUseCase)
|
factoryOf(::ObserveAddressBookContactsUseCase)
|
||||||
factoryOf(::DeleteAddressBookUseCase)
|
factoryOf(::ResetAddressBookUseCase)
|
||||||
factoryOf(::ValidateContactAddressUseCase)
|
factoryOf(::ValidateContactAddressUseCase)
|
||||||
factoryOf(::ValidateContactNameUseCase)
|
factoryOf(::ValidateContactNameUseCase)
|
||||||
factoryOf(::SaveContactUseCase)
|
factoryOf(::SaveContactUseCase)
|
||||||
|
|
|
@ -1,33 +1,46 @@
|
||||||
package co.electriccoin.zcash.ui.common.datasource
|
package co.electriccoin.zcash.ui.common.datasource
|
||||||
|
|
||||||
|
import co.electriccoin.zcash.spackle.Twig
|
||||||
import co.electriccoin.zcash.spackle.io.deleteSuspend
|
import co.electriccoin.zcash.spackle.io.deleteSuspend
|
||||||
import co.electriccoin.zcash.ui.common.model.AddressBook
|
import co.electriccoin.zcash.ui.common.model.AddressBook
|
||||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
||||||
import co.electriccoin.zcash.ui.common.provider.AddressBookProvider
|
import co.electriccoin.zcash.ui.common.provider.AddressBookProvider
|
||||||
import co.electriccoin.zcash.ui.common.provider.AddressBookStorageProvider
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
|
import java.io.IOException
|
||||||
|
import java.security.GeneralSecurityException
|
||||||
|
|
||||||
interface LocalAddressBookDataSource {
|
interface LocalAddressBookDataSource {
|
||||||
suspend fun getContacts(): AddressBook
|
suspend fun getContacts(addressBookKey: AddressBookKey): AddressBook
|
||||||
|
|
||||||
suspend fun saveContact(
|
suspend fun saveContact(
|
||||||
name: String,
|
name: String,
|
||||||
address: String
|
address: String,
|
||||||
|
addressBookKey: AddressBookKey
|
||||||
): AddressBook
|
): AddressBook
|
||||||
|
|
||||||
suspend fun updateContact(
|
suspend fun updateContact(
|
||||||
contact: AddressBookContact,
|
contact: AddressBookContact,
|
||||||
name: String,
|
name: String,
|
||||||
address: String
|
address: String,
|
||||||
|
addressBookKey: AddressBookKey
|
||||||
): AddressBook
|
): 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(
|
class LocalAddressBookDataSourceImpl(
|
||||||
|
@ -36,20 +49,20 @@ class LocalAddressBookDataSourceImpl(
|
||||||
) : LocalAddressBookDataSource {
|
) : LocalAddressBookDataSource {
|
||||||
private var addressBook: AddressBook? = null
|
private var addressBook: AddressBook? = null
|
||||||
|
|
||||||
override suspend fun getContacts(): AddressBook =
|
override suspend fun getContacts(addressBookKey: AddressBookKey): AddressBook =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val addressBook = this@LocalAddressBookDataSourceImpl.addressBook
|
val addressBook = this@LocalAddressBookDataSourceImpl.addressBook
|
||||||
|
|
||||||
if (addressBook == null) {
|
if (addressBook == null) {
|
||||||
var newAddressBook: AddressBook? = readLocalFileToAddressBook()
|
var newAddressBook: AddressBook? = readLocalFileToAddressBook(addressBookKey)
|
||||||
if (newAddressBook == null) {
|
if (newAddressBook == null) {
|
||||||
newAddressBook =
|
newAddressBook =
|
||||||
AddressBook(
|
AddressBook(
|
||||||
lastUpdated = Clock.System.now(),
|
lastUpdated = Clock.System.now(),
|
||||||
version = 1,
|
version = ADDRESS_BOOK_SERIALIZATION_V1,
|
||||||
contacts = emptyList(),
|
contacts = emptyList(),
|
||||||
)
|
)
|
||||||
writeAddressBookToLocalStorage(newAddressBook)
|
writeAddressBookToLocalStorage(newAddressBook, addressBookKey)
|
||||||
}
|
}
|
||||||
newAddressBook
|
newAddressBook
|
||||||
} else {
|
} else {
|
||||||
|
@ -59,14 +72,15 @@ class LocalAddressBookDataSourceImpl(
|
||||||
|
|
||||||
override suspend fun saveContact(
|
override suspend fun saveContact(
|
||||||
name: String,
|
name: String,
|
||||||
address: String
|
address: String,
|
||||||
|
addressBookKey: AddressBookKey
|
||||||
): AddressBook =
|
): AddressBook =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val lastUpdated = Clock.System.now()
|
val lastUpdated = Clock.System.now()
|
||||||
addressBook =
|
val newAddressBook =
|
||||||
AddressBook(
|
AddressBook(
|
||||||
lastUpdated = lastUpdated,
|
lastUpdated = lastUpdated,
|
||||||
version = 1,
|
version = ADDRESS_BOOK_SERIALIZATION_V1,
|
||||||
contacts =
|
contacts =
|
||||||
addressBook?.contacts.orEmpty() +
|
addressBook?.contacts.orEmpty() +
|
||||||
AddressBookContact(
|
AddressBookContact(
|
||||||
|
@ -75,21 +89,22 @@ class LocalAddressBookDataSourceImpl(
|
||||||
lastUpdated = lastUpdated,
|
lastUpdated = lastUpdated,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
writeAddressBookToLocalStorage(addressBook!!)
|
writeAddressBookToLocalStorage(newAddressBook, addressBookKey)
|
||||||
addressBook!!
|
newAddressBook
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateContact(
|
override suspend fun updateContact(
|
||||||
contact: AddressBookContact,
|
contact: AddressBookContact,
|
||||||
name: String,
|
name: String,
|
||||||
address: String
|
address: String,
|
||||||
|
addressBookKey: AddressBookKey
|
||||||
): AddressBook =
|
): AddressBook =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val lastUpdated = Clock.System.now()
|
val lastUpdated = Clock.System.now()
|
||||||
addressBook =
|
val newAddressBook =
|
||||||
AddressBook(
|
AddressBook(
|
||||||
lastUpdated = lastUpdated,
|
lastUpdated = lastUpdated,
|
||||||
version = 1,
|
version = ADDRESS_BOOK_SERIALIZATION_V1,
|
||||||
contacts =
|
contacts =
|
||||||
addressBook?.contacts.orEmpty().toMutableList()
|
addressBook?.contacts.orEmpty().toMutableList()
|
||||||
.apply {
|
.apply {
|
||||||
|
@ -104,17 +119,20 @@ class LocalAddressBookDataSourceImpl(
|
||||||
}
|
}
|
||||||
.toList(),
|
.toList(),
|
||||||
)
|
)
|
||||||
writeAddressBookToLocalStorage(addressBook!!)
|
writeAddressBookToLocalStorage(newAddressBook, addressBookKey)
|
||||||
addressBook!!
|
newAddressBook
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteContact(addressBookContact: AddressBookContact): AddressBook =
|
override suspend fun deleteContact(
|
||||||
|
addressBookContact: AddressBookContact,
|
||||||
|
addressBookKey: AddressBookKey
|
||||||
|
): AddressBook =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val lastUpdated = Clock.System.now()
|
val lastUpdated = Clock.System.now()
|
||||||
addressBook =
|
val newAddressBook =
|
||||||
AddressBook(
|
AddressBook(
|
||||||
lastUpdated = lastUpdated,
|
lastUpdated = lastUpdated,
|
||||||
version = 1,
|
version = ADDRESS_BOOK_SERIALIZATION_V1,
|
||||||
contacts =
|
contacts =
|
||||||
addressBook?.contacts.orEmpty().toMutableList()
|
addressBook?.contacts.orEmpty().toMutableList()
|
||||||
.apply {
|
.apply {
|
||||||
|
@ -122,27 +140,58 @@ class LocalAddressBookDataSourceImpl(
|
||||||
}
|
}
|
||||||
.toList(),
|
.toList(),
|
||||||
)
|
)
|
||||||
writeAddressBookToLocalStorage(addressBook!!)
|
writeAddressBookToLocalStorage(newAddressBook, addressBookKey)
|
||||||
addressBook!!
|
newAddressBook
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveContacts(contacts: AddressBook) {
|
override suspend fun saveAddressBook(
|
||||||
writeAddressBookToLocalStorage(contacts)
|
addressBook: AddressBook,
|
||||||
this@LocalAddressBookDataSourceImpl.addressBook = contacts
|
addressBookKey: AddressBookKey
|
||||||
|
) {
|
||||||
|
writeAddressBookToLocalStorage(addressBook, addressBookKey)
|
||||||
|
this.addressBook = addressBook
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteAddressBook() {
|
override suspend fun resetAddressBook() {
|
||||||
addressBookStorageProvider.getStorageFile()?.deleteSuspend()
|
|
||||||
addressBook = null
|
addressBook = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readLocalFileToAddressBook(): AddressBook? {
|
@Suppress("ReturnCount")
|
||||||
val file = addressBookStorageProvider.getStorageFile() ?: return null
|
private suspend fun readLocalFileToAddressBook(addressBookKey: AddressBookKey): AddressBook? {
|
||||||
return addressBookProvider.readAddressBookFromFile(file)
|
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) {
|
private fun writeAddressBookToLocalStorage(
|
||||||
val file = addressBookStorageProvider.getOrCreateStorageFile()
|
addressBook: AddressBook,
|
||||||
addressBookProvider.writeAddressBookToFile(file, 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
|
package co.electriccoin.zcash.ui.common.provider
|
||||||
|
|
||||||
import co.electriccoin.zcash.ui.common.model.AddressBook
|
import co.electriccoin.zcash.ui.common.model.AddressBook
|
||||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
import co.electriccoin.zcash.ui.common.serialization.addressbook.AddressBookEncryptor
|
||||||
import kotlinx.datetime.Instant
|
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.File
|
||||||
import java.io.FileOutputStream
|
import kotlin.LazyThreadSafetyMode.NONE
|
||||||
import java.io.InputStream
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.nio.ByteOrder
|
|
||||||
|
|
||||||
interface AddressBookProvider {
|
interface AddressBookProvider {
|
||||||
fun writeAddressBookToFile(
|
fun writeAddressBookToFile(
|
||||||
file: File,
|
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 {
|
class AddressBookProviderImpl : AddressBookProvider {
|
||||||
|
private val addressBookSerializer by lazy(NONE) { AddressBookSerializer() }
|
||||||
|
private val addressBookEncryptor by lazy(NONE) { AddressBookEncryptor() }
|
||||||
|
|
||||||
override fun writeAddressBookToFile(
|
override fun writeAddressBookToFile(
|
||||||
file: File,
|
file: File,
|
||||||
addressBook: AddressBook
|
addressBook: AddressBook,
|
||||||
|
addressBookKey: AddressBookKey
|
||||||
) {
|
) {
|
||||||
file.outputStream().use {
|
file.outputStream().buffered().use { stream ->
|
||||||
serializeAddressBookToByteArrayFile(it, addressBook)
|
addressBookEncryptor.encryptAddressBook(
|
||||||
|
addressBookKey,
|
||||||
|
addressBookSerializer,
|
||||||
|
stream,
|
||||||
|
addressBook
|
||||||
|
)
|
||||||
|
stream.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun readAddressBookFromFile(file: File): AddressBook {
|
override fun readAddressBookFromFile(
|
||||||
return file.inputStream().use {
|
file: File,
|
||||||
deserializeByteArrayFileToAddressBook(it)
|
addressBookKey: AddressBookKey
|
||||||
}
|
): AddressBook {
|
||||||
}
|
return file.inputStream().use { stream ->
|
||||||
|
addressBookEncryptor.decryptAddressBook(
|
||||||
private fun serializeAddressBookToByteArrayFile(
|
addressBookKey,
|
||||||
outputStream: FileOutputStream,
|
addressBookSerializer,
|
||||||
addressBook: AddressBook
|
stream
|
||||||
) {
|
|
||||||
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(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Int.createByteArray(): ByteArray = this.toLong().createByteArray()
|
override fun readLegacyUnencryptedAddressBookFromFile(file: File): AddressBook {
|
||||||
|
return file.inputStream().use { stream ->
|
||||||
private fun Long.createByteArray(): ByteArray =
|
addressBookSerializer.deserializeAddressBook(stream)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val BYTE_ORDER = ByteOrder.BIG_ENDIAN
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
package co.electriccoin.zcash.ui.common.provider
|
package co.electriccoin.zcash.ui.common.provider
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import co.electriccoin.zcash.ui.common.serialization.addressbook.AddressBookKey
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
interface AddressBookStorageProvider {
|
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.
|
* 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(
|
class AddressBookStorageProviderImpl(
|
||||||
private val context: Context
|
private val context: Context
|
||||||
) : AddressBookStorageProvider {
|
) : AddressBookStorageProvider {
|
||||||
override fun getStorageFile(): File? {
|
override fun getStorageFile(addressBookKey: AddressBookKey): File? {
|
||||||
return File(context.noBackupFilesDir, LOCAL_ADDRESS_BOOK_FILE_NAME)
|
return File(context.noBackupFilesDir, addressBookKey.fileIdentifier())
|
||||||
.takeIf { it.exists() && it.isFile }
|
.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)
|
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"
|
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
|
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.spackle.Twig
|
||||||
import co.electriccoin.zcash.ui.common.datasource.LocalAddressBookDataSource
|
import co.electriccoin.zcash.ui.common.datasource.LocalAddressBookDataSource
|
||||||
import co.electriccoin.zcash.ui.common.model.AddressBook
|
import co.electriccoin.zcash.ui.common.model.AddressBook
|
||||||
import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -38,7 +39,7 @@ interface AddressBookRepository {
|
||||||
|
|
||||||
suspend fun deleteContact(contact: AddressBookContact)
|
suspend fun deleteContact(contact: AddressBookContact)
|
||||||
|
|
||||||
suspend fun deleteAddressBook()
|
suspend fun resetAddressBook()
|
||||||
|
|
||||||
// fun onGoogleSignInSuccess()
|
// fun onGoogleSignInSuccess()
|
||||||
//
|
//
|
||||||
|
@ -50,6 +51,8 @@ interface AddressBookRepository {
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class AddressBookRepositoryImpl(
|
class AddressBookRepositoryImpl(
|
||||||
private val localAddressBookDataSource: LocalAddressBookDataSource,
|
private val localAddressBookDataSource: LocalAddressBookDataSource,
|
||||||
|
private val addressBookKeyStorageProvider: AddressBookKeyStorageProvider,
|
||||||
|
private val walletRepository: WalletRepository,
|
||||||
// private val remoteAddressBookDataSource: RemoteAddressBookDataSource,
|
// private val remoteAddressBookDataSource: RemoteAddressBookDataSource,
|
||||||
// private val context: Context
|
// private val context: Context
|
||||||
) : AddressBookRepository {
|
) : AddressBookRepository {
|
||||||
|
@ -92,9 +95,9 @@ class AddressBookRepositoryImpl(
|
||||||
InternalOperation.Delete(contact = contact)
|
InternalOperation.Delete(contact = contact)
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun deleteAddressBook() =
|
override suspend fun resetAddressBook() =
|
||||||
withNonCancellableSemaphore {
|
withNonCancellableSemaphore {
|
||||||
localAddressBookDataSource.deleteAddressBook()
|
localAddressBookDataSource.resetAddressBook()
|
||||||
addressBookCache.update { null }
|
addressBookCache.update { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,12 +153,18 @@ class AddressBookRepositoryImpl(
|
||||||
// }
|
// }
|
||||||
val merged =
|
val merged =
|
||||||
mergeContacts(
|
mergeContacts(
|
||||||
local = localAddressBookDataSource.getContacts(),
|
local =
|
||||||
|
localAddressBookDataSource.getContacts(
|
||||||
|
addressBookKey = getAddressBookKey()
|
||||||
|
),
|
||||||
// remote = remote,
|
// remote = remote,
|
||||||
remote = null,
|
remote = null,
|
||||||
fromOperation = operation
|
fromOperation = operation
|
||||||
)
|
)
|
||||||
localAddressBookDataSource.saveContacts(merged)
|
localAddressBookDataSource.saveAddressBook(
|
||||||
|
addressBook = merged,
|
||||||
|
addressBookKey = getAddressBookKey()
|
||||||
|
)
|
||||||
// executeRemoteAddressBookSafe {
|
// executeRemoteAddressBookSafe {
|
||||||
// remoteAddressBookDataSource.uploadContacts()
|
// remoteAddressBookDataSource.uploadContacts()
|
||||||
// Twig.info { "Address Book: ensureSynchronization - remote address book uploaded" }
|
// Twig.info { "Address Book: ensureSynchronization - remote address book uploaded" }
|
||||||
|
@ -232,12 +241,19 @@ class AddressBookRepositoryImpl(
|
||||||
when (operation) {
|
when (operation) {
|
||||||
is InternalOperation.Delete -> {
|
is InternalOperation.Delete -> {
|
||||||
Twig.info { "Address Book: executeInternalOperation - delete" }
|
Twig.info { "Address Book: executeInternalOperation - delete" }
|
||||||
localAddressBookDataSource.deleteContact(addressBookContact = operation.contact)
|
localAddressBookDataSource.deleteContact(
|
||||||
|
addressBookContact = operation.contact,
|
||||||
|
addressBookKey = getAddressBookKey()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is InternalOperation.Save -> {
|
is InternalOperation.Save -> {
|
||||||
Twig.info { "Address Book: executeInternalOperation - 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 -> {
|
is InternalOperation.Update -> {
|
||||||
|
@ -245,7 +261,8 @@ class AddressBookRepositoryImpl(
|
||||||
localAddressBookDataSource.updateContact(
|
localAddressBookDataSource.updateContact(
|
||||||
contact = operation.contact,
|
contact = operation.contact,
|
||||||
name = operation.name,
|
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")
|
// @Suppress("TooGenericExceptionCaught")
|
||||||
// private suspend fun <T> executeRemoteAddressBookSafe(block: suspend () -> T): T? {
|
// private suspend fun <T> executeRemoteAddressBookSafe(block: suspend () -> T): T? {
|
||||||
// if (hasGoogleDrivePermission().not()) {
|
// if (hasGoogleDrivePermission().not()) {
|
||||||
|
|
|
@ -99,6 +99,8 @@ interface WalletRepository {
|
||||||
suspend fun getAllServers(): List<LightWalletEndpoint>
|
suspend fun getAllServers(): List<LightWalletEndpoint>
|
||||||
|
|
||||||
suspend fun getSynchronizer(): Synchronizer
|
suspend fun getSynchronizer(): Synchronizer
|
||||||
|
|
||||||
|
suspend fun getPersistableWallet(): PersistableWallet
|
||||||
}
|
}
|
||||||
|
|
||||||
class WalletRepositoryImpl(
|
class WalletRepositoryImpl(
|
||||||
|
@ -355,6 +357,8 @@ class WalletRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSynchronizer(): Synchronizer = synchronizer.filterNotNull().first()
|
override suspend fun getSynchronizer(): Synchronizer = synchronizer.filterNotNull().first()
|
||||||
|
|
||||||
|
override suspend fun getPersistableWallet(): PersistableWallet = persistableWallet.filterNotNull().first()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Synchronizer.toCommonError(): Flow<SynchronizerError?> =
|
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
|
package co.electriccoin.zcash.ui.common.usecase
|
||||||
|
|
||||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
|
|
||||||
class GetPersistableWalletUseCase(
|
class GetPersistableWalletUseCase(
|
||||||
private val walletRepository: WalletRepository
|
private val walletRepository: WalletRepository
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke() =
|
suspend operator fun invoke() = walletRepository.getPersistableWallet()
|
||||||
walletRepository.persistableWallet
|
|
||||||
.filterNotNull()
|
|
||||||
.first()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.BalanceRepository
|
||||||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
||||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
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.GetSynchronizerUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase
|
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.preference.StandardPreferenceKeys
|
||||||
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
|
import co.electriccoin.zcash.ui.screen.account.ext.TransactionOverviewExt
|
||||||
import co.electriccoin.zcash.ui.screen.account.ext.getSortHeight
|
import co.electriccoin.zcash.ui.screen.account.ext.getSortHeight
|
||||||
|
@ -64,7 +64,7 @@ class WalletViewModel(
|
||||||
private val encryptedPreferenceProvider: EncryptedPreferenceProvider,
|
private val encryptedPreferenceProvider: EncryptedPreferenceProvider,
|
||||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||||
private val getAvailableServers: GetDefaultServersProvider,
|
private val getAvailableServers: GetDefaultServersProvider,
|
||||||
private val deleteAddressBookUseCase: DeleteAddressBookUseCase,
|
private val resetAddressBook: ResetAddressBookUseCase,
|
||||||
private val isFlexaAvailable: IsFlexaAvailableUseCase,
|
private val isFlexaAvailable: IsFlexaAvailableUseCase,
|
||||||
private val getSynchronizer: GetSynchronizerUseCase
|
private val getSynchronizer: GetSynchronizerUseCase
|
||||||
) : AndroidViewModel(application) {
|
) : AndroidViewModel(application) {
|
||||||
|
@ -227,7 +227,7 @@ class WalletViewModel(
|
||||||
val encryptedPrefsCleared =
|
val encryptedPrefsCleared =
|
||||||
encryptedPreferenceProvider()
|
encryptedPreferenceProvider()
|
||||||
.clearPreferences()
|
.clearPreferences()
|
||||||
deleteAddressBookUseCase()
|
resetAddressBook()
|
||||||
|
|
||||||
Twig.info { "Both preferences cleared: ${standardPrefsCleared && encryptedPrefsCleared}" }
|
Twig.info { "Both preferences cleared: ${standardPrefsCleared && encryptedPrefsCleared}" }
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue