An Encrypted Local Database Backed by the Phone's Secure Hardware

Photo Cleaner's keep/delete decisions live in Room over SQLCipher, keyed by a passphrase that never exists in plaintext on disk — wrapped by a StrongBox-backed Android Keystore key when the device has one.

An Encrypted Local Database Backed by the Phone's Secure Hardware

Every swipe decision in this app — keep, delete, undo — is written to a local Room database before anything ever touches your Google account. That database is encrypted at rest with SQLCipher, and getting the encryption key right meant not storing it anywhere a phone's own filesystem could reveal it.

The passphrase never touches plaintext disk
The passphrase never touches plaintext disk

Room + SQLCipher, keyed through Android Keystore, StrongBox where the device provides it.

The naive approach, and why it's wrong

The easy way to key a SQLCipher database is a hardcoded or app-generated passphrase stored in SharedPreferences or a plain file. That's encryption in name only: anyone who can read the app's data directory — a rooted device, a full filesystem image, an ADB backup on an unlocked bootloader — reads the passphrase file right next to the encrypted database and decrypts it trivially. The encryption adds a step, not a barrier.

What DatabaseKeyProvider.kt actually does

The passphrase is 32 random bytes generated once, on first run:

ByteArray(PASSPHRASE_BYTES).also { fresh ->
    SecureRandom().nextBytes(fresh)
    prefs.edit()
        .putString(KEY_DB_PASSPHRASE, Base64.encodeToString(fresh, Base64.NO_WRAP))
        .apply()
}

But prefs here is not plain SharedPreferences — it's EncryptedSharedPreferences, whose own master key is built like this:

val masterKey = MasterKey.Builder(context.applicationContext)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .setRequestStrongBoxBacked(true)
    .build()

setRequestStrongBoxBacked(true) asks Android to generate and hold that master key inside StrongBox — a separate, tamper-resistant hardware security module on devices that have one, distinct from and stronger than the regular TEE-backed Keystore. On devices without a StrongBox chip, Android falls back to the standard hardware-backed Keystore automatically.

The result is a three-layer chain: the 32-byte SQLCipher passphrase is wrapped by EncryptedSharedPreferences, which is wrapped by a Keystore master key that never leaves secure hardware in extractable form. Reading the app's data directory off an imaged device gets you ciphertext at every layer — never the passphrase in the clear.

The failure mode this creates, and how it's handled

Hardware-backed keys have a real downside: they can become permanently unreadable, for instance after a full-device restore onto new hardware where the old key material simply doesn't exist anymore. DatabaseKeyProvider handles that case explicitly rather than crashing:

val prefs = try {
    buildPrefs(context, masterKey)
} catch (e: Exception) {
    context.applicationContext.deleteSharedPreferences(PREFS_FILE)
    buildPrefs(context, masterKey)
}

If the encrypted prefs store is unreadable, it's deleted and rebuilt with a fresh key. That's a deliberate, low-stakes trade-off: the only thing lost is locally cached keep/delete decisions, which the app can regenerate by re-scanning the library. Losing that is annoying, not catastrophic — unlike, say, losing a signing key, which is unrecoverable by design (a topic later in this series).

Why this matters for what the database actually holds

The data being protected here isn't your photos — those never leave Google's servers, this app only ever references them by ID. What's in the local database is your review history: which photos you've already decided to keep or delete, which months are done. That's still worth protecting; it reveals what you were reviewing and what you decided, on a device that could be lost, stolen, or handed off. Hardware-backed encryption for something that modest might look like overkill, but the cost of doing it right — one DatabaseKeyProvider object — is small enough that there's no real reason not to.

Series: Google Photos Cleaner.