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’)