Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encrypt FHIR engine database via SQLCipher #787

Merged
merged 26 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e3f45e4
Import SQLCipher dependencies
stevenckngaa Sep 15, 2021
9e43cb9
Generate a 16 bytes key for database encryption
stevenckngaa Sep 15, 2021
b0d3388
Merge branch 'master' into ckng/db_encryption
stevenckngaa Sep 15, 2021
75a5006
Apply spotless
stevenckngaa Sep 15, 2021
5b03a8a
Allow database encryption on Android 6.0 or above
stevenckngaa Sep 20, 2021
4d1a969
Merge branch 'master' into ckng/db_encryption
stevenckngaa Sep 30, 2021
b44b9c8
Use a HMAC 256 signed empty string as the database key
stevenckngaa Sep 30, 2021
a9c1b94
Enable database encryption in the reference app by default
stevenckngaa Sep 30, 2021
ce43106
Load database key in RoomDatabase background thread
stevenckngaa Oct 8, 2021
98fa50d
Merge branch 'master' into ckng/db_encryption
stevenckngaa Oct 8, 2021
2746cc3
Add retry when Android keystore is busy
stevenckngaa Oct 18, 2021
a941caa
Reenable some tests
stevenckngaa Oct 18, 2021
d5ab15f
Merge branch 'master' into ckng/db_encryption
stevenckngaa Oct 27, 2021
bf9105c
Remove outdated TODO
stevenckngaa Oct 27, 2021
7335ec8
Use a different database name for unencrypted and encrypted database
stevenckngaa Oct 29, 2021
956f019
Update documentation
stevenckngaa Oct 29, 2021
68e0e52
Merge branch 'master' into ckng/db_encryption
stevenckngaa Nov 5, 2021
d7f7dc1
Move keystore timeout retry to SQLCipherSupportHelper
stevenckngaa Nov 5, 2021
e4d009b
Update kdoc
stevenckngaa Nov 5, 2021
6137ba6
Update documentation
stevenckngaa Nov 12, 2021
386808a
Merge branch 'master' into ckng/db_encryption
stevenckngaa Nov 12, 2021
d54b91c
Apply spotless
stevenckngaa Nov 12, 2021
73441e5
Allow SQLCipher license because it's effectively BSD-3
stevenckngaa Nov 15, 2021
00b2b9e
Fix SQLCipher license allowed dependency
stevenckngaa Nov 15, 2021
b00b47c
Merge branch 'master' into ckng/db_encryption
stevenckngaa Nov 26, 2021
650ac9a
Address Jing's comments
stevenckngaa Nov 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Load database key in RoomDatabase background thread
1. Load database key in RoomDatabase background thread
2. Add more tests
  • Loading branch information
stevenckngaa committed Oct 8, 2021
commit ce43106d5a09f1eb8272ce724127675a9a9f3028
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http:https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.db.impl

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import ca.uhn.fhir.context.FhirContext
import com.google.android.fhir.db.impl.DatabaseImpl.Companion.DATABASE_PASSPHRASE_NAME
import com.google.android.fhir.search.Order
import com.google.android.fhir.search.Search
import com.google.android.fhir.search.getQuery
import com.google.android.fhir.security.StorageKeyProvider
import com.google.common.truth.Truth.assertThat
import java.security.KeyStore
import kotlinx.coroutines.runBlocking
import net.sqlcipher.database.SQLiteException
import org.hl7.fhir.r4.model.Enumerations
import org.hl7.fhir.r4.model.Patient
import org.hl7.fhir.r4.model.ResourceType
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith

