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:
- 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.
- 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:
- Use
SQLCipherUtils.encryptTo()
to encrypt the database to a new database file - Rename the unencrypted database to a temporary name (
dbBackup
) - Rename the newly-encrypted database to our desired name (
dbFile
) - Delete the unencrypted database
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.