We are experiencing difficulties migrating our database from SQLCipher version 3.5.9 to 4.5.4.
android.database.sqlite.SQLiteException: file is not a database (code 26): ,
Steps Taken
Attempted Option 1 from Documentation
As per the Upgrading to SQLCipher 4 documentation, we tried the following approach:
long result = connection.executeForLong("PRAGMA cipher_migrate;", null, null);
This resulted in the same issue.
Custom Migration Attempt (Option 3)
We then attempted a custom migration strategy similar to Option 3 from the documentation:
CipherOpenHelper(@NonNull Context context, byte[] password) {
super(
context,
D.DATABASE_NAME,
password,
null,
DATABASE_VERSION,
MIN_DATABASE_VERSION,
null,
new SQLiteDatabaseHook() {
@Override
public void preKey(SQLiteConnection connection) {
applySQLCipherPragmas(connection, true);
}
@Override
public void postKey(SQLiteConnection connection) {
applySQLCipherPragmas(connection, true);
// if not vacuumed in a while, perform that operation
long currentTime = System.currentTimeMillis();
// 7 days
if (currentTime - getLastVacuumTime() > 604_800_000) {
connection.execute("VACUUM;", null, null);
Preferences.setLastVacuumNow();
}
}
},
// Note: Now that we support concurrent database reads the migrations are actually non-blocking
// because of this we need to initially open the database with writeAheadLogging (WAL mode) disabled
// and enable it once the database officially opens it's connection (which will cause it to re-connect
// in WAL mode) - this is a little inefficient but will prevent SQL-related errors/crashes due to
// incomplete migrations
false
);
this.context = context.getApplicationContext();
//this.databaseSecret = databaseSecret;
}
The applySQLCipherPragmas :
private static void applySQLCipherPragmas(SQLiteConnection connection, boolean useSQLCipher4) {
if (useSQLCipher4) {
connection.execute("PRAGMA cipher_compatibility = '4';",null, null);
connection.execute("PRAGMA kdf_iter = '256000';", null, null);
}
else {
connection.execute("PRAGMA cipher_compatibility = 3;", null, null);
connection.execute("PRAGMA kdf_iter = '1';", null, null);
}
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
}
And we have our specific method for migrate from 3 to 4 :
public static void migrateSqlCipher3To4IfNeeded(@NonNull Context context, byte[] key) {
String oldDbPath = context.getDatabasePath(CIPHER3_DATABASE_NAME).getAbsolutePath();
File oldDbFile = new File(oldDbPath);
// If the old SQLCipher3 database file doesn't exist then just return early
if (!oldDbFile.exists()) {
Log.d(TAG, "Old database file does not exist at: " + oldDbPath);
return;
}
// Log the path to ensure it's correct
Log.d(TAG, "Old database file path: " + oldDbPath);
// If the new database file already exists then we probably had a failed migration and it's likely in
// an invalid state so should delete it
String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath();
File newDbFile = new File(newDbPath);
if (newDbFile.exists()) {
if (!newDbFile.delete()) {
Log.e(TAG, "Failed to delete existing new database file at: " + newDbPath);
return;
}
}
try {
if (!newDbFile.createNewFile()) {
Log.e(TAG, "Failed to create new database file at: " + newDbPath);
return;
}
} catch (Exception e) {
Log.e(TAG, "Exception while creating new database file", e);
return;
}
try {
// Open the old database
SQLiteDatabase oldDb = SQLiteDatabase.openDatabase(oldDbPath, key, null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() {
@Override
public void preKey(SQLiteConnection connection) {
connection.execute("PRAGMA cipher_compatibility = 3;", null, null);
connection.execute("PRAGMA kdf_iter = 64000;", null, null);
connection.execute("PRAGMA cipher_page_size = 1024;", null, null);
}
@Override
public void postKey(SQLiteConnection connection) {
connection.execute("PRAGMA cipher_compatibility = 3;", null, null);
connection.execute("PRAGMA kdf_iter = 64000;", null, null);
connection.execute("PRAGMA cipher_page_size = 1024;", null, null);
}
});
// Verify if old database is opened successfully
if (oldDb == null) {
Log.e(TAG, "Failed to open old database. Database object is null.");
return;
}
Log.d(TAG, "Old database opened successfully");
// Check if the database is actually a valid SQLite database
if (!isDatabaseValid(oldDb)) {
Log.e(TAG, "Old database is not a valid SQLite database");
oldDb.close();
return;
}
// Export the old database to the new one (will have the default 'kdf_iter' and 'page_size' settings)
int oldDbVersion = oldDb.getVersion();
oldDb.rawExecSQL(String.format("ATTACH DATABASE '%s' AS sqlcipher4 KEY '%s'", newDbPath, key));
Cursor cursor = oldDb.rawQuery("SELECT sqlcipher_export('sqlcipher4')");
cursor.moveToLast();
cursor.close();
oldDb.rawExecSQL("DETACH DATABASE sqlcipher4");
oldDb.close();
// Open the new database
SQLiteDatabase newDb = SQLiteDatabase.openDatabase(newDbPath, key, null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() {
@Override
public void preKey(SQLiteConnection connection) {
connection.execute("PRAGMA cipher_default_kdf_iter = 256000;", null, null);
connection.execute("PRAGMA cipher_default_page_size = 4096;", null, null);
}
@Override
public void postKey(SQLiteConnection connection) {
connection.execute("PRAGMA cipher_default_kdf_iter = 256000;", null, null);
connection.execute("PRAGMA cipher_default_page_size = 4096;", null, null);
}
});
// Set the version of the new database to match the old database
newDb.setVersion(oldDbVersion);
newDb.close();
// Optional: Delete the old database file if migration is successful
// oldDbFile.delete();
} catch (Exception e) {
Log.e(TAG, "Exception during database migration", e);
// Clean up: Delete the new database file if migration failed
if (newDbFile.exists() && !newDbFile.delete()) {
Log.e(TAG, "Failed to delete new database file after migration failure");
}
}
}
Issue
Despite following the documentation and attempting a custom migration strategy, we continue to face issues with the migration process.
Could you provide guidance or suggestions on how to successfully migrate our database from SQLCipher 3.5.9 to 4.5.4? Any insights or alternative approaches would be greatly appreciated.
Thank you for your support.