← Back to books

Chapter 2: Secure Data Storage

Data storage is the foundation of mobile application security. Every piece of sensitive information your app handles—user credentials, personal data, API tokens, and business logic—must be protected from unauthorized access. In this chapter, we'll explore the Android storage landscape and implement production-ready solutions that protect user data even on compromised devices.

By the end of this chapter, you will understand the security implications of each storage option, implement encrypted storage solutions, and avoid common vulnerabilities that expose sensitive data.


2.1 Understanding Android Storage Options

Android provides multiple storage mechanisms, each with different security characteristics. Choosing the right storage option is your first security decision.

2.1.1 Storage Options Comparison

Storage Type Security Level Use Case Risk
Internal Storage Medium - App sandboxed App-specific files Root access, backup extraction
External Storage Low - World readable Shared media files Any app can access
SharedPreferences Medium - Plain XML Simple key-value data Plaintext storage
SQLite Database Medium - Unencrypted Structured data SQL injection, data theft
DataStore Medium - Protocol Buffers Modern preferences No built-in encryption
Android Keystore High - Hardware backed Cryptographic keys Implementation complexity

2.1.2 Internal Storage Security Model

Internal storage provides app-specific directories that are sandboxed by the Linux kernel. Each app runs under its own user ID (UID), and file permissions prevent other apps from accessing your data.

The internal storage path structure:

/data/data/<package_name>/
├── shared_prefs/     # SharedPreferences XML files
├── databases/        # SQLite database files
├── files/            # General files (Context.getFilesDir())
├── cache/            # Temporary cache (Context.getCacheDir())
└── no_backup/        # Excluded from backup

⚠️ Security Note: While internal storage is sandboxed, it is NOT encrypted by default. On rooted devices or through ADB backup, this data can be extracted in plaintext.

2.1.3 File-Based Encryption (FBE)

Starting with Android 7.0, File-Based Encryption (FBE) encrypts files using different keys. This enables Direct Boot functionality and provides two storage locations:

  • Credential Encrypted (CE) Storage: Available only after the user unlocks the device. Use this for sensitive data.
  • Device Encrypted (DE) Storage: Available immediately after boot. Use for data needed before unlock.

Accessing encryption-aware storage:

// Credential Encrypted storage (default, most secure)
val ceContext = context.createCredentialProtectedStorageContext()
val cePrefs = ceContext.getSharedPreferences("secure_prefs", MODE_PRIVATE)

// Device Encrypted storage (available before unlock)
val deContext = context.createDeviceProtectedStorageContext()
val dePrefs = deContext.getSharedPreferences("boot_prefs", MODE_PRIVATE)

2.1.4 Scoped Storage (Android 10+)

Scoped Storage fundamentally changed how apps access external storage. Apps now have:

  • Unrestricted access to their own app-specific directory
  • Mediated access to shared media collections via MediaStore
  • No direct access to other apps' files
// App-specific external storage (no permissions needed)
val appExternalDir = context.getExternalFilesDir(null)

// Access shared media (requires READ_EXTERNAL_STORAGE on API < 33)
val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME)
val cursor = contentResolver.query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    projection,
    null,
    null,
    null
)

2.2 SharedPreferences Security

SharedPreferences is the most commonly misused storage mechanism in Android. Developers often store sensitive data without understanding its security limitations.

2.2.1 The Problem with Plain SharedPreferences

SharedPreferences stores data as plain XML files. Here's what an attacker sees when extracting your app's data:

<!-- /data/data/com.example.app/shared_prefs/user_prefs.xml -->
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="auth_token">eyJhbGciOiJIUzI1NiIs...</string>
    <string name="user_email">user@example.com</string>
    <string name="api_key">sk_live_abc123xyz789</string>
    <boolean name="is_premium" value="true" />
</map>

🚨 Warning: This is exactly how your SharedPreferences look on the file system. Never store tokens, passwords, API keys, or any sensitive data in plain SharedPreferences.

