improvement: Show actual JSON when a cipher fails the decoding #224

This commit is contained in:
Artem Chepurnoy 2024-03-27 17:20:21 +02:00
parent 1d170fdaeb
commit a1210b4ef8
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
7 changed files with 177 additions and 78 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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;
}

View File

@ -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)
}

View File

@ -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 <reified T> createDecodingFailedServiceModel(
now: Instant,
model: T,
lens: SyncManager.Lens<T>,
): 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<FolderEntity>(
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<CipherEntity>(
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<CollectionEntity>(
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<CollectionEntity>(
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<OrganizationEntity>(
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<OrganizationEntity>(
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<SyncSends>(
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<SyncSends>(
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,

View File

@ -10,7 +10,7 @@ class SyncManager<Local : BitwardenService.Has<Local>, Remote : Any>(
private val local: LensLocal<Local>,
private val remote: Lens<Remote>,
) {
class Lens<T>(
data class Lens<T>(
val getId: (T) -> String,
val getRevisionDate: (T) -> Instant,
val getDeletedDate: (T) -> Instant? = { null },

View File

@ -82,6 +82,7 @@ class SyncByTokenImpl(
val syncEngine = SyncEngine(
httpClient = httpClient,
dbManager = db,
json = json,
base64Service = base64Service,
cryptoGenerator = cryptoGenerator,
cipherEncryptor = cipherEncryptor,