Securing Sensitive Data with flutter_secure_storage
Securing Sensitive Data with flutter_secure_storage
Regular SharedPreferences stores data in plain text on the device file system. That is fine for non-sensitive preferences, but it is completely unsuitable for authentication tokens, API keys, passwords, or any personal data that must be kept confidential. flutter_secure_storage solves this by delegating storage to the platform's native secure enclave on every supported platform.
Adding the Dependency
Add flutter_secure_storage to your pubspec.yaml:
pubspec.yaml
dependencies:
flutter_secure_storage: ^9.2.2
For Android, you must set a minimum SDK version of 23 in android/app/build.gradle:
android/app/build.gradle — minimum SDK requirement
android {
defaultConfig {
minSdkVersion 23 // Required for EncryptedSharedPreferences
}
}
Creating an Instance and Android Options
Instantiate FlutterSecureStorage once, ideally as a singleton or injected dependency. On Android you can pass AndroidOptions to control the encryption scheme and key storage behaviour:
Basic instantiation with Android options
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
// Recommended: create once and reuse
final _storage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true, // use EncryptedSharedPreferences (default true for API 23+)
),
);
// For advanced use you can also supply IOSOptions, LinuxOptions, etc.
// final _storage = FlutterSecureStorage(
// iOptions: IOSOptions(accountName: 'myApp', accessibility: KeychainAccessibility.first_unlock),
// );
Core CRUD Operations
The API is fully asynchronous and mirrors a simple key/value map. All operations return Futures and should be awaited inside an async function:
Write, Read, Delete, and Read-All
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureStorageService {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
// --- WRITE ---
// Stores (or overwrites) a value for the given key
static Future<void> writeToken(String token) async {
await _storage.write(key: 'auth_token', value: token);
}
// --- READ ---
// Returns null if the key does not exist
static Future<String?> readToken() async {
return _storage.read(key: 'auth_token');
}
// --- DELETE ---
// Removes a single key/value pair
static Future<void> deleteToken() async {
await _storage.delete(key: 'auth_token');
}
// --- DELETE ALL ---
// Removes every key stored by this app (use on logout)
static Future<void> clearAll() async {
await _storage.deleteAll();
}
// --- READ ALL ---
// Returns Map<String, String> of all stored pairs
static Future<Map<String, String>> readAll() async {
return _storage.readAll();
}
// --- CHECK EXISTENCE ---
static Future<bool> hasToken() async {
return _storage.containsKey(key: 'auth_token');
}
}
Real-World Usage: Storing and Restoring a JWT
A common pattern is to persist a JWT access token after login and restore it on app start. The example below shows a minimal AuthService that wraps this flow:
AuthService with secure token persistence
class AuthService {
static const _storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
static const _tokenKey = 'jwt_access_token';
static const _refreshKey = 'jwt_refresh_token';
/// Call after a successful login API response
static Future<void> saveTokens({
required String accessToken,
required String refreshToken,
}) async {
await Future.wait([
_storage.write(key: _tokenKey, value: accessToken),
_storage.write(key: _refreshKey, value: refreshToken),
]);
}
/// Returns null when the user has never logged in or has logged out
static Future<String?> getAccessToken() =>
_storage.read(key: _tokenKey);
/// Wipe all credentials on logout
static Future<void> logout() async {
await Future.wait([
_storage.delete(key: _tokenKey),
_storage.delete(key: _refreshKey),
]);
}
}
Future.wait([...]) when you need to perform multiple independent secure-storage operations simultaneously. It runs them in parallel and waits for all to complete, reducing total latency compared to sequential awaits.Error Handling and Edge Cases
Always wrap secure storage calls in try/catch blocks in production code. The underlying Keychain or Keystore can throw exceptions if the device is in an unusual state (e.g., first boot before device credential is set up, or an app reinstall that cleared Keychain access):
Defensive read with error handling
Future<String?> safeReadToken() async {
try {
return await _storage.read(key: 'auth_token');
} catch (e) {
// Log the error; do NOT expose raw exception to the user
debugPrint('SecureStorage read failed: $e');
// Treat as if no token exists — force re-login
return null;
}
}
encryptedSharedPreferences option between app versions (e.g., from false to true), existing data stored under the old scheme becomes unreadable. Plan your encryption strategy before shipping and migrate data explicitly if you must change it.What You Should and Should Not Store
- Store securely: OAuth tokens, JWT access/refresh tokens, API keys, session cookies, biometric flags, encryption keys.
- Do NOT store in secure storage: Large binary blobs (it is not designed for that), non-sensitive preferences (use
SharedPreferencesfor those — secure storage is slower), or derived cached data that can be re-fetched.
Summary
flutter_secure_storage provides a simple key/value API backed by the iOS Keychain and Android Keystore/EncryptedSharedPreferences. The five core operations — write, read, delete, deleteAll, and readAll — are all asynchronous. On Android you must set minSdkVersion 23 and enable AndroidOptions(encryptedSharedPreferences: true). Always handle exceptions, avoid storing large data, and call deleteAll on logout to fully clear credentials.