SQLCipher cannot build database after restarting app

I am using the SafeRoom encrypt function to encrypt my un-encrypted DB with a passphrase. However, upon restarting the app, I get the following error:

Caused by: net.sqlcipher.database.SQLiteException: file is not a database: , while compiling: select count(*) from sqlite_master;

This is how I am currently encrypting the database. This method is called when the app first starts up. I am creating a secure passphrase, and saving it in shared preferences. Upon returning to the app, I retrieve the key again to use for creating a new instance of the database. However, the above error keeps occurring when any database transactions occur.

    @JvmStatic
    fun getInstance(context: Context): AppDatabase {
        return INSTANCE ?: synchronized(this) {
            INSTANCE ?: buildDatabase(context).also { INSTANCE = it }
        }
    }

    private fun buildDatabase(context: Context): AppDatabase {
        val sharedPrefs = MySharedPreferences(context)

        val passphrase = if (sharedPrefs.databaseEncryptionKey == null) {
            val symKey = SecurityUtils.generate256AESKey()

            try {
                val db = RoomDatabaseUtils.getDatabaseFromDatabaseName(context, DB_NAME)
                RoomDatabaseUtils.encrypt(context, db, symKey.encoded)
            } catch (exception: Exception) {
                Log.d(TAG, "Could not encrypt database: ${exception.stackTrace}")
            }

            sharedPrefs.databaseEncryptionKey = symKey.encoded // store key in shared prefs
            symKey.encoded
        } else {
            sharedPrefs.databaseEncryptionKey
        }

        val factory = SupportFactory(passphrase, null, false)
        return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DB_NAME)
                .openHelperFactory(factory)
                .addMigrations(MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12)
                .fallbackToDestructiveMigrationFrom(1, 2, 3, 4)
                .build()
    }

the encrypt function I use above is directly copy-pasted from SafeRoom. I am using the following, most up-to-date gradle implementations:

implementation "net.zetetic:android-database-sqlcipher:4.4.0"
implementation "androidx.sqlite:sqlite:2.1.0"

Hi @json

Are you able to reproduce the behavior in the SQLCipher for Android test suite?

@developernotes I have, and to be honest, those tests are in no means comprehensive.

At this point, the only way I can reproduce this is after a few hours of use on the app, which that test suite doesn’t cover. The encryption process seems to work, but after an hour or two, that error appears.
The way I reproduced this last was by the following steps:

  1. Start with an app with an un-encrypted Room database that contains data
  2. Rebuild app with new code (above) that encrypts the DB and rebuilds the database/recreates the Instance using SQLCipher (in strict accordance with the documentation and code provided)
  3. Turn off device for some hours
  4. Turn on device and open/close app a few times

The act of just using the device by opening/closing the app and turning off/on the tablet seems to have caused this bug, as it functions seemingly fine initially.
I also noticed, upon inspection of the database, that the database is only set to -rw-------, whereas before it was -rw-rw----, such that the group owner’s permission was also rw. Should encrypting the db also remove the group owner’s RW ability?

@json Given the scenario you are describing, I think it would be helpful to narrow down whether this is occurring because of an issue with the database library, or the encryption key. Purely as a test, can you make a minor change to your implementation so that a static / hardcoded key is used every time? Then try to see if you can reproduce the situation when there is no generation of a random key or use of shared preferences.

@sjlombardo Yes, I’ve been testing that, and have been able to narrow it down to the fact that using a new passphrase to encrypt the database causes this error. My guess is that upon recreating the database, we must actually delete it first.
Is there any other way to change passphrase?

The issue is as follows:
I encrypt the DB for a user. If the user logs out, I clear the passphrase, but the database remains encrypted.
Upon logging in again, a new passphrase is created, and attempts to be used when creating the database. It appears that the database needs to be deleted in this event.

@json I am not really clear on what you are trying to do here. Is the data in the database supposed to persist between logins, or is it only of a temporary nature that should be discarded and recreated anew each time a user logs in? Do you still have the old encryption key at the point that you are generating a new passphrase?

The data in the database should be discarded and recreated anew each time a user logs in.
I don’t have the old encryption key, as that is removed from shared preferences at the point of logout.
Also, the user can clear the cache at any point when going into the System app settings and “clear app data”. This would force a logout when the user returns to the app, but it is another case where the passphrase would be cleared.
This is why I am aiming to recreate a new passphrase in the event a new database is created. However, Room doesn’t rebuild the database, it only recreates the instance pointing to it. Therefore, I need to ensure the database is also deleted when the passphrase is deleted, otherwise the new passphrase would be used to try to open a database that’s locked with the old passphrase.

Hello @json, thank you for explaining the situation. Yes, in that case you should delete the database file at the time that your clear the old encryption key, or before generating a new one. It is certainly possible to change the encryption key for a database using PRAGMA rekey, which will rewrite a database to use a new encryption key. However, it requires that you provide the old encryption key first, since each page of the database must be decrypted with the old key and then re-encrypted with the new key. Since you have discarded the old key your best option is to destroy the old database entirely and then recreate it again from scratch with the new key.