From e3be565699f3ef41b21ae688c1ecf80a6994a632 Mon Sep 17 00:00:00 2001 From: Artem Chepurnoy Date: Thu, 28 Mar 2024 13:31:35 +0200 Subject: [PATCH] improvement: Support new per-cipher encryption key --- .../core/store/bitwarden/BitwardenCipher.kt | 1 + .../provider/bitwarden/api/SyncEngine.kt | 89 +++++++++++++++++-- .../bitwarden/api/builder/ServerEnvApi.kt | 2 +- .../bitwarden/crypto/BitwardenCrypto.kt | 6 ++ .../provider/bitwarden/crypto/CipherCrypto.kt | 20 +++-- .../bitwarden/crypto/CipherDecoder.kt | 1 + .../provider/bitwarden/entity/CipherEntity.kt | 3 + .../bitwarden/entity/request/CipherRequest.kt | 4 + .../provider/bitwarden/usecase/AddCipher.kt | 16 ++++ 9 files changed, 125 insertions(+), 17 deletions(-) diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenCipher.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenCipher.kt index 11e367b..ed77f79 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenCipher.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/core/store/bitwarden/BitwardenCipher.kt @@ -26,6 +26,7 @@ data class BitwardenCipher( // service fields override val service: BitwardenService, // common + val keyBase64: String? = null, val name: String?, val notes: String?, val favorite: Boolean, 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 60c7f53..6f29a8d 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 @@ -226,6 +226,59 @@ class SyncEngine( ) } + fun getCipherCodecPair( + mode: BitwardenCrCta.Mode, + key: ByteArray?, + organizationId: String?, + ) = kotlin.run { + val globalCrypto = getCodec( + mode = mode, + organizationId = organizationId, + ) + val itemCrypto = if (key != null) kotlin.run { + val symmetricCryptoKey = key + .let(CryptoKey.Companion::decodeSymmetricOrThrow) + val cryptoKey = BitwardenCrKey.CryptoKey( + symmetricCryptoKey = symmetricCryptoKey, + ) + val cryptoKeyEnv = BitwardenCrCta.BitwardenCrCtaEnv( + key = cryptoKey, + ) + crypto.cta( + env = cryptoKeyEnv, + mode = mode, + ) + } else { + globalCrypto + } + itemCrypto to globalCrypto + } + + fun getCipherCodecPairFromEncrypted( + mode: BitwardenCrCta.Mode, + keyCipherText: String?, + organizationId: String?, + ) = kotlin.run { + val key = if (keyCipherText != null) { + val decoderKey = if (organizationId != null) { + BitwardenCrKey.OrganizationToken( + id = organizationId, + ) + } else { + BitwardenCrKey.UserToken + } + crypto.decoder(decoderKey)(keyCipherText) + .data + } else { + null + } + getCipherCodecPair( + mode = mode, + key = key, + organizationId = organizationId, + ) + } + // // Profile // @@ -449,6 +502,7 @@ class SyncEngine( fun BitwardenCrCta.cipherDecoder( entity: CipherEntity, + codec2: BitwardenCrCta, localCipherId: String?, ) = kotlin.run { val folderId = entity.folderId @@ -478,7 +532,7 @@ class SyncEngine( folderId = folderId, entity = entity, ) - .transform(this) + .transform(this, codec2) } val cipherDao = db.cipherQueries @@ -505,11 +559,20 @@ class SyncEngine( model }, localDecoder = { local, remote -> - val codec = getCodec( + val itemKey = local.keyBase64 + ?.let(base64Service::decode) + val ( + itemCrypto, + globalCrypto, + ) = getCipherCodecPair( mode = BitwardenCrCta.Mode.ENCRYPT, + key = itemKey, organizationId = local.organizationId, ) - val encryptedCipher = local.transform(codec) + val encryptedCipher = local.transform( + itemCrypto = itemCrypto, + globalCrypto = globalCrypto, + ) CipherUpdate.of( model = encryptedCipher, folders = localToRemoteFolders, @@ -562,13 +625,18 @@ class SyncEngine( remoteItems = response.ciphers.orEmpty(), remoteLens = cipherRemoteLens, remoteDecoder = { remote, local -> - val codec = getCodec( + val ( + itemCrypto, + globalCrypto, + ) = getCipherCodecPairFromEncrypted( mode = BitwardenCrCta.Mode.DECRYPT, + keyCipherText = remote.key, organizationId = remote.organizationId, ) - codec + itemCrypto .cipherDecoder( entity = remote, + codec2 = globalCrypto, localCipherId = local?.cipherId, ) .let { remoteDecoded -> @@ -715,13 +783,20 @@ class SyncEngine( } } - val codec = getCodec( + val itemKey = local.keyBase64 + ?.let(base64Service::decode) + val ( + itemCrypto, + globalCrypto, + ) = getCipherCodecPair( mode = BitwardenCrCta.Mode.DECRYPT, + key = itemKey, organizationId = r.source.organizationId, ) - codec + itemCrypto .cipherDecoder( entity = cipherResponse, + codec2 = globalCrypto, localCipherId = r.source.cipherId, ) .let { remoteDecoded -> diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/builder/ServerEnvApi.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/builder/ServerEnvApi.kt index dc8b8a7..47e7539 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/builder/ServerEnvApi.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/api/builder/ServerEnvApi.kt @@ -554,7 +554,7 @@ fun HttpRequestBuilder.headers(env: ServerEnv) { // Seems like now Bitwarden now requires you to specify // the client name and version. header("Bitwarden-Client-Name", "web") - header("Bitwarden-Client-Version", "2023.10.1") + header("Bitwarden-Client-Version", "2024.03.0") // App does not work if hidden behind reverse-proxy under // a subdirectory. We should specify the 'referer' so the server // generates correct urls for us. diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/BitwardenCrypto.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/BitwardenCrypto.kt index ae82f07..09470f6 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/BitwardenCrypto.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/BitwardenCrypto.kt @@ -342,6 +342,12 @@ fun BitwardenCrFactoryScope.appendOrganizationToken2( appendEncoder(key, encoder) } +// +// Cipher +// + +fun CryptoGenerator.makeCipherCryptoKeyMaterial() = seed(length = 64) + // // Sends // diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/CipherCrypto.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/CipherCrypto.kt index 160b476..d7152b5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/CipherCrypto.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/CipherCrypto.kt @@ -3,18 +3,20 @@ package com.artemchep.keyguard.provider.bitwarden.crypto import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher fun BitwardenCipher.transform( - crypto: BitwardenCrCta, + itemCrypto: BitwardenCrCta, + globalCrypto: BitwardenCrCta, ) = copy( // common - name = crypto.transformString(name), - notes = crypto.transformString(notes), - fields = fields.transform(crypto), - attachments = attachments.transform(crypto), + keyBase64 = keyBase64?.let(globalCrypto::transformBase64), + name = itemCrypto.transformString(name), + notes = itemCrypto.transformString(notes), + fields = fields.transform(itemCrypto), + attachments = attachments.transform(itemCrypto), // types - login = login?.transform(crypto), - secureNote = secureNote?.transform(crypto), - card = card?.transform(crypto), - identity = identity?.transform(crypto), + login = login?.transform(itemCrypto), + secureNote = secureNote?.transform(itemCrypto), + card = card?.transform(itemCrypto), + identity = identity?.transform(itemCrypto), ) @JvmName("encryptListOfBitwardenCipherAttachment") diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/CipherDecoder.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/CipherDecoder.kt index 370c354..6b913e5 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/CipherDecoder.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/crypto/CipherDecoder.kt @@ -30,6 +30,7 @@ fun BitwardenCipher.Companion.encrypted( createdDate = entity.creationDate, revisionDate = entity.revisionDate, deletedDate = entity.deletedDate, + keyBase64 = entity.key, // service fields service = service, // common diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/entity/CipherEntity.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/entity/CipherEntity.kt index fb311d1..921508a 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/entity/CipherEntity.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/entity/CipherEntity.kt @@ -22,6 +22,9 @@ data class CipherEntity( @JsonNames("userId") @SerialName("UserId") val userId: String? = null, + @JsonNames("key") + @SerialName("Key") + val key: String? = null, @JsonNames("edit") @SerialName("Edit") val edit: Boolean = true, diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/entity/request/CipherRequest.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/entity/request/CipherRequest.kt index 2c900e1..9888423 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/entity/request/CipherRequest.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/entity/request/CipherRequest.kt @@ -15,6 +15,8 @@ import kotlinx.serialization.Serializable @Serializable data class CipherRequest( + @SerialName("key") + val key: String?, @SerialName("type") val type: CipherTypeEntity, @SerialName("organizationId") @@ -93,7 +95,9 @@ fun CipherRequest.Companion.of( attachments to attachments2 } + val keyBase64 = model.keyBase64 CipherRequest( + key = keyBase64, type = type, organizationId = model.organizationId, folderId = model.folderId diff --git a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt index 4dc16f9..9de8313 100644 --- a/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt +++ b/common/src/commonMain/kotlin/com/artemchep/keyguard/provider/bitwarden/usecase/AddCipher.kt @@ -16,6 +16,7 @@ import com.artemchep.keyguard.common.model.DSecret import com.artemchep.keyguard.common.model.canDelete import com.artemchep.keyguard.common.model.create.CreateRequest import com.artemchep.keyguard.common.service.crypto.CryptoGenerator +import com.artemchep.keyguard.common.service.text.Base64Service import com.artemchep.keyguard.common.usecase.AddCipher import com.artemchep.keyguard.common.usecase.AddFolder import com.artemchep.keyguard.common.usecase.GetPasswordStrength @@ -23,6 +24,9 @@ import com.artemchep.keyguard.common.usecase.TrashCipherById import com.artemchep.keyguard.core.store.bitwarden.BitwardenCipher import com.artemchep.keyguard.core.store.bitwarden.BitwardenService import com.artemchep.keyguard.feature.confirmation.organization.FolderInfo +import com.artemchep.keyguard.platform.util.isRelease +import com.artemchep.keyguard.provider.bitwarden.crypto.makeCipherCryptoKeyMaterial +import com.artemchep.keyguard.provider.bitwarden.crypto.makeSendCryptoKeyMaterial import com.artemchep.keyguard.provider.bitwarden.usecase.util.ModifyDatabase import kotlinx.collections.immutable.toPersistentList import kotlinx.datetime.Clock @@ -39,6 +43,7 @@ class AddCipherImpl( private val trashCipherById: TrashCipherById, private val cryptoGenerator: CryptoGenerator, private val getPasswordStrength: GetPasswordStrength, + private val base64Service: Base64Service, ) : AddCipher { companion object { private const val TAG = "AddCipher.bitwarden" @@ -52,6 +57,7 @@ class AddCipherImpl( trashCipherById = directDI.instance(), cryptoGenerator = directDI.instance(), getPasswordStrength = directDI.instance(), + base64Service = directDI.instance(), ) override fun invoke( @@ -106,6 +112,7 @@ class AddCipherImpl( val old = oldCiphersMap[cipherId]?.data_ BitwardenCipher.of( cryptoGenerator = cryptoGenerator, + base64Service = base64Service, getPasswordStrength = getPasswordStrength, now = now, request = request, @@ -233,6 +240,7 @@ class AddCipherImpl( private suspend fun BitwardenCipher.Companion.of( cryptoGenerator: CryptoGenerator, + base64Service: Base64Service, getPasswordStrength: GetPasswordStrength, request: CreateRequest, now: Instant, @@ -374,6 +382,13 @@ private suspend fun BitwardenCipher.Companion.of( } } + val keyBase64 = old?.keyBase64 + ?: if (!isRelease) { + val key = cryptoGenerator.makeCipherCryptoKeyMaterial() + base64Service.encodeToString(key) + } else { + null + } val cipherId = old?.cipherId ?: cryptoGenerator.uuid() val createdDate = old?.createdDate ?: request.now val deletedDate = old?.deletedDate @@ -386,6 +401,7 @@ private suspend fun BitwardenCipher.Companion.of( revisionDate = now, createdDate = createdDate, deletedDate = deletedDate, + keyBase64 = keyBase64, // service fields service = BitwardenService( remote = old?.service?.remote,