Migrating to Encryption

Normal Room migrations work with SQLCipher for Android, for adjusting your database schema to account for changes in your entities, DAOs, and so forth.

However, with SQLCipher for Android, you may have a special “migration” to consider: migrating from having an ordinary database to having an encrypted one. That would occur if you started with ordinary SQLite and only decided to add encryption later on.

So, suppose you initially shipped your app with ordinary SQLite as version 1.0.0. Later, in version 2.0.0 of your app, you shipped support for encrypted databases. Now your RoomDatabase will need to handle two cases:

  1. Version 2.0.0 of your app is being installed by a new user. In that case, you have no existing database, and you can start with the encrypted database from the outset.
  2. An existing 1.x.y user upgrades to 2.0.0. They already have an unencrypted database. Presumably, they would like to keep that data, and so you need to encrypt that database.

The SQLCipherUtils code that we saw earlier in this chapter can handle this scenario as well, as we can see in the cryptMigrate pair of modules of the book’s primary sample project.

That directory contains two modules. CMBefore is the same to-do app that we saw in earlier SQLCipher for Android samples, just without any encryption. Theoretically, this represents version 1.0.0 of your app. CMAfter is mostly the same as the ToDoCrypt sample from previous chapters, where we use SQLCipher for Android with a hardcoded passphrase (to simplify the sample). However, this time, ToDoDatabase has a substantially more complex version of newInstance():

package com.commonsware.todo.repo

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import net.sqlcipher.database.SupportFactory
import java.io.IOException

private const val DB_NAME = "stuff.db"
private const val PASSPHRASE = "sekr1t"

@Database(entities = [ToDoEntity::class], version = 1)
@TypeConverters(TypeTransmogrifier::class)
abstract class ToDoDatabase : RoomDatabase() {
  abstract fun todoStore(): ToDoEntity.Store

  companion object {
    fun newInstance(context: Context): ToDoDatabase {
      val dbFile = context.getDatabasePath(DB_NAME)
      val passphrase = PASSPHRASE.toByteArray()
      val state = SQLCipherUtils.getDatabaseState(context, dbFile)

      if (state == SQLCipherUtils.State.UNENCRYPTED) {
        val dbTemp = context.getDatabasePath("_temp.db")

        dbTemp.delete()

        SQLCipherUtils.encryptTo(context, dbFile, dbTemp, passphrase)

        val dbBackup = context.getDatabasePath("_backup.db")

        if (dbFile.renameTo(dbBackup)) {
          if (dbTemp.renameTo(dbFile)) {
            dbBackup.delete()
          } else {
            dbBackup.renameTo(dbFile)
            throw IOException("Could not rename $dbTemp to $dbFile")
          }
        } else {
          dbTemp.delete()
          throw IOException("Could not rename $dbFile to $dbBackup")
        }
      }

      return Room.databaseBuilder(context, ToDoDatabase::class.java, DB_NAME)
        .openHelperFactory(SupportFactory(passphrase))
        .build()
    }
  }
}

We start by checking the database state using getDatabaseState(). If the state is ENCRYPTED, we are set — this implies that the user has already run this version of the app before and we are already have our encrypted database. If the state is DOES_NOT_EXIST, we also do not need to do anything special, as Room will lazy-create our encrypted database for us. In either of those cases, we proceed directly to using Room.databaseBuilder() with our SupportFactory to create and/or open the database.

The scenario where we need to do extra work is if our state is UNENCRYPTED. That means the user was using CMBefore and has an existing unencrypted database. We want to keep the data, but encrypt it. So, we:

If something goes wrong with one of our file I/O operations, we try to switch back to the unencrypted database and fail with an exception.

If you run CMBefore, fill in some to-do items, and then run CMAfter, you will find that your to-do items remain intact, but that the resulting database is encrypted.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.