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:
- ADB Backup Extraction: If
android:allowBackup="true"(default), users can extract app data via ADB. - Root Access: On rooted devices, any app with root can read your SharedPreferences.
- Device Theft: Physical access to an unlocked device allows data extraction.
- 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:
- Store user authentication token securely using EncryptedSharedPreferences
- Store notes in an encrypted Room database with SQLCipher
- Implement secure file storage for note attachments
- Add proper data cleanup on logout
- 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.