Skip to content
advanced Phase 12 · Security

Data Encryption

Encrypt local data with EncryptedSharedPreferences, Room encryption, and the Jetpack Security library.

45m
2 problems
Topic Progress 0%

Jetpack Security Library

Why Encrypt Data at Rest?

Android devices can be lost, stolen, or accessed physically. Any data stored in plaintext on the filesystem—SharedPreferences, files, databases—is vulnerable if the device is compromised or the storage is extracted.

The Jetpack Security library (androidx.security.crypto) provides a simple API for encrypting data using industry-standard algorithms. It wraps Tink, Google's cryptographic library, and handles key generation, storage, and rotation automatically.

Setup

Add the dependency:

// build.gradle.kts
implementation("androidx.security:security-crypto:1.1.0-alpha06")

Key Generation

The library manages encryption keys using the Android Keystore system. You create a MasterKey that wraps (encrypts) your data encryption keys.

import androidx.security.crypto.MasterKey
import androidx.security.crypto.MasterKeys

val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .setRequestStrongBoxBacked(true)  // Use hardware security if available
    .build()

Key scheme options:

  • AES256_GCM — Recommended. Uses AES with 256-bit keys in GCM mode.
  • AES128_GCM — Lighter, suitable for less sensitive data.

StrongBox vs TEE:

  • StrongBox — Dedicated hardware security module. More secure but slower.
  • TEE (Trusted Execution Environment) — Hardware-backed but shares the main processor.
  • Use setRequestStrongBoxBacked(true) for highly sensitive data (payment info, auth tokens). Fall back to TEE if StrongBox is unavailable.

Checking Hardware Support

val isStrongBoxAvailable = try {
    context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
} catch (e: Exception) {
    false
}

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

How Keys Are Managed

The MasterKey never leaves the Android Keystore. When you encrypt data:

  1. The library generates a data encryption key (DEK)
  2. The DEK is encrypted (wrapped) by the master key
  3. The encrypted DEK is stored alongside the encrypted data
  4. On decryption, the master key unwraps the DEK, which then decrypts the data

This means even if an attacker extracts the encrypted file, they still need the hardware-backed master key to decrypt it.

EncryptedSharedPreferences and EncryptedFile

EncryptedSharedPreferences

EncryptedSharedPreferences is a drop-in replacement for the standard SharedPreferences. It automatically encrypts both keys and values using AES-256-GCM.

import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

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

