If you're shipping an Android app that handles authentication tokens, API keys, or any user data that could harm your reputation if leaked, you've probably wondered: is SharedPreferences with a custom encryption wrapper enough? The short answer is no. Android's EncryptedSharedPreferences, part of the Jetpack Security library, provides a robust, standardised way to store sensitive data with authenticated encryption (AES-256 GCM) tied to the device's hardware-backed keystore. This guide explains how to use it correctly, what mistakes to avoid, and how it fits into a release-ready compliance posture.
Why EncryptedSharedPreferences Matters for Release & Store Compliance
When you submit an app to Google Play, the review process doesn't just check for malware. It also looks for proper handling of sensitive data. Storing plaintext credentials or tokens in SharedPreferences is a common violation that can lead to rejection or, worse, a security incident after release. Even if your app passes review, a data breach from insecure local storage can damage user trust and trigger regulatory scrutiny under GDPR, CCPA, or other privacy laws.
EncryptedSharedPreferences addresses this by encrypting both keys and values using AES-256 GCM with a key derived from the user's Android Keystore. This means even if an attacker gains file-system access (via root or backup), the data remains unreadable. For compliance teams, this is a clear win: you can document that sensitive data at rest is encrypted with a well-known algorithm and key management scheme.
But implementing it isn't as simple as swapping one line of code. You need to understand key generation, initialisation vector handling, and the trade-offs between performance and security. This guide will take you through each step, with code examples and pitfalls to watch for.
We'll assume you're targeting API level 23 (Marshmallow) or higher, which covers the vast majority of active Android devices. If you need to support older versions, you'll need a fallback strategy, which we'll discuss later.
Who Should Read This
This guide is for Android developers who are already familiar with SharedPreferences and want to upgrade to a secure storage solution without adding a third-party library. It's also useful for technical leads and compliance officers who need to verify that the implementation meets security requirements.
Foundations: How EncryptedSharedPreferences Works
Before diving into code, it's important to understand the underlying mechanism. EncryptedSharedPreferences is built on two core classes from the Jetpack Security library: MasterKey and EncryptedSharedPreferences. The MasterKey class generates and stores a strong encryption key in the Android Keystore, which is hardware-backed on devices that support it (most modern devices). This key is then used to encrypt the actual data keys and values.
Encryption uses AES-256 in Galois/Counter Mode (GCM), which provides both confidentiality and authenticity (integrity verification). This means an attacker cannot tamper with encrypted data without detection. The library also generates a random initialisation vector (IV) for each write, so identical plaintexts produce different ciphertexts.
One common point of confusion is the difference between encrypting the values only versus encrypting the keys as well. By default, EncryptedSharedPreferences encrypts both, which prevents an attacker from learning which keys exist (e.g., "auth_token" or "credit_card"). If you only encrypt values, the key names leak metadata, which could be valuable to an adversary.
Another key detail: the library uses a two-layer key scheme. The master key stored in the Keystore is used to encrypt a data encryption key (DEK), which is then stored alongside the encrypted preferences. On each read, the DEK is decrypted using the master key and then used to decrypt the actual data. This design allows key rotation without re-encrypting all data: you can generate a new DEK and re-encrypt only the DEK blob.
Prerequisites
- Add the dependency:
implementation 'androidx.security:security-crypto:1.1.0-alpha06'(check for latest stable version) - Minimum SDK: 23 (or use
security-crypto-ktxfor Kotlin extensions) - Device with Android Keystore support (most devices running API 23+)
Step-by-Step Implementation
Let's walk through the actual code. We'll create a helper class that encapsulates the setup and provides simple get/put methods.
Step 1: Generate a Master Key
The MasterKey class handles key generation and storage. By default, it uses AES-256 GCM and stores the key in the Android Keystore with a strongbox-backed key if available.
import androidx.security.crypto.MasterKey
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
This creates a master key that is stored in the Keystore and cannot be extracted (on hardware-backed devices). You can also specify additional parameters like key alias or timeout, but the default is sufficient for most apps.
Step 2: Create EncryptedSharedPreferences Instance
import androidx.security.crypto.EncryptedSharedPreferences
val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
Note the two encryption schemes: AES256_SIV for keys and AES256_GCM for values. The SIV mode for keys is deterministic (same key always produces same ciphertext), which is necessary for key lookup. GCM for values is non-deterministic (each write uses a new IV).
Step 3: Read and Write Data
Use the returned SharedPreferences object exactly as you would with regular SharedPreferences. The encryption and decryption happen transparently.
// Writing
sharedPreferences.edit().putString("auth_token", token).apply()
// Reading
val token = sharedPreferences.getString("auth_token", null)
That's it. The library handles encryption, decryption, and integrity checks on every read and write.
Step 4: Handle Exceptions
Encryption operations can throw exceptions, especially if the Keystore becomes unavailable (e.g., device reboot during key creation). Wrap your reads and writes in try-catch blocks:
try {
val token = sharedPreferences.getString("auth_token", null)
} catch (e: GeneralSecurityException) {
// Handle: maybe fall back to a different storage or prompt user
}
Anti-Patterns and Why Teams Revert
EncryptedSharedPreferences is not a silver bullet. Many teams implement it incorrectly and end up with a false sense of security. Here are the most common mistakes we see in code reviews.
Using a Hardcoded Key
Some developers bypass the MasterKey and try to use a static password or hardcoded byte array. This defeats the purpose—if an attacker decompiles the app, they get the key. Always use the Keystore-backed MasterKey.
Encrypting Only Values
As mentioned earlier, encrypting only values leaves key names in plaintext. If you store a key named "credit_card_number", an attacker knows exactly what data is stored, even if the value is encrypted. Use the AES256_SIV scheme for keys.
Storing Large Blobs
SharedPreferences is designed for small key-value pairs (typically under 100 KB). Storing large JSON strings or binary data will degrade performance and may cause ANRs (Application Not Responding) on slow devices. For larger data, consider using EncryptedFile from the same library or a database like SQLCipher.
Ignoring Key Rotation
If a device is compromised, the master key might be extracted (though unlikely on strongbox-backed devices). Without a key rotation mechanism, all encrypted data remains at risk. The library supports key rotation via MasterKey.Builder.setKeyRotation(), but many teams skip this step. Plan for periodic rotation, especially for long-lived tokens.
Not Handling Keystore Unavailability
On some devices, the Keystore may be locked or unavailable (e.g., after a factory reset without proper key migration). If you don't handle exceptions, your app will crash. Always have a fallback or clear error handling.
Maintenance, Drift, and Long-Term Costs
Implementing EncryptedSharedPreferences is straightforward, but maintaining it over time requires vigilance. Here's what to watch for.
Library Updates
The Jetpack Security library is still evolving. New versions may change default algorithms or deprecate older schemes. For example, early versions used AES-256 CBC with HMAC, which was later replaced by GCM. If you pin an old version, you might miss security patches. Set up automated dependency updates (e.g., Dependabot) and test encryption changes in a staging environment.
Device-Specific Issues
Not all Android devices implement the Keystore correctly. Some cheap devices have software-only keystores that are slower and less secure. On devices without strongbox, the master key is stored in the TEE (Trusted Execution Environment) but may still be vulnerable to certain attacks. You cannot fully control this, but you can log Keystore features at runtime and alert your backend if weak security is detected.
Backup and Migration
If a user backs up their app data via Android Backup Service, encrypted SharedPreferences files are included in the backup. However, the master key is not backed up (Keystore keys are device-bound). This means restoring a backup to a different device will fail—the encrypted data cannot be decrypted. You need a backup strategy that either excludes encrypted data or re-encrypts it with a cloud-managed key. Google's Backup API allows you to exclude specific files, so you can store only non-sensitive data in the backup.
Performance Overhead
Each read and write involves decryption/encryption and integrity verification. On older devices, this can add tens of milliseconds per operation. If you're reading many keys on app startup, consider caching decrypted values in memory (but be aware of memory pressure). Measure your app's startup time with and without EncryptedSharedPreferences to ensure it's acceptable.
When Not to Use EncryptedSharedPreferences
EncryptedSharedPreferences is not the right tool for every scenario. Here are cases where you should consider alternatives.
Large Data Sets
If you need to store more than a few hundred key-value pairs, or if values exceed a few kilobytes each, a database is more appropriate. SQLCipher (SQLite with encryption) provides better performance for structured data and supports complex queries. For file-level encryption, use EncryptedFile from the same library.
Cross-Platform or Server-Side Access
EncryptedSharedPreferences is tied to the Android Keystore on a specific device. If you need to share encrypted data across devices or platforms (e.g., sync between phone and tablet), you need a cloud-based encryption scheme where the key is derived from the user's password or stored in a secure server. Consider using the Tink library with a remote key management service.
High-Frequency Writes
If you're writing to preferences every few seconds (e.g., logging sensor data), the encryption overhead can drain battery and cause UI jank. Use a buffer or a dedicated database with write-ahead logging.
Older API Levels (Below 23)
If your app must support devices running API 21 or 22, the Android Keystore is not reliably available. You can use the security-crypto-ktx library which provides a fallback using a user-supplied password, but this is less secure. Consider using a third-party library like Conceal or SQLCipher, or require a minimum API of 23.
Compliance Requirements That Demand FIPS 140-2
EncryptedSharedPreferences uses AES-256 GCM, which is a NIST-approved algorithm. However, the Android Keystore implementation may not be FIPS 140-2 validated on all devices. If your app must comply with strict government standards, you may need a dedicated cryptographic module like Bouncy Castle's FIPS variant, but this adds complexity.
Open Questions / FAQ
We've gathered the most common questions from developer forums and code reviews.
Can I use EncryptedSharedPreferences with Kotlin Multiplatform?
Currently, the Jetpack Security library is Android-only. For Kotlin Multiplatform projects, you'll need to provide an Android-specific implementation using EncryptedSharedPreferences and a different solution for iOS (e.g., Keychain via a native expect/actual pattern).
Is it safe to store OAuth refresh tokens this way?
Yes, EncryptedSharedPreferences is suitable for refresh tokens, access tokens, and other secrets. However, remember that if the device is rooted or compromised, the data in memory (after decryption) is still accessible. Use biometric authentication or app-level encryption for extremely sensitive tokens.
How do I migrate from regular SharedPreferences to EncryptedSharedPreferences?
You cannot decrypt existing plaintext data. The safest approach is to read all values from the old preferences, write them to the new encrypted instance, and then delete the old file. Do this once on first launch after the update. Be aware that the old plaintext data may still be recoverable from backups or file system remnants.
What happens if the Keystore key is deleted?
If the user clears app data or factory resets the device, the Keystore key is lost. The encrypted file becomes unreadable. Your app should handle this gracefully—for example, by logging the user out and re-initializing the preferences.
Can I use a custom key alias?
Yes, MasterKey.Builder accepts a setKeyAlias() method. This is useful if you need to manage multiple keys or if you want to use the same key across different apps (though sharing keys between apps is not recommended for security reasons).
Summary and Next Steps
EncryptedSharedPreferences is a solid choice for securing small amounts of sensitive data on Android devices. It uses industry-standard encryption, integrates with the hardware-backed Keystore, and is easy to implement. However, it's not a one-size-fits-all solution. You need to consider your data size, API level requirements, and compliance needs.
Here are concrete next steps for your project:
- Audit your current storage: Identify all places where you store sensitive data (tokens, keys, PII). Determine which ones can be moved to EncryptedSharedPreferences and which need a database or server-side storage.
- Implement with proper error handling: Use the code pattern shown above, including try-catch blocks for
GeneralSecurityException. Test on a variety of devices, especially older ones. - Set up key rotation: If your app handles long-lived secrets, implement periodic key rotation using
MasterKey.Builder.setKeyRotation(). Test the migration path. - Plan for backup and restore: Decide whether to exclude encrypted files from backup or implement a cloud-based key escrow. Document your decision.
- Monitor for device-specific issues: Add logging to detect Keystore failures or weak implementations. Consider using Firebase Crashlytics to track exceptions.
- Stay updated: Subscribe to the Jetpack Security release notes. When a new version comes out, test it in a feature branch before updating your production app.
By following these steps, you'll not only secure your users' data but also build a compliance-friendly app that stands up to review and real-world threats. Remember, security is a process, not a one-time implementation. Revisit your approach as the library and threat landscape evolve.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!