Flutter plugin to store data behind biometric authentication (ie. fingerprint)
198
stars
257
commits
Dart
primary language
Aug 26, 2026
updated
Encrypted file store, optionally secured by biometric lock for Android, iOS, MacOS and partial support for Linux, Windows and Web.
Meant as a way to store small data in a hardware encrypted fashion. E.g. to store passwords, secret keys, etc. but not massive amounts of data.
Check out AuthPass Password Manager for a app which makes heavy use of this plugin.
Always required:
android/app/build.gradle minSdkVersion 23)Required only if you actually prompt for authentication, that is, if any
storage uses the default authenticationRequired: true. Storage created with
authenticationRequired: false never shows a BiometricPrompt, so neither of
the following applies to it:
MainActivity must extend FlutterFragmentActivity. BiometricPrompt
needs a FragmentActivity to host its dialog. If the plugin is attached to a
plain FlutterActivity it logs an error and every authenticated read or write
fails with AuthError:Failed — unauthenticated storage keeps working.
The activity theme must descend from Theme.AppCompat — but only for
devices where androidx.biometric falls back to drawing its own fingerprint
dialog, which it builds with androidx.appcompat.app.AlertDialog. That
fallback is used below API 28, on API 28 devices without a fingerprint
sensor, and on a short manufacturer allow-list where a crypto object forces
it. From API 28 onwards the system BiometricPrompt is used and the theme
does not matter.
If you do need it:
android/app/src/main/AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme">
[...]
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
</activity>
android/app/src/main/res/values/styles.xml:
<resources>
<style name="LaunchTheme" parent="Theme.AppCompat.NoActionBar">
...
</style>
<style name="NormalTheme" parent="Theme.AppCompat.NoActionBar">
...
</style>
</resources>
The plugin writes to android.util.Log under the tag BiometricStorage. A
debug build logs everything; a release build logs nothing below INFO until you
ask for it:
adb shell setprop log.tag.BiometricStorage VERBOSE
That suits reading logs off a device. If your app collects its own — a file
appender, a crash reporter, slf4j, Timber — install a sink instead, from
Application.onCreate or your FlutterActivity, before the first call into
the plugin:
import android.util.Log
import design.codeux.biometric_storage.BiometricStorageLogging
import org.slf4j.LoggerFactory
BiometricStorageLogging.sink =
BiometricStorageLogging.Sink { priority, tag, message, throwable ->
val log = LoggerFactory.getLogger(tag)
when (priority) {
Log.VERBOSE -> log.trace(message, throwable)
Log.DEBUG -> log.debug(message, throwable)
Log.INFO -> log.info(message, throwable)
Log.WARN -> log.warn(message, throwable)
else -> log.error(message, throwable)
}
}
Installing a sink turns every level on, since it says something wants the records. To decide the level yourself — including keeping verbose logging in a release build without touching a device property — set it explicitly:
BiometricStorageLogging.level = Log.VERBOSE
throwable is passed separately rather than flattened into message, so you
can hand the real exception to whatever you report to. The sink is called on
whichever thread produced the record, so it must be safe to call from the main
thread and from a background executor.
Known Issue: since iOS 15 the simulator seem to no longer support local authentication: https://developer.apple.com/forums/thread/685773
IosPromptInfo.saveTitle / accessTitle are invisible on Face ID devices.
Face ID authenticates against a HUD that shows its glyph and the words "Face ID"
and nothing else, and the "not recognized" alert after a failure offers only
retry and cancel. The strings do reach the system — they are set as
LAContext.localizedReason — but iOS does not draw them. Touch ID devices and
macOS do show them, so they are still worth setting.
SecurityError, Error while writing data: -34018: A required entitlement isn't present.
The iOS and macOS implementations ship both a Package.swift and a podspec, so
they work with either dependency manager. Adding this plugin to an app that has
migrated to Swift Package Manager
does not bring CocoaPods back — no Podfile is generated.
You basically only need 4 methods.
final response = await BiometricStorage().canAuthenticate()
if (response != CanAuthenticateResponse.success) {
// panic..
}
final storageFile = await BiometricStorage().getStorage('mystorage');
final data = await storageFile.read();
final myNewData = 'Hello World';
await storageFile.write(myNewData);
Storing without a biometric prompt — for a value a background task has to be able to refresh, for example — is
authenticationRequired: false. The value is still encrypted at rest; it is simply not gated behind an authentication.final storageFile = await BiometricStorage().getStorage( 'mystorage', options: StorageFileInitOptions(authenticationRequired: false), );
See also the API documentation: https://pub.dev/documentation/biometric_storage/latest/biometric_storage/BiometricStorageFile-class.html#instance-methods
Dart
34.6%
Kotlin
24.0%
C++
17.8%
Swift
11.5%
CMake
9.4%
C
1.2%
Flutter plugin to store data behind biometric authentication (ie. fingerprint)
198
stars
257
commits
Dart
primary language
Aug 26, 2026
updated
Encrypted file store, optionally secured by biometric lock for Android, iOS, MacOS and partial support for Linux, Windows and Web.
Meant as a way to store small data in a hardware encrypted fashion. E.g. to store passwords, secret keys, etc. but not massive amounts of data.
Check out AuthPass Password Manager for a app which makes heavy use of this plugin.
Always required:
android/app/build.gradle minSdkVersion 23)Required only if you actually prompt for authentication, that is, if any
storage uses the default authenticationRequired: true. Storage created with
authenticationRequired: false never shows a BiometricPrompt, so neither of
the following applies to it:
MainActivity must extend FlutterFragmentActivity. BiometricPrompt
needs a FragmentActivity to host its dialog. If the plugin is attached to a
plain FlutterActivity it logs an error and every authenticated read or write
fails with AuthError:Failed — unauthenticated storage keeps working.
The activity theme must descend from Theme.AppCompat — but only for
devices where androidx.biometric falls back to drawing its own fingerprint
dialog, which it builds with androidx.appcompat.app.AlertDialog. That
fallback is used below API 28, on API 28 devices without a fingerprint
sensor, and on a short manufacturer allow-list where a crypto object forces
it. From API 28 onwards the system BiometricPrompt is used and the theme
does not matter.
If you do need it:
android/app/src/main/AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme">
[...]
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
</activity>
android/app/src/main/res/values/styles.xml:
<resources>
<style name="LaunchTheme" parent="Theme.AppCompat.NoActionBar">
...
</style>
<style name="NormalTheme" parent="Theme.AppCompat.NoActionBar">
...
</style>
</resources>
The plugin writes to android.util.Log under the tag BiometricStorage. A
debug build logs everything; a release build logs nothing below INFO until you
ask for it:
adb shell setprop log.tag.BiometricStorage VERBOSE
That suits reading logs off a device. If your app collects its own — a file
appender, a crash reporter, slf4j, Timber — install a sink instead, from
Application.onCreate or your FlutterActivity, before the first call into
the plugin:
import android.util.Log
import design.codeux.biometric_storage.BiometricStorageLogging
import org.slf4j.LoggerFactory
BiometricStorageLogging.sink =
BiometricStorageLogging.Sink { priority, tag, message, throwable ->
val log = LoggerFactory.getLogger(tag)
when (priority) {
Log.VERBOSE -> log.trace(message, throwable)
Log.DEBUG -> log.debug(message, throwable)
Log.INFO -> log.info(message, throwable)
Log.WARN -> log.warn(message, throwable)
else -> log.error(message, throwable)
}
}
Installing a sink turns every level on, since it says something wants the records. To decide the level yourself — including keeping verbose logging in a release build without touching a device property — set it explicitly:
BiometricStorageLogging.level = Log.VERBOSE
throwable is passed separately rather than flattened into message, so you
can hand the real exception to whatever you report to. The sink is called on
whichever thread produced the record, so it must be safe to call from the main
thread and from a background executor.
Known Issue: since iOS 15 the simulator seem to no longer support local authentication: https://developer.apple.com/forums/thread/685773
IosPromptInfo.saveTitle / accessTitle are invisible on Face ID devices.
Face ID authenticates against a HUD that shows its glyph and the words "Face ID"
and nothing else, and the "not recognized" alert after a failure offers only
retry and cancel. The strings do reach the system — they are set as
LAContext.localizedReason — but iOS does not draw them. Touch ID devices and
macOS do show them, so they are still worth setting.
SecurityError, Error while writing data: -34018: A required entitlement isn't present.
The iOS and macOS implementations ship both a Package.swift and a podspec, so
they work with either dependency manager. Adding this plugin to an app that has
migrated to Swift Package Manager
does not bring CocoaPods back — no Podfile is generated.
You basically only need 4 methods.
final response = await BiometricStorage().canAuthenticate()
if (response != CanAuthenticateResponse.success) {
// panic..
}
final storageFile = await BiometricStorage().getStorage('mystorage');
final data = await storageFile.read();
final myNewData = 'Hello World';
await storageFile.write(myNewData);
Storing without a biometric prompt — for a value a background task has to be able to refresh, for example — is
authenticationRequired: false. The value is still encrypted at rest; it is simply not gated behind an authentication.final storageFile = await BiometricStorage().getStorage( 'mystorage', options: StorageFileInitOptions(authenticationRequired: false), );
See also the API documentation: https://pub.dev/documentation/biometric_storage/latest/biometric_storage/BiometricStorageFile-class.html#instance-methods
Dart
34.6%
Kotlin
24.0%
C++
17.8%
Swift
11.5%
CMake
9.4%
C
1.2%