val encryptedPrefs = EncryptedSharedPreferences.create(
    context,
    "secret_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

// Use like normal SharedPreferences
encryptedPrefs.edit()
    .putString("auth_token", "eyJhbGciOiJIUzI1NiIs...")
    .putBoolean("is_premium", true)
    .apply()

val token = encryptedPrefs.getString("auth_token", null)

Key vs Value encryption schemes:

  • PrefKeyEncryptionScheme.AES256_SIV — Deterministic encryption for keys (enables lookups by exact key match)
  • PrefValueEncryptionScheme.AES256_GCM — Authenticated encryption for values (provides confidentiality and integrity)

When NOT to Use EncryptedSharedPreferences

  • Large datasets — SharedPreferences loads everything into memory. For large data, use Room with encryption.
  • Frequent writes — Encryption overhead per write can degrade performance.
  • Complex queries — SharedPreferences has no query capability.

EncryptedFile

For encrypting files (documents, cached data, downloaded content):

import androidx.security.crypto.EncryptedFile
import androidx.security.crypto.MasterKey
import java.io.BufferedReader
import java.io.InputStreamReader

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

val encryptedFile = EncryptedFile.Builder(
    context,
    File(context.filesDir, "secret_document.txt"),
    masterKey,
    EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

// Write encrypted content
encryptedFile.openFileOutput().use { output ->
    output.write("Sensitive document content".toByteArray())
}

// Read and decrypt
encryptedFile.openFileInput().use { input ->
    val reader = BufferedReader(InputStreamReader(input))
    val content = reader.readText()
    println(content) // "Sensitive document content"
}

File encryption scheme:

  • AES256_GCM_HKDF_4KB — AES-GCM with a 4KB chunk size. Suitable for files of any size.

Choosing Between SharedPreferences, Files, and Room

Use Case Solution
Small key-value config EncryptedSharedPreferences
Documents, cached files EncryptedFile
Structured data with queries Room + SQLCipher
Sensitive lists/collections Room + SQLCipher

Room Database Encryption with SQLCipher

Encrypting Room Databases

Room databases store data in SQLite files. Without encryption, these files are readable if the device is rooted or the storage is extracted. SQLCipher provides transparent AES-256 encryption for SQLite databases.

Setup

// build.gradle.kts
implementation("androidx.sqlite:sqlite-ktx:2.4.0")
implementation("net.zetetic:android-database-sqlcipher:4.5.4")
implementation("androidx.sqlite:sqlite-framework:2.4.0")

Implementation

import androidx.room.Room
import net.sqlcipher.database.SupportFactory
import androidx.sqlite.db.SupportSQLiteOpenHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory

fun createEncryptedDatabase(context: Context): AppDatabase {
    // Generate or retrieve the encryption key
    val passphrase = getOrCreateDatabasePassphrase(context)
    val factory = SupportFactory(passphrase)
    
    return Room.databaseBuilder(
        context,
        AppDatabase::class.java,
        "app_database"
    )
        .openHelperFactory(factory)
        .build()
}

private fun getOrCreateDatabasePassphrase(context: Context): ByteArray {
    val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()
    
    val prefs = EncryptedSharedPreferences.create(
        context,
        "db_prefs",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )
    
    val existing = prefs.getString("db_passphrase", null)
    if (existing != null) {
        return android.util.Base64.decode(existing, android.util.Base64.DEFAULT)
    }
    
    // Generate a new passphrase
    val bytes = ByteArray(32)
    java.security.SecureRandom().nextBytes(bytes)
    prefs.edit()
        .putString("db_passphrase", android.util.Base64.encodeToString(bytes, android.util.Base64.DEFAULT))
        .apply()
    return bytes
}

Migration Strategy

If you add encryption to an existing unencrypted database, you need a migration:

// Migrate from unencrypted to encrypted
fun migrateToEncryptedDatabase(context: Context, oldDb: AppDatabase) {
    val passphrase = getOrCreateDatabasePassphrase(context)
    val factory = SupportFactory(passphrase)
    
    // Export old data
    val cursor = oldDb.openHelper.readableDatabase.query("SELECT * FROM users")
    val users = mutableListOf<User>()
    while (cursor.moveToNext()) {
        users.add(User(
            id = cursor.getLong(0),
            name = cursor.getString(1),
            email = cursor.getString(2)
        ))
    }
    cursor.close()
    oldDb.close()
    
    // Create encrypted database and import
    val newDb = Room.databaseBuilder(context, AppDatabase::class.java, "app_database_encrypted")
        .openHelperFactory(factory)
        .build()
    
    CoroutineScope(Dispatchers.IO).launch {
        newDb.userDao().insertAll(users)
    }
}

Key Management Best Practices

  1. Never hardcode encryption keys in source code.
  2. Use the Android Keystore (via MasterKey) to protect your database passphrase.
  3. Implement key rotation for long-lived apps. Store a key version identifier and re-encrypt data during migrations.
  4. Don't store the passphrase in plaintext even in EncryptedSharedPreferences—use the MasterKey wrapping.
  5. Test on devices without StrongBox to ensure graceful fallback.

Quiz

1. What encryption algorithm does `MasterKey.KeyScheme.AES256_GCM` use?

Question 1 options

2. Why use `setRequestStrongBoxBacked(true)` when building a MasterKey?

Question 2 options

3. What is the primary difference between EncryptedSharedPreferences key encryption (AES256_SIV) and value encryption (AES256_GCM)?

Question 3 options

4. When migrating an existing Room database to use encryption, what must you do first?

Question 4 options

Flashcards

Question

What library provides EncryptedSharedPreferences and EncryptedFile?

Answer

The Jetpack Security library (androidx.security.crypto) wraps Google's Tink library and handles key management via the Android Keystore.

Question

Why is EncryptedSharedPreferences not suitable for large datasets?

Answer

EncryptedSharedPreferences loads all key-value pairs into memory. For large datasets, Room with SQLCipher is more appropriate as it handles data on disk with lazy loading.

Question

What is the passphrase storage strategy for encrypted Room databases?

Answer

Generate a random passphrase, store it in EncryptedSharedPreferences (protected by a MasterKey from Android Keystore), and pass it to SQLCipher's SupportFactory when building the database.

Question

What does StrongBox provide that TEE does not?

Answer

StrongBox is a dedicated hardware security module (HSM) separate from the main processor, offering physical isolation for cryptographic operations. TEE shares the main CPU and is theoretically more vulnerable to side-channel attacks.

Revision Notes

Key Takeaways

  • 1. EncryptedSharedPreferences is a drop-in replacement for standard SharedPreferences with automatic encryption
  • 2. MasterKey wraps data encryption keys via the Android Keystore — never store raw keys in code
  • 3. SQLCipher provides transparent database encryption for Room — requires manual migration from unencrypted databases
  • 4. StrongBox (HSM) provides stronger key protection than TEE but may not be available on all devices
  • 5. Always test encryption on devices without hardware security support for graceful fallback

Interview Tips

  • Explain the difference between data encryption key (DEK) and key encryption key (KEK)
  • Know why AES-GCM is preferred over AES-CBC (authentication built-in)
  • Be ready to discuss key rotation strategies and migration paths
  • Explain how Android Keystore protects keys even on rooted devices

Cheat Sheet

Data Encryption Cheat Sheet

Jetpack Security Library:

  • MasterKey — wraps data keys via Android Keystore
  • EncryptedSharedPreferences — drop-in encrypted key-value storage
  • EncryptedFile — encrypted file I/O

Key Algorithms:

  • MasterKey: AES-256-GCM
  • Pref Keys: AES-256-SIV (deterministic, supports lookups)
  • Pref Values: AES-256-GCM (randomized, authenticated)
  • Files: AES-256-GCM-HKDF-4KB

Room + SQLCipher:

  • SupportFactory(passphrase) as the openHelperFactory
  • Store passphrase in EncryptedSharedPreferences
  • Manual migration from unencrypted databases required

Best Practices:

  • Never hardcode keys
  • Use StrongBox for sensitive data (payment, auth)
  • Implement key rotation for long-lived apps
  • Test on devices without StrongBox