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

Conversation

stevenckngaa
Copy link
Collaborator

@stevenckngaa stevenckngaa commented Sep 15, 2021

IMPORTANT: All PRs must be linked to an issue (except for extremely trivial and straightforward changes).

Fixes #392

Description
Here are the changes:

  1. Introduce a new dependency: SQLCipher for database encryption
  2. Generate a private key in Android keystore for signing messages using HMAC SHA256.
  3. Sign an empty string with the private key in 2. Then use the signed message as the SQLCipher security key.

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:

  1. Implement an encryption wrapper around Datastore
    Pros:

    • Avoid shared preferences issue, such as blocking disk I/O on UI thread, lack of type safety, inefficient thread usage / disk usage, no guarantee of data written after apply() etc

    Cons:

    • more work
    • we don't benefit from the goodies of data store that much because we are only storing one key.
  2. EncryptedSharedPreferences
    Pros:

    • less work. We don't need to maintain the encryption code.

    Cons:

    • See Datastore pros above
    • Only support Android M or above
  3. Use HMAC SHA256 signing algorithm to sign an empty string. Then use the signed byte array as database key (chosen)
    Pros:

    • Easy to implement
    • No need to generate key in the application layer. Avoid a potential attack vector.
    • No need to store the encrypted database key to the disk. No need to handle I/O error if the encrypted database key isn't available.

    Cons:

    • Since this solution is backed by Android keystore. To avoid known keystore security issues, we only support Android M or above.

Type
Feature

Screenshots (if applicable)
N/A

Checklist

  • I have read and acknowledged the Code of conduct
  • I have read How to Contribute
  • I have read the Developer's guide
  • I have signed the Google Individual CLA, or I am covered by my company's Corporate CLA
  • I have discussed my proposed solution with code owners in the linked issue(s) and we have agreed upon the general approach
  • I have run ./gradlew spotlessApply and ./gradlew spotlessCheck to check my code follows the style guide of this project
  • I have run ./gradlew check and ./gradlew connectedCheck to test my changes locally
  • I have built and run the reference app(s) to verify my change fixes the issue and/or does not break the reference app(s)

Also store the key in EncryptedSharedPreference, which is encrypted by an
application main key from the Android key store.
@stevenckngaa stevenckngaa changed the title [WIP] Introduce a SQLCipher encryption to FHIR engine database [WIP] Encrypt FHIR engine database via SQLCipher Sep 15, 2021
@stevenckngaa stevenckngaa marked this pull request as draft September 15, 2021 22:32
@Tarun-Bhardwaj Tarun-Bhardwaj added this to In progress in FHIR engine library via automation Sep 20, 2021
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
@stevenckngaa stevenckngaa changed the title [WIP] Encrypt FHIR engine database via SQLCipher Encrypt FHIR engine database via SQLCipher Sep 20, 2021
@stevenckngaa stevenckngaa marked this pull request as ready for review September 20, 2021 23:10
@codecov-commenter
Copy link

codecov-commenter commented Sep 20, 2021

Codecov Report

Merging #787 (650ac9a) into master (6e722c6) will decrease coverage by 0.38%.
The diff coverage is 68.00%.

Impacted file tree graph

@@             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     
Impacted Files Coverage Δ
...gle/android/fhir/db/DatabaseEncryptionException.kt 0.00% <0.00%> (ø)
.../main/java/com/google/android/fhir/FhirServices.kt 75.00% <57.14%> (-25.00%) ⬇️
...java/com/google/android/fhir/FhirEngineProvider.kt 63.63% <65.00%> (-11.37%) ⬇️
...gle/android/fhir/db/impl/SQLCipherSupportHelper.kt 71.42% <71.42%> (ø)
...roid/fhir/db/impl/DatabaseEncryptionKeyProvider.kt 72.72% <72.72%> (ø)
...va/com/google/android/fhir/db/impl/DatabaseImpl.kt 88.88% <94.44%> (+5.25%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6e722c6...650ac9a. Read the comment docs.

Copy link

@jmarkoff jmarkoff left a 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.

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
val databaseErrorStrategy: DatabaseErrorStrategy
)

suspend fun <R> RoomDatabase.withWrappedTransaction(
Copy link
Collaborator Author

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?

Copy link
Collaborator Author

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?

Copy link

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).

Copy link
Contributor

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?)

Copy link
Collaborator Author

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?

buildSrc/src/main/kotlin/Dependencies.kt Outdated Show resolved Hide resolved
Comment on lines 46 to 50
FhirServices.builder(context.applicationContext)
.apply {
if (fhirEngineConfiguration.enableEncryption) enableEncryption()
setDatabaseErrorStrategy(fhirEngineConfiguration.databaseErrorStrategy)
}
Copy link
Collaborator

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.

@yigit

Copy link
Contributor

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?

FHIR engine library automation moved this from In progress to Review in progress Nov 4, 2021
Comment on lines 46 to 50
FhirServices.builder(context.applicationContext)
.apply {
if (fhirEngineConfiguration.enableEncryption) enableEncryption()
setDatabaseErrorStrategy(fhirEngineConfiguration.databaseErrorStrategy)
}
Copy link
Contributor

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?

val databaseErrorStrategy: DatabaseErrorStrategy
)

suspend fun <R> RoomDatabase.withWrappedTransaction(
Copy link
Contributor

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?)

Copy link
Collaborator

@jingtang10 jingtang10 left a 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.

FHIR engine library automation moved this from Review in progress to Reviewer approved Nov 29, 2021
@stevenckngaa stevenckngaa merged commit 596683b into master Nov 29, 2021
@stevenckngaa stevenckngaa deleted the ckng/db_encryption branch November 29, 2021 22:08
FHIR engine library automation moved this from Reviewer approved to Done Nov 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Support encryption of sqllite DB with SQLCipher
6 participants