File is not a database (code 26)

Users say they get “file is not a database (code 26): , while compiling: SELECT COUN…”.
I can’t reproduce the error. I’ve read all the similar threads but haven’t found an answer. I hope you can help.

class DatabaseProvider @Inject constructor(
@param:Named("database_name") private val databaseName: String, //"cache.db"
private val context: Context,
private val sqlCipherKeyManager: SqlCipherKeyManager,
private val isCipherEnabled: Boolean
) {
@volatile
private var database: MyRoomDatabase? = null

@Synchronized
fun getDatabase(recreate: Boolean = false): MyRoomDatabase {
    if (recreate) {
        closeAndDelete()
    }
    return database ?: buildDatabase().also { database = it }
}

private fun closeAndDelete() {
    database?.close()
    deleteDatabaseFiles(context, databaseName)
    sqlCipherKeyManager.clearEncryptionKeys()
    database = null
}

private fun buildDatabase(): MyRoomDatabase {
    val builder = Room.databaseBuilder(context, MyRoomDatabase::class.java, databaseName)

    if (isCipherEnabled) {
        builder.openHelperFactory(sqlCipherKeyManager.getSupportFactory())
    }

    return builder
        .allowMainThreadQueries()
        .setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
        .addCallback(object : RoomDatabase.Callback() {
            override fun onOpen(db: SupportSQLiteDatabase) {
                super.onOpen(db)
                Log.d("DATABASE_LOG", "opened ${db.version}")
            }

            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
                Log.d("DATABASE_LOG", "created ${db.version}")
            }
        })
        .build()
}

private fun deleteDatabaseFiles(context: Context, databaseName: String) {
    val dbFile = context.getDatabasePath(databaseName)
    val filesToDelete = listOf(dbFile, File("${dbFile.absolutePath}-shm"), File("${dbFile.absolutePath}-wal"))
    filesToDelete.forEach { if (it.exists()) it.delete() }
}
}
class SqlCipherKeyManager @Inject constructor(
private val sharedPreferences: SharedPreferences,
private val errorReporter: ErrorReporter
) {
private val keyStore: KeyStore = KeyStore.getInstance(KEY_ANDROID_KEYSTORE).apply { load(null) }

init {
    System.loadLibrary("sqlcipher")
    initialize()
}

private fun initialize() {
    generateKeystoreKeyIfNeeded()
    if (!sharedPreferences.contains(KEY_ENCRYPTED)) {
        generateAndEncryptSqlCipherKey()
    }
}

private fun generateKeystoreKeyIfNeeded() {
    if (!keyStore.containsAlias(KEY_SQLCIPHER_KEYSTORE)) {
        val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_ANDROID_KEYSTORE)
        val keyGenSpec = KeyGenParameterSpec.Builder(
            KEY_SQLCIPHER_KEYSTORE,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .build()
        keyGenerator.init(keyGenSpec)
        keyGenerator.generateKey()
    }
}

private fun generateAndEncryptSqlCipherKey() {
    val secretKey = getSecretKey(KEY_SQLCIPHER_KEYSTORE)
    val cipher = getCipher()
    cipher.init(Cipher.ENCRYPT_MODE, secretKey)

    val sqlCipherKey = ByteArray(size = 32)
    SecureRandom().nextBytes(sqlCipherKey)

    val encryptedKey = cipher.doFinal(sqlCipherKey)
    val iv = cipher.iv

    sharedPreferences.edit {
        putString(KEY_ENCRYPTED, Base64.encodeToString(encryptedKey, Base64.NO_WRAP))
        putString(KEY_ENCRYPTION_IV, Base64.encodeToString(iv, Base64.NO_WRAP))
    }
    sqlCipherKey.fill(0)
}

private fun getDecryptedSqlCipherKey(keyAlias: String, key: String, iv: String): ByteArray {
    val encryptedKey = Base64.decode(key, Base64.NO_WRAP)
    val ivBytes = Base64.decode(iv, Base64.NO_WRAP)

    if (encryptedKey.isEmpty() || iv.isEmpty()) {
        throw CantOpenDatabaseException("Encryption key or IV is missing. Database cannot be opened.")
    }

    val secretKey = getSecretKey(keyAlias)
    val cipher = getCipher()
    cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(TAG_LENGTH, ivBytes))

    return cipher.doFinal(encryptedKey)
}