2.2.2 Attack Vectors for SharedPreferences

Understanding how attackers extract SharedPreferences data:

  1. ADB Backup Extraction: If android:allowBackup="true" (default), users can extract app data via ADB.
  2. Root Access: On rooted devices, any app with root can read your SharedPreferences.
  3. Device Theft: Physical access to an unlocked device allows data extraction.
  4. Backup Services: Cloud backups may inadvertently include sensitive preferences.

ADB backup extraction example:

# Attacker extracts backup from device
adb backup -f backup.ab -noapk com.example.app

# Convert to tar and extract
java -jar abe.jar unpack backup.ab backup.tar
tar -xvf backup.tar

# Read SharedPreferences in plaintext
cat apps/com.example.app/sp/user_prefs.xml

2.2.3 Securing SharedPreferences Access

Even for non-sensitive data, follow these best practices:

// ❌ WRONG: World-readable mode (deprecated and dangerous)
getSharedPreferences("prefs", Context.MODE_WORLD_READABLE)

// ✅ CORRECT: Private mode only
getSharedPreferences("prefs", Context.MODE_PRIVATE)

Disable backup for sensitive preferences:

<!-- AndroidManifest.xml -->
<application
    android:allowBackup="false"
    android:fullBackupContent="@xml/backup_rules">
<!-- res/xml/backup_rules.xml -->
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
    <exclude domain="sharedpref" path="sensitive_prefs.xml"/>
    <exclude domain="database" path="secure.db"/>
</full-backup-content>

2.3 EncryptedSharedPreferences Implementation

EncryptedSharedPreferences is part of the Jetpack Security library and provides transparent encryption for SharedPreferences. It encrypts both keys and values using AES-256-GCM.

2.3.1 Setup and Dependencies

Add the security-crypto dependency to your build.gradle:

dependencies {
    implementation("androidx.security:security-crypto:1.1.0-alpha06")
    
    // For Android 5.0 (API 21) support
    implementation("androidx.security:security-crypto-ktx:1.1.0-alpha06")
}

2.3.2 Basic Implementation

Creating an EncryptedSharedPreferences instance:

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

class SecurePreferencesManager(private val context: Context) {
    
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()
    
    private val securePreferences: SharedPreferences by lazy {
        EncryptedSharedPreferences.create(
            context,
            "secure_prefs",
            masterKey,
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )
    }
    
    fun saveAuthToken(token: String) {
        securePreferences.edit()
            .putString(KEY_AUTH_TOKEN, token)
            .apply()
    }
    
    fun getAuthToken(): String? {
        return securePreferences.getString(KEY_AUTH_TOKEN, null)
    }
    
    fun saveUserCredentials(email: String, refreshToken: String) {
        securePreferences.edit()
            .putString(KEY_USER_EMAIL, email)
            .putString(KEY_REFRESH_TOKEN, refreshToken)
            .apply()
    }
    
    fun clearAll() {
        securePreferences.edit().clear().apply()
    }
    
    companion object {
        private const val KEY_AUTH_TOKEN = "auth_token"
        private const val KEY_USER_EMAIL = "user_email"
        private const val KEY_REFRESH_TOKEN = "refresh_token"
    }
}

2.3.3 What Encrypted Storage Looks Like

After encryption, the XML file contents are unreadable:

<!-- /data/data/com.example.app/shared_prefs/secure_prefs.xml -->
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">
        12a901c7e8f2d4a6b8c9e0f1a2b3c4d5e6f7...</string>
    <string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">
        08b7e4d2f1a9c8b7e6d5c4b3a2918070...</string>
    <string name="AXjK8mN2pQ==">AWxYz9kL3mN7pR2sT5vW8xY...</string>
</map>

2.3.4 Production-Ready Secure Preferences Manager

Here's a complete, production-ready implementation with error handling:

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import java.io.IOException
import java.security.GeneralSecurityException

class SecureStorageManager private constructor(context: Context) {
    
