First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.encrypteddb"
buildTypes {
release {
isMinifyEnabled = false
consumerProguardFiles("consumer-proguard-rules.pro")
}
}
}
dependencies {
implementation(libs.sqldelight.driver.android)
implementation(libs.sqlcipher)
implementation(libs.sqlite)
implementation(libs.google.tink)
implementation(projects.libraries.androidutils)
}
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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
import android.content.Context
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 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.
*/
class SqlCipherDriverFactory(
private val passphraseProvider: PassphraseProvider,
) {
/**
* 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.
*/
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)
}
}
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector 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.crypto
import android.content.Context
import com.google.crypto.tink.KeyTemplates
import com.google.crypto.tink.RegistryConfiguration
import com.google.crypto.tink.StreamingAead
import com.google.crypto.tink.integration.android.AndroidKeysetManager
import com.google.crypto.tink.streamingaead.StreamingAeadConfig
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
/**
* This class is used to write and read encrypted data to/from a file.
*
* It's a simplified version of the same class in [androidx.security.crypto](https://developer.android.com/reference/androidx/security/crypto/package-summary).
*
* It uses hardcoded constants that are used in that library, for backwards compatibility reasons.
*/
internal class EncryptedFile(
private val context: Context,
private val file: File
) {
companion object {
/**
* The file content is encrypted using StreamingAead with AES-GCM, with the file name as associated data.
*/
private const val KEYSET_ENCRYPTION_SCHEME = "AES256_GCM_HKDF_4KB"
/**
* The keyset is stored in a shared preference file with this name.
*/
private const val KEYSET_PREF_FILE_NAME = "__androidx_security_crypto_encrypted_file_pref__"
/**
* The keyset is stored in a shared preference with this key, inside the specified file.
*/
private const val KEYSET_ALIAS = "__androidx_security_crypto_encrypted_file_keyset__"
/**
* The URI referencing the master key in the Android Keystore used to encrypt/decrypt the keyset.
*/
private const val MASTER_KEY_URI = "android-keystore://_androidx_security_master_key_"
}
private val androidKeysetManager by lazy {
val keysetManagerBuilder = AndroidKeysetManager.Builder()
.withKeyTemplate(KeyTemplates.get(KEYSET_ENCRYPTION_SCHEME))
.withSharedPref(context, KEYSET_ALIAS, KEYSET_PREF_FILE_NAME)
.withMasterKeyUri(MASTER_KEY_URI)
keysetManagerBuilder.build()
}
private val streamingAead: StreamingAead by lazy {
val streamingAeadKeysetHandle = androidKeysetManager.keysetHandle
streamingAeadKeysetHandle.getPrimitive(RegistryConfiguration.get(), StreamingAead::class.java)
}
init {
StreamingAeadConfig.register()
}
fun openFileOutput(): FileOutputStream {
val fos = FileOutputStream(file)
val stream = streamingAead.newEncryptingStream(fos, file.name.toByteArray())
return EncryptedFileOutputStream(fos.fd, stream)
}
fun openFileInput(): FileInputStream {
val fis = FileInputStream(file)
val stream = streamingAead.newDecryptingStream(fis, file.name.toByteArray())
return EncryptedFileInputStream(fis.fd, stream)
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector 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.crypto
import android.os.Build
import androidx.annotation.RequiresApi
import java.io.FileDescriptor
import java.io.FileInputStream
import java.io.InputStream
/**
* This class is used to read encrypted data from a file.
*
* It comes directly from [androidx.security.crypto](https://developer.android.com/reference/androidx/security/crypto/package-summary).
*/
internal class EncryptedFileInputStream(
fileDescriptor: FileDescriptor,
private val inputStream: InputStream,
) : FileInputStream(fileDescriptor) {
private val lock = Any()
override fun read(): Int = inputStream.read()
override fun read(b: ByteArray?): Int = inputStream.read(b)
override fun read(b: ByteArray?, off: Int, len: Int): Int = inputStream.read(b, off, len)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun readAllBytes(): ByteArray? = inputStream.readAllBytes()
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun readNBytes(b: ByteArray?, off: Int, len: Int): Int = inputStream.readNBytes(b, off, len)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun readNBytes(len: Int): ByteArray? = inputStream.readNBytes(len)
override fun skip(n: Long): Long = inputStream.skip(n)
override fun available(): Int = inputStream.available()
override fun mark(readlimit: Int) = synchronized(lock) { inputStream.mark(readlimit) }
override fun markSupported(): Boolean = inputStream.markSupported()
override fun reset() = synchronized(lock) { inputStream.reset() }
override fun close() = inputStream.close()
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector 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.crypto
import java.io.FileDescriptor
import java.io.FileOutputStream
import java.io.OutputStream
/**
* This class is used to write encrypted data to a file.
*
* It comes directly from [androidx.security.crypto](https://developer.android.com/reference/androidx/security/crypto/package-summary).
*/
internal class EncryptedFileOutputStream(
fileDescriptor: FileDescriptor,
private val outputStream: OutputStream
) : FileOutputStream(fileDescriptor) {
override fun write(b: ByteArray?) = outputStream.write(b)
override fun write(b: ByteArray?, off: Int, len: Int) = outputStream.write(b, off, len)
override fun write(b: Int) = outputStream.write(b)
override fun flush() = outputStream.flush()
override fun close() = outputStream.close()
}
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.passphrase
/**
* An abstraction to implement secure providers for SQLCipher passphrases.
*/
interface PassphraseProvider {
/**
* Returns a passphrase for SQLCipher in [ByteArray] format.
*/
fun getPassphrase(): ByteArray
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.passphrase
import android.content.Context
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].
* @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.
*/
class RandomSecretPassphraseProvider(
private val context: Context,
private val file: File,
private val secretSize: Int = 256,
) : PassphraseProvider {
override fun getPassphrase(): ByteArray {
val encryptedFile = EncryptedFile(context, file)
return if (!file.exists()) {
val secret = generateSecret()
encryptedFile.openFileOutput().use { it.write(secret) }
secret
} else {
encryptedFile.openFileInput().use { it.readBytes() }
}
}
private fun generateSecret(): ByteArray {
val buffer = ByteArray(size = secretSize)
SecureRandom().nextBytes(buffer)
return buffer
}
}