private fun getSecretKey(keyAlias: String): SecretKey =
    (keyStore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry).secretKey

fun getSupportFactory(): SupportOpenHelperFactory {
    if (!keyStore.containsAlias(KEY_SQLCIPHER_KEYSTORE)) {
        initialize()
    }

    val encryptedKey = sharedPreferences.getString(KEY_ENCRYPTED, null).orEmpty()
    val iv = sharedPreferences.getString(KEY_ENCRYPTION_IV, null).orEmpty()
    val decryptedKey = getDecryptedSqlCipherKey(KEY_SQLCIPHER_KEYSTORE, encryptedKey, iv)
    return DisposableKeySupportFactory(decryptedKey)
}


fun clearEncryptionKeys() {
    try {
        sharedPreferences.edit {
            remove(KEY_ENCRYPTED)
            remove(KEY_ENCRYPTION_IV)
        }

        if (keyStore.containsAlias(KEY_SQLCIPHER_KEYSTORE)) {
            keyStore.deleteEntry(KEY_SQLCIPHER_KEYSTORE)
        }
    } catch (e: Exception) {
        errorReporter.reportError("$TAG clearEncryptionKeys", e)
    }
}

private fun getCipher() = Cipher.getInstance(CIPHER_TRANSFORMATION)

companion object {
    private const val KEY_ANDROID_KEYSTORE = "AndroidKeyStore"
    private const val KEY_SQLCIPHER_KEYSTORE = "sab_sqlcipher_keystore_key"
    private const val KEY_ENCRYPTED = "encrypted_key"
    private const val KEY_ENCRYPTION_IV = "encryption_iv"
    private const val CIPHER_TRANSFORMATION = "AES/GCM/NoPadding"
    private const val TAG_LENGTH = 128
}
}

When the application starts, a large number of API requests are made, the results of which are saved to the database.
And somewhere around this time, this error occurs.

