Secure Storage

Medium PriorityAsked in ~45% of mid-level interviews

3 min read

Security

DataWhere it belongs
Auth tokens, refresh tokensflutter_secure_storage
API keys the client must hold (rare — prefer server-side)flutter_secure_storage, plus consider obfuscation
Encryption keys, biometric handlesflutter_secure_storage
User prefs (dark mode, last tab)shared_preferences
Onboarding completed flagsshared_preferences
Cached API responsessqflite / Hive / Isar / disk cache
Large filesgetApplicationDocumentsDirectory() + file APIs
Anything truly sensitive that should not be on deviceDon'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

ClassMeaning
first_unlock (recommended for tokens)Available after first unlock; survives reboots once unlocked
first_unlock_this_device_onlySame, but doesn't sync via iCloud Keychain
unlockedOnly available when device is unlocked (good for things needing fresh auth)
passcodeOnly 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

RiskMitigation
Rooted / jailbroken deviceDetect and refuse to run, or downgrade features
Reverse-engineering of in-binary keysMove key derivation server-side; never ship long-lived secrets in the app
Backup exfiltrationMark items isThisDeviceOnly: true (iOS) / disable Android auto-backup for that key
Lock-screen bypassPair 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

  1. Why isn't SharedPreferences enough for tokens? SharedPreferences writes 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.

  2. 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_storage defaults to EncryptedSharedPreferences on Android.

  3. 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.

  4. 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.