Secure storage in Flutter
By Ann Tech · 2 October 2025
Secure storage in Flutter means keeping sensitive data — tokens, encryption keys, PINs — in platform-protected storage rather than SharedPreferences or local files. On iOS this is the Keychain; on Android it's EncryptedSharedPreferences backed by the Android Keystore.
flutter_secure_storage setup
dependencies:
flutter_secure_storage: ^9.0.0
const storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
// More secure cipher (requires API 23+, which is 99%+ of active devices)
keyCipherAlgorithm: KeyCipherAlgorithm.RSA_ECB_PKCS1Padding,
storageCipherAlgorithm: StorageCipherAlgorithm.AES_GCM_NoPadding,
),
iOptions: IOSOptions(
// Only decrypt when device is unlocked (not accessible with locked screen)
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
Basic operations
// Write
await storage.write(key: 'auth_token', value: token);
await storage.write(key: 'refresh_token', value: refreshToken);
// Read
final token = await storage.read(key: 'auth_token');
// Returns null if key doesn't exist
// Check existence
final hasToken = await storage.containsKey(key: 'auth_token');
// Delete single key
await storage.delete(key: 'auth_token');
// Delete all (use on logout)
await storage.deleteAll();
// Read all keys
final all = await storage.readAll();
Token storage pattern
class TokenStorage {
const TokenStorage(this._storage);
final FlutterSecureStorage _storage;
static const _accessTokenKey = 'access_token';
static const _refreshTokenKey = 'refresh_token';
static const _expiryKey = 'token_expiry';
Future<void> saveTokens({
required String accessToken,
required String refreshToken,
required DateTime expiry,
}) async {
await Future.wait([
_storage.write(key: _accessTokenKey, value: accessToken),
_storage.write(key: _refreshTokenKey, value: refreshToken),
_storage.write(key: _expiryKey, value: expiry.toIso8601String()),
]);
}
Future<String?> getAccessToken() async {
final expiry = await _storage.read(key: _expiryKey);
if (expiry != null && DateTime.parse(expiry).isBefore(DateTime.now())) {
return null; // Expired
}
return _storage.read(key: _accessTokenKey);
}
Future<String?> getRefreshToken() =>
_storage.read(key: _refreshTokenKey);
Future<void> clear() => _storage.deleteAll();
}
Platform-specific storage limits
iOS Keychain:
- No practical size limit per entry
- Persists through app uninstall by default
- To clear on uninstall, set
synchronizable: falseand use a unique service name
- To clear on uninstall, set
Android EncryptedSharedPreferences:
- Data is cleared on uninstall
- Keys are encrypted with Android Keystore (hardware-backed on most devices)
Clearing on reinstall (iOS)
iOS Keychain data survives uninstall. This can be a security risk (old tokens persisted) or useful (users stay logged in). To clear on reinstall:
// Check if this is a fresh install by looking for a flag in
// regular SharedPreferences (which IS cleared on uninstall)
Future<void> clearKeychainOnFirstRun() async {
final prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey('has_launched')) {
await storage.deleteAll(); // Clear any old Keychain data
await prefs.setBool('has_launched', true);
}
}
What NOT to store in secure storage
Secure storage is for secrets — small amounts of data. It's not a database:
Store here: Access token, refresh token, encryption key,
biometric enrollment ID, PIN hash
Don't store here: User profile, settings, cached API responses,
anything large or frequently read/written
(use encrypted Hive or Drift for that)
Encrypted database for larger datasets
dependencies:
drift: ^2.15.0
drift_flutter: ^0.2.0
sqflite_sqlcipher: ^2.2.0 # SQLCipher backend
// Generate and store the database encryption key in secure storage
Future<Uint8List> getDatabaseKey() async {
var keyHex = await storage.read(key: 'db_key');
if (keyHex == null) {
// Generate new key
final key = List<int>.generate(32, (_) => Random.secure().nextInt(256));
keyHex = key.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
await storage.write(key: 'db_key', value: keyHex);
}
return Uint8List.fromList(
List.generate(32, (i) => int.parse(keyHex!.substring(i * 2, i * 2 + 2), radix: 16)),
);
}
Testing with secure storage
// Use in-memory implementation for tests
class InMemorySecureStorage implements FlutterSecureStorage {
final _store = <String, String>{};
@override
Future<void> write({required String key, required String? value, ...}) async {
if (value == null) _store.remove(key);
else _store[key] = value;
}
@override
Future<String?> read({required String key, ...}) async => _store[key];
@override
Future<void> deleteAll({...}) async => _store.clear();
// ... other methods
}
Common pitfalls
Using SharedPreferences for auth tokens. SharedPreferences data is stored as plaintext in the app's sandbox. On rooted Android devices it's trivially readable. Always use secure storage for authentication credentials.
Not handling null on read. storage.read() returns null if the key doesn't exist — not an empty string. Always null-check the returned value before using it.
Storing large objects. Secure storage is backed by platform Keychain/Keystore which has performance overhead per operation. Storing a 50KB JSON blob in secure storage is slow. Store the encryption key there, and encrypt the large data with it in a regular file.
Sign in to like, dislike, or report.