    private val applicationContext = context.applicationContext
    private var encryptedPrefs: SharedPreferences? = null
    
    init {
        initializeEncryptedPrefs()
    }
    
    private fun initializeEncryptedPrefs() {
        try {
            val masterKey = MasterKey.Builder(applicationContext)
                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
                .setRequestStrongBoxBacked(true) // Use StrongBox if available
                .build()
            
            encryptedPrefs = EncryptedSharedPreferences.create(
                applicationContext,
                ENCRYPTED_PREFS_FILE,
                masterKey,
                EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
            )
        } catch (e: GeneralSecurityException) {
            handleEncryptionError(e)
        } catch (e: IOException) {
            handleEncryptionError(e)
        }
    }
    
    private fun handleEncryptionError(e: Exception) {
        // Log error securely (never log sensitive data)
        // Consider clearing corrupted preferences and re-initializing
        applicationContext.getSharedPreferences(ENCRYPTED_PREFS_FILE, Context.MODE_PRIVATE)
            .edit()
            .clear()
            .apply()
        
        // Retry initialization
        try {
            initializeEncryptedPrefs()
        } catch (retryException: Exception) {
            // Fall back to in-memory storage or notify user
            encryptedPrefs = null
        }
    }
    
    // Secure string storage
    fun putString(key: String, value: String): Boolean {
        return encryptedPrefs?.edit()?.putString(key, value)?.commit() ?: false
    }
    
    fun getString(key: String, defaultValue: String? = null): String? {
        return encryptedPrefs?.getString(key, defaultValue)
    }
    
    // Secure boolean storage
    fun putBoolean(key: String, value: Boolean): Boolean {
        return encryptedPrefs?.edit()?.putBoolean(key, value)?.commit() ?: false
    }
    
    fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
        return encryptedPrefs?.getBoolean(key, defaultValue) ?: defaultValue
    }
    
    // Secure long storage (for timestamps, IDs)
    fun putLong(key: String, value: Long): Boolean {
        return encryptedPrefs?.edit()?.putLong(key, value)?.commit() ?: false
    }
    
    fun getLong(key: String, defaultValue: Long = 0L): Long {
        return encryptedPrefs?.getLong(key, defaultValue) ?: defaultValue
    }
    
    // Remove specific key
    fun remove(key: String): Boolean {
        return encryptedPrefs?.edit()?.remove(key)?.commit() ?: false
    }
    
    // Clear all encrypted data
    fun clearAll(): Boolean {
        return encryptedPrefs?.edit()?.clear()?.commit() ?: false
    }
    
    // Check if key exists
    fun contains(key: String): Boolean {
        return encryptedPrefs?.contains(key) ?: false
    }
    
    companion object {
        private const val ENCRYPTED_PREFS_FILE = "secure_app_prefs"
        
        @Volatile
        private var instance: SecureStorageManager? = null
        
        fun getInstance(context: Context): SecureStorageManager {
            return instance ?: synchronized(this) {
                instance ?: SecureStorageManager(context).also { instance = it }
            }
        }
    }
}

2.3.5 Using the Secure Storage Manager

class AuthRepository(context: Context) {
    
    private val secureStorage = SecureStorageManager.getInstance(context)
    
    fun saveSession(accessToken: String, refreshToken: String, expiresAt: Long) {
        secureStorage.putString(Keys.ACCESS_TOKEN, accessToken)
        secureStorage.putString(Keys.REFRESH_TOKEN, refreshToken)
        secureStorage.putLong(Keys.TOKEN_EXPIRES_AT, expiresAt)
        secureStorage.putBoolean(Keys.IS_LOGGED_IN, true)
    }
    
    fun getAccessToken(): String? = secureStorage.getString(Keys.ACCESS_TOKEN)
    
    fun isSessionValid(): Boolean {
        val expiresAt = secureStorage.getLong(Keys.TOKEN_EXPIRES_AT)
        return System.currentTimeMillis() < expiresAt
    }
    
    fun clearSession() {
        secureStorage.remove(Keys.ACCESS_TOKEN)
        secureStorage.remove(Keys.REFRESH_TOKEN)
        secureStorage.remove(Keys.TOKEN_EXPIRES_AT)
        secureStorage.putBoolean(Keys.IS_LOGGED_IN, false)
    }
    
    private object Keys {
        const val ACCESS_TOKEN = "access_token"
        const val REFRESH_TOKEN = "refresh_token"
        const val TOKEN_EXPIRES_AT = "token_expires_at"
        const val IS_LOGGED_IN = "is_logged_in"
    }
}

2.4 File Encryption with Jetpack Security

For larger files that don't fit the key-value model, use EncryptedFile from Jetpack Security.

2.4.1 Writing Encrypted Files

import androidx.security.crypto.EncryptedFile
import androidx.security.crypto.MasterKey
import java.io.File

class SecureFileManager(private val context: Context) {
    
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()
    
    fun writeSecureFile(fileName: String, content: String) {
        val file = File(context.filesDir, fileName)
        
        // Delete existing file (EncryptedFile doesn't support overwrite)
        if (file.exists()) {
            file.delete()
        }
        
        val encryptedFile = EncryptedFile.Builder(
            context,
            file,
            masterKey,
            EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
        ).build()
        
        encryptedFile.openFileOutput().use { outputStream ->
            outputStream.write(content.toByteArray(Charsets.UTF_8))
        }
    }
    
    fun readSecureFile(fileName: String): String? {
        val file = File(context.filesDir, fileName)
        
        if (!file.exists()) {
            return null
        }
        
        val encryptedFile = EncryptedFile.Builder(
            context,
            file,
            masterKey,
            EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
        ).build()
        
        return encryptedFile.openFileInput().use { inputStream ->
            inputStream.bufferedReader().readText()
        }
    }
    
    fun deleteSecureFile(fileName: String): Boolean {
        val file = File(context.filesDir, fileName)
        return file.delete()
    }
}

2.4.2 Encrypting Binary Data

fun writeSecureBinaryFile(fileName: String, data: ByteArray) {
    val file = File(context.filesDir, fileName)
    
    if (file.exists()) {
        file.delete()
    }
    
    val encryptedFile = EncryptedFile.Builder(
        context,
        file,
        masterKey,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
    ).build()
    
    encryptedFile.openFileOutput().use { outputStream ->
        outputStream.write(data)
    }
}

fun readSecureBinaryFile(fileName: String): ByteArray? {
    val file = File(context.filesDir, fileName)
    
    if (!file.exists()) {
        return null
    }
    
    val encryptedFile = EncryptedFile.Builder(
        context,
        file,
        masterKey,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
    ).build()
    
    return encryptedFile.openFileInput().use { inputStream ->
        inputStream.readBytes()
    }
}

2.5 Database Security with Room and SQLCipher

SQLite databases are prime targets for attackers. Room combined with SQLCipher provides transparent database encryption.

2.5.1 The Risk of Unencrypted Databases

# Attacker extracts database from device
adb pull /data/data/com.example.app/databases/app.db

# Opens directly in SQLite browser - all data visible
sqlite3 app.db "SELECT * FROM users;"

2.5.2 Setting Up SQLCipher with Room

Add dependencies:

dependencies {
    // Room
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")
    kapt("androidx.room:room-compiler:2.6.1")
    
    // SQLCipher
    implementation("net.zetetic:android-database-sqlcipher:4.5.4")
    implementation("androidx.sqlite:sqlite-ktx:2.4.0")
}

2.5.3 Creating an Encrypted Database

Define your entities:

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    
    @ColumnInfo(name = "email")
    val email: String,
    
    @ColumnInfo(name = "encrypted_data")
    val encryptedData: String,
    
    @ColumnInfo(name = "created_at")
    val createdAt: Long = System.currentTimeMillis()
)

@Entity(tableName = "secure_tokens")
data class SecureToken(
    @PrimaryKey
    val tokenId: String,
    
    @ColumnInfo(name = "token_value")
    val tokenValue: String,
    
    @ColumnInfo(name = "expires_at")
    val expiresAt: Long
)

Create the DAO:

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User): Long
    
    @Query("SELECT * FROM users WHERE email = :email LIMIT 1")
    suspend fun getUserByEmail(email: String): User?
    
    @Query("SELECT * FROM users")
    fun getAllUsers(): Flow<List<User>>
    
    @Delete
    suspend fun deleteUser(user: User)
    
    @Query("DELETE FROM users")
    suspend fun deleteAllUsers()
}

@Dao
interface SecureTokenDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertToken(token: SecureToken)
    
    @Query("SELECT * FROM secure_tokens WHERE tokenId = :id")
    suspend fun getToken(id: String): SecureToken?
    
    @Query("DELETE FROM secure_tokens WHERE expires_at < :currentTime")
    suspend fun deleteExpiredTokens(currentTime: Long)
}

Create the encrypted database:

@Database(
    entities = [User::class, SecureToken::class],
    version = 1,
    exportSchema = false
)
abstract class SecureAppDatabase : RoomDatabase() {
    
    abstract fun userDao(): UserDao
    abstract fun secureTokenDao(): SecureTokenDao
    
    companion object {
        private const val DATABASE_NAME = "secure_app.db"
        
        @Volatile
        private var instance: SecureAppDatabase? = null
        
        fun getInstance(context: Context): SecureAppDatabase {
            return instance ?: synchronized(this) {
                instance ?: buildDatabase(context).also { instance = it }
            }
        }
        
        private fun buildDatabase(context: Context): SecureAppDatabase {
            // Get or generate encryption key
            val passphrase = getOrCreateDatabaseKey(context)
            val factory = SupportFactory(passphrase)
            
            return Room.databaseBuilder(
                context.applicationContext,
                SecureAppDatabase::class.java,
                DATABASE_NAME
            )
                .openHelperFactory(factory)
                .fallbackToDestructiveMigration()
                .build()
        }
        
        private fun getOrCreateDatabaseKey(context: Context): ByteArray {
            val secureStorage = SecureStorageManager.getInstance(context)
            val existingKey = secureStorage.getString(DB_KEY_ALIAS)
            
            return if (existingKey != null) {
                Base64.decode(existingKey, Base64.NO_WRAP)
            } else {
                // Generate new 256-bit key
                val newKey = ByteArray(32)
                java.security.SecureRandom().nextBytes(newKey)
                
                // Store encrypted key
                secureStorage.putString(DB_KEY_ALIAS, Base64.encodeToString(newKey, Base64.NO_WRAP))
                newKey
            }
        }
        
        private const val DB_KEY_ALIAS = "secure_db_encryption_key"
    }
}

2.5.4 Using the Encrypted Database

class UserRepository(context: Context) {
    
    private val database = SecureAppDatabase.getInstance(context)
    private val userDao = database.userDao()
    private val tokenDao = database.secureTokenDao()
    
    suspend fun createUser(email: String, sensitiveData: String): Long {
        val user = User(
            email = email,
            encryptedData = sensitiveData
        )
        return userDao.insertUser(user)
    }
    
    suspend fun findUser(email: String): User? {
        return userDao.getUserByEmail(email)
    }
    
    fun observeAllUsers(): Flow<List<User>> {
        return userDao.getAllUsers()
    }
    
    suspend fun saveToken(tokenId: String, tokenValue: String, expiresAt: Long) {
        val token = SecureToken(
            tokenId = tokenId,
            tokenValue = tokenValue,
            expiresAt = expiresAt
        )
        tokenDao.insertToken(token)
    }
    
    suspend fun cleanupExpiredTokens() {
        tokenDao.deleteExpiredTokens(System.currentTimeMillis())
    }
}

2.5.5 Preventing SQL Injection

Even with encryption, SQL injection is still a risk. Always use parameterized queries:

// ❌ DANGEROUS: String concatenation
@Query("SELECT * FROM users WHERE email = " + email)  // NEVER DO THIS

// ✅ SAFE: Parameterized query
@Query("SELECT * FROM users WHERE email = :email")
suspend fun getUserByEmail(email: String): User?

// ✅ SAFE: Using LIKE with parameters
@Query("SELECT * FROM users WHERE email LIKE '%' || :searchTerm || '%'")
suspend fun searchUsers(searchTerm: String): List<User>

2.6 DataStore Security Considerations

DataStore is the modern replacement for SharedPreferences but doesn't include built-in encryption. Here's how to secure it.

2.6.1 Preferences DataStore with Encryption Layer

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

// Extension property for DataStore
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

class SecureDataStoreManager(
    private val context: Context,
    private val cryptoManager: CryptoManager
) {
    private val dataStore = context.dataStore
    
    // Keys for DataStore
    private object PreferencesKeys {
        val ENCRYPTED_USER_DATA = stringPreferencesKey("encrypted_user_data")
        val ENCRYPTED_SETTINGS = stringPreferencesKey("encrypted_settings")
        val APP_THEME = stringPreferencesKey("app_theme") // Non-sensitive
    }
    
    // Save encrypted data
    suspend fun saveUserData(userData: UserData) {
        val json = Json.encodeToString(userData)
        val encryptedData = cryptoManager.encrypt(json)
        
        dataStore.edit { preferences ->
            preferences[PreferencesKeys.ENCRYPTED_USER_DATA] = 
                Base64.encodeToString(encryptedData, Base64.NO_WRAP)
        }
    }
    
    // Read and decrypt data
    val userData: Flow<UserData?> = dataStore.data.map { preferences ->
        val encryptedString = preferences[PreferencesKeys.ENCRYPTED_USER_DATA]
        
        encryptedString?.let {
            try {
                val encryptedData = Base64.decode(it, Base64.NO_WRAP)
                val decryptedJson = cryptoManager.decrypt(encryptedData)
                Json.decodeFromString<UserData>(decryptedJson)
            } catch (e: Exception) {
                null
            }
        }
    }
    
    // Non-sensitive data doesn't need encryption
    suspend fun setAppTheme(theme: String) {
        dataStore.edit { preferences ->
            preferences[PreferencesKeys.APP_THEME] = theme
        }
    }
    
    val appTheme: Flow<String> = dataStore.data.map { preferences ->
        preferences[PreferencesKeys.APP_THEME] ?: "system"
    }
}

@Serializable
data class UserData(
    val userId: String,
    val email: String,
    val preferences: Map<String, String>
)

2.6.2 Proto DataStore with Encryption

For type-safe storage with Protocol Buffers:

// Define your proto schema (user_preferences.proto)
/*
syntax = "proto3";

option java_package = "com.example.app";
option java_multiple_files = true;

message SecureUserPreferences {
    string encrypted_payload = 1;
    int64 last_updated = 2;
}
*/

class SecureProtoDataStore(
    private val context: Context,
    private val cryptoManager: CryptoManager
) {
    private val dataStore: DataStore<SecureUserPreferences> = context.securePreferencesDataStore
    
    suspend fun saveSecurePreferences(data: UserPreferencesData) {
        val json = Json.encodeToString(data)
        val encrypted = cryptoManager.encrypt(json)
        
        dataStore.updateData { currentPrefs ->
            currentPrefs.toBuilder()
                .setEncryptedPayload(Base64.encodeToString(encrypted, Base64.NO_WRAP))
                .setLastUpdated(System.currentTimeMillis())
                .build()
        }
    }
    
    val securePreferences: Flow<UserPreferencesData?> = dataStore.data.map { prefs ->
        if (prefs.encryptedPayload.isEmpty()) {
            null
        } else {
            try {
                val encrypted = Base64.decode(prefs.encryptedPayload, Base64.NO_WRAP)
                val decrypted = cryptoManager.decrypt(encrypted)
                Json.decodeFromString<UserPreferencesData>(decrypted)
            } catch (e: Exception) {
                null
            }
        }
    }
}

