Saturday, March 29, 2014

Migrating to SQLCipher for Android 3.x

In my last post I wrote why you have seriously consider upgrading to SQLCipher for Android 3.0.2. Version 3.x introduced new default configuration that is not out-of-the-box compatible with databases created with previous versions (1.x/2.x). Particularly, the default key derivation function's (KDF) iteration count increased from 4000 to 64000 (yes, x16), and therefore some kind of migration of the current database required.

You won't be able to open the existing database file if you'll try to upgrade the SQLCipher libraries without migrating it (the database file) first. The following error will be thrown in attempt to open existing database:
03-29 16:14:04.199  18107-18107/info.osom.sqlciphertest I/Database? sqlite returned: error code = 26, msg = file is encrypted or is not a database
03-29 16:14:04.199  18107-18107/info.osom.sqlciphertest E/Database? CREATE TABLE android_metadata failed
03-29 16:14:04.199  18107-18107/info.osom.sqlciphertest E/Database? Failed to setLocale() when constructing, closing the database
    net.sqlcipher.database.SQLiteException: file is encrypted or is not a database
            at net.sqlcipher.database.SQLiteDatabase.native_setLocale(Native Method)
            at net.sqlcipher.database.SQLiteDatabase.setLocale(SQLiteDatabase.java:2092)
            at net.sqlcipher.database.SQLiteDatabase.(SQLiteDatabase.java:1958)
            at net.sqlcipher.database.SQLiteDatabase.openDatabase(SQLiteDatabase.java:875)
            at net.sqlcipher.database.SQLiteDatabase.openOrCreateDatabase(SQLiteDatabase.java:907)
            at net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:132)
            at net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:99)
            at info.osom.sqlciphertest.MainActivity$PlaceholderFragment$1.onClick(MainActivity.java:69)
            at android.view.View.performClick(View.java:4202)
            at android.view.View$PerformClick.run(View.java:17340)
            at android.os.Handler.handleCallback(Handler.java:725)
            at android.os.Handler.dispatchMessage(Handler.java:92)
            at android.os.Looper.loop(Looper.java:137)
            at android.app.ActivityThread.main(ActivityThread.java:5039)
            at java.lang.reflect.Method.invokeNative(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:511)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
            at dalvik.system.NativeStart.main(Native Method)
You have two options to solve that. The first one is to reset iteration count back to 4000 using appropriate PRAGMA setting. The second option is to migrate the database in place using PRAGMA cipher_migrate setting. I will demonstrate below how to implement the cipher_migrate in your code.

Generally speaking, all you have to do is implement the SQLiteDatabaseHook interface that executes the cipher_migrate PRAGMA command on the existing database. It should be executing only once, and therefore you have to maintain a flag that tells you whether you already migrated or not. Initially, passing the instance of the class was not so convenient, and Mark Murphy proposed alternative for SQLiteOpenHelper class. Eventually, SQLCipher guys added another constructor that receives implementation of SQLiteDatabaseHook as a parameter.

In my solution, I slightly modificated Mark's implementation of SQLiteDatabaseHook:



Passing the SQLCipherV3Helper instance as a fifth argument to the SQLiteOpenHelper constructor:


Additional relevant links:
SQLCipher 3.0.0 release statement

2 comments:

  1. Hi Alex

    I've implemented you code above but unfortunately i'm still getting the usual 'file is encrypted or not a database' error.

    Do i have to explicitly call any methods or do i simply let the DBHelper constuctutor with 5 params execute?

    thanks

    Matt

    ReplyDelete
    Replies
    1. Hi Matt, 'file is encrypted or is not a database' error can be thrown in many flows, while this post focusing on aforementioned error only in context of upgrade to 3.x version. Can you confirm that this is your case?

      Delete