Difficulty Migrating Database from SQLCipher 3.5.9 to 4.5.4 - android.database.sqlite.SQLiteException: file is not a database (code 26): ,

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.

Hi @Ahlem_Jarrar,

It appears you were using a non-default configuration for a SQLCipher 3 database with a kdf_iter set to 1. Because of this, PRAGMA cipher_migrate will not be able to migrate the format from 3 to 4 by default. Instead, you will need to utilize the sqlcipher_export(...) [1] convenience function to customize the conversion process based on your SQLCipher 3 configuration to SQLCipher 4.


  1. SQLCipher API - Full Database Encryption PRAGMAs, Functions, and Settings | Zetetic ↩ī¸Ž

I read in a post that the PRAGMA cipher command has been disabled and is no longer supported after a multi-year deprecation. To migrate, we need to create a custom build of SQLCipher for Android to meet our requirements. Could you please provide more details on how to proceed with this?

Hi @Ahlem_Jarrar,

We would be happy to assist you with this. Please reach out privately via support@zetetic.net and we can discuss your needs in further detail. Thanks!

1 Like