2.7 Secure Data Deletion

Deleting sensitive data properly is as important as protecting it during use.

2.7.1 The Problem with Standard Deletion

Standard file deletion only removes the file system reference—data remains on disk until overwritten:

// ❌ INSECURE: Data can be recovered
file.delete()

// ❌ INSECURE: SharedPreferences data remains in memory and on disk
sharedPreferences.edit().clear().apply()

2.7.2 Secure File Deletion

object SecureDelete {
    
    /**
     * Securely delete a file by overwriting with random data before deletion.
     * Note: This is less effective on flash storage due to wear leveling,
     * but still provides better security than simple deletion.
     */
    fun secureDeleteFile(file: File): Boolean {
        if (!file.exists()) return true
        
        return try {
            val length = file.length()
            val random = java.security.SecureRandom()
            
            // Overwrite with random data (3 passes)
            repeat(3) {
                RandomAccessFile(file, "rws").use { raf ->
                    val buffer = ByteArray(4096)
                    var position = 0L
                    
                    while (position < length) {
                        random.nextBytes(buffer)
                        val bytesToWrite = minOf(buffer.size.toLong(), length - position).toInt()
                        raf.seek(position)
                        raf.write(buffer, 0, bytesToWrite)
                        position += bytesToWrite
                    }
                    
                    raf.fd.sync() // Force write to disk
                }
            }
            
            // Finally delete the file
            file.delete()
        } catch (e: Exception) {
            // Fall back to regular deletion
            file.delete()
        }
    }
    
    /**
     * Securely delete all files in a directory
     */
    fun secureDeleteDirectory(directory: File): Boolean {
        if (!directory.exists()) return true
        if (!directory.isDirectory) return secureDeleteFile(directory)
        
        var success = true
        directory.listFiles()?.forEach { file ->
            success = if (file.isDirectory) {
                secureDeleteDirectory(file) && success
            } else {
                secureDeleteFile(file) && success
            }
        }
        
        return directory.delete() && success
    }
}

2.7.3 Clearing Sensitive Data from Memory

object SecureMemory {
    
    /**
     * Clear a character array containing sensitive data
     */
    fun clearCharArray(array: CharArray?) {
        array?.fill('\u0000')
    }
    
    /**
     * Clear a byte array containing sensitive data
     */
    fun clearByteArray(array: ByteArray?) {
        array?.fill(0)
    }
    
    /**
     * Execute a block with sensitive data, ensuring cleanup
     */
    inline fun <T> withSecureCharArray(
        array: CharArray,
        block: (CharArray) -> T
    ): T {
        return try {
            block(array)
        } finally {
            clearCharArray(array)
        }
    }
}

// Usage example
fun processPassword(password: String) {
    val passwordChars = password.toCharArray()
    
    SecureMemory.withSecureCharArray(passwordChars) { chars ->
        // Use the password
        authenticateUser(chars)
    }
    // passwordChars is now cleared
}

2.7.4 Complete Data Wipe on Logout

class SecureLogoutManager(
    private val context: Context,
    private val secureStorage: SecureStorageManager,
    private val database: SecureAppDatabase
) {
    
    suspend fun performSecureLogout() {
        withContext(Dispatchers.IO) {
            // 1. Clear encrypted preferences
            secureStorage.clearAll()
            
            // 2. Clear database
            database.clearAllTables()
            
            // 3. Clear app cache
            clearCache()
            
            // 4. Clear WebView data
            clearWebViewData()
            
            // 5. Clear any temporary files
            clearTempFiles()
            
            // 6. Request garbage collection (hint only)
            System.gc()
        }
    }
    
    private fun clearCache() {
        context.cacheDir.deleteRecursively()
        context.externalCacheDir?.deleteRecursively()
    }
    
    private fun clearWebViewData() {
        // Clear WebView cache and data
        WebStorage.getInstance().deleteAllData()
        CookieManager.getInstance().removeAllCookies(null)
        CookieManager.getInstance().flush()
    }
    
    private fun clearTempFiles() {
        val tempDir = File(context.filesDir, "temp")
        SecureDelete.secureDeleteDirectory(tempDir)
    }
}

