mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
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:
committed by
GitHub
parent
4cafdae22d
commit
1e00353f29
+32
-8
@@ -9,30 +9,54 @@
|
||||
package io.element.encrypteddb
|
||||
|
||||
import android.content.Context
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import app.cash.sqldelight.db.QueryResult
|
||||
import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.db.SqlSchema
|
||||
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
|
||||
import io.element.encrypteddb.passphrase.PassphraseProvider
|
||||
import io.element.android.libraries.androidutils.crypto.ClientSecret
|
||||
import io.element.encrypteddb.passphrase.DatabaseSecretProvider
|
||||
import net.zetetic.database.sqlcipher.SupportOpenHelperFactory
|
||||
|
||||
/**
|
||||
* Creates an encrypted version of the [SqlDriver] using SQLCipher's [SupportOpenHelperFactory].
|
||||
* @param passphraseProvider Provides the passphrase needed to use the SQLite database with SQLCipher.
|
||||
* @param databaseSecretProvider Provides the passphrase needed to use the SQLite database with SQLCipher.
|
||||
*/
|
||||
class SqlCipherDriverFactory(
|
||||
private val passphraseProvider: PassphraseProvider,
|
||||
private val databaseSecretProvider: DatabaseSecretProvider,
|
||||
) {
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("sqlcipher")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid [SqlDriver] with SQLCipher support.
|
||||
* @param schema The SQLite DB schema.
|
||||
* @param name The name of the database to create.
|
||||
* @param context Android [Context], used to instantiate the driver.
|
||||
* @param onUpgradeCallback Optional callback to handle database upgrades, which will be called in the [AndroidSqliteDriver.Callback.onUpgrade] method.
|
||||
*/
|
||||
fun create(schema: SqlSchema<QueryResult.Value<Unit>>, name: String, context: Context): SqlDriver {
|
||||
System.loadLibrary("sqlcipher")
|
||||
val passphrase = passphraseProvider.getPassphrase()
|
||||
val factory = SupportOpenHelperFactory(passphrase)
|
||||
return AndroidSqliteDriver(schema = schema, context = context, name = name, factory = factory)
|
||||
fun create(
|
||||
schema: SqlSchema<QueryResult.Value<Unit>>,
|
||||
name: String,
|
||||
context: Context,
|
||||
onUpgradeCallback: ((driver: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) -> Unit)? = null,
|
||||
): SqlDriver {
|
||||
val key = when (val secret = databaseSecretProvider.getSecret()) {
|
||||
// For a raw key, we need the blob representation (`x'...'`) as bytes
|
||||
is ClientSecret.RawKey -> secret.formattedAsString().toByteArray()
|
||||
// For a passphrase, we need the bytes for the hex string representation
|
||||
is ClientSecret.Passphrase -> secret.formattedAsString().hexToByteArray()
|
||||
}
|
||||
val factory = SupportOpenHelperFactory(key)
|
||||
return AndroidSqliteDriver(schema = schema, context = context, name = name, factory = factory, callback = object : AndroidSqliteDriver.Callback(
|
||||
schema
|
||||
) {
|
||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
onUpgradeCallback?.invoke(db, oldVersion, newVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+10
-3
@@ -8,12 +8,19 @@
|
||||
|
||||
package io.element.encrypteddb.passphrase
|
||||
|
||||
import io.element.android.libraries.androidutils.crypto.ClientSecret
|
||||
|
||||
/**
|
||||
* An abstraction to implement secure providers for SQLCipher passphrases.
|
||||
*/
|
||||
interface PassphraseProvider {
|
||||
interface DatabaseSecretProvider {
|
||||
/**
|
||||
* Returns a passphrase for SQLCipher in [ByteArray] format.
|
||||
* Returns a secret for SQLCipher.
|
||||
*/
|
||||
fun getPassphrase(): ByteArray
|
||||
fun getSecret(): ClientSecret
|
||||
|
||||
/**
|
||||
* Resets the passphrase, for example by deleting the persisted secret. Returns `true` if the reset was successful, `false` otherwise.
|
||||
*/
|
||||
fun reset(): Boolean
|
||||
}
|
||||
+16
-10
@@ -9,35 +9,41 @@
|
||||
package io.element.encrypteddb.passphrase
|
||||
|
||||
import android.content.Context
|
||||
import io.element.android.libraries.androidutils.crypto.ClientSecret
|
||||
import io.element.encrypteddb.crypto.EncryptedFile
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
/**
|
||||
* Provides a secure passphrase for SQLCipher by generating a random secret and storing it into an [EncryptedFile].
|
||||
* Provides a secure secret for SQLCipher by generating a random secret and storing it into an [EncryptedFile].
|
||||
* @param context Android [Context], used by [EncryptedFile] for cryptographic operations.
|
||||
* @param file Destination file where the key will be stored.
|
||||
* @param secretSize Length of the generated secret.
|
||||
* @param secretSizeBytes Length of the generated secret.
|
||||
*/
|
||||
class RandomSecretPassphraseProvider(
|
||||
class RandomDatabaseSecretProvider(
|
||||
private val context: Context,
|
||||
private val file: File,
|
||||
private val secretSize: Int = 256,
|
||||
) : PassphraseProvider {
|
||||
override fun getPassphrase(): ByteArray {
|
||||
private val secretSizeBytes: Int = 32,
|
||||
) : DatabaseSecretProvider {
|
||||
override fun getSecret(): ClientSecret {
|
||||
val encryptedFile = EncryptedFile(context, file)
|
||||
return if (!file.exists()) {
|
||||
val bytes = if (!file.exists()) {
|
||||
val secret = generateSecret()
|
||||
encryptedFile.openFileOutput().use { it.write(secret) }
|
||||
secret
|
||||
} else {
|
||||
encryptedFile.openFileInput().use { it.readBytes() }
|
||||
}
|
||||
return ClientSecret.fromRawBytes(bytes)
|
||||
}
|
||||
|
||||
override fun reset(): Boolean {
|
||||
return file.delete()
|
||||
}
|
||||
|
||||
private fun generateSecret(): ByteArray {
|
||||
val buffer = ByteArray(size = secretSize)
|
||||
SecureRandom().nextBytes(buffer)
|
||||
return buffer
|
||||
// Generate a random secret of the specified size using a secure random generator.
|
||||
return ByteArray(size = secretSizeBytes)
|
||||
.also { SecureRandom().nextBytes(it) }
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.encrypteddb.utils
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import io.element.encrypteddb.passphrase.RandomDatabaseSecretProvider
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* A utility class to replace the encryption key of an existing SQLCipher database.
|
||||
* This is used during database migrations when we want to change the encryption key.
|
||||
*
|
||||
* @param databaseSecretProvider The provider for generating new secrets.
|
||||
*/
|
||||
class ReplaceDatabaseKey(
|
||||
private val databaseSecretProvider: RandomDatabaseSecretProvider
|
||||
) {
|
||||
fun replaceKey(name: String, database: SupportSQLiteDatabase) {
|
||||
Timber.d("Re-keying database $name")
|
||||
// Reset the passphrase provider to generate a new passphrase
|
||||
databaseSecretProvider.reset()
|
||||
|
||||
// Get the new secret and convert it to the format expected by SQLCipher
|
||||
val newSecret = databaseSecretProvider.getSecret()
|
||||
val key = newSecret.formattedAsString()
|
||||
|
||||
// Use the PRAGMA rekey command to change the encryption key of the database
|
||||
database.query("PRAGMA rekey = \"$key\";").close()
|
||||
|
||||
// Verify that the database can be accessed with the new key by running a simple query
|
||||
val result = database.query("select count(*) from sqlite_master").use { cursor ->
|
||||
if (cursor.moveToNext()) cursor.getLong(0) else -1L
|
||||
}
|
||||
if (result >= 0) {
|
||||
Timber.d("Re-keying database $name completed")
|
||||
} else {
|
||||
Timber.e("Re-keying database $name didn't work as expected.")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user