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
Generate a 16 bytes key for database encryption
Also store the key in EncryptedSharedPreference, which is encrypted by an
application main key from the Android key store.
  • Loading branch information
stevenckngaa committed Sep 15, 2021
commit 9e43cb90c3ca7289ac200314f7f1d719517d70a9
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/Sdk.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@

object Sdk {
const val compileSdk = 30
const val minSdk = 21
const val minSdk = 23
const val targetSdk = 30
}
1 change: 1 addition & 0 deletions engine/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ configurations {
}

dependencies {
implementation("androidx.security:security-crypto:1.0.0")
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
implementation("org.fhir:ucum:1.0.3")
androidTestImplementation(Dependencies.AndroidxTest.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import com.google.android.fhir.db.impl.entities.SyncedResourceEntity
import com.google.android.fhir.logicalId
import com.google.android.fhir.resource.getResourceType
import com.google.android.fhir.search.SearchQuery
import net.sqlcipher.database.SQLiteDatabase
import com.google.android.fhir.security.StorageKeyProvider
import net.sqlcipher.database.SupportFactory
import org.hl7.fhir.r4.model.Resource
import org.hl7.fhir.r4.model.ResourceType
Expand All @@ -49,14 +49,17 @@ internal class DatabaseImpl(context: Context, private val iParser: IParser, inMe
} else {
Room.databaseBuilder(context, ResourceDatabase::class.java, DEFAULT_DATABASE_NAME)
}
val factory = SupportFactory(SQLiteDatabase.getBytes("placeholder".toCharArray()))
val db =
builder
.openHelperFactory(factory)
// TODO https://github.com/jingtang10/fhir-engine/issues/32
// don't allow main thread queries
.allowMainThreadQueries()
.build()
val db: ResourceDatabase
init {
stevenckngaa marked this conversation as resolved.
Show resolved Hide resolved
val key = StorageKeyProvider.getOrCreatePassphrase(context, DATABASE_PASSPHRASE_NAME)
db =
builder
.openHelperFactory(SupportFactory(key))
// TODO https://github.com/jingtang10/fhir-engine/issues/32
// don't allow main thread queries
.allowMainThreadQueries()
.build()
}
private val resourceDao by lazy { db.resourceDao().also { it.iParser = iParser } }
private val syncedResourceDao = db.syncedResourceDao()
private val localChangeDao = db.localChangeDao().also { it.iParser = iParser }
Expand Down Expand Up @@ -134,5 +137,6 @@ internal class DatabaseImpl(context: Context, private val iParser: IParser, inMe

companion object {
private const val DEFAULT_DATABASE_NAME = "fhirEngine"
private const val DATABASE_PASSPHRASE_NAME = "fhirEngine_db_passphrase"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.google.android.fhir.security

import android.content.Context
import android.os.Build
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import java.security.SecureRandom

/** A singleton object for generating or getting previously generated storage keys. */
object StorageKeyProvider {
/**
* Returns a previous generated storage passphrase with name [passphraseName].
*
* If there is no key associated with [passphraseName], generates a storage passphrase with length
* [keyLength] and stores the passphrase in an encrypted storage.
*/
@Synchronized
fun getOrCreatePassphrase(
context: Context,
passphraseName: String,
keyLength: Int = STORAGE_KEY_LENGTH
): ByteArray {
val mainKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val encryptedSharedPreferences = EncryptedSharedPreferences.create(
STORAGE_KEY_PREFERENCES_NAME,
mainKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
encryptedSharedPreferences.getString(passphraseName, null)?.decodeHex()?.let {
return it
}
val passphrase = generatePassphrase(keyLength)
with(encryptedSharedPreferences.edit()) {
putString(passphraseName, passphrase.toHexString())
apply()
}
return passphrase
}

@Synchronized
@SuppressWarnings("newApi") // API check in the code
private fun generatePassphrase(keyLength: Int): ByteArray {
val passphrase = ByteArray(keyLength)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
SecureRandom.getInstanceStrong().nextBytes(passphrase)
} else {
SecureRandom().nextBytes(passphrase)
}
return passphrase
}

private const val STORAGE_KEY_PREFERENCES_NAME = "store_key_preferences"
private const val STORAGE_KEY_LENGTH = 16
}

fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }

fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}