How to Secure Your Android Room Database with SQLCipher and Keystore
Learn step-by-step how to encrypt an Android Room database using SQLCipher, generate a random AES-256 key, protect it with the Android Keystore, and ensure the key is cleared from memory, providing production-grade data security against reverse engineering and local tampering.
When developing Android apps, we often use Room for local data storage. If the app is reverse‑engineered, extracting data is easy. Encrypting the Room database is a crucial step to protect user data. This article explains how to add encryption to a Room database using SQLCipher and Android Keystore.
Why encrypt the Room database?
Room itself does not provide encryption. We can use SQLCipher for Android, a drop‑in replacement for SQLite that includes built‑in encryption, is reliable, maintained, and widely trusted.
Step 1: Add dependencies
First add the required dependencies to build.gradle:
// SQLCipher
implementation "net.zetetic:sqlcipher-android:4.9.0" // or latest version
// Room
implementation("androidx.room:room-runtime:$room_version")
ksp("androidx.room:room-compiler:$room_version")Step 2: Securely generate encryption key
Do not hard‑code the password. Generate a random 256‑bit key at runtime and protect it with Android Keystore.
Incorrect example :
val key = SQLiteDatabase.getBytes("your-password".toCharArray())This is insecure because the password can be extracted from the APK.
Correct approach :
val sqlCipherKey = ByteArray(32)
SecureRandom().nextBytes(sqlCipherKey)The key still needs secure storage; using SharedPreferences alone is insufficient.
Step 3: Protect the key with Android Keystore
Before storing, encrypt the generated key with Android Keystore. Create SqlCipherKeyManager:
class SqlCipherKeyManager(private val sharedPreferences: SharedPreferences) {
private val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
init { initialize() }
private fun initialize() {
generateKeyStoreKeyIfNeeded()
if (!sharedPreferences.contains("encrypted_key")) {
generateAndEncryptSqlCipherKey()
}
}
private fun generateKeyStoreKeyIfNeeded() {
if (!keyStore.containsAlias("sqlcipher_keystore_key")) {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES)
val keySpec = KeyGenParameterSpec.Builder(
"sqlcipher_keystore_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
keyGenerator.init(keySpec)
keyGenerator.generateKey()
}
}
private fun generateAndEncryptSqlCipherKey() {
val secretKey = getSecretKey("sqlcipher_keystore_key")
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val sqlCipherKey = ByteArray(32)
SecureRandom().nextBytes(sqlCipherKey)
val encryptedKey = cipher.doFinal(sqlCipherKey)
val iv = cipher.iv
sharedPreferences.edit {
putString("encrypted_key", Base64.encodeToString(encryptedKey, Base64.NO_WRAP))
putString("encryption_iv", Base64.encodeToString(iv, Base64.NO_WRAP))
}
// Clear key from memory
sqlCipherKey.fill(0)
}
private fun getDecryptedSqlCipherKey(): ByteArray {
val encryptedKey = Base64.decode(sharedPreferences.getString("encrypted_key", null)!!, Base64.NO_WRAP)
val iv = Base64.decode(sharedPreferences.getString("encryption_iv", null)!!, Base64.NO_WRAP)
val secretKey = getSecretKey("sqlcipher_keystore_key")
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, iv))
return cipher.doFinal(encryptedKey)
}
private fun getSecretKey(keyAlias: String): SecretKey {
return (keyStore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry).secretKey
}
fun getSupportFactory(): SupportOpenHelperFactory {
val decryptedKey = getDecryptedSqlCipherKey()
return WipeAfterUseSupportFactory(decryptedKey)
}
private class WipeAfterUseSupportFactory(private val decryptedKey: ByteArray) : SupportOpenHelperFactory(decryptedKey) {
override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper {
val helper = super.create(configuration)
decryptedKey.fill(0)
return helper
}
}
}Step 4: Clear sensitive data from memory
To ensure the key does not linger in memory, customize SupportOpenHelperFactory:
class DisposableKeySupportFactory(private val decryptedKey: ByteArray) : SupportOpenHelperFactory(decryptedKey) {
override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper {
val helper = super.create(configuration)
decryptedKey.fill(0)
return helper
}
}Room will clear the key from memory immediately after use.
Step 5: Integrate with Room
Initialize Room with the encrypted SupportFactory:
// Load SQLCipher library (important!)
System.loadLibrary("sqlcipher")
val sqlCipherKeyManager = SqlCipherKeyManager(sharedPreferences)
val db = Room.databaseBuilder(context, YourDatabase::class.java, "your-db-name")
.openHelperFactory(sqlCipherKeyManager.getSupportFactory())
.build()Summary
This method uses SQLCipher to apply AES‑256 encryption to the entire Room database.
The encryption key is randomly generated and protected by Android Keystore.
After decryption, the key is immediately cleared from memory.
By writing a few classes and handling the encryption key carefully, you can significantly improve the security of local storage in your app. This solution is production‑grade, leveraging mature cryptographic primitives to defend against reverse engineering and local tampering. Of course, the safest approach is to avoid storing highly sensitive data on the device whenever possible.
AndroidPub
Senior Android Developer & Interviewer, regularly sharing original tech articles, learning resources, and practical interview guides. Welcome to follow and contribute!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
