From a1210b4ef84be901a071c99aaa48412610eb37e4 Mon Sep 17 00:00:00 2001 From: Artem Chepurnoy Date: Wed, 27 Mar 2024 17:20:21 +0200 Subject: [PATCH] improvement: Show actual JSON when a cipher fails the decoding #224 --- .../core/store/bitwarden/BitwardenService.kt | 1 + .../vault/component/VaultViewErrorItem.kt | 86 ++++++++++ .../feature/home/vault/model/VaultViewItem.kt | 2 + .../vault/screen/VaultViewStateProducer.kt | 12 ++ .../provider/bitwarden/api/SyncEngine.kt | 151 +++++++++--------- .../provider/bitwarden/sync/SyncManager.kt | 2 +- .../usecase/internal/SyncByTokenImpl.kt | 1 + 7 files changed, 177 insertions(+), 78 deletions(-) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenService.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenService.kt index 4b98879..4cceda8 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenService.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenService.kt @@ -31,6 +31,7 @@ data class BitwardenService( data class Error( val code: Int, val message: String? = null, + val blob: String? = null, val revisionDate: Instant, ) { companion object { diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewErrorItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewErrorItem.kt index 051d64b..1ed2ec2 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewErrorItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/component/VaultViewErrorItem.kt @@ -1,8 +1,12 @@ package com.artemchep.keyguard.feature.home.vault.component +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn @@ -11,6 +15,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AccessTime +import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.Sync import androidx.compose.material3.Icon @@ -24,7 +29,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.artemchep.keyguard.feature.home.vault.model.VaultViewItem import com.artemchep.keyguard.ui.DisabledEmphasisAlpha import com.artemchep.keyguard.ui.ExpandedIfNotEmpty @@ -122,6 +131,83 @@ fun VaultViewErrorItem( .width(8.dp), ) } + if (item.blob != null) Row( + modifier = Modifier + .padding(vertical = 8.dp) + .height(IntrinsicSize.Min) + .background( + color = LocalContentColor.current + .combineAlpha(0.1f), + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer( + modifier = Modifier + .width(8.dp), + ) + Box( + modifier = Modifier + .width(8.dp) + .background( + color = LocalContentColor.current + .combineAlpha(MediumEmphasisAlpha), + ) + .fillMaxHeight(), + ) + Spacer( + modifier = Modifier + .width(8.dp), + ) + Column( + modifier = Modifier + .weight(1f), + ) { + Text( + modifier = modifier + .padding( + vertical = 8.dp, + ), + text = "BLOB", + style = MaterialTheme.typography.labelLarge, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = LocalContentColor.current + .combineAlpha(MediumEmphasisAlpha), + ) + Text( + modifier = Modifier + .padding(), + text = item.blob, + maxLines = 3, + fontFamily = FontFamily.Monospace, + fontSize = 13.sp, + overflow = TextOverflow.Ellipsis, + ) + Spacer( + modifier = Modifier + .height(8.dp), + ) + } + Spacer( + modifier = Modifier + .width(8.dp), + ) + val updatedOnCopy by rememberUpdatedState(item.onCopyBlob) + IconButton( + onClick = { + updatedOnCopy?.invoke() + }, + ) { + Icon( + imageVector = Icons.Outlined.ContentCopy, + contentDescription = null, + ) + } + Spacer( + modifier = Modifier + .width(8.dp), + ) + } HorizontalDivider() Row( modifier = Modifier diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/model/VaultViewItem.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/model/VaultViewItem.kt index d601842..9d18efb 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/model/VaultViewItem.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/model/VaultViewItem.kt @@ -111,8 +111,10 @@ sealed interface VaultViewItem { override val id: String, val name: String, val message: String?, + val blob: String? = null, val timestamp: String, val onRetry: (() -> Unit)? = null, + val onCopyBlob: (() -> Unit)? = null, ) : VaultViewItem { companion object; } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultViewStateProducer.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultViewStateProducer.kt index 1f33784..e5f04cf 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultViewStateProducer.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/feature/home/vault/screen/VaultViewStateProducer.kt @@ -819,6 +819,7 @@ private fun RememberStateFlowScope.oh( id = "error", name = "Couldn't sync the item", message = cipherError.message(), + blob = cipherError.blob, timestamp = time, onRetry = if (cipherError.canRetry(cipher.revisionDate)) { // lambda @@ -830,6 +831,17 @@ private fun RememberStateFlowScope.oh( } else { null }, + onCopyBlob = if (cipherError.blob != null) { + // lambda + { + copy.copy( + text = cipherError.blob, + hidden = false, + ) + } + } else { + null + }, ) emit(model) } diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt index 793c293..60c7f53 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/SyncEngine.kt @@ -58,6 +58,8 @@ import io.ktor.client.HttpClient import io.ktor.client.call.NoTransformationFoundException import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import kotlin.RuntimeException import kotlin.String import kotlin.TODO @@ -73,6 +75,7 @@ import kotlin.to class SyncEngine( private val httpClient: HttpClient, private val dbManager: DatabaseManager, + private val json: Json, private val base64Service: Base64Service, private val cryptoGenerator: CryptoGenerator, private val cipherEncryptor: CipherEncryptor, @@ -89,6 +92,32 @@ class SyncEngine( class DecodeVaultException(message: String, e: Throwable) : RuntimeException(message, e) + private inline fun createDecodingFailedServiceModel( + now: Instant, + model: T, + lens: SyncManager.Lens, + ): BitwardenService { + val revisionDate = lens.getRevisionDate(model) + .takeUnless { it == Instant.DISTANT_FUTURE } + ?: now + + val blob = json.encodeToString(model) + return BitwardenService( + remote = BitwardenService.Remote( + id = lens.getId(model), + revisionDate = revisionDate, + deletedDate = lens.getDeletedDate(model), + ), + error = BitwardenService.Error( + code = BitwardenService.Error.CODE_DECODING_FAILED, + blob = blob, + revisionDate = now, + ), + deleted = false, + version = BitwardenService.VERSION, + ) + } + suspend fun sync() = kotlin.run { val env = user.env.back() val api = env.api @@ -264,6 +293,10 @@ class SyncEngine( } val folderDao = db.folderQueries + val folderRemoteLens = SyncManager.Lens( + getId = { it.id }, + getRevisionDate = { it.revisionDate }, + ) val existingFolders = folderDao .getByAccountId( accountId = user.id, @@ -345,18 +378,10 @@ class SyncEngine( val localId = localOrNull?.folderId ?: cryptoGenerator.uuid() - val service = BitwardenService( - remote = BitwardenService.Remote( - id = remote.id, - revisionDate = remote.revisionDate, - deletedDate = null, - ), - error = BitwardenService.Error( - code = BitwardenService.Error.CODE_DECODING_FAILED, - revisionDate = now, - ), - deleted = false, - version = BitwardenService.VERSION, + val service = createDecodingFailedServiceModel( + now = now, + model = remote, + lens = folderRemoteLens, ) val model = localOrNull?.copy(service = service) ?: BitwardenFolder( accountId = user.id, @@ -457,6 +482,11 @@ class SyncEngine( } val cipherDao = db.cipherQueries + val cipherRemoteLens = SyncManager.Lens( + getId = { it.id }, + getRevisionDate = { cipher -> cipher.revisionDate }, + getDeletedDate = { cipher -> cipher.deletedDate }, + ) val existingCipher = cipherDao .getByAccountId( accountId = user.id, @@ -530,11 +560,7 @@ class SyncEngine( false }, remoteItems = response.ciphers.orEmpty(), - remoteLens = SyncManager.Lens( - getId = { it.id }, - getRevisionDate = { cipher -> cipher.revisionDate }, - getDeletedDate = { cipher -> cipher.deletedDate }, - ), + remoteLens = cipherRemoteLens, remoteDecoder = { remote, local -> val codec = getCodec( mode = BitwardenCrCta.Mode.DECRYPT, @@ -571,18 +597,10 @@ class SyncEngine( val localId = localOrNull?.cipherId ?: cryptoGenerator.uuid() val folderId = remote.folderId?.let { remoteToLocalFolders[it] } - val service = BitwardenService( - remote = BitwardenService.Remote( - id = remote.id, - revisionDate = remote.revisionDate, - deletedDate = remote.deletedDate, - ), - error = BitwardenService.Error( - code = BitwardenService.Error.CODE_DECODING_FAILED, - revisionDate = now, - ), - deleted = false, - version = BitwardenService.VERSION, + val service = createDecodingFailedServiceModel( + now = now, + model = remote, + lens = cipherRemoteLens, ) val model = localOrNull?.copy(service = service) ?: BitwardenCipher( accountId = user.id, @@ -732,6 +750,10 @@ class SyncEngine( } val collectionDao = db.collectionQueries + val collectionRemoteLens = SyncManager.Lens( + getId = { it.id }, + getRevisionDate = { Instant.DISTANT_FUTURE }, + ) val existingCollections = collectionDao .getByAccountId( accountId = user.id, @@ -772,10 +794,7 @@ class SyncEngine( } }, remoteItems = response.collections.orEmpty(), - remoteLens = SyncManager.Lens( - getId = { it.id }, - getRevisionDate = { Instant.DISTANT_FUTURE }, - ), + remoteLens = collectionRemoteLens, remoteDecoder = { remote, local -> val codec = getCodec( mode = BitwardenCrCta.Mode.DECRYPT, @@ -796,18 +815,10 @@ class SyncEngine( ) recordException(logE) - val service = BitwardenService( - remote = BitwardenService.Remote( - id = remote.id, - revisionDate = now, - deletedDate = null, - ), - error = BitwardenService.Error( - code = BitwardenService.Error.CODE_DECODING_FAILED, - revisionDate = now, - ), - deleted = false, - version = BitwardenService.VERSION, + val service = createDecodingFailedServiceModel( + now = now, + model = remote, + lens = collectionRemoteLens, ) val model = localOrNull?.copy(service = service) ?: BitwardenCollection( accountId = user.id, @@ -848,6 +859,10 @@ class SyncEngine( } val organizationDao = db.organizationQueries + val organizationRemoteLens = SyncManager.Lens( + getId = { it.id }, + getRevisionDate = { Instant.DISTANT_FUTURE }, + ) val existingOrganizations = organizationDao .getByAccountId( accountId = user.id, @@ -888,10 +903,7 @@ class SyncEngine( } }, remoteItems = response.profile.organizations.orEmpty(), - remoteLens = SyncManager.Lens( - getId = { it.id }, - getRevisionDate = { Instant.DISTANT_FUTURE }, - ), + remoteLens = organizationRemoteLens, remoteDecoder = { remote, local -> val codec = getCodec( mode = BitwardenCrCta.Mode.DECRYPT, @@ -911,18 +923,10 @@ class SyncEngine( ) recordException(logE) - val service = BitwardenService( - remote = BitwardenService.Remote( - id = remote.id, - revisionDate = now, - deletedDate = null, - ), - error = BitwardenService.Error( - code = BitwardenService.Error.CODE_DECODING_FAILED, - revisionDate = now, - ), - deleted = false, - version = BitwardenService.VERSION, + val service = createDecodingFailedServiceModel( + now = now, + model = remote, + lens = organizationRemoteLens, ) val model = localOrNull?.copy(service = service) ?: BitwardenOrganization( accountId = user.id, @@ -965,6 +969,10 @@ class SyncEngine( } val sendDao = db.sendQueries + val sendRemoteLens = SyncManager.Lens( + getId = { it.id }, + getRevisionDate = { it.revisionDate }, + ) val existingSends = sendDao .getByAccountId( accountId = user.id, @@ -1025,10 +1033,7 @@ class SyncEngine( } }, remoteItems = response.sends.orEmpty(), - remoteLens = SyncManager.Lens( - getId = { it.id }, - getRevisionDate = { it.revisionDate }, - ), + remoteLens = sendRemoteLens, remoteDecoder = { remote, local -> val ( itemCrypto, @@ -1053,18 +1058,10 @@ class SyncEngine( }, remoteDecodedFallback = { remote, localOrNull, e -> e.printStackTrace() - val service = BitwardenService( - remote = BitwardenService.Remote( - id = remote.id, - revisionDate = remote.revisionDate, - deletedDate = null, - ), - error = BitwardenService.Error( - code = BitwardenService.Error.CODE_DECODING_FAILED, - revisionDate = now, - ), - deleted = false, - version = BitwardenService.VERSION, + val service = createDecodingFailedServiceModel( + now = now, + model = remote, + lens = sendRemoteLens, ) val model = localOrNull?.copy(service = service) ?: BitwardenSend( accountId = user.id, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/sync/SyncManager.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/sync/SyncManager.kt index 9222266..968b404 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/sync/SyncManager.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/sync/SyncManager.kt @@ -10,7 +10,7 @@ class SyncManager, Remote : Any>( private val local: LensLocal, private val remote: Lens, ) { - class Lens( + data class Lens( val getId: (T) -> String, val getRevisionDate: (T) -> Instant, val getDeletedDate: (T) -> Instant? = { null }, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/SyncByTokenImpl.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/SyncByTokenImpl.kt index ffb0650..1ba3aae 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/SyncByTokenImpl.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/internal/SyncByTokenImpl.kt @@ -82,6 +82,7 @@ class SyncByTokenImpl( val syncEngine = SyncEngine( httpClient = httpClient, dbManager = db, + json = json, base64Service = base64Service, cryptoGenerator = cryptoGenerator, cipherEncryptor = cipherEncryptor,