Add Google Tink dependency, replacing androidx.security.crypto (#4405)

* Add Google Tink dependency, replacing `androidx.security.crypto`

* Replace the `EncryptedFile` implementation too

* Extract constants, add some more docs
This commit is contained in:
Jorge Martin Espinosa
2025-03-17 10:04:21 +01:00
committed by GitHub
parent 27299607f9
commit e2afa9a691
10 changed files with 171 additions and 38 deletions

View File

@@ -81,6 +81,7 @@ google_firebase_bom = "com.google.firebase:firebase-bom:33.10.0"
firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" } firebase_appdistribution_gradle = { module = "com.google.firebase:firebase-appdistribution-gradle", version.ref = "firebaseAppDistribution" }
autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" } autonomousapps_dependencyanalysis_plugin = { module = "com.autonomousapps:dependency-analysis-gradle-plugin", version.ref = "dependencyAnalysis" }
ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } ksp_plugin = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
google_tink = "com.google.crypto.tink:tink-android:1.16.0"
# AndroidX # AndroidX
androidx_core = { module = "androidx.core:core", version.ref = "core" } androidx_core = { module = "androidx.core:core", version.ref = "core" }
@@ -100,7 +101,6 @@ androidx_browser = "androidx.browser:browser:1.8.0"
androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
androidx_splash = "androidx.core:core-splashscreen:1.0.1" androidx_splash = "androidx.core:core-splashscreen:1.0.1"
androidx_security_crypto = "androidx.security:security-crypto:1.1.0-alpha06"
androidx_media3_exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } androidx_media3_exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "media3" } androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }
androidx_biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05" androidx_biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05"

View File

@@ -31,7 +31,6 @@ dependencies {
implementation(libs.androidx.activity.activity) implementation(libs.androidx.activity.activity)
implementation(libs.androidx.recyclerview) implementation(libs.androidx.recyclerview)
implementation(libs.androidx.exifinterface) implementation(libs.androidx.exifinterface)
implementation(libs.androidx.security.crypto)
api(libs.androidx.browser) api(libs.androidx.browser)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)

View File

@@ -1,30 +0,0 @@
/*
* Copyright 2023, 2024 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.android.libraries.androidutils.file
import android.content.Context
import androidx.security.crypto.EncryptedFile
import androidx.security.crypto.MasterKey
import java.io.File
class EncryptedFileFactory(
private val context: Context,
) {
fun create(file: File): EncryptedFile {
// We need to use the same key for all the encrypted files.
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedFile.Builder(
context,
file,
masterKey,
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}
}

View File

@@ -23,7 +23,7 @@ dependencies {
implementation(libs.sqldelight.driver.android) implementation(libs.sqldelight.driver.android)
implementation(libs.sqlcipher) implementation(libs.sqlcipher)
implementation(libs.sqlite) implementation(libs.sqlite)
implementation(libs.androidx.security.crypto) implementation(libs.google.tink)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
} }

View File

@@ -0,0 +1,82 @@
/*
* 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)
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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()
}

View File

@@ -0,0 +1,32 @@
/*
* 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()
}

View File

@@ -8,8 +8,7 @@
package io.element.encrypteddb.passphrase package io.element.encrypteddb.passphrase
import android.content.Context import android.content.Context
import androidx.security.crypto.EncryptedFile import io.element.encrypteddb.crypto.EncryptedFile
import io.element.android.libraries.androidutils.file.EncryptedFileFactory
import java.io.File import java.io.File
import java.security.SecureRandom import java.security.SecureRandom
@@ -25,7 +24,7 @@ class RandomSecretPassphraseProvider(
private val secretSize: Int = 256, private val secretSize: Int = 256,
) : PassphraseProvider { ) : PassphraseProvider {
override fun getPassphrase(): ByteArray { override fun getPassphrase(): ByteArray {
val encryptedFile = EncryptedFileFactory(context).create(file) val encryptedFile = EncryptedFile(context, file)
return if (!file.exists()) { return if (!file.exists()) {
val secret = generateSecret() val secret = generateSecret()
encryptedFile.openFileOutput().use { it.write(secret) } encryptedFile.openFileOutput().use { it.write(secret) }

View File

@@ -27,7 +27,6 @@ dependencies {
implementation(libs.dagger) implementation(libs.dagger)
implementation(libs.androidx.corektx) implementation(libs.androidx.corektx)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.security.crypto)
implementation(platform(libs.network.retrofit.bom)) implementation(platform(libs.network.retrofit.bom))
implementation(libs.network.retrofit) implementation(libs.network.retrofit)
implementation(libs.serialization.json) implementation(libs.serialization.json)

View File

@@ -28,7 +28,6 @@ dependencies {
implementation(libs.sqldelight.driver.android) implementation(libs.sqldelight.driver.android)
implementation(libs.sqlcipher) implementation(libs.sqlcipher)
implementation(libs.sqlite) implementation(libs.sqlite)
implementation(libs.androidx.security.crypto)
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(libs.sqldelight.coroutines) implementation(libs.sqldelight.coroutines)