Library / SDK
juliansteenbakker/flutter_secure_storage avatar
juliansteenbakker/flutter_secure_storage

flutter_secure_storage turns the platform keychain into one Dart API

A Flutter plugin for securely storing sensitive data using encrypted storage.

1,286 stars532 forksC++BSD-3-Clause

At a glance

What is it?
flutter_secure_storage is a Flutter plugin that stores key-value pairs in the platform's own secure storage, using the Keychain on Apple platforms and custom ciphers with optional biometric authentication on Android. Version 10 changed the Android ciphers and raised the minimum SDK, and the documentation has two traps that produce runtime exceptions rather than compile errors.
Who is it for?
Adopt flutter_secure_storage when your Flutter app has tokens, keys or small secrets that belong in the platform keychain rather than in shared preferences, and you are willing to configure each platform's options separately. Do not adopt it as a general purpose database or for bulk data, since the API is a key-value store and the platform mechanisms behind it are not designed for large payloads.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The platform keychain behind a Dart API

flutter_secure_storage is a Flutter plugin to securely store sensitive data in a key-value pair format using platform-specific secure storage solutions, and it supports Android, iOS, macOS, Windows and Linux.

The point of the plugin is that each of those platforms has a different secure store with a different API, and the plugin presents one Dart interface over all of them. On iOS and macOS it is the Keychain. On Android it is custom secure ciphers with optional biometric authentication. On Windows, Linux and Web it uses the platform-specific secure mechanisms.

The API is the smallest possible one. You create an instance, and then you write, read, delete, delete all, and check whether a key exists.

dart
await storage.write(key: 'username', value: 'flutter_user');
dart
String? username = await storage.read(key: 'username');

That is deliberately narrow. There is no query language, no table, and no transaction, because the underlying stores are not databases and pretending otherwise would be the wrong abstraction.

The encryption is done by the platform side of the plugin rather than by the Dart layer, and the default on Android is RSA OAEP for the key cipher and AES-GCM for the storage cipher. The package is BSD-3-Clause licensed, and the last push was on 2026-10-14 with the current release line at flutter_secure_storage v11.2.0.

Version 10 changed the Android ciphers and the minimum SDK

There is an Important notice for Android in the readme, and it is the first thing to read if you are upgrading an existing app.

Version 10.0.0 introduces a major security update with custom cipher implementations, and the deprecated Jetpack Security library's encryptedSharedPreferences is no longer recommended. The key changes are listed: new default ciphers, RSA OAEP for the key cipher and AES-GCM for the storage cipher, new AndroidOptions() and AndroidOptions.biometric() constructors, automatic migration from the old ciphers via migrateOnAlgorithmChange, which is enabled by default, and a minimum Android SDK of 23, which is Android 6.0.

That last item is a hard requirement rather than a recommendation. An app that still supports Android 5 or older cannot use the current version of the plugin at all, which turns a routine dependency bump into a platform support decision.

The migration behaviour is the part that needs thought. migrateOnAlgorithmChange is on by default, so when the plugin finds data written with the previous ciphers it re-encrypts it rather than failing. That is convenient, and it also means the old key material and the new key material both exist for a moment during the upgrade, and a rollout that half-completes leaves a user with data written one way and an app expecting the other.

The minimum SDK raise is the part most likely to break a build, and the deprecated Jetpack Security path is the part most likely to leave a security gap in an app that nobody has touched in two years.

Two biometric modes, and the difference is about failure

Biometric authentication is optional, and the readme shows the two ways to ask for it, which differ in what happens when the device cannot do it.

The first is graceful degradation.

dart
final storage = FlutterSecureStorage(
  aOptions: AndroidOptions.biometric(
    enforceBiometrics: false, // Works without biometrics
    biometricPromptTitle: 'Authenticate to access data',
  ),
);

The second is strict enforcement.

dart
final storage = FlutterSecureStorage(
  aOptions: AndroidOptions.biometric(
    enforceBiometrics: true, // Requires biometric/PIN/pattern
    biometricPromptTitle: 'Authentication Required',
  ),
);

The comments are the documentation. With enforceBiometrics false the storage still works without biometrics, so a user whose device has no fingerprint sensor or whose enrolment has been removed is not locked out of their own data. With enforceBiometrics true the key requires a biometric, a PIN or a pattern, and a device without device security cannot use the storage at all.

The readme describes the enhancement as enhanced biometric authentication with graceful degradation, and biometric support is available on Android from API 23 and on iOS and macOS.

That choice has a product consequence. Enforce it and you need a fallback path for the user whose enrolment breaks, because a bank of tokens that can only be read with a fingerprint is a support queue. Relax it and you have to decide what the biometric prompt is actually protecting, since the underlying key is still on the device.

The iOS accessibility option decides when the keychain opens

Each platform has its own options object, and the readme uses iOS as the example because the accessibility option is the one with real design consequences.

The accessibility option determines when the app can access secure values stored in the Keychain, and three values are listed. first_unlock enables access after the device is unlocked for the first time after a reboot. first_unlock_this_device allows access only after the device is unlocked for the first time since installation on this device. And unlocked, which is the default, means values are accessible only when the device is unlocked.

dart
final options = IOSOptions(accessibility: KeychainAccessibility.first_unlock);
await storage.write(key: key, value: value, iOptions: options);

The default is the strictest of the three, which is the right default for a secret, but it is wrong for some categories of data. A background refresh token that the app needs to renew while the phone is locked will fail with the default, and the fix is first_unlock, which lets the system read the value after the first unlock following a reboot. That is the same trade-off native iOS developers make with keychain accessibility classes, and it is the reason this option is exposed rather than hard coded.

first_unlock_this_device sits between them, tied to installation rather than to the boot, and it matters for the case where a backup is restored onto a new device.

The readme notes that similar platform-specific options are available for the other platforms, and that the configuration can also cover encryption algorithms, accessibility attributes and biometric requirements.

The Android auto backup trap produces an exception, not a warning

There is a section on disabling auto backup in the Android configuration, and it exists because of a specific crash.

By default Android backs up data on Google Drive, and it can cause the exception java.security.InvalidKeyException: Failed to unwrap key. The readme points to two documentation pages, one on disabling autobackup and one on excluding shared preferences.

The mechanism is straightforward once you see it. The plugin stores its encrypted values in shared preferences, and the key that unwraps them is held in the Android keystore, which is not backed up. When the preferences come back on a new device after a restore and the keystore key does not, the plugin tries to unwrap with a key that is not there, and the unwrap fails.

The fix is either to turn backup off in the manifest.

xml
<application
  android:allowBackup="false"
  ...>
</application>

or to exclude the shared preferences that FlutterSecureStorage uses.

The second option is the better one when the rest of the app does want backup, because allowBackup false applies to everything the application stores. The failure is also a good illustration of a general point about platform keychains. A backup that restores encrypted data without its key is not a recovery, it is a corrupt file that looks like data.

The web build only works on HTTPS or localhost

There is a separate notice for the web, and it is short enough to miss.

flutter_secure_storage only works on HTTPS or localhost environments, and the readme links an issue for the reasoning.

The reason is the browser security model. The web implementation uses the Web Crypto API, and that API is only exposed in a secure context, which means HTTPS or localhost. A page served over plain HTTP has no access to it at all, so the plugin cannot function.

For development this is usually invisible, because a development server is on localhost. The moment the same build is served from a staging host over HTTP, the storage calls fail, and the failure looks like a plugin bug rather than a browser policy.

The feature list also says the plugin works across Android, iOS, macOS, Windows, Linux and Web, so web is a supported target with a narrow deployment condition.

There is a second small prerequisite in the Installation section, and it catches people out on the first run. If it is not present already, you should call WidgetsFlutterBinding.ensureInitialized() in your main before you do anything with the MethodChannel, because the plugin communicates over a method channel and the binding has to exist before the channel is touched.

