-
Notifications
You must be signed in to change notification settings - Fork 927
Improve CXF message handling #5982
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,31 @@ | ||
| package com.x8bit.bitwarden.data.vault.manager | ||
|
|
||
| import androidx.credentials.providerevents.exception.ImportCredentialsInvalidJsonException | ||
| import androidx.credentials.providerevents.exception.ImportCredentialsUnknownErrorException | ||
| import com.bitwarden.core.data.util.asFailure | ||
| import com.bitwarden.core.data.util.asSuccess | ||
| import com.bitwarden.core.data.util.decodeFromStringOrNull | ||
| import com.bitwarden.core.data.util.flatMap | ||
| import com.bitwarden.cxf.model.CredentialExchangeExportResponse | ||
| import com.bitwarden.cxf.model.CredentialExchangeProtocolMessage | ||
| import com.bitwarden.network.model.ImportCiphersJsonRequest | ||
| import com.bitwarden.network.model.ImportCiphersResponseJson | ||
| import com.bitwarden.network.service.CiphersService | ||
| import com.bitwarden.network.util.base64UrlDecodeOrNull | ||
| import com.x8bit.bitwarden.data.vault.datasource.sdk.VaultSdkSource | ||
| import com.x8bit.bitwarden.data.vault.manager.model.ImportCxfPayloadResult | ||
| import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult | ||
| import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkCipher | ||
| import kotlinx.serialization.SerializationException | ||
| import kotlinx.serialization.json.Json | ||
|
|
||
| private val SUPPORTED_CXP_FORMAT_VERSIONS = mapOf( | ||
| 0 to setOf(0), | ||
| ) | ||
| private val SUPPORTED_CXF_FORMAT_VERSIONS = mapOf( | ||
| 0 to setOf(0), | ||
| 1 to setOf(0), | ||
| ) | ||
|
|
||
| /** | ||
| * Default implementation of [CredentialExchangeImportManager]. | ||
|
|
@@ -19,58 +34,109 @@ class CredentialExchangeImportManagerImpl( | |
| private val vaultSdkSource: VaultSdkSource, | ||
| private val ciphersService: CiphersService, | ||
| private val vaultSyncManager: VaultSyncManager, | ||
| private val json: Json, | ||
| ) : CredentialExchangeImportManager { | ||
|
|
||
| @Suppress("LongMethod") | ||
| override suspend fun importCxfPayload( | ||
| userId: String, | ||
| payload: String, | ||
| ): ImportCxfPayloadResult = vaultSdkSource | ||
| .importCxf( | ||
| userId = userId, | ||
| payload = payload, | ||
| ) | ||
| .flatMap { cipherList -> | ||
| if (cipherList.isEmpty()) { | ||
| // If no ciphers were returned, we can skip the remaining steps and return the | ||
| // appropriate result. | ||
| return ImportCxfPayloadResult.NoItems | ||
| } | ||
| ciphersService | ||
| .importCiphers( | ||
| request = ImportCiphersJsonRequest( | ||
| ciphers = cipherList.map { | ||
| it.toEncryptedNetworkCipher( | ||
| encryptedFor = userId, | ||
| ) | ||
| }, | ||
| folders = emptyList(), | ||
| folderRelationships = emptyList(), | ||
| ), | ||
| ) | ||
| .flatMap { importCiphersResponseJson -> | ||
| when (importCiphersResponseJson) { | ||
| is ImportCiphersResponseJson.Invalid -> { | ||
| ImportCredentialsUnknownErrorException().asFailure() | ||
| } | ||
| ): ImportCxfPayloadResult { | ||
| val credentialExchangeExportResult = json | ||
| .decodeFromStringOrNull<CredentialExchangeProtocolMessage>(payload) | ||
| ?: return ImportCxfPayloadResult.Error( | ||
| ImportCredentialsInvalidJsonException("Invalid CXP JSON."), | ||
| ) | ||
|
|
||
| if (SUPPORTED_CXP_FORMAT_VERSIONS[credentialExchangeExportResult.version.major] | ||
| ?.contains(credentialExchangeExportResult.version.minor) != true | ||
| ) { | ||
| return ImportCxfPayloadResult.Error( | ||
| ImportCredentialsInvalidJsonException( | ||
| "Unsupported CXF version: ${credentialExchangeExportResult.version}.", | ||
| ), | ||
| ) | ||
| } | ||
|
|
||
| ImportCiphersResponseJson.Success -> { | ||
| ImportCxfPayloadResult | ||
| .Success(itemCount = cipherList.size) | ||
| .asSuccess() | ||
| val decodedPayload = credentialExchangeExportResult.payload | ||
| .base64UrlDecodeOrNull() | ||
| ?: return ImportCxfPayloadResult.Error( | ||
| ImportCredentialsInvalidJsonException("Unable to decode payload."), | ||
| ) | ||
|
|
||
| val exportResponse = json | ||
| .decodeFromStringOrNull<CredentialExchangeExportResponse>(decodedPayload) | ||
| ?: return ImportCxfPayloadResult.Error( | ||
| ImportCredentialsInvalidJsonException("Unable to decode header."), | ||
| ) | ||
|
|
||
| if (SUPPORTED_CXF_FORMAT_VERSIONS[exportResponse.version.major] | ||
| ?.contains(exportResponse.version.minor) != true | ||
| ) { | ||
| return ImportCxfPayloadResult.Error( | ||
| ImportCredentialsInvalidJsonException("Unsupported CXF version."), | ||
| ) | ||
| } | ||
|
|
||
| if (exportResponse.accounts.isEmpty()) { | ||
| return ImportCxfPayloadResult.NoItems | ||
| } | ||
|
|
||
| val accountsJson = try { | ||
| json.encodeToString(exportResponse.accounts.firstOrNull()) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it OK to encode null here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could check it's not empty and exit early. This effectively achieves the same because we're catching
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parsing null will not throw, it will make the string
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah. True. I'll check if it's empty first, in that case. |
||
| } catch (_: SerializationException) { | ||
| return ImportCxfPayloadResult.Error( | ||
| ImportCredentialsInvalidJsonException("Unable to re-encode accounts."), | ||
| ) | ||
| } | ||
| return vaultSdkSource | ||
| .importCxf( | ||
| userId = userId, | ||
| payload = accountsJson, | ||
| ) | ||
| .flatMap { cipherList -> | ||
| if (cipherList.isEmpty()) { | ||
| // If no ciphers were returned, we can skip the remaining steps and return the | ||
| // appropriate result. | ||
| return ImportCxfPayloadResult.NoItems | ||
| } | ||
| ciphersService | ||
| .importCiphers( | ||
| request = ImportCiphersJsonRequest( | ||
| ciphers = cipherList.map { | ||
| it.toEncryptedNetworkCipher( | ||
| encryptedFor = userId, | ||
| ) | ||
| }, | ||
| folders = emptyList(), | ||
| folderRelationships = emptyList(), | ||
| ), | ||
| ) | ||
| .flatMap { importCiphersResponseJson -> | ||
| when (importCiphersResponseJson) { | ||
| is ImportCiphersResponseJson.Invalid -> { | ||
| ImportCredentialsUnknownErrorException().asFailure() | ||
| } | ||
|
|
||
| ImportCiphersResponseJson.Success -> { | ||
| ImportCxfPayloadResult | ||
| .Success(itemCount = cipherList.size) | ||
| .asSuccess() | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| .map { | ||
| when (val syncResult = vaultSyncManager.syncForResult(forced = true)) { | ||
| is SyncVaultDataResult.Success -> it | ||
| is SyncVaultDataResult.Error -> { | ||
| ImportCxfPayloadResult.SyncFailed(error = syncResult.throwable) | ||
| } | ||
| .map { | ||
| when (val syncResult = vaultSyncManager.syncForResult(forced = true)) { | ||
| is SyncVaultDataResult.Success -> it | ||
| is SyncVaultDataResult.Error -> { | ||
| ImportCxfPayloadResult.SyncFailed(error = syncResult.throwable) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| .fold( | ||
| onSuccess = { it }, | ||
| onFailure = { ImportCxfPayloadResult.Error(error = it) }, | ||
| ) | ||
| .fold( | ||
| onSuccess = { it }, | ||
| onFailure = { ImportCxfPayloadResult.Error(error = it) }, | ||
| ) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this work exactly, the major version contains a set of supported minor versions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. This may change later, once we determine how much of the validation and payload mutation we can offload to the SDK. IMO, the SDK should be handling all of this, but it only accepts the
accountJSON at the moment. ๐There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha