Secure Storage
3 min read
Security
| Data | Where it belongs |
|---|---|
| Auth tokens, refresh tokens | flutter_secure_storage |
| API keys the client must hold (rare — prefer server-side) | flutter_secure_storage, plus consider obfuscation |
| Encryption keys, biometric handles | flutter_secure_storage |
| User prefs (dark mode, last tab) | shared_preferences |
| Onboarding completed flags | shared_preferences |
| Cached API responses | sqflite / Hive / Isar / disk cache |
| Large files | getApplicationDocumentsDirectory() + file APIs |
| Anything truly sensitive that should not be on device | Don't store it — fetch from server when needed |
Code in action
class SecureStore {
final _store = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
Future<void> saveToken(String token) =>
_store.write(key: 'auth_token', value: token);
Future<String?> readToken() =>
_store.read(key: 'auth_token');
Future<void> logout() => _store.deleteAll();
}
Choosing the iOS accessibility class
| Class | Meaning |
|---|---|
first_unlock (recommended for tokens) | Available after first unlock; survives reboots once unlocked |
first_unlock_this_device_only | Same, but doesn't sync via iCloud Keychain |
unlocked | Only available when device is unlocked (good for things needing fresh auth) |
passcode | Only available when device has a passcode set (extra requirement) |
Choose first_unlock_this_device_only if you don't want iCloud sync; choose passcode for the strongest local guarantee.
When secure storage is not enough
| Risk | Mitigation |
|---|---|
| Rooted / jailbroken device | Detect and refuse to run, or downgrade features |
| Reverse-engineering of in-binary keys | Move key derivation server-side; never ship long-lived secrets in the app |
| Backup exfiltration | Mark items isThisDeviceOnly: true (iOS) / disable Android auto-backup for that key |
| Lock-screen bypass | Pair with biometric prompt before unlocking the token |
Common mistakes to avoid
// ❌ Storing tokens in SharedPreferences
SharedPreferences.getInstance().then((p) => p.setString('token', t));
// Plain text on disk; readable on rooted devices, included in backups
// ❌ Logging tokens / secrets
debugPrint('Saved token: $token'); // ends up in adb / crash logs
// ✅ Never log secrets, even in debug
// ❌ Wrong accessibility level
KeychainAccessibility.unlocked // user gets logged out every time the device locks
// ✅ KeychainAccessibility.first_unlock for tokens
// ❌ Forgetting to clear on logout
// Old user's token sits in storage; next user inherits it
// ✅ store.deleteAll() (or namespaced delete) on logout
// ❌ Putting large binary blobs (DB backups, images) in secure storage
// Slow + designed for small values
// ✅ Encrypt the blob, store the KEY in secure storage
Interview follow-ups
-
Why isn't
SharedPreferencesenough for tokens?SharedPreferenceswrites plain XML/plist files in the app's sandbox. On a rooted/jailbroken device, on an Android backup, or via ADB on a debug build, that file is trivially readable. Secure storage uses platform-grade encryption (Keychain on iOS, EncryptedSharedPreferences + Keystore on Android) so the data is encrypted at rest. -
What's the difference between Android Keystore and EncryptedSharedPreferences? Keystore is a system service that holds keys — keys never leave Keystore (TEE/StrongBox where available). EncryptedSharedPreferences is a SharedPreferences implementation that uses a key from Keystore to encrypt the values.
flutter_secure_storagedefaults to EncryptedSharedPreferences on Android. -
How do you handle biometric-gated secrets? Pair secure storage with a biometric prompt — the prompt unlocks access to a key in Keystore/Keychain, which then unwraps the stored token. Packages like
local_auth+flutter_secure_storage's biometric options handle the wiring. -
What should you do about secrets that need to be in the binary (e.g., third-party SDK keys)? Recognize they're not truly secret — anyone can extract them. Mitigate: obfuscate the binary (
--obfuscate), restrict the key on the third-party side (allowed bundle ID / app signature), rate-limit, and move sensitive operations server-side where the real secret lives.
How helpful was this content?
Please sign in to rate this article.