I’m using net.zetetic:sqlcipher-android:4.13.0 and androidx.room:room-ktx:2.7.2 (not use implementation(group: ‘androidx.sqlite’, name: ‘sqlite’)

The invalid keys scenario seems unlikely to me, as keys are generated when they don’t exist, and this condition is met upon first login to the application. Keys are cleared only when a different user logs in. DatabaseProvider and SqlCipherKeyManager are created as singletons in the application.

could the reason be obfuscation?

What do you recommend setJournalMode(JournalMode.WRITE_AHEAD_LOGGING) or setJournalMode(RoomDatabase.JournalMode.TRUNCATE)?

Hi @GooDi,

There are many benefits to using WAL mode as it’s generally faster in most scenarios, and provides more concurrency readers and writers do not block each other.

With regard to your keying issue, in any environment where this occurs, are you able to retrieve the database off the device along with the password to see if you’re able to access the database using the SQLCipher command line shell?

I don’t know how to access the database using the SQLCipher command line shell.
In fact, I can’t understand why the keys could be incorrect. How could this even happen?

If i use implementations ‘net.zetetic:sqlcipher-android:4.16.0’ and ‘androidx.room:room-ktx:2.8.4’ do i need use “androidx.sqlite:sqlite:2.6.2”?

Hi @GooDi,

It is difficult to speculate as to why you are experiencing this behavior given the sample you have provided. In general, the most likely cause in these scenarios is incorrect key material being provided to open the database, though corruption cannot be ruled out entirely.

There is an article [1] covering the evolution of the Android Keystore and the various failure modes that can occur across Android versions. A few specific scenarios worth being aware of:

  • Biometric invalidation: By default, if setInvalidatedByBiometricEnrollment is not explicitly set to false [2], enrolling a new biometric (fingerprint, face) will permanently invalidate the key. This is a common and easy-to-miss cause of key loss.
  • Lock screen removal: Keys created with setUserAuthenticationRequired(true) are permanently invalidated if the user disables their lock screen entirely.
  • Version binding: From Android 7 onward [3], keys are bound to the OS and patch level. If a device is rolled back to a previous OS version, existing keys become unusable.

If you are experiencing this issue sporadically and cannot reproduce it, it is possible one of the environment-specific scenarios above may be the cause.

For graceful recovery, a recommended pattern is to catch KeyPermanentlyInvalidatedException and UnrecoverableKeyException when attempting to access the key, delete the invalidated Keystore entry, and generate a new key. Keep in mind that if the original key used to encrypt the database is gone, the existing database will be unrecoverable. The application would need to handle this by re-creating the database and re-populating it for your use case.

If you are able to reproduce the issue on a specific device, it would be helpful to extract the database and test it against the SQLCipher command-line shell using the expected key material. This would help narrow down whether the root cause is invalid key material or actual database corruption. For guidance on using the SQLCipher command-line shell, please refer to this documentation [4]. If your device is attached to a development machine you can use the Device Explorer in Android Studio [5]. Alternatively, you can use the Android SDK tools to pull it from the device using adb:

adb shell
run-as <com.your.package.name>
cp databases/<database>.db /sdcard/
exit
exit
adb pull /sdcard/<database>.db

We offer general guidance on key management [6] and note hardware-based keystores as an option, but it is important to understand that the Android Keystore carries inherent limitations and failure scenarios that must be accounted for in your application’s error handling strategy.

Please keep us posted as to your findings.


  1. Android Security: The Forgetful Keystore – SystemDotRun – Dorian Cussen's Super Blog ↩︎

  2. https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setInvalidatedByBiometricEnrollment(boolean) ↩︎

  3. https://source.android.com/docs/security/features/keystore#android7 ↩︎

  4. SQLCipher for Linux - Community Edition | Zetetic ↩︎

  5. https://developer.android.com/studio/debug/device-file-explorer ↩︎

  6. SQLCipher Database Key Material and Selection | Zetetic ↩︎

I made changes so that the byte array does not contain null bytes or characters that are not included in UTF-8

   private fun generateAndEncryptSqlCipherKeyIfNeeded() {
        if (!sharedPreferences.contains(KEY_ENCRYPTED) || !sharedPreferences.contains(KEY_ENCRYPTION_IV)) {
            val cipher = getCipher()
            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())

            val sqlCipherKey = ByteArray(size = 32)
            SecureRandom().nextBytes(sqlCipherKey)

            val sqlCipherKeyString = Base64.encodeToString(sqlCipherKey, Base64.NO_WRAP)
            sqlCipherKey.fill(0)
            val encryptedKey = cipher.doFinal(sqlCipherKeyString.toByteArray().copyOf(newSize = 32))
  
            val iv = cipher.iv

            sharedPreferences.edit(commit = true) {
                putString(KEY_ENCRYPTED, Base64.encodeToString(encryptedKey, Base64.NO_WRAP))
                putString(KEY_ENCRYPTION_IV, Base64.encodeToString(iv, Base64.NO_WRAP))
            }
        }
    }

obtaining the key has generally remained the same

private fun getDecryptedSqlCipherKey(): ByteArray {
    val key = sharedPreferences.getString(KEY_ENCRYPTED, null).orEmpty()
    val iv = sharedPreferences.getString(KEY_ENCRYPTION_IV, null).orEmpty()
    val encryptedKey = Base64.decode(key, Base64.NO_WRAP)
    val ivBytes = Base64.decode(iv, Base64.NO_WRAP)

    if (encryptedKey.isEmpty() || ivBytes.isEmpty()) {
        throw CantOpenDatabaseException("Encryption key or IV is missing. Database cannot be opened.")
    }

    val cipher = getCipher()
    cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), GCMParameterSpec(TAG_LENGTH, ivBytes))
    return cipher.doFinal(encryptedKey)
}

I downloaded the database through Device Explorer in Android Studio and have been trying to open the database file in DB Browser for two days.

class DisposableKeySupportFactory(private val decryptedKey: ByteArray) : SupportOpenHelperFactory(decryptedKey) {

    override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper {
        val helper = super.create(configuration)
        Log.d("debug", "SQLCipher key hex = ${decryptedKey.toHexString()}")
        decryptedKey.fill(0)
        return helper
    }
}

This “SQLCipher key hex” does not work although I checked the value against the one before encryption (before calling doFinal). What am I doing wrong?!

If do it this way

SupportOpenHelperFactory("12345".toByteArray())

then the database is opened using the phrase “12345”