Secure Storage with flutter_secure_storage
Secure Storage with flutter_secure_storage
Most Flutter apps need to persist sensitive data — authentication tokens, API keys, refresh tokens, or user credentials — between sessions. Storing this data in plain text (for example via SharedPreferences) is a serious security risk. The flutter_secure_storage package solves this by writing data to the OS-level secure enclave: iOS Keychain on Apple devices and the Android Keystore backed by hardware-level encryption on Android.
SharedPreferences stores key-value pairs in an XML file on Android (shared_prefs/) and NSUserDefaults on iOS. Both are plain-text and readable by anyone with access to the device filesystem or a backup. Never store tokens or passwords there.SharedPreferences vs flutter_secure_storage
Understanding the difference is critical before choosing a storage mechanism:
- SharedPreferences — Unencrypted, suitable for non-sensitive preferences (theme, language, onboarding flags).
- flutter_secure_storage — AES-256 encrypted on Android (via Keystore), Keychain on iOS. Suitable for tokens, passwords, and API secrets.
- On Android, data is encrypted with a key that is itself stored in the Android Keystore and never leaves secure hardware on supported devices.
- On iOS, items are stored in the Keychain with the accessibility option you configure (e.g., only accessible when the device is unlocked).
Adding the Dependency
Add the package to pubspec.yaml:
pubspec.yaml
dependencies:
flutter_secure_storage: ^9.2.2
On Android you must set the minimum SDK version to 18 in android/app/build.gradle:
android/app/build.gradle
android {
defaultConfig {
minSdkVersion 18
// ...
}
}
Core API: Read, Write, Delete
The API is intentionally simple. All methods are async and return Futures. Instantiate FlutterSecureStorage once (or use a singleton/injected instance):
Basic CRUD Operations
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class TokenRepository {
final FlutterSecureStorage _storage = const FlutterSecureStorage();
static const _accessTokenKey = 'access_token';
static const _refreshTokenKey = 'refresh_token';
// Write / overwrite a value
Future<void> saveTokens({
required String accessToken,
required String refreshToken,
}) async {
await _storage.write(key: _accessTokenKey, value: accessToken);
await _storage.write(key: _refreshTokenKey, value: refreshToken);
}
// Read (returns null if key does not exist)
Future<String?> getAccessToken() async {
return _storage.read(key: _accessTokenKey);
}
// Check existence without reading the value
Future<bool> hasSession() async {
final token = await _storage.read(key: _accessTokenKey);
return token != null && token.isNotEmpty;
}
// Delete a single key
Future<void> deleteAccessToken() async {
await _storage.delete(key: _accessTokenKey);
}
// Wipe all entries written by this app (use on logout)
Future<void> clearAll() async {
await _storage.deleteAll();
}
}
Platform-Specific Options
You can customise behaviour per platform by passing option objects to the constructor. This is important for production apps that need specific accessibility or authentication policies:
Android & iOS Options
final storage = FlutterSecureStorage(
// Android: require user authentication before reading the key
aOptions: const AndroidOptions(
encryptedSharedPreferences: true, // uses EncryptedSharedPreferences API
),
// iOS: item is only accessible when device is unlocked
iOptions: const IOSOptions(
accessibility: KeychainAccessibility.unlocked,
),
);
// Reading with the same options object
Future<String?> readSensitiveKey(String key) async {
return storage.read(key: key);
}
AndroidOptions (e.g., toggle encryptedSharedPreferences) after data has already been written, the old entries become unreadable because the encryption scheme differs. Always migrate or wipe existing storage when changing options in a production update.Storing and Restoring Auth State on App Start
A common pattern is to check for a stored token when the app launches and redirect to the main screen if the session is still valid:
Splash / Auth Gate Pattern
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
Future<bool> _checkSession() async {
const storage = FlutterSecureStorage();
final token = await storage.read(key: 'access_token');
return token != null && token.isNotEmpty;
}
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: _checkSession(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
final isLoggedIn = snapshot.data ?? false;
return isLoggedIn
? const HomeScreen()
: const LoginScreen();
},
);
}
}
Security Best Practices
- Always call
deleteAll()on logout — never leave tokens in storage after the session ends. - Prefer short-lived access tokens with refresh tokens, rotating on each use.
- Do not store plaintext passwords — use tokens or hashed credentials only.
- Combine with
local_auth(biometrics) for an additional layer of security before reading from the Keystore/Keychain. - On Android, set
encryptedSharedPreferences: trueto use Jetpack Security'sEncryptedSharedPreferencesAPI rather than the legacy RSA-wrapped AES scheme.
FlutterSecureStorage. The package's constructor accepts no default, so wrapping it behind an abstract interface (e.g., abstract class SecureStorage) makes your repository 100% testable without touching device Keychain/Keystore.Summary
flutter_secure_storage is the standard solution for persisting sensitive data in Flutter. It wraps iOS Keychain and Android Keystore behind a clean async API. Use write, read, delete, and deleteAll for full lifecycle management of secrets. Always prefer it over SharedPreferences for any data you would not want a malicious actor to read, and call deleteAll() on logout to leave no credentials behind on the device.