Avoid SQLCipher key derivation (#6774)

* Avoid SQLCipher key derivation by manually providing the generated as a raw key: this needs a series of DB re-key operations, performed as part of a 'dummy' DB migration

* Set key generation length to 32 bytes in `RandomSecretPassphraseProvider`.

* Use `ClientSecret` for handling the keys for SQLCipher. Move `ClientSecret` to `:libraries:androidutils` so it's shared.
This commit is contained in:
Jorge Martin Espinosa
2026-05-27 17:25:51 +02:00
committed by GitHub
parent 4cafdae22d
commit 1e00353f29
21 changed files with 185 additions and 43 deletions
@@ -1,66 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl
import okio.ByteString.Companion.decodeHex
/**
* Represents a client secret used to encrypt/decrypt data from databases, which can be either a passphrase or a raw key.
*/
sealed interface ClientSecret {
/**
* A passphrase that can be used to derive a key for encryption/decryption.
*/
data class Passphrase(val value: String) : ClientSecret {
override fun formattedAsString(): String = toString()
}
/**
* A raw key that can be directly used for encryption/decryption. The key is represented as a byte array, and is formatted as a string in the form of
* `x'...'` where the bytes are encoded as hex characters.
*/
data class RawKey(val bytes: ByteArray) : ClientSecret {
override fun formattedAsString() = "x'${bytes.toHexString()}'"
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return bytes.contentEquals((other as RawKey).bytes)
}
override fun hashCode(): Int {
return bytes.contentHashCode()
}
override fun toString(): String {
return formattedAsString()
}
}
/**
* Format the client secret as a string that can be parsed back with [fromString].
* For a passphrase, this is just the passphrase value. For a raw key, this is the hex-encoded representation of the key formatted as `x'...'`.
*/
fun formattedAsString(): String
companion object {
/**
* Parse a string representation of a client secret, which can be either a passphrase or a raw key formatted as `x'...'`.
*/
fun fromString(secret: String): ClientSecret {
val regex = Regex("^x'([0-9a-fA-F]+)'$")
val rawKeyMatch = regex.matchEntire(secret)
return if (rawKeyMatch != null) {
RawKey(rawKeyMatch.groupValues[1].decodeHex().toByteArray())
} else {
Passphrase(secret)
}
}
}
}
@@ -9,6 +9,7 @@
package io.element.android.libraries.matrix.impl
import dev.zacsweers.metro.Inject
import io.element.android.libraries.androidutils.crypto.ClientSecret
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.ByteUnit
import io.element.android.libraries.core.data.megaBytes
@@ -12,6 +12,7 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.crypto.ClientSecret
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
@@ -30,7 +31,6 @@ import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.impl.ClientBuilderSlidingSync
import io.element.android.libraries.matrix.impl.ClientSecret
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.auth.qrlogin.QrErrorMapper
import io.element.android.libraries.matrix.impl.auth.qrlogin.SdkQrCodeLoginData
@@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.impl.keys
import android.util.Base64
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.matrix.impl.ClientSecret
import io.element.android.libraries.androidutils.crypto.ClientSecret
import java.security.SecureRandom
@ContributesBinding(AppScope::class)
@@ -8,7 +8,7 @@
package io.element.android.libraries.matrix.impl.keys
import io.element.android.libraries.matrix.impl.ClientSecret
import io.element.android.libraries.androidutils.crypto.ClientSecret
private const val PASSPHRASE_SIZE = 256
private const val KEY_SIZE = 32
@@ -7,9 +7,9 @@
package io.element.android.libraries.matrix.impl.storage
import io.element.android.libraries.androidutils.crypto.ClientSecret
import io.element.android.libraries.core.data.ByteUnit
import io.element.android.libraries.core.data.megaBytes
import io.element.android.libraries.matrix.impl.ClientSecret
import io.element.android.libraries.matrix.impl.paths.SessionPaths
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.SqliteStoreBuilder as SdkSqliteStoreBuilder
@@ -8,7 +8,7 @@
package io.element.android.libraries.matrix.impl.auth
import io.element.android.libraries.matrix.impl.ClientSecret
import io.element.android.libraries.androidutils.crypto.ClientSecret
import io.element.android.libraries.matrix.impl.keys.SecretGenerator
import io.element.android.libraries.matrix.test.A_PASSPHRASE
@@ -9,7 +9,7 @@
package io.element.android.libraries.matrix.impl.keys
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.impl.ClientSecret
import io.element.android.libraries.androidutils.crypto.ClientSecret
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -7,7 +7,7 @@
package io.element.android.libraries.matrix.impl.storage
import io.element.android.libraries.matrix.impl.ClientSecret
import io.element.android.libraries.androidutils.crypto.ClientSecret
import org.matrix.rustcomponents.sdk.ClientBuilder
class FakeSqliteStoreBuilder : SqliteStoreBuilder {