improvement: Support new per-cipher encryption key

This commit is contained in:
Artem Chepurnoy 2024-03-28 13:31:35 +02:00
parent a1210b4ef8
commit e3be565699
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
9 changed files with 125 additions and 17 deletions

View File

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

View File

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

View File

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

View File

@ -342,6 +342,12 @@ fun BitwardenCrFactoryScope.appendOrganizationToken2(
appendEncoder(key, encoder)
}
//
// Cipher
//
fun CryptoGenerator.makeCipherCryptoKeyMaterial() = seed(length = 64)
//
// Sends
//

View File

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

View File

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

View File

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

View File

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

View File

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