A federated package set with release automation

The repository layout is the standard Flutter federated plugin shape, and it explains why the recent release list looks like it does.

There is flutter_secure_storage/, the package you depend on, and then flutter_secure_storage_darwin/, flutter_secure_storage_linux/, flutter_secure_storage_windows/ and flutter_secure_storage_web/, one per platform, plus flutter_secure_storage_platform_interface/, the shared interface the platform implementations sit behind. The archived_packages/ directory holds the packages that have been superseded, which is the fate of the old Android and iOS implementations once the platform packages took over.

The recent releases show the same split. flutter_secure_storage v11.2.0, flutter_secure_storage_darwin v0.4.3 and flutter_secure_storage_platform_interface v2.1.1 were all published on 2026-09-16, which is what a coordinated multi-package release looks like.

The release process is automated, with a.release-please-manifest.json and a release-please-config.json in the repository, and the version is stamped across the packages from those files. The default branch is develop.

The quality tooling is visible too. The readme carries a Code Quality badge for very_good_analysis, the Dart linter package, a codecov badge for coverage, and a CI workflow badge. The repository also has a SECURITY.md and a CODE_OF_CONDUCT.md, and the primary language recorded for the project is C++, which is the native code inside the platform implementations rather than the Dart surface you call.

Editorial conclusion

Adopt flutter_secure_storage when your Flutter app has tokens, keys or small secrets that belong in the platform keychain rather than in shared preferences, and you are willing to configure each platform's options separately. Do not adopt it as a general purpose database or for bulk data, since the API is a key-value store and the platform mechanisms behind it are not designed for large payloads. Verify first that your Android minimum SDK is 23 or newer, that you have disabled Android auto backup or excluded the shared preferences it uses, and that your iOS accessibility choice matches how your app is expected to behave when the device has not been unlocked.

Frequently asked questions

What does flutter_secure_storage do?

It is a Flutter plugin that stores sensitive data in a key-value pair format using platform-specific secure storage: the Keychain on iOS and macOS, custom secure ciphers with optional biometric authentication on Android, and the platform specific secure mechanisms on Windows, Linux and Web.

What changed in flutter_secure_storage version 10?

Version 10.0.0 introduced custom cipher implementations, with new defaults of RSA OAEP for the key cipher and AES-GCM for the storage cipher, new AndroidOptions() and AndroidOptions.biometric() constructors, automatic migration from the old ciphers via migrateOnAlgorithmChange, and a minimum Android SDK of 23. The deprecated Jetpack Security encryptedSharedPreferences is no longer recommended.

What is the difference between enforceBiometrics true and false?

With enforceBiometrics false the storage still works without biometrics, so a device without a fingerprint sensor is not locked out. With enforceBiometrics true the key requires a biometric, a PIN or a pattern, and a device without device security cannot use the storage.

Why do I get an InvalidKeyException on Android?

Because Android backs up app data to Google Drive by default, so the shared preferences holding the encrypted values can be restored on a new device while the keystore key used to unwrap them is not. Disable auto backup in the manifest or exclude the shared preferences used by FlutterSecureStorage.

Does flutter_secure_storage work on the web?

It works on the web only on HTTPS or localhost environments, because the browser exposes the Web Crypto API only in a secure context. The readme links an issue for the details.

What does the iOS accessibility option control?

It determines when the app can access secure values stored in the Keychain. first_unlock allows access after the first unlock following a reboot, first_unlock_this_device ties access to the first unlock since installation on that device, and unlocked, the default, means values are accessible only while the device is unlocked.

Official sources

  1. juliansteenbakker/flutter_secure_storage on GitHub
  2. License: BSD-3-Clause
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/juliansteenbakker-flutter-secure-storage.svg)](https://hysenlabs.com/projects/juliansteenbakker-flutter-secure-storage)

Was this useful?