@MediumTest
@RunWith(AndroidJUnit4::class)
class EncryptedDatabaseErrorTest {
private val context: Context = ApplicationProvider.getApplicationContext()
private val parser = FhirContext.forR4().newJsonParser()

@After
fun tearDown() {
context.getDatabasePath("fhirEngine").delete()
}

@Test
fun unencryptedDatabase_thenEncryptedDatabase_shouldThrowSQLiteException() {
assertThrows(SQLiteException::class.java) {
runBlocking {
// GIVEN an unencrypted database.
DatabaseImpl(context, parser, DatabaseConfig(inMemory = false, enableEncryption = false))
.let {
it.insert(TEST_PATIENT_1)
it.db.close()
}

// WHEN requesting an encrypted database.
// THEN it should throw SQLiteException
DatabaseImpl(context, parser, DatabaseConfig(inMemory = false, enableEncryption = true))
.let {
it.search<Patient>(
Search(ResourceType.Patient)
.apply {
sort(Patient.GIVEN, Order.ASCENDING)
count = 100
from = 0
}
.getQuery()
)
}
}
}
}

@Test
fun encryptedDatabase_thenLostKey_shouldThrowSQLiteException() {
assertThrows(SQLiteException::class.java) {
runBlocking {
// GIVEN an unencrypted database.
DatabaseImpl(context, parser, DatabaseConfig(inMemory = false, enableEncryption = true))
.let {
it.insert(TEST_PATIENT_1)
it.db.close()
}

// GIVEN the key is lost.
val keyStore = KeyStore.getInstance(StorageKeyProvider.ANDROID_KEYSTORE_NAME)
keyStore.load(/* param = */ null)
keyStore.deleteEntry(DATABASE_PASSPHRASE_NAME)

// WHEN requesting an encrypted database.
// THEN it should throw SQLiteException
DatabaseImpl(context, parser, DatabaseConfig(inMemory = false, enableEncryption = true))
.let {
it.search<Patient>(
Search(ResourceType.Patient)
.apply {
sort(Patient.GIVEN, Order.ASCENDING)
count = 100
from = 0
}
.getQuery()
)
}
}
}
}

@Test
fun encryptedDatabase_thenUnencrypted_shouldRecreateDatabase() {
stevenckngaa marked this conversation as resolved.
Show resolved Hide resolved
runBlocking {
// GIVEN an unencrypted database.
DatabaseImpl(context, parser, DatabaseConfig(inMemory = false, enableEncryption = true)).let {
it.insert(TEST_PATIENT_1)
it.db.close()
}

// WHEN requesting an encrypted database.
// THEN it should recreate database.
DatabaseImpl(context, parser, DatabaseConfig(inMemory = false, enableEncryption = false))
.let {
assertThat(
it.search<Patient>(
Search(ResourceType.Patient)
.apply {
sort(Patient.GIVEN, Order.ASCENDING)
count = 100
from = 0
}
.getQuery()
)
)
.isEmpty()
}
}
}

private companion object {
const val TEST_PATIENT_1_ID = "patient_1"
val TEST_PATIENT_1 = Patient()

init {
TEST_PATIENT_1.id = TEST_PATIENT_1_ID
TEST_PATIENT_1.gender = Enumerations.AdministrativeGender.MALE
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@

package com.google.android.fhir.security

import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import java.security.KeyStore
Expand All @@ -30,7 +28,6 @@ import org.junit.runner.RunWith
/** Integration test for [StorageKeyProviderTest]. */
@SmallTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
class StorageKeyProviderTest {
@Before fun setup() = deleteTestKeys()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package com.google.android.fhir.db.impl

import android.content.Context
import android.os.Build
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.room.Room
import androidx.room.withTransaction
import androidx.sqlite.db.SimpleSQLiteQuery
Expand All @@ -33,8 +33,6 @@ import com.google.android.fhir.logicalId
import com.google.android.fhir.resource.getResourceType
import com.google.android.fhir.search.SearchQuery
import com.google.android.fhir.security.StorageKeyProvider
import java.lang.Exception
import net.sqlcipher.database.SupportFactory
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType

Expand All @@ -59,16 +57,12 @@ internal class DatabaseImpl(

init {
stevenckngaa marked this conversation as resolved.
Show resolved Hide resolved
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && databaseConfig.enableEncryption) {
stevenckngaa marked this conversation as resolved.
Show resolved Hide resolved
try {
builder.openHelperFactory {
SQLCipherSupportHelper(it) {
StorageKeyProvider.getOrCreatePassphrase(DATABASE_PASSPHRASE_NAME)
} catch (e: Exception) {
Log.e(LOG_TAG, "Fail to create / get database encryption key", e)
throw e
}
} else {
null
}
?.let { builder.openHelperFactory(SupportFactory(it)) }
}
db = builder.build()
}
private val resourceDao by lazy { db.resourceDao().also { it.iParser = iParser } }
Expand Down Expand Up @@ -149,7 +143,7 @@ internal class DatabaseImpl(
companion object {
private const val LOG_TAG = "Fhir-DatabaseImpl"
private const val DEFAULT_DATABASE_NAME = "fhirEngine"
private const val DATABASE_PASSPHRASE_NAME = "fhirEngine_db_passphrase"
@VisibleForTesting const val DATABASE_PASSPHRASE_NAME = "fhirEngine_db_passphrase"
stevenckngaa marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http:https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.db.impl

import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteDatabaseHook
import net.sqlcipher.database.SQLiteException
import net.sqlcipher.database.SQLiteOpenHelper

/** A [SupportSQLiteOpenHelper] which initializes a [SQLiteDatabase] with a passphrase. */
class SQLCipherSupportHelper(
val configuration: SupportSQLiteOpenHelper.Configuration,
hook: SQLiteDatabaseHook? = null,
val passphraseFetcher: () -> ByteArray,
) : SupportSQLiteOpenHelper {

init {
SQLiteDatabase.loadLibs(configuration.context)
}

private val standardHelper =
object :
SQLiteOpenHelper(
configuration.context,
configuration.name,
/* factory= */ null,
configuration.callback.version,
hook
) {
override fun onCreate(db: SQLiteDatabase) {
configuration.callback.onCreate(db)
}

override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
configuration.callback.onUpgrade(db, oldVersion, newVersion)
}

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 fun getDatabaseName() = standardHelper.databaseName

override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
standardHelper.setWriteAheadLoggingEnabled(enabled)
}

override fun getWritableDatabase(): SupportSQLiteDatabase? {
val result: SQLiteDatabase
try {
result = standardHelper.getWritableDatabase(passphraseFetcher())
} catch (ex: SQLiteException) {
throw ex
}
return result
}

override fun getReadableDatabase() = writableDatabase

override fun close() {
standardHelper.close()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ object StorageKeyProvider {
* [keyLength] and stores the passphrase in an encrypted storage.
*/
@Synchronized
fun getOrCreatePassphrase(keyName: String): ByteArray? {
fun getOrCreatePassphrase(keyName: String): ByteArray {
if (!isDatabaseEncryptionSupported()) {
throw UnsupportedOperationException("Database encryption is not supported on this device.")
}
Expand Down