Skip to content

Commit

Permalink
Address book encryption (#1643)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Honza <[email protected]>
  • Loading branch information
3 people authored Nov 15, 2024
1 parent f59add8 commit 6aee0e2
Show file tree
Hide file tree
Showing 23 changed files with 500 additions and 152 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/whatsNew/WHATS_NEW_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/whatsNew/WHATS_NEW_ES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions ui-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -20,4 +22,5 @@ val providerModule =
factoryOf(::GetMonetarySeparatorProvider)
factoryOf(::AddressBookStorageProviderImpl) bind AddressBookStorageProvider::class
factoryOf(::AddressBookProviderImpl) bind AddressBookProvider::class
factoryOf(::AddressBookKeyStorageProviderImpl) bind AddressBookKeyStorageProvider::class
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -59,7 +59,7 @@ val useCaseModule =
factoryOf(::RescanBlockchainUseCase)
factoryOf(::GetTransparentAddressUseCase)
factoryOf(::ObserveAddressBookContactsUseCase)
factoryOf(::DeleteAddressBookUseCase)
factoryOf(::ResetAddressBookUseCase)
factoryOf(::ValidateContactAddressUseCase)
factoryOf(::ValidateContactNameUseCase)
factoryOf(::SaveContactUseCase)
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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 {
Expand All @@ -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(
Expand All @@ -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 {
Expand All @@ -104,45 +119,79 @@ 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 {
remove(addressBookContact)
}
.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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 6aee0e2

Please sign in to comment.