improvement: Show actual JSON when a cipher fails the decoding #224
This commit is contained in:
parent
1d170fdaeb
commit
a1210b4ef8
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 },
|
||||
|
@ -82,6 +82,7 @@ class SyncByTokenImpl(
|
||||
val syncEngine = SyncEngine(
|
||||
httpClient = httpClient,
|
||||
dbManager = db,
|
||||
json = json,
|
||||
base64Service = base64Service,
|
||||
cryptoGenerator = cryptoGenerator,
|
||||
cipherEncryptor = cipherEncryptor,
|
||||
|
Loading…
x
Reference in New Issue
Block a user