I have been working on migrating our DB on a Kotlin Multiplatform project to be encrypted from unencrypted. It's all done on Android, however the iOS part is proving tricky. Finally I got it somewhat working, however when I return the DB driver, I get this error:
Function doesn't have or inherit @Throws annotation and thus exception isn't propagated from Kotlin to Objective-C/Swift as NSError. It is considered unexpected and unhandled instead. Program will be terminated. Uncaught Kotlin exception: kotlin.Exception: android/database/sqlite/SQLiteDatabaseCorruptException - file is not a database (code 26): , while compiling: PRAGMA journal_mode
It's strange as it mentions android/database and I'm not sure why. Anyways for the migration, I have logs setup and I can see that it performs it, and if I debug the app and pull the DB, it does look like the DB has now been encrypted and has old data on it too. It seems to crash when it gets to this code:
NativeSqliteDriver(DatabaseConfiguration(
name = DatabaseName,
version = AppDatabase.Schema.version,
create = { connection -> wrapConnection(connection) { AppDatabase.Schema.create(it) } },
upgrade = { connection, oldVersion, newVersion ->
try {
wrapConnection(connection) {
NSLog("old version is ${oldVersion} new version is ${newVersion}")
AppDatabase.Schema.migrate(it, oldVersion, newVersion)
}
} catch (exception: Exception) {
NSLog("exception is ${exception.toString()}")
}
}
//Workaround for DatabaseConnection.setCipherKey causing an exception on iOS 14
configConnection = { connection, _ ->
val statement = "PRAGMA key = \"$password\";"
connection.withStatement(statement) {
stringForQuery()
}
}
))
Breakpoints never trigger in the upgrade try/catch. The migration logic looks like this and is performed before returning the NativeSqlLiteDriver.
@ExperimentalUnsignedTypes
override fun migrateToEncryptedDatabase(databasePath: String, temporaryDatabasePath: String, password: String) {
val fileManager = NSFileManager.defaultManager()
fileManager.createFileAtPath(temporaryDatabasePath, null, null)
if (fileManager.fileExistsAtPath(databasePath)) {
memScoped {
val unencryptedDb: CPointerVar<sqlite3> = allocPointerTo()
val encryptedDb: CPointerVar<sqlite3> = allocPointerTo()
if (sqlite3_open(databasePath, unencryptedDb.ptr) == SQLITE_OK) {
val exec1 = sqlite3_exec(unencryptedDb.value, "ATTACH DATABASE '$temporaryDatabasePath' AS encrypted KEY '$password';", null, null, null)
val exec2 = sqlite3_exec(unencryptedDb.value, "SELECT sqlcipher_export('encrypted')", null, null, null)
val exec3 = sqlite3_exec(unencryptedDb.value, "DETACH DATABASE encrypted;", null, null, null)
val version = sqlite3_version
sqlite3_close(unencryptedDb.value)
if (sqlite3_open(temporaryDatabasePath, encryptedDb.ptr) == SQLITE_OK) {
sqlite3_key(encryptedDb.value, password.cstr, password.cstr.size)
}
sqlite3_close(unencryptedDb.value)
val error: ObjCObjectVar<NSError?> = alloc()
val removeResult = fileManager.removeItemAtPath(databasePath, error.ptr)
if (removeResult == false) {
NSLog("Error removing db file: " + error.value)
} else {
}
val result = fileManager.moveItemAtPath(temporaryDatabasePath, databasePath, error.ptr)
if (result == false) {
NSLog("Error moving db file: " + error.value)
} else {
}
} else {
NSLog("Failed to open the unencrypted DB with message: " + sqlite3_errmsg(unencryptedDb.value))
sqlite3_close(unencryptedDb.value)
}
}
}
}
Thanks for any help
This was actually caused by not updating the version correctly. When testing, I thought I had updated the version correctly, however it was still returning as 0 instead of 1. I solved it by doing this. First a method to retrieve the current DB version:
private fun getUserVersion(unencryptedDBPointer: CPointerVar<sqlite3>): Int? {
memScoped {
val sqliteStatementPointer: CPointerVar<sqlite3_stmt> = allocPointerTo()
var databaseVersion: Int? = null
if (sqlite3_prepare_v2(unencryptedDBPointer.value, "PRAGMA user_version;", USER_VERSION_STATEMENT_MAX_LENGTH, sqliteStatementPointer.ptr, null) == SQLITE_OK) {
while (sqlite3_step(sqliteStatementPointer.value) == SQLITE_ROW) {
databaseVersion = sqlite3_column_int(sqliteStatementPointer.value, COLUMN_TO_USE)
}
} else {
Logger.d("Error preparing the database: ${sqlite3_errmsg(unencryptedDBPointer.value)}")
}
sqlite3_finalize(sqliteStatementPointer.value)
return databaseVersion
}
}
Then, inside my migration method, I had this code:
val dbVersion = getUserVersion(unencryptedDbPointer)
if (sqlite3_open(temporaryDatabasePath, encryptedDbPointer.ptr) == SQLITE_OK && dbVersion != null) {
sqlite3_key(encryptedDbPointer.value, password.cstr, password.cstr.size)
sqlite3_exec(encryptedDbPointer.value, "PRAGMA user_version=$dbVersion", null, null, null)
}
This correctly set the version on the DB and fixed the issue.