2.8 Best Practices Summary

2.8.1 Data Classification Matrix

Data Type Storage Method Encryption Backup
Auth tokens EncryptedSharedPreferences AES-256-GCM Exclude
User credentials Android Keystore Hardware-backed Never
Session data EncryptedSharedPreferences AES-256-GCM Exclude
Personal info Encrypted Room DB SQLCipher Exclude
App settings DataStore/SharedPreferences Optional Allow
Cache files Internal cache None Exclude
Downloaded media Scoped storage Optional User choice

2.8.2 Security Checklist

Before releasing your app, verify:

  • All sensitive data uses EncryptedSharedPreferences or encrypted files
  • Database is encrypted with SQLCipher
  • Backup is disabled for sensitive data (android:allowBackup="false" or selective rules)
  • No sensitive data logged (check all Log.* calls)
  • No sensitive data in error messages
  • Proper data cleanup on logout
  • Secure deletion implemented for temporary files
  • MODE_PRIVATE used for all SharedPreferences
  • No hardcoded secrets in code
  • Parameterized queries used for all database operations

2.8.3 Common Mistakes to Avoid

// ❌ MISTAKE 1: Logging sensitive data
Log.d(TAG, "User token: $authToken")

// ❌ MISTAKE 2: Storing secrets in BuildConfig
BuildConfig.API_SECRET // Visible in APK

// ❌ MISTAKE 3: Using external storage for sensitive data
Environment.getExternalStorageDirectory() // World readable

// ❌ MISTAKE 4: Hardcoded encryption keys
val key = "my_secret_key_123" // Easily extracted

// ❌ MISTAKE 5: Storing passwords instead of hashes
sharedPrefs.putString("password", userPassword) // Never store passwords

// ❌ MISTAKE 6: Not clearing sensitive data
password.toString() // String is immutable, stays in memory

2.9 Hands-On Exercise

Exercise: Build a Secure Notes Application

Create a simple notes app that implements all the secure storage techniques from this chapter.

Requirements:

  1. Store user authentication token securely using EncryptedSharedPreferences
  2. Store notes in an encrypted Room database with SQLCipher
  3. Implement secure file storage for note attachments
  4. Add proper data cleanup on logout
  5. Disable backup for all sensitive data

Starter Code:

// Your implementation here
data class Note(
    val id: Long,
    val title: String,
    val content: String,
    val attachmentPath: String?,
    val createdAt: Long
)

interface SecureNotesRepository {
    suspend fun saveNote(note: Note)
    suspend fun getNote(id: Long): Note?
    suspend fun getAllNotes(): List<Note>
    suspend fun deleteNote(id: Long)
    suspend fun saveAttachment(noteId: Long, data: ByteArray): String
    suspend fun getAttachment(path: String): ByteArray?
    suspend fun clearAllData()
}

Challenge: Implement the SecureNotesRepository interface using the techniques learned in this chapter.


2.10 Summary

In this chapter, we covered:

  • Android storage options and their security implications
  • EncryptedSharedPreferences for secure key-value storage
  • Jetpack Security EncryptedFile for secure file storage
  • Room with SQLCipher for encrypted database storage
  • DataStore security patterns with manual encryption
  • Secure data deletion techniques
  • Best practices for data classification and storage

The key takeaway is that encryption should be your default approach for any sensitive data. With modern libraries like Jetpack Security and SQLCipher, implementing secure storage requires minimal additional effort while providing significant protection against data theft.

In the next chapter, we'll explore the Android Keystore system and advanced cryptography implementation patterns that form the foundation of all these encryption solutions.


References