diff --git a/buildSrc/src/main/kotlin/LicenseeConfig.kt b/buildSrc/src/main/kotlin/LicenseeConfig.kt index 102eb51058..329953c984 100644 --- a/buildSrc/src/main/kotlin/LicenseeConfig.kt +++ b/buildSrc/src/main/kotlin/LicenseeConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 Google LLC + * Copyright 2023-2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,13 +80,12 @@ private fun Project.configureLicensee() { } // SQLCipher - allowDependency("net.zetetic", "android-database-sqlcipher", "4.5.0") { + allowDependency("net.zetetic", "android-database-sqlcipher", "4.5.4") { because("Custom license, essentially BSD-3. https://www.zetetic.net/sqlcipher/license/") } - allowDependency("net.zetetic", "android-database-sqlcipher", "4.5.4") { + allowDependency("net.zetetic", "sqlcipher-android", "4.12.0") { because("Custom license, essentially BSD-3. https://www.zetetic.net/sqlcipher/license/") } - // Jakarta XML Binding API allowDependency("jakarta.xml.bind", "jakarta.xml.bind-api", "4.0.1") { because("BSD 3-clause.") @@ -145,20 +144,54 @@ private fun Project.configureLicensee() { // More utility classes // https://developers.google.com/android/reference/com/google/android/gms/common/package-summary - allowDependency("com.google.android.gms", "play-services-basement", "17.4.0") { because("") } - allowDependency("com.google.android.gms", "play-services-basement", "18.0.0") { because("") } - allowDependency("com.google.android.gms", "play-services-basement", "18.1.0") { because("") } + allowDependency( + "com.google.android.gms", + "play-services-basement", + "17.4.0", + ) { + because("") + } + allowDependency( + "com.google.android.gms", + "play-services-basement", + "18.0.0", + ) { + because("") + } + allowDependency( + "com.google.android.gms", + "play-services-basement", + "18.1.0", + ) { + because("") + } // https://developers.google.com/android/reference/com/google/android/gms/common/package-summary - allowDependency("com.google.android.gms", "play-services-clearcut", "17.0.0") { because("") } + allowDependency( + "com.google.android.gms", + "play-services-clearcut", + "17.0.0", + ) { + because("") + } // ML Kit barcode scanning https://developers.google.com/ml-kit/vision/barcode-scanning/android - allowDependency("com.google.android.gms", "play-services-mlkit-barcode-scanning", "16.1.4") { + allowDependency( + "com.google.android.gms", + "play-services-mlkit-barcode-scanning", + "16.1.4", + ) { because("") } // Play Services Phenotype - allowDependency("com.google.android.gms", "play-services-phenotype", "17.0.0") { because("") } + allowDependency( + "com.google.android.gms", + "play-services-phenotype", + "17.0.0", + ) { + because("") + } // Tasks API Android https://developers.google.com/android/guides/tasks allowDependency("com.google.android.gms", "play-services-tasks", "17.2.0") { because("") } diff --git a/engine/src/main/java/com/google/android/fhir/db/impl/SQLCipherSupportHelper.kt b/engine/src/main/java/com/google/android/fhir/db/impl/SQLCipherSupportHelper.kt index 34f5a213ac..8701c1bc17 100644 --- a/engine/src/main/java/com/google/android/fhir/db/impl/SQLCipherSupportHelper.kt +++ b/engine/src/main/java/com/google/android/fhir/db/impl/SQLCipherSupportHelper.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 Google LLC + * Copyright 2026 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,58 +27,32 @@ import com.google.android.fhir.db.impl.DatabaseImpl.Companion.UNENCRYPTED_DATABA import java.time.Duration import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking -import net.sqlcipher.database.SQLiteDatabase -import net.sqlcipher.database.SQLiteDatabaseHook -import net.sqlcipher.database.SQLiteOpenHelper +import net.zetetic.database.sqlcipher.SQLiteDatabaseHook +import net.zetetic.database.sqlcipher.SupportOpenHelperFactory import timber.log.Timber -/** A [SupportSQLiteOpenHelper] which initializes a [SQLiteDatabase] with a passphrase. */ +/** A [SupportSQLiteOpenHelper] which initializes SQLCipher with a passphrase. */ internal class SQLCipherSupportHelper( private val configuration: SupportSQLiteOpenHelper.Configuration, - hook: SQLiteDatabaseHook? = null, + private val hook: SQLiteDatabaseHook? = null, private val databaseErrorStrategy: DatabaseErrorStrategy, private val passphraseFetcher: () -> ByteArray, ) : SupportSQLiteOpenHelper { init { - SQLiteDatabase.loadLibs(configuration.context) + System.loadLibrary("sqlcipher") } - private val standardHelper = - object : - SQLiteOpenHelper( - configuration.context, - configuration.name, - /* factory= */ null, - configuration.callback.version, - hook, - ) { - override fun onCreate(db: SQLiteDatabase) { - configuration.callback.onCreate(db) - } + @Volatile private var delegate: SupportSQLiteOpenHelper? = null - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - configuration.callback.onUpgrade(db, oldVersion, newVersion) - } + @Volatile private var walEnabled: Boolean = false - override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - configuration.callback.onDowngrade(db, oldVersion, newVersion) - } - - override fun onOpen(db: SQLiteDatabase) { - configuration.callback.onOpen(db) - } - - override fun onConfigure(db: SQLiteDatabase) { - configuration.callback.onConfigure(db) - } - } - - override val databaseName - get() = standardHelper.databaseName + override val databaseName: String? + get() = configuration.name override fun setWriteAheadLoggingEnabled(enabled: Boolean) { - standardHelper.setWriteAheadLoggingEnabled(enabled) + walEnabled = enabled + delegate?.setWriteAheadLoggingEnabled(enabled) } override val writableDatabase: SupportSQLiteDatabase @@ -87,20 +61,52 @@ internal class SQLCipherSupportHelper( "Unexpected unencrypted database, $UNENCRYPTED_DATABASE_NAME, already exists. " + "Check if you have accidentally disabled database encryption across releases." } - val key = runBlocking { getPassphraseWithRetry() } + + val helper = delegate ?: createDelegate().also { delegate = it } + return try { - standardHelper.getWritableDatabase(key) + helper.writableDatabase } catch (ex: SQLiteException) { if (databaseErrorStrategy == DatabaseErrorStrategy.RECREATE_AT_OPEN) { Timber.w("Fail to open database. Recreating database.") configuration.context.getDatabasePath(databaseName).delete() - standardHelper.getWritableDatabase(key) + + // Reset and retry with a fresh helper instance + delegate?.close() + delegate = null + createDelegate().also { delegate = it }.writableDatabase } else { throw ex } } } + override val readableDatabase: SupportSQLiteDatabase + get() = writableDatabase + + override fun close() { + delegate?.close() + delegate = null + } + + /** Creates a SQLCipher-aware SupportSQLiteOpenHelper using SupportOpenHelperFactory. */ + private fun createDelegate(): SupportSQLiteOpenHelper { + val passphrase = runBlocking { getPassphraseWithRetry() } + + val factory = + if (hook == null) { + SupportOpenHelperFactory(passphrase) + } else { + SupportOpenHelperFactory( + passphrase, + hook, + /* enableWriteAheadLogging = */ walEnabled, + ) + } + + return factory.create(configuration) + } + private suspend fun getPassphraseWithRetry(): ByteArray { var lastException: DatabaseEncryptionException? = null for (retryAttempt in 1..MAX_RETRY_ATTEMPTS) { @@ -120,13 +126,6 @@ internal class SQLCipherSupportHelper( throw lastException ?: DatabaseEncryptionException(Exception(), UNKNOWN) } - override val readableDatabase - get() = writableDatabase - - override fun close() { - standardHelper.close() - } - private companion object { const val MAX_RETRY_ATTEMPTS = 3 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b9586c4937..4e6215a7e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -78,7 +78,7 @@ retrofit = "2.9.0" robolectric = "4.10.3" ruler-gradle-plugin = "1.4.0" spotless-plugin-gradle = "6.22.0" -sqlcipher = "4.5.4" +sqlcipher-android = "4.12.0" timber = "5.0.1" truth = "1.1.5" uiautomator = "2.3.0" @@ -89,7 +89,7 @@ zxing = "3.4.1" lifecycle-runtime-testing = "2.10.0" [libraries] -accompanist-themeadapter-material3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version.ref = "accompanist-themeadapter-material3"} +accompanist-themeadapter-material3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version.ref = "accompanist-themeadapter-material3" } android-fhir-common = { module = "com.google.android.fhir:common", version.ref = "android-fhir-common" } android-fhir-engine = { module = "com.google.android.fhir:engine", version.ref = "android-fhir-engine" } android-fhir-knowledge = { module = "com.google.android.fhir:knowledge", version.ref = "android-fhir-knowledge" } @@ -101,7 +101,7 @@ androidx-benchmark-junit4 = { module = "androidx.benchmark:benchmark-junit4", ve androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark-macro" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } -androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidx-compose-ui"} +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidx-compose-ui" } androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "androidx-compose-ui" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "androidx-compose-ui" } androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "androidx-compose-ui" } @@ -160,7 +160,7 @@ json-assert = { module = "org.skyscreamer:jsonassert", version.ref = "json-asser json-tools-patch = { module = "com.github.java-json-tools:json-patch", version.ref = "json-tools-patch" } junit = { module = "junit:junit", version.ref = "junit" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } -kotest-assertions-core = {module = "io.kotest:kotest-assertions-core", version.ref="kotest"} +kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } kotlin-fhir = { module = "com.google.fhir:fhir-model", version.ref = "kotlin-fhir" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } @@ -196,7 +196,7 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } ruler-gradle-plugin = { module = "com.spotify.ruler:ruler-gradle-plugin", version.ref = "ruler-gradle-plugin" } spotless-plugin-gradle = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless-plugin-gradle" } -sqlcipher = { module = "net.zetetic:android-database-sqlcipher", version.ref = "sqlcipher" } +sqlcipher = { module = "net.zetetic:sqlcipher-android", version.ref = "sqlcipher-android" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } truth = { module = "com.google.truth:truth", version.ref = "truth" } woodstox = { module = "com.fasterxml.woodstox:woodstox-core", version.ref = "woodstox" } @@ -224,4 +224,4 @@ kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization" } kotlin-serialization-build-src = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -ksp = { id = "com.google.devtools.ksp" } +ksp = { id = "com.google.devtools.ksp" } \ No newline at end of file