Metadata encryption keys (#1781)
* Metadata encryption keys * Decryption hotfix * Transaction detail bottom bar design hotfix * Auto backup enabled for metadata
This commit is contained in:
parent
13695bbd97
commit
2c37d086f0
|
@ -3,4 +3,7 @@
|
|||
<include
|
||||
domain="file"
|
||||
path="address_book/." />
|
||||
<include
|
||||
domain="file"
|
||||
path="metadata/." />
|
||||
</full-backup-content>
|
|
@ -4,5 +4,8 @@
|
|||
<include
|
||||
domain="file"
|
||||
path="address_book/." />
|
||||
<include
|
||||
domain="file"
|
||||
path="metadata/." />
|
||||
</cloud-backup>
|
||||
</data-extraction-rules>
|
|
@ -11,6 +11,11 @@ interface PreferenceProvider {
|
|||
value: String?
|
||||
)
|
||||
|
||||
suspend fun putStringSet(
|
||||
key: PreferenceKey,
|
||||
value: Set<String>?
|
||||
)
|
||||
|
||||
suspend fun putLong(
|
||||
key: PreferenceKey,
|
||||
value: Long?
|
||||
|
@ -20,6 +25,8 @@ interface PreferenceProvider {
|
|||
|
||||
suspend fun getString(key: PreferenceKey): String?
|
||||
|
||||
suspend fun getStringSet(key: PreferenceKey): Set<String>?
|
||||
|
||||
fun observe(key: PreferenceKey): Flow<String?>
|
||||
|
||||
suspend fun clearPreferences(): Boolean
|
||||
|
|
|
@ -15,6 +15,10 @@ class MockPreferenceProvider(
|
|||
|
||||
override suspend fun getString(key: PreferenceKey) = map[key.key]
|
||||
|
||||
override suspend fun getStringSet(key: PreferenceKey): Set<String>? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
// For the mock implementation, does not support observability of changes
|
||||
override fun observe(key: PreferenceKey): Flow<String?> = flow { emit(getString(key)) }
|
||||
|
||||
|
@ -32,6 +36,13 @@ class MockPreferenceProvider(
|
|||
map[key.key] = value
|
||||
}
|
||||
|
||||
override suspend fun putStringSet(
|
||||
key: PreferenceKey,
|
||||
value: Set<String>?
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun putLong(
|
||||
key: PreferenceKey,
|
||||
value: Long?
|
||||
|
|
|
@ -59,6 +59,22 @@ class AndroidPreferenceProvider private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override suspend fun putStringSet(
|
||||
key: PreferenceKey,
|
||||
value: Set<String>?
|
||||
) = withContext(dispatcher) {
|
||||
mutex.withLock {
|
||||
val editor = sharedPreferences.edit()
|
||||
|
||||
editor.putStringSet(key.key, value)
|
||||
|
||||
editor.commit()
|
||||
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override suspend fun putLong(
|
||||
key: PreferenceKey,
|
||||
|
@ -91,6 +107,11 @@ class AndroidPreferenceProvider private constructor(
|
|||
sharedPreferences.getString(key.key, null)
|
||||
}
|
||||
|
||||
override suspend fun getStringSet(key: PreferenceKey): Set<String>? =
|
||||
withContext(dispatcher) {
|
||||
sharedPreferences.getStringSet(key.key, null)
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override suspend fun clearPreferences() =
|
||||
withContext(dispatcher) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.design.component
|
|||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
@ -22,6 +23,7 @@ import co.electriccoin.zcash.ui.design.util.stringRes
|
|||
fun ZashiBottomBar(
|
||||
isElevated: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
|
@ -29,7 +31,9 @@ fun ZashiBottomBar(
|
|||
color = ZashiColors.Surfaces.bgPrimary,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
content()
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
|
|
@ -36,13 +36,17 @@ fun Modifier.scaffoldScrollPadding(
|
|||
)
|
||||
|
||||
@Stable
|
||||
fun PaddingValues.asScaffoldPaddingValues() =
|
||||
PaddingValues(
|
||||
top = calculateTopPadding() + ZashiDimensions.Spacing.spacingLg,
|
||||
bottom = calculateBottomPadding() + ZashiDimensions.Spacing.spacing3xl,
|
||||
start = ZashiDimensions.Spacing.spacing3xl,
|
||||
end = ZashiDimensions.Spacing.spacing3xl
|
||||
)
|
||||
fun PaddingValues.asScaffoldPaddingValues(
|
||||
top: Dp = calculateTopPadding() + ZashiDimensions.Spacing.spacingLg,
|
||||
bottom: Dp = calculateBottomPadding() + ZashiDimensions.Spacing.spacing3xl,
|
||||
start: Dp = ZashiDimensions.Spacing.spacing3xl,
|
||||
end: Dp = ZashiDimensions.Spacing.spacing3xl
|
||||
) = PaddingValues(
|
||||
top = top,
|
||||
bottom = bottom,
|
||||
start = start,
|
||||
end = end,
|
||||
)
|
||||
|
||||
@Stable
|
||||
fun PaddingValues.asScaffoldScrollPaddingValues(
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package co.electriccoin.zcash.ui.common.datasource
|
||||
|
||||
import cash.z.ecc.android.sdk.model.AccountUuid
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.model.AccountMetadata
|
||||
import co.electriccoin.zcash.ui.common.model.AnnotationMetadata
|
||||
|
@ -8,6 +7,7 @@ import co.electriccoin.zcash.ui.common.model.BookmarkMetadata
|
|||
import co.electriccoin.zcash.ui.common.model.Metadata
|
||||
import co.electriccoin.zcash.ui.common.provider.MetadataProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.MetadataStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.serialization.METADATA_SERIALIZATION_V1
|
||||
import co.electriccoin.zcash.ui.common.serialization.metada.MetadataKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
@ -20,26 +20,22 @@ interface MetadataDataSource {
|
|||
|
||||
suspend fun flipTxAsBookmarked(
|
||||
txId: String,
|
||||
account: AccountUuid,
|
||||
key: MetadataKey
|
||||
): Metadata
|
||||
|
||||
suspend fun createOrUpdateTxNote(
|
||||
txId: String,
|
||||
note: String,
|
||||
account: AccountUuid,
|
||||
key: MetadataKey
|
||||
): Metadata
|
||||
|
||||
suspend fun deleteTxNote(
|
||||
txId: String,
|
||||
account: AccountUuid,
|
||||
key: MetadataKey
|
||||
): Metadata
|
||||
|
||||
suspend fun markTxMemoAsRead(
|
||||
txId: String,
|
||||
account: AccountUuid,
|
||||
key: MetadataKey
|
||||
): Metadata
|
||||
|
||||
|
@ -47,8 +43,6 @@ interface MetadataDataSource {
|
|||
metadata: Metadata,
|
||||
key: MetadataKey
|
||||
)
|
||||
|
||||
suspend fun resetMetadata()
|
||||
}
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
|
@ -56,8 +50,6 @@ class MetadataDataSourceImpl(
|
|||
private val metadataStorageProvider: MetadataStorageProvider,
|
||||
private val metadataProvider: MetadataProvider,
|
||||
) : MetadataDataSource {
|
||||
private var metadata: Metadata? = null
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
override suspend fun getMetadata(key: MetadataKey): Metadata =
|
||||
|
@ -67,11 +59,10 @@ class MetadataDataSourceImpl(
|
|||
|
||||
override suspend fun flipTxAsBookmarked(
|
||||
txId: String,
|
||||
account: AccountUuid,
|
||||
key: MetadataKey,
|
||||
): Metadata =
|
||||
mutex.withLock {
|
||||
updateMetadataBookmark(txId = txId, account = account, key = key) {
|
||||
updateMetadataBookmark(txId = txId, key = key) {
|
||||
it.copy(
|
||||
isBookmarked = !it.isBookmarked,
|
||||
lastUpdated = Instant.now(),
|
||||
|
@ -82,13 +73,11 @@ class MetadataDataSourceImpl(
|
|||
override suspend fun createOrUpdateTxNote(
|
||||
txId: String,
|
||||
note: String,
|
||||
account: AccountUuid,
|
||||
key: MetadataKey
|
||||
): Metadata =
|
||||
mutex.withLock {
|
||||
updateMetadataAnnotation(
|
||||
txId = txId,
|
||||
account = account,
|
||||
key = key
|
||||
) {
|
||||
it.copy(
|
||||
|
@ -100,13 +89,11 @@ class MetadataDataSourceImpl(
|
|||
|
||||
override suspend fun deleteTxNote(
|
||||
txId: String,
|
||||
account: AccountUuid,
|
||||
key: MetadataKey
|
||||
): Metadata =
|
||||
mutex.withLock {
|
||||
updateMetadataAnnotation(
|
||||
txId = txId,
|
||||
account = account,
|
||||
key = key
|
||||
) {
|
||||
it.copy(
|
||||
|
@ -118,12 +105,10 @@ class MetadataDataSourceImpl(
|
|||
|
||||
override suspend fun markTxMemoAsRead(
|
||||
txId: String,
|
||||
account: AccountUuid,
|
||||
key: MetadataKey
|
||||
): Metadata =
|
||||
mutex.withLock {
|
||||
updateMetadata(
|
||||
account = account,
|
||||
key = key,
|
||||
transform = { metadata ->
|
||||
metadata.copy(
|
||||
|
@ -138,14 +123,8 @@ class MetadataDataSourceImpl(
|
|||
key: MetadataKey
|
||||
) = mutex.withLock {
|
||||
writeToLocalStorage(metadata, key)
|
||||
this.metadata = metadata
|
||||
}
|
||||
|
||||
override suspend fun resetMetadata() =
|
||||
mutex.withLock {
|
||||
metadata = null
|
||||
}
|
||||
|
||||
private suspend fun getMetadataInternal(key: MetadataKey): Metadata {
|
||||
fun readLocalFileToMetadata(key: MetadataKey): Metadata? {
|
||||
val encryptedFile =
|
||||
|
@ -158,46 +137,38 @@ class MetadataDataSourceImpl(
|
|||
}
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
val inMemory = metadata
|
||||
|
||||
if (inMemory == null) {
|
||||
var new: Metadata? = readLocalFileToMetadata(key)
|
||||
if (new == null) {
|
||||
new =
|
||||
Metadata(
|
||||
version = 1,
|
||||
lastUpdated = Instant.now(),
|
||||
accountMetadata = emptyMap(),
|
||||
).also {
|
||||
this@MetadataDataSourceImpl.metadata = it
|
||||
}
|
||||
writeToLocalStorage(new, key)
|
||||
}
|
||||
new
|
||||
} else {
|
||||
inMemory
|
||||
var new: Metadata? = readLocalFileToMetadata(key)
|
||||
if (new == null) {
|
||||
new =
|
||||
Metadata(
|
||||
version = METADATA_SERIALIZATION_V1,
|
||||
lastUpdated = Instant.now(),
|
||||
accountMetadata = defaultAccountMetadata(),
|
||||
)
|
||||
writeToLocalStorage(new, key)
|
||||
}
|
||||
new
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun writeToLocalStorage(
|
||||
metadata: Metadata,
|
||||
key: MetadataKey
|
||||
) = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val file = metadataStorageProvider.getOrCreateStorageFile(key)
|
||||
metadataProvider.writeMetadataToFile(file, metadata, key)
|
||||
}.onFailure { e -> Twig.warn(e) { "Failed to write address book" } }
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val file = metadataStorageProvider.getOrCreateStorageFile(key)
|
||||
metadataProvider.writeMetadataToFile(file, metadata, key)
|
||||
}.onFailure { e -> Twig.warn(e) { "Failed to write address book" } }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateMetadataAnnotation(
|
||||
txId: String,
|
||||
account: AccountUuid,
|
||||
key: MetadataKey,
|
||||
transform: (AnnotationMetadata) -> AnnotationMetadata
|
||||
): Metadata {
|
||||
return updateMetadata(
|
||||
account = account,
|
||||
key = key,
|
||||
transform = { metadata ->
|
||||
metadata.copy(
|
||||
|
@ -217,12 +188,10 @@ class MetadataDataSourceImpl(
|
|||
|
||||
private suspend fun updateMetadataBookmark(
|
||||
txId: String,
|
||||
account: AccountUuid,
|
||||
key: MetadataKey,
|
||||
transform: (BookmarkMetadata) -> BookmarkMetadata
|
||||
): Metadata {
|
||||
return updateMetadata(
|
||||
account = account,
|
||||
key = key,
|
||||
transform = { metadata ->
|
||||
metadata.copy(
|
||||
|
@ -240,30 +209,21 @@ class MetadataDataSourceImpl(
|
|||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private suspend fun updateMetadata(
|
||||
account: AccountUuid,
|
||||
key: MetadataKey,
|
||||
transform: (AccountMetadata) -> AccountMetadata
|
||||
): Metadata {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val metadata = getMetadataInternal(key)
|
||||
|
||||
val accountMetadata = metadata.accountMetadata[account.value.toHexString()] ?: defaultAccountMetadata()
|
||||
val accountMetadata = metadata.accountMetadata
|
||||
|
||||
val updatedMetadata =
|
||||
metadata.copy(
|
||||
lastUpdated = Instant.now(),
|
||||
accountMetadata =
|
||||
metadata.accountMetadata
|
||||
.toMutableMap()
|
||||
.apply {
|
||||
put(account.value.toHexString(), transform(accountMetadata))
|
||||
}
|
||||
.toMap()
|
||||
accountMetadata = transform(accountMetadata)
|
||||
)
|
||||
|
||||
this@MetadataDataSourceImpl.metadata = updatedMetadata
|
||||
writeToLocalStorage(updatedMetadata, key)
|
||||
|
||||
updatedMetadata
|
||||
|
|
|
@ -13,7 +13,7 @@ data class Metadata(
|
|||
@Serializable(InstantSerializer::class)
|
||||
val lastUpdated: Instant,
|
||||
@SerialName("accountMetadata")
|
||||
val accountMetadata: Map<String, AccountMetadata>
|
||||
val accountMetadata: AccountMetadata
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
|
@ -2,8 +2,8 @@ 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.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.serialization.metada.MetadataKey
|
||||
import com.google.crypto.tink.InsecureSecretKeyAccess
|
||||
import com.google.crypto.tink.SecretKeyAccess
|
||||
|
@ -12,9 +12,12 @@ import kotlin.io.encoding.Base64
|
|||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
|
||||
interface MetadataKeyStorageProvider {
|
||||
suspend fun get(): MetadataKey?
|
||||
suspend fun get(account: WalletAccount): MetadataKey?
|
||||
|
||||
suspend fun store(key: MetadataKey)
|
||||
suspend fun store(
|
||||
key: MetadataKey,
|
||||
account: WalletAccount
|
||||
)
|
||||
}
|
||||
|
||||
class MetadataKeyStorageProviderImpl(
|
||||
|
@ -22,40 +25,74 @@ class MetadataKeyStorageProviderImpl(
|
|||
) : MetadataKeyStorageProvider {
|
||||
private val default = MetadataKeyPreferenceDefault()
|
||||
|
||||
override suspend fun get(): MetadataKey? {
|
||||
return default.getValue(encryptedPreferenceProvider())
|
||||
override suspend fun get(account: WalletAccount): MetadataKey? {
|
||||
return default.getValue(
|
||||
walletAccount = account,
|
||||
preferenceProvider = encryptedPreferenceProvider(),
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun store(key: MetadataKey) {
|
||||
default.putValue(encryptedPreferenceProvider(), key)
|
||||
override suspend fun store(
|
||||
key: MetadataKey,
|
||||
account: WalletAccount
|
||||
) {
|
||||
default.putValue(
|
||||
newValue = key,
|
||||
walletAccount = account,
|
||||
preferenceProvider = encryptedPreferenceProvider(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class MetadataKeyPreferenceDefault : PreferenceDefault<MetadataKey?> {
|
||||
private class MetadataKeyPreferenceDefault {
|
||||
private val secretKeyAccess: SecretKeyAccess?
|
||||
get() = InsecureSecretKeyAccess.get()
|
||||
|
||||
override val key: PreferenceKey = PreferenceKey("metadata_key")
|
||||
|
||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) = preferenceProvider.getString(key)?.decode()
|
||||
|
||||
override suspend fun putValue(
|
||||
suspend fun getValue(
|
||||
walletAccount: WalletAccount,
|
||||
preferenceProvider: PreferenceProvider,
|
||||
newValue: MetadataKey?
|
||||
) = preferenceProvider.putString(key, newValue?.encode())
|
||||
): MetadataKey? {
|
||||
return preferenceProvider.getStringSet(
|
||||
key = getKey(walletAccount)
|
||||
)?.decode()
|
||||
}
|
||||
|
||||
suspend fun putValue(
|
||||
newValue: MetadataKey?,
|
||||
walletAccount: WalletAccount,
|
||||
preferenceProvider: PreferenceProvider,
|
||||
) {
|
||||
preferenceProvider.putStringSet(
|
||||
key = getKey(walletAccount),
|
||||
value = newValue?.encode()
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private fun getKey(walletAccount: WalletAccount): PreferenceKey =
|
||||
PreferenceKey("metadata_key_${walletAccount.sdkAccount.accountUuid.value.toHexString()}")
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
private fun MetadataKey?.encode() =
|
||||
if (this != null) {
|
||||
Base64.encode(this.key.toByteArray(secretKeyAccess))
|
||||
private fun MetadataKey?.encode(): Set<String>? {
|
||||
return if (this != null) {
|
||||
setOfNotNull(
|
||||
Base64.encode(this.encryptionBytes.toByteArray(secretKeyAccess)),
|
||||
this.decryptionBytes?.let { Base64.encode(it.toByteArray(secretKeyAccess)) }
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
private fun String?.decode() =
|
||||
private fun Set<String>?.decode() =
|
||||
if (this != null) {
|
||||
MetadataKey(SecretBytes.copyFrom(Base64.decode(this), secretKeyAccess))
|
||||
MetadataKey(
|
||||
encryptionBytes = SecretBytes.copyFrom(Base64.decode(this.toList()[0]), secretKeyAccess),
|
||||
decryptionBytes =
|
||||
this.toList().getOrNull(1)
|
||||
?.let { SecretBytes.copyFrom(Base64.decode(it), secretKeyAccess) },
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
|
@ -1,26 +1,29 @@
|
|||
package co.electriccoin.zcash.ui.common.repository
|
||||
|
||||
import cash.z.ecc.android.sdk.model.AccountUuid
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.MetadataDataSource
|
||||
import co.electriccoin.zcash.ui.common.model.Metadata
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.provider.MetadataKeyStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
|
||||
import co.electriccoin.zcash.ui.common.serialization.metada.MetadataKey
|
||||
import co.electriccoin.zcash.ui.util.CloseableScopeHolder
|
||||
import co.electriccoin.zcash.ui.util.CloseableScopeHolderImpl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.onSubscription
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface MetadataRepository {
|
||||
val metadata: Flow<Metadata?>
|
||||
|
@ -36,125 +39,158 @@ interface MetadataRepository {
|
|||
|
||||
suspend fun markTxMemoAsRead(txId: String)
|
||||
|
||||
suspend fun resetMetadata()
|
||||
|
||||
fun observeTransactionMetadataByTxId(txId: String): Flow<TransactionMetadata?>
|
||||
}
|
||||
|
||||
class MetadataRepositoryImpl(
|
||||
private val accountDataSource: AccountDataSource,
|
||||
private val metadataDataSource: MetadataDataSource,
|
||||
private val metadataKeyStorageProvider: MetadataKeyStorageProvider,
|
||||
private val accountDataSource: AccountDataSource,
|
||||
private val persistableWalletProvider: PersistableWalletProvider,
|
||||
) : MetadataRepository {
|
||||
private val semaphore = Mutex()
|
||||
|
||||
private val cache = MutableStateFlow<Metadata?>(null)
|
||||
) : MetadataRepository, CloseableScopeHolder by CloseableScopeHolderImpl(Dispatchers.IO) {
|
||||
private val command = Channel<Command>()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val metadata: Flow<Metadata?> =
|
||||
cache
|
||||
.onSubscription {
|
||||
withNonCancellableSemaphore {
|
||||
ensureSynchronization()
|
||||
accountDataSource
|
||||
.selectedAccount
|
||||
.flatMapLatest { account ->
|
||||
channelFlow {
|
||||
send(null)
|
||||
|
||||
if (account != null) {
|
||||
val metadataKey = getMetadataKey(account)
|
||||
send(metadataDataSource.getMetadata(metadataKey))
|
||||
|
||||
launch {
|
||||
command
|
||||
.receiveAsFlow()
|
||||
.filter {
|
||||
it.account.sdkAccount.accountUuid == account.sdkAccount.accountUuid
|
||||
}
|
||||
.collect { command ->
|
||||
val new =
|
||||
when (command) {
|
||||
is Command.CreateOrUpdateTxNote ->
|
||||
metadataDataSource.createOrUpdateTxNote(
|
||||
txId = command.txId,
|
||||
key = metadataKey,
|
||||
note = command.note
|
||||
)
|
||||
|
||||
is Command.DeleteTxNote ->
|
||||
metadataDataSource.deleteTxNote(
|
||||
txId = command.txId,
|
||||
key = metadataKey,
|
||||
)
|
||||
|
||||
is Command.FlipTxBookmark ->
|
||||
metadataDataSource.flipTxAsBookmarked(
|
||||
txId = command.txId,
|
||||
key = metadataKey,
|
||||
)
|
||||
|
||||
is Command.MarkTxMemoAsRead ->
|
||||
metadataDataSource.markTxMemoAsRead(
|
||||
txId = command.txId,
|
||||
key = metadataKey,
|
||||
)
|
||||
}
|
||||
|
||||
send(new)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
override suspend fun flipTxBookmark(txId: String) =
|
||||
mutateMetadata {
|
||||
metadataDataSource.flipTxAsBookmarked(
|
||||
txId = txId,
|
||||
key = getMetadataKey(),
|
||||
account = accountDataSource.getSelectedAccount().sdkAccount.accountUuid
|
||||
override suspend fun flipTxBookmark(txId: String) {
|
||||
scope.launch {
|
||||
command.send(
|
||||
Command.FlipTxBookmark(
|
||||
txId = txId,
|
||||
account = accountDataSource.getSelectedAccount()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun createOrUpdateTxNote(
|
||||
txId: String,
|
||||
note: String
|
||||
) = mutateMetadata {
|
||||
metadataDataSource.createOrUpdateTxNote(
|
||||
txId = txId,
|
||||
note = note,
|
||||
key = getMetadataKey(),
|
||||
account = accountDataSource.getSelectedAccount().sdkAccount.accountUuid
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun deleteTxNote(txId: String) =
|
||||
mutateMetadata {
|
||||
metadataDataSource.deleteTxNote(
|
||||
txId = txId,
|
||||
key = getMetadataKey(),
|
||||
account = accountDataSource.getSelectedAccount().sdkAccount.accountUuid
|
||||
) {
|
||||
scope.launch {
|
||||
command.send(
|
||||
Command.CreateOrUpdateTxNote(
|
||||
txId = txId,
|
||||
note = note,
|
||||
account = accountDataSource.getSelectedAccount()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteTxNote(txId: String) {
|
||||
scope.launch {
|
||||
command.send(
|
||||
Command.DeleteTxNote(
|
||||
txId = txId,
|
||||
account = accountDataSource.getSelectedAccount()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun markTxMemoAsRead(txId: String) {
|
||||
scope.launch {
|
||||
command.send(
|
||||
Command.MarkTxMemoAsRead(
|
||||
txId = txId,
|
||||
account = accountDataSource.getSelectedAccount()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun markTxMemoAsRead(txId: String) =
|
||||
mutateMetadata {
|
||||
metadataDataSource.markTxMemoAsRead(
|
||||
txId = txId,
|
||||
key = getMetadataKey(),
|
||||
account = accountDataSource.getSelectedAccount().sdkAccount.accountUuid
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun resetMetadata() {
|
||||
withNonCancellableSemaphore {
|
||||
metadataDataSource.resetMetadata()
|
||||
cache.update { null }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override fun observeTransactionMetadataByTxId(txId: String): Flow<TransactionMetadata?> =
|
||||
combine<Metadata?, AccountUuid, TransactionMetadata?>(
|
||||
metadata,
|
||||
accountDataSource.selectedAccount.filterNotNull().map { it.sdkAccount.accountUuid }.distinctUntilChanged()
|
||||
) { metadata, account ->
|
||||
val accountMetadata = metadata?.accountMetadata?.get(account.value.toHexString())
|
||||
metadata
|
||||
.map<Metadata?, TransactionMetadata?> { metadata ->
|
||||
val accountMetadata = metadata?.accountMetadata
|
||||
|
||||
TransactionMetadata(
|
||||
isBookmarked = accountMetadata?.bookmarked?.find { it.txId == txId }?.isBookmarked == true,
|
||||
isRead = accountMetadata?.read?.any { it == txId } == true,
|
||||
note = accountMetadata?.annotations?.find { it.txId == txId }?.content,
|
||||
)
|
||||
}.distinctUntilChanged().onStart { emit(null) }
|
||||
TransactionMetadata(
|
||||
isBookmarked = accountMetadata?.bookmarked?.find { it.txId == txId }?.isBookmarked == true,
|
||||
isRead = accountMetadata?.read?.any { it == txId } == true,
|
||||
note = accountMetadata?.annotations?.find { it.txId == txId }?.content,
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onStart { emit(null) }
|
||||
|
||||
private suspend fun ensureSynchronization() {
|
||||
if (cache.value == null) {
|
||||
val metadata = metadataDataSource.getMetadata(key = getMetadataKey())
|
||||
metadataDataSource.save(metadata = metadata, key = getMetadataKey())
|
||||
cache.update { metadata }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun mutateMetadata(block: suspend () -> Metadata) =
|
||||
withNonCancellableSemaphore {
|
||||
ensureSynchronization()
|
||||
val new = block()
|
||||
cache.update { new }
|
||||
}
|
||||
|
||||
private suspend fun withNonCancellableSemaphore(block: suspend () -> Unit) =
|
||||
withContext(NonCancellable + Dispatchers.Default) {
|
||||
semaphore.withLock { block() }
|
||||
}
|
||||
|
||||
private suspend fun getMetadataKey(): MetadataKey {
|
||||
val key = metadataKeyStorageProvider.get()
|
||||
private suspend fun getMetadataKey(selectedAccount: WalletAccount): MetadataKey {
|
||||
val key = metadataKeyStorageProvider.get(selectedAccount)
|
||||
|
||||
return if (key != null) {
|
||||
key
|
||||
} else {
|
||||
val account = accountDataSource.getZashiAccount()
|
||||
val persistableWallet = persistableWalletProvider.getPersistableWallet()
|
||||
val zashiAccount = accountDataSource.getZashiAccount()
|
||||
val newKey =
|
||||
MetadataKey.derive(
|
||||
seedPhrase = persistableWallet.seedPhrase,
|
||||
network = persistableWallet.network,
|
||||
account = account
|
||||
zashiAccount = zashiAccount,
|
||||
selectedAccount = selectedAccount
|
||||
)
|
||||
metadataKeyStorageProvider.store(newKey)
|
||||
metadataKeyStorageProvider.store(newKey, selectedAccount)
|
||||
newKey
|
||||
}
|
||||
}
|
||||
|
@ -165,3 +201,28 @@ data class TransactionMetadata(
|
|||
val isRead: Boolean,
|
||||
val note: String?
|
||||
)
|
||||
|
||||
private sealed interface Command {
|
||||
val account: WalletAccount
|
||||
|
||||
data class FlipTxBookmark(
|
||||
val txId: String,
|
||||
override val account: WalletAccount
|
||||
) : Command
|
||||
|
||||
data class CreateOrUpdateTxNote(
|
||||
val txId: String,
|
||||
val note: String,
|
||||
override val account: WalletAccount
|
||||
) : Command
|
||||
|
||||
data class DeleteTxNote(
|
||||
val txId: String,
|
||||
override val account: WalletAccount
|
||||
) : Command
|
||||
|
||||
data class MarkTxMemoAsRead(
|
||||
val txId: String,
|
||||
override val account: WalletAccount
|
||||
) : Command
|
||||
}
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.common.serialization
|
||||
|
||||
import com.google.crypto.tink.subtle.ChaCha20Poly1305
|
||||
import com.google.crypto.tink.subtle.Random
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
interface Encryptor<KEY : Key, T> {
|
||||
fun encrypt(
|
||||
key: KEY,
|
||||
outputStream: OutputStream,
|
||||
data: T
|
||||
)
|
||||
|
||||
fun decrypt(
|
||||
key: KEY,
|
||||
inputStream: InputStream
|
||||
): T
|
||||
}
|
||||
|
||||
abstract class BaseEncryptor<KEY : Key, T> : BaseSerializer(), Encryptor<KEY, T> {
|
||||
abstract val version: Int
|
||||
abstract val saltSize: Int
|
||||
|
||||
protected abstract fun serialize(
|
||||
outputStream: ByteArrayOutputStream,
|
||||
data: T
|
||||
)
|
||||
|
||||
protected abstract fun deserialize(inputStream: ByteArrayInputStream): T
|
||||
|
||||
override fun encrypt(
|
||||
key: KEY,
|
||||
outputStream: OutputStream,
|
||||
data: T
|
||||
) {
|
||||
// Generate a fresh one-time key for this ciphertext.
|
||||
val salt = Random.randBytes(saltSize)
|
||||
val cipherText =
|
||||
ByteArrayOutputStream()
|
||||
.use { stream ->
|
||||
serialize(stream, data)
|
||||
stream.toByteArray()
|
||||
}.let {
|
||||
val derivedKey = key.deriveEncryptionKey(salt)
|
||||
// Tink encodes the ciphertext as `nonce || ciphertext || tag`.
|
||||
val cipher = ChaCha20Poly1305.create(derivedKey)
|
||||
cipher.encrypt(it, null)
|
||||
}
|
||||
|
||||
outputStream.write(version.createByteArray())
|
||||
outputStream.write(salt)
|
||||
outputStream.write(cipherText)
|
||||
}
|
||||
|
||||
override fun decrypt(
|
||||
key: KEY,
|
||||
inputStream: InputStream
|
||||
): T {
|
||||
val version = inputStream.readInt()
|
||||
if (version != this.version) {
|
||||
throw UnknownEncryptionVersionException()
|
||||
}
|
||||
|
||||
val salt = ByteArray(saltSize)
|
||||
require(inputStream.read(salt) == salt.size) { "Input is too short" }
|
||||
|
||||
val ciphertext = inputStream.readBytes()
|
||||
|
||||
val derivedKey = key.deriveEncryptionKey(salt)
|
||||
val cipher = ChaCha20Poly1305.create(derivedKey)
|
||||
val plaintext = cipher.decrypt(ciphertext, null)
|
||||
|
||||
return plaintext.inputStream().use { stream ->
|
||||
deserialize(stream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UnknownEncryptionVersionException : RuntimeException("Unknown encryption version")
|
|
@ -0,0 +1,3 @@
|
|||
package co.electriccoin.zcash.ui.common.serialization
|
||||
|
||||
class UnknownEncryptionVersionException : RuntimeException("Unknown encryption version")
|
|
@ -3,27 +3,89 @@ package co.electriccoin.zcash.ui.common.serialization.addressbook
|
|||
import co.electriccoin.zcash.ui.common.model.AddressBook
|
||||
import co.electriccoin.zcash.ui.common.serialization.ADDRESS_BOOK_ENCRYPTION_V1
|
||||
import co.electriccoin.zcash.ui.common.serialization.ADDRESS_BOOK_SALT_SIZE
|
||||
import co.electriccoin.zcash.ui.common.serialization.BaseEncryptor
|
||||
import co.electriccoin.zcash.ui.common.serialization.Encryptor
|
||||
import co.electriccoin.zcash.ui.common.serialization.BaseSerializer
|
||||
import co.electriccoin.zcash.ui.common.serialization.UnknownEncryptionVersionException
|
||||
import com.google.crypto.tink.subtle.ChaCha20Poly1305
|
||||
import com.google.crypto.tink.subtle.Random
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
interface AddressBookEncryptor : Encryptor<AddressBookKey, AddressBook>
|
||||
interface AddressBookEncryptor {
|
||||
fun encrypt(
|
||||
key: AddressBookKey,
|
||||
outputStream: OutputStream,
|
||||
data: AddressBook
|
||||
)
|
||||
|
||||
fun decrypt(
|
||||
key: AddressBookKey,
|
||||
inputStream: InputStream
|
||||
): AddressBook
|
||||
}
|
||||
|
||||
class AddressBookEncryptorImpl(
|
||||
private val addressBookSerializer: AddressBookSerializer,
|
||||
) : AddressBookEncryptor, BaseEncryptor<AddressBookKey, AddressBook>() {
|
||||
override val version: Int = ADDRESS_BOOK_ENCRYPTION_V1
|
||||
override val saltSize: Int = ADDRESS_BOOK_SALT_SIZE
|
||||
) : AddressBookEncryptor, BaseSerializer() {
|
||||
private val version: Int = ADDRESS_BOOK_ENCRYPTION_V1
|
||||
private val saltSize: Int = ADDRESS_BOOK_SALT_SIZE
|
||||
|
||||
override fun serialize(
|
||||
override fun encrypt(
|
||||
key: AddressBookKey,
|
||||
outputStream: OutputStream,
|
||||
data: AddressBook
|
||||
) {
|
||||
// Generate a fresh one-time key for this ciphertext.
|
||||
val salt = Random.randBytes(saltSize)
|
||||
val cipherText =
|
||||
ByteArrayOutputStream()
|
||||
.use { stream ->
|
||||
serialize(stream, data)
|
||||
stream.toByteArray()
|
||||
}.let {
|
||||
val derivedKey = key.deriveEncryptionKey(salt)
|
||||
// Tink encodes the ciphertext as `nonce || ciphertext || tag`.
|
||||
val cipher = ChaCha20Poly1305.create(derivedKey)
|
||||
cipher.encrypt(it, null)
|
||||
}
|
||||
|
||||
outputStream.write(version.createByteArray())
|
||||
outputStream.write(salt)
|
||||
outputStream.write(cipherText)
|
||||
}
|
||||
|
||||
override fun decrypt(
|
||||
key: AddressBookKey,
|
||||
inputStream: InputStream
|
||||
): AddressBook {
|
||||
val version = inputStream.readInt()
|
||||
if (version != this.version) {
|
||||
throw UnknownEncryptionVersionException()
|
||||
}
|
||||
|
||||
val salt = ByteArray(saltSize)
|
||||
require(inputStream.read(salt) == salt.size) { "Input is too short" }
|
||||
|
||||
val ciphertext = inputStream.readBytes()
|
||||
|
||||
val derivedKey = key.deriveEncryptionKey(salt)
|
||||
val cipher = ChaCha20Poly1305.create(derivedKey)
|
||||
val plaintext = cipher.decrypt(ciphertext, null)
|
||||
|
||||
return plaintext.inputStream().use { stream ->
|
||||
deserialize(stream)
|
||||
}
|
||||
}
|
||||
|
||||
private fun serialize(
|
||||
outputStream: ByteArrayOutputStream,
|
||||
data: AddressBook
|
||||
) {
|
||||
addressBookSerializer.serializeAddressBook(outputStream, data)
|
||||
}
|
||||
|
||||
override fun deserialize(inputStream: ByteArrayInputStream): AddressBook {
|
||||
private fun deserialize(inputStream: ByteArrayInputStream): AddressBook {
|
||||
return addressBookSerializer.deserializeAddressBook(inputStream)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,38 @@
|
|||
package co.electriccoin.zcash.ui.common.serialization.metada
|
||||
|
||||
import co.electriccoin.zcash.ui.common.model.Metadata
|
||||
import co.electriccoin.zcash.ui.common.serialization.BaseEncryptor
|
||||
import co.electriccoin.zcash.ui.common.serialization.Encryptor
|
||||
import co.electriccoin.zcash.ui.common.serialization.BaseSerializer
|
||||
import co.electriccoin.zcash.ui.common.serialization.METADATA_ENCRYPTION_V1
|
||||
import co.electriccoin.zcash.ui.common.serialization.METADATA_SALT_SIZE
|
||||
import co.electriccoin.zcash.ui.common.serialization.UnknownEncryptionVersionException
|
||||
import com.google.crypto.tink.aead.ChaCha20Poly1305Key
|
||||
import com.google.crypto.tink.subtle.ChaCha20Poly1305
|
||||
import com.google.crypto.tink.subtle.Random
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
interface MetadataEncryptor : Encryptor<MetadataKey, Metadata>
|
||||
interface MetadataEncryptor {
|
||||
fun encrypt(
|
||||
key: MetadataKey,
|
||||
outputStream: OutputStream,
|
||||
data: Metadata
|
||||
)
|
||||
|
||||
@Throws(DecryptionException::class)
|
||||
fun decrypt(
|
||||
key: MetadataKey,
|
||||
inputStream: InputStream
|
||||
): Metadata
|
||||
}
|
||||
|
||||
class MetadataEncryptorImpl(
|
||||
private val metadataSerializer: MetadataSerializer,
|
||||
) : MetadataEncryptor, BaseEncryptor<MetadataKey, Metadata>() {
|
||||
override val version: Int = METADATA_ENCRYPTION_V1
|
||||
override val saltSize: Int = METADATA_SALT_SIZE
|
||||
|
||||
override fun serialize(
|
||||
outputStream: ByteArrayOutputStream,
|
||||
data: Metadata
|
||||
) {
|
||||
metadataSerializer.serialize(outputStream, data)
|
||||
}
|
||||
|
||||
override fun deserialize(inputStream: ByteArrayInputStream): Metadata {
|
||||
return metadataSerializer.deserialize(inputStream)
|
||||
}
|
||||
) : MetadataEncryptor, BaseSerializer() {
|
||||
private val version: Int = METADATA_ENCRYPTION_V1
|
||||
private val saltSize: Int = METADATA_SALT_SIZE
|
||||
|
||||
override fun encrypt(
|
||||
key: MetadataKey,
|
||||
|
@ -72,12 +74,34 @@ class MetadataEncryptorImpl(
|
|||
|
||||
val ciphertext = inputStream.readBytes()
|
||||
|
||||
val derivedKey = key.deriveEncryptionKey(salt)
|
||||
val cipher = ChaCha20Poly1305.create(derivedKey)
|
||||
val plaintext = cipher.decrypt(ciphertext, null)
|
||||
return decrypt(key.deriveFirstDecryptionKey(salt), ciphertext)
|
||||
?: decrypt(key.deriveSecondDecryptionKey(salt), ciphertext)
|
||||
?: throw DecryptionException()
|
||||
}
|
||||
|
||||
return plaintext.inputStream().use { stream ->
|
||||
deserialize(stream)
|
||||
}
|
||||
private fun decrypt(
|
||||
key: ChaCha20Poly1305Key?,
|
||||
ciphertext: ByteArray
|
||||
): Metadata? {
|
||||
if (key == null) return null
|
||||
|
||||
return runCatching {
|
||||
val cipher = ChaCha20Poly1305.create(key)
|
||||
val plaintext = cipher.decrypt(ciphertext, null)
|
||||
plaintext.inputStream().use { stream -> deserialize(stream) }
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun serialize(
|
||||
outputStream: ByteArrayOutputStream,
|
||||
data: Metadata
|
||||
) {
|
||||
metadataSerializer.serialize(outputStream, data)
|
||||
}
|
||||
|
||||
private fun deserialize(inputStream: ByteArrayInputStream): Metadata {
|
||||
return metadataSerializer.deserialize(inputStream)
|
||||
}
|
||||
}
|
||||
|
||||
class DecryptionException : Exception()
|
||||
|
|
|
@ -3,8 +3,9 @@ package co.electriccoin.zcash.ui.common.serialization.metada
|
|||
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 co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.serialization.Key
|
||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||
import co.electriccoin.zcash.ui.common.serialization.METADATA_ENCRYPTION_KEY_SIZE
|
||||
import co.electriccoin.zcash.ui.common.serialization.METADATA_FILE_IDENTIFIER_SIZE
|
||||
import co.electriccoin.zcash.ui.common.serialization.METADATA_SALT_SIZE
|
||||
|
@ -16,17 +17,20 @@ import com.google.crypto.tink.util.SecretBytes
|
|||
/**
|
||||
* The long-term key that can decrypt an account's encrypted address book.
|
||||
*/
|
||||
class MetadataKey(val key: SecretBytes) : Key {
|
||||
class MetadataKey(
|
||||
val encryptionBytes: SecretBytes,
|
||||
val decryptionBytes: SecretBytes?
|
||||
) {
|
||||
/**
|
||||
* Derives the filename that this key is able to decrypt.
|
||||
*/
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override fun fileIdentifier(): String {
|
||||
fun fileIdentifier(): String {
|
||||
val access = InsecureSecretKeyAccess.get()
|
||||
val fileIdentifier =
|
||||
Hkdf.computeHkdf(
|
||||
"HMACSHA256",
|
||||
key.toByteArray(access),
|
||||
encryptionBytes.toByteArray(access),
|
||||
null,
|
||||
"file_identifier".toByteArray(),
|
||||
METADATA_FILE_IDENTIFIER_SIZE
|
||||
|
@ -34,19 +38,13 @@ class MetadataKey(val key: SecretBytes) : Key {
|
|||
return "zashi-metadata-" + 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.
|
||||
*/
|
||||
override fun deriveEncryptionKey(salt: ByteArray): ChaCha20Poly1305Key {
|
||||
fun deriveEncryptionKey(salt: ByteArray): ChaCha20Poly1305Key {
|
||||
assert(salt.size == METADATA_SALT_SIZE)
|
||||
val access = InsecureSecretKeyAccess.get()
|
||||
val subKey =
|
||||
Hkdf.computeHkdf(
|
||||
"HMACSHA256",
|
||||
key.toByteArray(access),
|
||||
encryptionBytes.toByteArray(access),
|
||||
null,
|
||||
salt + "encryption_key".toByteArray(),
|
||||
METADATA_ENCRYPTION_KEY_SIZE
|
||||
|
@ -54,28 +52,64 @@ class MetadataKey(val key: SecretBytes) : Key {
|
|||
return ChaCha20Poly1305Key.create(SecretBytes.copyFrom(subKey, access))
|
||||
}
|
||||
|
||||
fun deriveFirstDecryptionKey(salt: ByteArray): ChaCha20Poly1305Key {
|
||||
return deriveDecryptionkey(salt, encryptionBytes, "encryption_key")
|
||||
}
|
||||
|
||||
fun deriveSecondDecryptionKey(salt: ByteArray): ChaCha20Poly1305Key? {
|
||||
if (decryptionBytes == null) return null
|
||||
return deriveDecryptionkey(salt, decryptionBytes, "decryption_key")
|
||||
}
|
||||
|
||||
private fun deriveDecryptionkey(
|
||||
salt: ByteArray,
|
||||
decryptionBytes: SecretBytes,
|
||||
infoKey: String
|
||||
): ChaCha20Poly1305Key {
|
||||
assert(salt.size == METADATA_SALT_SIZE)
|
||||
val access = InsecureSecretKeyAccess.get()
|
||||
val subKey =
|
||||
Hkdf.computeHkdf(
|
||||
"HMACSHA256",
|
||||
decryptionBytes.toByteArray(access),
|
||||
null,
|
||||
salt + infoKey.toByteArray(),
|
||||
METADATA_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: WalletAccount
|
||||
zashiAccount: ZashiAccount,
|
||||
selectedAccount: WalletAccount
|
||||
): MetadataKey {
|
||||
val key =
|
||||
DerivationTool.getInstance().deriveArbitraryAccountKey(
|
||||
contextString = "ZashiMetadataEncryptionV1".toByteArray(),
|
||||
seed = seedPhrase.toByteArray(),
|
||||
network = network,
|
||||
accountIndex = account.hdAccountIndex,
|
||||
)
|
||||
return MetadataKey(SecretBytes.copyFrom(key, InsecureSecretKeyAccess.get()))
|
||||
DerivationTool.getInstance()
|
||||
.deriveAccountMetadataKey(
|
||||
seed = seedPhrase.toByteArray(),
|
||||
network = network,
|
||||
accountIndex = zashiAccount.hdAccountIndex,
|
||||
)
|
||||
.derivePrivateUseMetadataKey(
|
||||
ufvk =
|
||||
when (selectedAccount) {
|
||||
is KeystoneAccount -> selectedAccount.sdkAccount.ufvk
|
||||
is ZashiAccount -> null
|
||||
},
|
||||
network = network,
|
||||
privateUseSubject = "metadata".toByteArray()
|
||||
)
|
||||
return MetadataKey(
|
||||
encryptionBytes = SecretBytes.copyFrom(key[0], InsecureSecretKeyAccess.get()),
|
||||
decryptionBytes =
|
||||
key.getOrNull(1)
|
||||
?.let {
|
||||
SecretBytes.copyFrom(it, InsecureSecretKeyAccess.get())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.AddressBookRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.MetadataRepository
|
||||
|
||||
class ResetInMemoryDataUseCase(
|
||||
private val addressBookRepository: AddressBookRepository,
|
||||
private val metadataRepository: MetadataRepository
|
||||
) {
|
||||
suspend operator fun invoke() {
|
||||
addressBookRepository.resetAddressBook()
|
||||
metadataRepository.resetMetadata()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation
|
|||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.util.asScaffoldPaddingValues
|
||||
import co.electriccoin.zcash.ui.design.util.orDark
|
||||
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
|
@ -143,7 +144,7 @@ private fun BottomBar(
|
|||
) {
|
||||
ZashiBottomBar(
|
||||
isElevated = scrollState.value > 0,
|
||||
modifier = Modifier.scaffoldPadding(paddingValues, top = 0.dp, bottom = 0.dp)
|
||||
contentPadding = paddingValues.asScaffoldPaddingValues(top = 0.dp, bottom = 0.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
|
|
Loading…
Reference in New Issue