はじめに
SQLCipherとは、SQLiteのオープンソースの拡張機能で、データベースを暗号化して保存できる便利なツールとなっています。
今回は、このSQLCipherを使ったAndroidアプリをSQLiteOpenHelperからRoomに移行した際にハマったポイントを紹介しようと思います。
SQLiteOpenHelperを利用したコード
SQLiteOpenHelperを利用したコードは以下のようにimportするSQLiteOpenHelperをsqlcipherのものにするだけで簡単に利用できます。
import net.sqlcipher.Cursor;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteOpenHelper;
public class PasswordMemoOpenHelper extends SQLiteOpenHelper {
public PasswordMemoOpenHelper(Context context) {
super(context, "PasswordMemoDB", null, 3);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("create table passworddata("
+ "id long not null,"
+ "title text default '',"
+ "account text default '',"
+ "password text default '',"
+ "url text default '',"
+ "group_id long default 1,"
+ "memo text default '',"
+ "inputdate text default ''"
+ ");");
}
}
上記コードをRoomを利用したコードに変換していく際に、ハマったポイントを後述していきます。
ハマりポイント1:RoomではSQLCipher4を利用する必要がある
Roomを利用してSQLCipherを使うには、net.sqlcipher.database.SupportFactory
を利用する必要があり、そのために、SQLCipher3系からSQLCipher4系にアップグレードする必要がありました。
なので、以下のようにgradleを修正。
dependencies {
implementation 'net.zetetic:android-database-sqlcipher:3.5.9@aar'
}
↓
dependencies {
implementation 'net.zetetic:android-database-sqlcipher:4.4.2'
}
RoomDatabaseを利用したDB生成処理は、SupportFactoryを利用するように以下のように修正。
@Database(entities = [PasswordEntity::class, GroupEntity::class], version = 4, exportSchema = false)
abstract class PasswordMemoRoomDatabase : RoomDatabase() {
companion object {
@Volatile
private var INSTANCE: PasswordMemoRoomDatabase? = null
fun getDatabase(
context: Context,
): PasswordMemoRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
PasswordMemoRoomDatabase::class.java,
"PasswordMemoDB"
).allowMainThreadQueries()
.fallbackToDestructiveMigration()
.openHelperFactory(SupportFactory(SQLiteDatabase.getBytes(context.getString(R.string.db_secret_key).toCharArray())))
.build()
INSTANCE = instance
instance
}
}
}
}
ハマりポイント2:SQLCipher3系のDB読み込みエラー
上記のように修正後、SQLCipher3系で作られたDBを読み込むと以下のようなExceptionが発生。
net.sqlcipher.database.SQLiteException: file is not a database: , while compiling: select count(*) from sqlite_master;
どうやら、3系から4系に移行する場合PRAGMA cipher_migrate
を設定する必要があるらしく、公式サイトでは
このような案内がされていました。
で、実装方法について色々調べたところ、こちらの記事に出会しました。
SQLiteDatabaseHook
を利用してPRAGMA cipher_migrate
を設定することは分かったのですが、SupportFactory
を利用している場合の実装方法が分からんって思っていたら、
こちらの記事にある通り、SupportFactory
はオーバーロードされており、第二引数でSQLiteDatabaseHook
を渡せばいいことが分かりました。
Roomを利用したコード
上記のハマりポイントを解決した、Roomを利用したコードは以下となります。
import androidx.room.Room
import androidx.room.RoomDatabase
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteDatabaseHook
import net.sqlcipher.database.SupportFactory
@Database(entities = [PasswordEntity::class, GroupEntity::class], version = 4, exportSchema = false)
abstract class PasswordMemoRoomDatabase : RoomDatabase() {
companion object {
@Volatile
private var INSTANCE: PasswordMemoRoomDatabase? = null
fun getDatabase(
context: Context,
): PasswordMemoRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
PasswordMemoRoomDatabase::class.java,
"PasswordMemoDB"
).allowMainThreadQueries()
.fallbackToDestructiveMigration()
.openHelperFactory(SupportFactory(SQLiteDatabase.getBytes(context.getString(R.string.db_secret_key).toCharArray()), object : SQLiteDatabaseHook {
override fun preKey(database: SQLiteDatabase?) {}
override fun postKey(database: SQLiteDatabase?) {
val cursor = database?.rawQuery("PRAGMA cipher_migrate", null)
var migrationOccurred = false
if (cursor?.count == 1) {
cursor.moveToFirst()
val selection: String = cursor.getString(0)
migrationOccurred = selection == "0"
Log.d("selection", selection)
}
cursor?.close()
Log.d("migrationOccurred:", migrationOccurred.toString())
}
}))
.build()
INSTANCE = instance
instance
}
}
}
}
さいごに
まだアップデートに向けて開発途中ですが、GitHubでコードを公開していますのでぜひ参考にしてみて下さい。