Securing a Room Database With Passcode-Based Encryption

Using available Java APIs

Daniel Ochoa
VMware 360
6 min readJun 22, 2020

--

As developers we are often tasked with securing the data we store in our applications. In a previous article I talked about how to secure arbitrary data using Android’s EncryptedSharedPreferences. In this post, I’m going to talk about encrypting Room with SQLCipher using PBE (Passcode Base Encryption).

What is PBE?

To summarize, PBE is a popular method for creating strong cryptographic encryption keys based on a user-supplied passcode. Without access to the user passcode, you can not decrypt the database, which means the user’s data is secure. Below is a diagram explaining how a user’s password is used to encrypt the database key which can then be safely stored away in insecure storage.

PBE Diagram

What is SQLCipher

SQLCipher is an extension to SQLite which allows for 256-bit encryption of SQLite databases. You can use the SQLCipher namespace instead of the identically named SQLite APIs for an easy transition.

Required dependencies

You’ll need to add these dependencies in order to add support for encrypting your Room database.

implementation "net.zetetic:android-database-sqlcipher:4.4.0"
implementation "androidx.sqlite:sqlite:2.1.0"

SQLCipher key formats

There are three different key formats that SQLCipher supports. They are listed here so you’re aware of your options but for this guide we’ll be focusing on the “Raw key data” format.

Passphrase with key derivation

This option takes the user’s passcode and converts it to a key using PBKDF2 key derivation. This is the slowest option as it delays the opening of the database in order to perform this conversion.

Raw key data

This method uses the raw key bytes, that we’ll be creating below, to decrypt the database. The expected format is a 64 character hex encoded string which will be converted directly to 32 bytes (256 bits) of key data.

Raw key data with explicit salt

Lastly, you can optionally provide a database salt along with the raw key if desired. Normally SQLCipher generates a database salt and stores it in the first 16 bytes of the database. This key method requires 96 characters hex encoded in a BLOB .

Overview

Here’s a breakdown of what we’ll be discussing:

  • Generate a random key
  • SQLCipher key formats
  • Create a hex encoded db key
  • PBE encrypt + store the database key
  • Restore + decrypt the database key
  • Encrypt your Room database
  • Things to know about PBE
  • Summary

Generate a random key

Let me explain what’s going on above. On API 26 (Oreo) and above we have access to SecureRandom.getInstanceStrong(), which will ensure a suitable, strong SecureRandom instance is used for generating a random value. We pass in 32 as that is how many bytes we want the key to be (256 bits). For older APIs, we’ll just use the default PRNG the system has identified.

Note: The above will produce a pseudo random number. The issue is on some OEMs and APIs, there is not enough entropy which can result in a vulnerability where your random number isn’t random enough and can be predictable. This affects all Java APIs on Android. I’ll leave this for another discussion, though, as there isn’t an answer for it at the Java layer. In most cases this is sufficient.

Create a hex encoded db key

So now that we have a raw byte key let us put it in the hex encoded format that SQLCipher is looking for.

Let’s first create a nifty extension function for converting a ByteArray to a hex encoded CharArray.

Now let’s utilize this function to create a random key and then convert it to hex encoded CharArray.

PBE encrypt + store the database key

At this point we have a valid database key that you can use to key your Room database and begin using it. But we want to be able to store this key to disk so we have it if the app is killed or the device is rebooted. So, without further ado let’s jump in.

Encapsulate the data we need to store

Here we have a simple data class that encapsulates everything we need to be able to decrypt this database later on.

Encrypt the database key using PBE

Here’s the fun part. Let’s take the user’s passcode and encrypt the database key so we can store it in SharedPreferences securely.

What we’ve done is encrypted the database key with the user’s passcode using the AES algorithm and then converted the initialization vector + encrypted key + salt into base64 format so it can be stored as a String.

Persist the key to disk

Now let’s take this Storable object and persist it to SharedPreferences.

Our database key is now encrypted using a KEK (key encrypting key) and persisted to disk so we can decrypt the database when the app gets killed or the device gets rebooted.

Restore + decrypt the database key

Now let’s show how to do everything in reverse so you can decrypt the database key and use it.

Retrieve our Storable instance from prefs

We previously stored a serialized copy of our Storable instance in SharedPreferences as a JSON String. Let’s retrieve that JSON String and deserialize it back to a Storable instance.

Decrypt the raw byte key

We’ll take our storable instance along with the user passcode and decrypt the aesWrappedKey to obtain the raw byte key.

Hex encode the decrypted raw byte key

Here we retrieved the serialized Storable instance from prefs, decrypted the raw byte key, and then created a hex encoded db key from the raw byte key. We now have everything we need to again decrypt the database.

Encrypt your Room database

Now let’s show how to encrypt that Room database. Thankfully, SQLCipher has now added support for Room encryption so let’s look at what this looks like.

And that’s it. Your database is now encrypted. Simple as that.

Things to know about PBE

Encrypting your database with PBE means without the user’s passcode you will not be able to decrypt your database. That means your application’s logic will need to completely change to accommodate this. Any attempts to query this database before it has been decrypted will result in an app crash. To be clear, once you’ve decrypted the database everything will be fine. But if the app is killed or the device is rebooted then you no longer have a decrypted database and you will now need to prompt the user for a passcode. So, you’ll need to think about your application design to ensure when the database is encrypted you have no code that will attempt to access it. After implementing PBE, make sure you do thorough testing around this. It would make sense to create a singleton that manages the locked state of your application so after the passcode has been supplied and database successfully decrypted, then the locked state becomes unlocked and your application code is then free to access the db.

Summary

To finish, you’ll just need to wire up a passcode screen and make sure none of your code accesses this database until you’ve unlocked the application with the user’s passcode. Under no circumstances should you save the user’s passcode as that would defeat all of the security we’ve just implemented. You can take this a step further and implement a timeout for your session so that after five minutes or one hour you unset rawByteKey, dbCharKey, shutdown your database, and require the user to re-authenticate.

Thank you for reading along and I hope you learned something today that can help you in your project. If you’ve found this useful, hit me up on Twitter!

References

--

--