skip to content

Secure Storage

flutter_secure_storage keeps tokens and keys in the iOS Keychain and in Android storage encrypted with Keystore-wrapped keys. Interviewers probe what survives a reinstall and what belongs there.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

In a Flutter app, why does a refresh token belong in flutter_secure_storage rather than shared_preferences, and where does each platform keep it?

level: juniorimportance: must knowfreq 65%

answer

  1. plain key-value file versus OS keystore
  2. iOS and macOS: Keychain
  3. Android: AES-GCM values, Keystore-wrapped key
  4. Windows files, Linux libsecret, web WebCrypto
  5. strings only; write(value: null) deletes

basics

~20 s

shared_preferences writes plain values to an app file anyone with file access can read, while flutter_secure_storage encrypts them with keys held by the OS: the Keychain on iOS and macOS, Keystore-wrapped AES keys on Android.

solid answer

~40 s

`shared_preferences` is for non-secret settings; its values sit unencrypted in platform preference files, so a rooted device, a backup or a debugging session can expose them. `flutter_secure_storage` is a key-value store of strings whose protection comes from the operating system. On iOS and macOS each value is a Keychain item. On Android, version 10 encrypts values with AES-GCM and stores them in a private SharedPreferences file, while the AES key is wrapped by an RSA key that lives in the Android Keystore. Windows writes encrypted files, Linux uses libsecret, and the web version relies on WebCrypto and only works over HTTPS or localhost. The API is `write`, `read`, `delete`, `deleteAll`, `containsKey` and `readAll`, all async and able to throw `PlatformException`. It suits short secrets such as a wallet's refresh token, not large data.

code

dart · 17 lines
dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class WalletSession {
  WalletSession(this._storage);

  final FlutterSecureStorage _storage;
  static const _refreshKey = 'wallet_refresh_token';

  Future<void> saveRefreshToken(String token) =>
      _storage.write(key: _refreshKey, value: token);

  Future<String?> refreshToken() => _storage.read(key: _refreshKey);

  Future<void> signOut() => _storage.delete(key: _refreshKey);
}

final session = WalletSession(const FlutterSecureStorage());

go deeper

for a junior

Recall the split: settings in shared_preferences, tokens and keys in flutter_secure_storage, plus the basic write, read and delete calls.

for a middle

Explain what backs the store on each platform, especially the Android Keystore-wrapped AES key and the web's HTTPS-only limitation.

for a senior

Discuss failure modes such as locked-device reads and restored backups, and design the app so a missing token degrades to sign-in.

for a principal

Frame which secrets the device should hold at all, and when a server-side session or short token lifetime is the better control.

