biometric_vault 1.0.0-dev.2
biometric_vault: ^1.0.0-dev.2 copied to clipboard
Secure storage with optional biometric protection for Android, iOS, and macOS. Linux, Windows, and web fall back to unauthenticated storage.
biometric_vault #
Encrypted storage for small secrets (tokens, passwords, key material) with optional biometric protection. Secrets are encrypted by the platform keystore and, when you ask for it, unlocked only after Face ID / Touch ID / fingerprint / device-credential authentication.
Highlights #
- Typed errors, no surprises. Every failure is a subtype of the sealed
BiometricVaultException; oneswitchhandles all outcomes exhaustively. RawPlatformExceptions never escape. - Hardware-backed encryption. Android Keystore (StrongBox when available,
TEE fallback) with AES-GCM; iOS/macOS Keychain with
SecAccessControl. - Precise capability reporting.
canAuthenticate()tells you exactly why authentication is unavailable: nothing enrolled, no hardware, locked out, pending security update, and more. - Fail-fast validation. Invalid store names or impossible option
combinations throw
ArgumentErroratgetStorage, not a native error at first use in production. - Modern toolchains. Swift Package Manager on iOS/macOS (CocoaPods still supported), Built-in Kotlin and Gradle Kotlin DSL on Android, and a UI-thread-free native implementation on every platform.
This package stores small secrets, not large datasets. Store a key or token
in biometric_vault and encrypt your bulk data with it.
Platform support #
| Platform | Backing store | Authentication gate |
|---|---|---|
| Android 7.0+ (API 24) | AES-GCM key in the Android Keystore (StrongBox when available, TEE fallback) | BiometricPrompt: Class 3 biometrics, optional device credential fallback |
| iOS 13+ | Keychain with access control | Face ID / Touch ID, optional passcode fallback |
| macOS 10.15+ | Keychain with access control | Touch ID / Apple Watch, optional password fallback |
| Linux | libsecret keyring | none |
| Windows | Credential Manager | none |
| Web | plaintext localStorage, not secure |
none |
Release baseline: Flutter 3.44 / Dart 3.12.
Getting started #
Add the dependency:
dependencies:
biometric_vault: ^1.0.0
Android #
- Android 7.0 (API level
24) or newer. - The host activity must extend
FlutterFragmentActivity, otherwise every authenticated operation fails withAuthExceptionCode.failedToStart. - The activity theme must inherit from
Theme.AppCompat.
android/app/src/main/kotlin/.../MainActivity.kt
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity : FlutterFragmentActivity()
android/app/src/main/res/values/styles.xml
<resources>
<style name="LaunchTheme" parent="Theme.AppCompat.NoActionBar" />
<style name="NormalTheme" parent="Theme.AppCompat.NoActionBar" />
</resources>
iOS #
- Include the
NSFaceIDUsageDescriptionkey in your app'sInfo.plist; without it, iOS terminates the app when Face ID is first used.
Known issue: local authentication in the iOS simulator is unreliable on recent iOS versions; test biometric flows on a real device. https://developer.apple.com/forums/thread/685773
macOS #
- Include the
NSFaceIDUsageDescriptionkey in your app'sInfo.plistif you rely on biometrics. - Enable signing and the Keychain Sharing capability for the host app.
Without it you will see a
BiometricVaultPluginExceptionwith codeSecurityErrorand message-34018: A required entitlement isn't present.
Linux, Windows, and web #
- Linux and Windows store values through the OS (libsecret / Credential
Manager) without any authentication prompt;
canAuthenticatereportserrorHwUnavailablethere. - Web stores plaintext values in browser
localStorage. Do not treat the web implementation as secure storage; it exists so cross-platform code keeps working during development.
Usage #
You only need four calls for the whole lifecycle:
import 'package:biometric_vault/biometric_vault.dart';
// 1. Check what the device supports.
final support = await BiometricVault().canAuthenticate();
if (support != CanAuthenticateResponse.success &&
support != CanAuthenticateResponse.statusUnknown) {
// Inspect the value: no hardware, nothing enrolled, locked out, ...
return;
}
// 2. Open (or create) a named store.
final store = await BiometricVault().getStorage('my_token');
// 3. Write a value (may show the system authentication prompt).
await store.write('my secret');
// 4. Read it back (may prompt again, depending on the options).
final value = await store.read(); // null when nothing was stored
Options #
Options are applied when a store is first created; see
StorageFileInitOptions for the full documentation of each flag.
final store = await BiometricVault().getStorage(
'my_token',
options: StorageFileInitOptions(
// One authentication unlocks the store for 30 seconds (Android).
androidAuthenticationValidityDuration: Duration(seconds: 30),
// Allow PIN/pattern/password fallback (requires the duration above).
androidBiometricOnly: false,
// Allow the device passcode as fallback on iOS/macOS.
darwinBiometricOnly: false,
// Reuse one authenticated context for 30 seconds (iOS/macOS).
darwinTouchIDAuthenticationForceReuseContextDuration:
Duration(seconds: 30),
),
promptInfo: const PromptInfo(
androidPromptInfo: AndroidPromptInfo(title: 'Unlock your vault'),
iosPromptInfo: DarwinPromptInfo(accessTitle: 'Unlock your vault'),
),
);
Set authenticationRequired: false to store values through the platform
keystore without any authentication prompt (useful as a fallback when
canAuthenticate reports that biometry is unavailable).
Error handling #
Every failure is a subtype of the sealed BiometricVaultException, so a
single switch handles all outcomes exhaustively:
try {
final value = await store.read();
} on BiometricVaultException catch (e) {
switch (e) {
case AuthException(code: AuthExceptionCode.userCanceled):
break; // The user knows they canceled; no error UI needed.
case AuthException(code: AuthExceptionCode.lockedOut):
showSnackBar('Too many attempts. Try again in a few seconds.');
case AuthException(code: AuthExceptionCode.lockedOutPermanently):
showSnackBar('Biometry locked. Unlock your device with PIN first.');
case AuthException(:final code):
showSnackBar('Authentication failed: $code');
case StorageInvalidatedException():
// The value is permanently unrecoverable, for example after the user
// changed biometric enrollment on Android. Re-provision the secret.
await store.delete();
await promptUserToSignInAgain();
case BiometricVaultPluginException(:final code):
reportToCrashlytics('biometric_vault error $code: ${e.message}');
}
}
AuthException means the user could not or did not authenticate; the stored
data is untouched and the call can be retried. StorageInvalidatedException
means the data is gone for good. BiometricVaultPluginException wraps
unexpected platform errors and preserves the raw code and details.
Resources #
- API documentation: https://pub.dev/documentation/biometric_vault/latest/
- Android data security: https://developer.android.com/topic/security/data
- Apple keychain + biometry: https://developer.apple.com/documentation/localauthentication/accessing-keychain-items-with-face-id-or-touch-id
Credits and license #
MIT licensed. biometric_vault began as a fork of
biometric_storage by
Herbert Poul and was rebuilt from the ground up; the original copyright is
preserved in the LICENSE alongside the current maintainer's.
biometric_vault is an independent package: it registers its own platform
channel and its own native storage, and does not read values stored by
biometric_storage.