Storing raw user credentials, OAuth tokens, or sensitive transactions in a standard local SQLite database is a major security vulnerability. Anyone with a rooted device or access to a backup can extract the database file and read the tables in plain text.
To secure your local storage, you need to encrypt the database using SQLCipher and generate the encryption keys inside the device's hardware-backed Keystore.
Here is the exact implementation details.
1. Setup SQLCipher Dependency
First, add the SQLCipher library to your module's dependency configuration:
// build.gradle.kts
dependencies {
implementation("net.zetetic:android-database-sqlcipher:4.5.4")
implementation("androidx.sqlite:sqlite-ktx:2.4.0")
}
2. Generating the Encryption Key in the TEE (Keystore)
Never hardcode your database passphrase in the Kotlin source. Instead, generate an AES key inside the device's secure hardware enclave—the Trusted Execution Environment (TEE)—and require biometric verification before the key can be accessed.
fun generateSecretKey() {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
if (!keyStore.containsAlias("DatabaseKeyAlias")) {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
val spec = KeyGenParameterSpec.Builder(
"DatabaseKeyAlias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true) // Requires biometrics (fingerprint/face)
.setUserAuthenticationValidityDurationSeconds(-1) // Authenticate per session/call
.setInvalidatedByBiometricEnrollment(true) // Key invalidates if new fingers enrolled
.build()
keyGenerator.init(spec)
keyGenerator.generateKey()
}
}
3. Initializing Room with the SQLCipher Open Helper
Once the key is generated in the hardware, construct a helper factory that feeds the passphrase to SQLCipher's open helper during database initialization:
fun getEncryptedDatabase(context: Context, passphraseString: String): AppDatabase {
// Convert passphrase string to byte array
val passphrase = SQLiteDatabase.getBytes(passphraseString.toCharArray())
// Create the SQLCipher OpenHelper Factory
val factory = SupportOpenHelperFactory(passphrase)
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"secure_app.db"
)
.openHelperFactory(factory) // Wraps standard Room with SQLCipher
.build()
}
4. Handling Biometric Key Access
To open the database, trigger the system biometric prompt and pass the resulting cipher object to decrypt your passphrase:
fun decryptPassphrase(cipher: Cipher, encryptedData: ByteArray): String {
val decryptedBytes = cipher.doFinal(encryptedData)
return String(decryptedBytes, Charsets.UTF_8)
}
If the device is rooted or someone tries to clone the app, the SQLite database file remains completely unreadable (displays as corrupted binary noise).
Open-Source Reference
This security implementation is part of the open-source Android System Design & Architecture Checklist. You can clone the full repository of cryptographic configurations, SSL pinning helpers, and offline sync transaction queues here:
👉 GitHub: Android System Design & Architecture Checklist (A print-ready, A4 PDF version is also pinned in the repository description).
Top comments (0)