## Two stores with different threat models Flutter apps usually meet two key-value packages, and interviewers expect a candidate to know which one a secret goes in. - **`shared_preferences`** persists simple values (booleans, numbers, strings, string lists) in the platform's preference store. Nothing is encrypted. Anyone who can read the app's files, through a rooted device, a device backup or a debugging session, can read the values. - **`flutter_secure_storage`** (version 10 at the time of writing) is a key-value store of **strings** whose confidentiality comes from **operating-system key storage**. The Dart side is a thin method-channel API; the protection lives in native code on each platform. In a crypto-wallet app, the refresh token lets the app mint new access tokens without asking the user to sign in again. Leaking it is close to leaking the session, so it goes into secure storage, while a setting like "show balances in fiat" stays in preferences. ## Where each platform actually keeps the value | Platform | Backing store in flutter_secure_storage 10 | |---|---| | iOS, macOS | Keychain items, via `flutter_secure_storage_darwin` | | Android | values encrypted with AES-GCM in a private SharedPreferences file; the AES key is wrapped by an RSA key held in the Android Keystore | | Windows | encrypted files (earlier versions used the Credential system) | | Linux | libsecret, which needs a keyring service such as gnome-keyring | | Web | WebCrypto-generated keys with data in local storage; experimental, HTTPS or localhost only | The Android row is worth being able to explain. The **Keystore** is Android's facility for keys that app code can use but not export. The plugin keeps only the wrapped (encrypted) AES key and the encrypted values in the preferences file; without the Keystore key on that specific device, the file is useless. Android support starts at API 23 in version 10. ## The API a candidate should know 1. Create one instance, optionally with platform options: `const FlutterSecureStorage(aOptions: AndroidOptions(), iOptions: IOSOptions(...))`. 2. `await storage.write(key: 'refresh_token', value: token)` stores a string; writing `value: null` deletes the key. 3. `await storage.read(key: 'refresh_token')` returns `String?`, `null` when absent. 4. `delete`, `deleteAll`, `containsKey` and `readAll` round out the API. Per-call options override the instance's options. 5. On mobile and desktop every call crosses a platform channel, so it is asynchronous and can throw `PlatformException`; call `WidgetsFlutterBinding.ensureInitialized()` first if you read in `main`. For widget and unit tests, `FlutterSecureStorage.setMockInitialValues({...})` swaps in an in-memory platform. ## A walk-through for the wallet 1. The user signs in; the server returns an access token and a refresh token. 2. The app keeps the short-lived access token in memory only and writes the refresh token with `storage.write(key: 'wallet_refresh_token', value: token)`. 3. On the next cold start, the app reads it before showing the home screen. A value means it can silently renew the session; `null` means it shows sign-in. 4. On sign-out, the app calls `delete` for that key, or `deleteAll()` if the store holds nothing else worth keeping. 5. The fiat-currency preference, the last opened tab and the onboarding flag go to `shared_preferences`, because reading them needs no protection and they are read far more often. Keeping one small wrapper class around the storage instance, as in the code example, gives the rest of the app a single place where the key names, options and error handling live, which also makes the store easy to fake in tests. ## What secure storage does not do - It does not make a secret safe from code running inside your own app; any Dart code in the process can call `read`. - It is not a database. Store small strings such as tokens or a key, and put bulk data in an encrypted database or files. - It does not prevent a secret compiled into the app binary from being extracted; secure storage protects values the app receives at runtime. - Reads can fail when the device is locked (on iOS, depending on the accessibility setting) or after a backup restore on Android, so the app must treat "token missing" as a normal state that leads to sign-in. ## Choosing, in one line each - Theme, onboarding-done flag, last selected tab: `shared_preferences`. - Refresh token, API key issued to this user, database encryption key: `flutter_secure_storage`. - Anything large or queryable: a database or files, optionally encrypted with a key kept in secure storage.

  • With flutter_secure_storage, how do you store a structured secret such as a token plus its expiry?
    The store holds strings only, so serialise a small object to JSON and write it under one key, or use two keys. Keep it small; for large or queryable data, store an encryption key in secure storage and encrypt a database or file with it.
  • Why can flutter_secure_storage calls fail at app start even though the code looks correct?
    They go through a platform channel, so the binding must be initialised before `main` awaits them, and the native side can refuse access, for example a Keychain item that is only readable while the device is unlocked. Wrap reads in error handling and treat failure like a missing token.

saying these in an interview costs you the question

  • shared_preferences encrypts values on iOS and Android by default.
  • flutter_secure_storage keeps its decryption key in plain form beside the data.
  • Secure storage is a good place for megabytes of cached records.
  • A secret hardcoded in Dart source is safe once copied to secure storage.
  • On the web, flutter_secure_storage is as strong as the iOS Keychain.
open as a page

After a user reinstalls or restores your Flutter app, what can happen to tokens saved with flutter_secure_storage on iOS and Android, and how do you handle it?

level: seniorimportance: must knowfreq 45%

basics

~20 s

On iOS, Keychain items can survive uninstall, so a reinstalled app may find an old token; clear secure storage on first run. On Android, a restored backup brings the encrypted file but not the Keystore key, so values cannot be decrypted.

open as a page

With flutter_secure_storage on iOS, how does IOSOptions accessibility decide when a stored token can be read, and why might a background refresh fail?

level: middleimportance: should knowfreq 30%

basics

~20 s

IOSOptions.accessibility maps to the Keychain's kSecAttrAccessible. The default, unlocked, makes items readable only while the device is unlocked, so a background refresh on a locked phone fails; first_unlock allows reads after the first unlock since boot.

open as a page

How do you make a Flutter app require biometrics before flutter_secure_storage releases a secret on Android and iOS, and what can go wrong?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Use AndroidOptions.biometric(enforceBiometrics: true) on Android and IOSOptions(accessControlFlags: [...]) such as biometryCurrentSet or userPresence on iOS, so the OS demands authentication before the key or item is used, not just before a screen opens.

open as a page

In flutter_secure_storage 10, what replaced Android's encryptedSharedPreferences option, and how do the new cipher defaults and migration flags behave?

level: middleimportance: nice to knowfreq 20%

basics

~10 s

Version 10 deprecated encryptedSharedPreferences because Jetpack Security is deprecated, and switched to custom ciphers: an RSA-OAEP key cipher wrapping an AES-GCM storage cipher. Data migrates automatically because migrateOnAlgorithmChange defaults to true.

open as a page