Android’s SharedPreferences is convenient for persisting small key‑value pairs, but the data is stored as clear‑text XML in the app’s internal storage. If a device is rooted, backed up, or extracted via forensic tools, anyone with file‑system access can read those values – a serious risk for passwords, API tokens, or personal identifiers.
The Android Keystore system provides hardware‑backed or software‑backed key storage that never leaves the secure enclave in a readable form. By combining the Keystore with EncryptedSharedPreferences, you get a drop‑in replacement for SharedPreferences that encrypts keys and values automatically.
EncryptedSharedPreferences is part of AndroidX Security library, which works back to API 21.Open your app module build.gradle (or build.gradle.kts) and add the Security dependency:
dependencies {
implementation "androidx.security:security-crypto:1.1.0-alpha03"
}
Sync the project. The library bundles EncryptedSharedPreferences and the underlying MasterKey helper class.
The MasterKey is a symmetric AES‑256 key generated and stored by the Keystore. It will be used to encrypt/decrypt the SharedPreferences file.
Place the following Kotlin code in a singleton or a utility object, e.g., SecurePrefs.kt:
import android.content.Context
import androidx.security.crypto.MasterKey
import androidx.security.crypto.EncryptedSharedPreferences
object SecurePrefs {
private const val PREF_FILE = "secure_prefs"
fun getEncryptedPrefs(context: Context): EncryptedSharedPreferences {
// Build or retrieve the MasterKey. The default configuration uses AES256_GCM.
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
context,
PREF_FILE,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
) as EncryptedSharedPreferences
}
}
Explanation:
PrefKeyEncryptionScheme.AES256_SIV encrypts the preference names, preventing an attacker from learning the keys even if they can read the file.PrefValueEncryptionScheme.AES256_GCM encrypts the values with authenticated encryption, ensuring integrity.Using the helper you just created, reading and writing is identical to normal SharedPreferences, except the object type is EncryptedSharedPreferences.
// Example usage inside an Activity or ViewModel
val encryptedPrefs = SecurePrefs.getEncryptedPrefs(this)
// Store a secret token
encryptedPrefs.edit().putString("auth_token", "my‑super‑secret-token").apply()
// Retrieve the token later
val token = encryptedPrefs.getString("auth_token", null)
After the .apply() call, the data is written to /data/data/your.package.name/shared_prefs/secure_prefs.xml, but the file contents are fully encrypted. Opening the file with a text editor will show only gibberish.
To convince yourself that the data is really encrypted, you can use adb shell to cat the file:
adb shell "run-as com.example.myapp cat /data/data/com.example.myapp/shared_prefs/secure_prefs.xml"
You should see a block of seemingly random characters wrapped in XML tags – no plain‑text token.
Most modern devices support hardware‑backed Keystore keys. However, on some low‑end or heavily customized ROMs the Keystore may fall back to a software implementation, which is still secure but may be slower. The AndroidX library abstracts this away, but you may notice a small delay the first time a key is generated.
If your app targets API 31+ and you want to enforce hardware‑backed keys, you can add a check:
if (masterKey.isUserAuthenticationRequired) {
// Hardware‑backed key is present
} else {
// Fallback – consider showing a warning or disabling high‑risk features
}
Many apps start with plain SharedPreferences and later switch to encrypted storage. A safe migration path is:
SharedPreferences.EncryptedSharedPreferences using the same key.val legacyPrefs = context.getSharedPreferences("legacy_prefs", Context.MODE_PRIVATE)
val token = legacyPrefs.getString("auth_token", null)
if (token != null) {
encryptedPrefs.edit().putString("auth_token", token).apply()
legacyPrefs.edit().remove("auth_token").apply()
}
SecurePrefs.getEncryptedPrefs() from a background thread before the app’s Application context is fully initialized. The Keystore initialization must happen on the main thread..apply() over .commit() for async writes, unless you need immediate persistence.EncryptedSharedPreferences. For larger blobs, consider using EncryptedFile or a database with SQLCipher.setUserAuthenticationRequired(true) on the MasterKey builder to require a fingerprint or device credential before decrypting.If you want the secret data to be accessible only after the user authenticates with biometrics, extend the MasterKey configuration:
val biometricKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.setUserAuthenticationRequired(true, 5_000) // 5‑second timeout
.build()
val securePrefs = EncryptedSharedPreferences.create(
context,
"biometric_prefs",
biometricKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
Now any attempt to read or write will prompt the user for a fingerprint, face unlock, or device PIN, depending on the device’s configuration.
By following these steps you have replaced insecure plain preferences with a robust, Android‑native encryption solution that leverages the device’s Keystore. The approach works on the vast majority of Android phones, requires no root, and adds only a negligible performance overhead. Remember to handle edge cases—factory resets, device‑specific Keystore quirks, and migration from legacy data—to keep the user experience smooth.
Secure storage is a foundational piece of any privacy‑focused app. With EncryptedSharedPreferences and the Keystore, you can confidently store tokens, API keys, and other secrets without exposing them to the wide range of attacks that target plain files.









