-
Notifications
You must be signed in to change notification settings - Fork 249
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
Conversation
Also store the key in EncryptedSharedPreference, which is encrypted by an application main key from the Android key store.
Also 1. Keep minSdk to 21 2. Add interface for key storage operations 3. Implement a KeyStore backed by EncryptedSharedPreferences 4. Generate key with vary length ranging 16 to 31 bytes 5. Add unit tests
Codecov Report
@@ Coverage Diff @@
## master #787 +/- ##
============================================
- Coverage 88.68% 88.30% -0.39%
- Complexity 469 489 +20
============================================
Files 107 110 +3
Lines 9147 9309 +162
Branches 614 635 +21
============================================
+ Hits 8112 8220 +108
- Misses 704 751 +47
- Partials 331 338 +7
Continue to review full report at Codecov.
|
1. Load database key in RoomDatabase background thread 2. Add more tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly needs more Keystore specific error handling, otherwise LGTM.
engine/src/main/java/com/google/android/fhir/security/StorageKeyProvider.kt
Outdated
Show resolved
Hide resolved
engine/src/main/java/com/google/android/fhir/security/StorageKeyProvider.kt
Outdated
Show resolved
Hide resolved
engine/src/main/java/com/google/android/fhir/security/StorageKeyProvider.kt
Outdated
Show resolved
Hide resolved
engine/src/main/java/com/google/android/fhir/security/StorageKeyProvider.kt
Outdated
Show resolved
Hide resolved
engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt
Outdated
Show resolved
Hide resolved
Also 1. Rename StorageKeyProvider to DatabaseEncryptionKeyProvider and make it private to database/impl package so that it is only used in FHIR database 2. better handle database corruption at open
reference/src/main/java/com/google/android/fhir/reference/FhirApplication.kt
Outdated
Show resolved
Hide resolved
engine/src/androidTest/java/com/google/android/fhir/db/impl/EncryptedDatabaseErrorTest.kt
Outdated
Show resolved
Hide resolved
val databaseErrorStrategy: DatabaseErrorStrategy | ||
) | ||
|
||
suspend fun <R> RoomDatabase.withWrappedTransaction( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yigit @jingtang10 In the case of keystore busy error, we would like to get the key from the keystore again. Is there any concern of adding this wrapper around RoomDatabase.withTransaction to fetch the key again with an exponential backoff?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I am not sure how to test key store error because there is no robolectric shadow for keystore. I could write a fake DatabaseEncryptionKeyProvider and then use it in JUnit test. Any suggestion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can test with an emulator as well, but I'm not sure of a good way to test the keystore without an emulator or real device unfortunately. You would need to build out some stubs. For Jetpack Security, we just require that you test on a device (real or virtual).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is going to be easy to get wrong. as in you are not really convering all of the access to room database.
Any reason why we cannot do this when reading the key in open helper callback instead of here? I mean, do the retry in the open helper callback (and literally block by blocking the thread so that the rest of the database access do not get complicated?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point @yigit. The only reason I put the retry here is to utilize the suspend function. Given that we are okay to block whatever thread the getWriteableDatabase, I have moved the retry to SQLCipherSupportHelper
.
@yigit do you think I should write a fake for DatabaseEncryptionKeyProvider
in order to stimulate the key store error?
engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt
Outdated
Show resolved
Hide resolved
engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt
Show resolved
Hide resolved
engine/src/main/java/com/google/android/fhir/FhirEngineProvider.kt
Outdated
Show resolved
Hide resolved
FhirServices.builder(context.applicationContext) | ||
.apply { | ||
if (fhirEngineConfiguration.enableEncryption) enableEncryption() | ||
setDatabaseErrorStrategy(fhirEngineConfiguration.databaseErrorStrategy) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the more i look at this block the more i feel FhirServices should just be inlined in this class.. not sure how you feel about that? no need to do that in this PR but i think this can be a follow-up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what you mean by inlined?
engine/src/main/java/com/google/android/fhir/db/DatabaseEncryptionException.kt
Show resolved
Hide resolved
reference/src/main/java/com/google/android/fhir/reference/FhirApplication.kt
Outdated
Show resolved
Hide resolved
engine/src/main/java/com/google/android/fhir/db/impl/SQLCipherSupportHelper.kt
Outdated
Show resolved
Hide resolved
engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt
Outdated
Show resolved
Hide resolved
engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt
Show resolved
Hide resolved
engine/src/androidTest/java/com/google/android/fhir/db/impl/DatabaseImplTest.kt
Outdated
Show resolved
Hide resolved
FhirServices.builder(context.applicationContext) | ||
.apply { | ||
if (fhirEngineConfiguration.enableEncryption) enableEncryption() | ||
setDatabaseErrorStrategy(fhirEngineConfiguration.databaseErrorStrategy) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure what you mean by inlined?
engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt
Outdated
Show resolved
Hide resolved
val databaseErrorStrategy: DatabaseErrorStrategy | ||
) | ||
|
||
suspend fun <R> RoomDatabase.withWrappedTransaction( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is going to be easy to get wrong. as in you are not really convering all of the access to room database.
Any reason why we cannot do this when reading the key in open helper callback instead of here? I mean, do the retry in the open helper callback (and literally block by blocking the thread so that the rest of the database access do not get complicated?)
engine/src/main/java/com/google/android/fhir/db/impl/DatabaseImpl.kt
Outdated
Show resolved
Hide resolved
engine/src/main/java/com/google/android/fhir/db/impl/SQLCipherSupportHelper.kt
Show resolved
Hide resolved
reference/src/main/java/com/google/android/fhir/reference/FhirApplication.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ❤️ this. This is #lit.
Thanks @stevenckngaa for this exemplary work!
Left some comments but mostly cosmetic.
IMPORTANT: All PRs must be linked to an issue (except for extremely trivial and straightforward changes).
Fixes #392
Description
Here are the changes:
Alternative(s) considered
Have you considered any alternatives? And if so, why have you chosen the approach in this PR?
Database encryption: SQLCipher is the one sqlite3 encryption library that is available.
Key encryption:
Implement an encryption wrapper around Datastore
Pros:
apply()
etcCons:
EncryptedSharedPreferences
Pros:
Cons:
Use HMAC SHA256 signing algorithm to sign an empty string. Then use the signed byte array as database key (chosen)
Pros:
Cons:
Type
Feature
Screenshots (if applicable)
N/A
Checklist
./gradlew spotlessApply
and./gradlew spotlessCheck
to check my code follows the style guide of this project./gradlew check
and./gradlew connectedCheck
to test my changes locally