webauthn_secure_storage 0.1.0
webauthn_secure_storage: ^0.1.0 copied to clipboard
App-facing package for the webauthn_secure_storage federated plugin.
webauthn_secure_storage #
Encrypted file store, optionally secured by biometric lock, plus standards-based WebAuthn / passkey APIs for challenge-response authentication flows.
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.
- Android: Uses androidx with KeyStore.
- iOS and MacOS: LocalAuthentication with KeyChain.
- Linux: Stores values in Keyring using libsecret. (No biometric authentication support).
- Windows: Uses wincreds.h to store into read/write into credential store.
- Web: Uses WebAuthn for passkey authentication and WebAuthn PRF for local secret storage only when the browser can prove PRF support at runtime. Unsupported PRF browsers cleanly report storage support as unavailable instead of falling back to weaker storage.
Web support and security #
This package is intended to unlock secrets on-device using platform security primitives such as Android Keystore and Apple Keychain, optionally gated by biometrics.
The app-facing API now has two related surfaces:
- biometric / secure-storage APIs for local secret protection
- standards-based WebAuthn / passkey DTO APIs for server-driven registration/authentication flows
On the web, the federated implementation is intentionally stricter than a plain localStorage wrapper:
- passkey authentication only requires a secure context plus browser WebAuthn support
- it requires a secure context (
https:or equivalent localhost secure context) - secure local secret storage additionally requires runtime-advertised support for the PRF extension
- both flows require a user-verifying platform authenticator (Touch ID, Face ID, Android device unlock, etc.)
- PRF-backed storage stores only encrypted ciphertext plus WebAuthn credential metadata in browser storage
If the browser cannot honestly prove PRF support — for example, many
Windows/browser combinations today still do not expose it —
isSupported() returns false and canAuthenticate() returns
unsupported for the storage surface even though passkey authentication may
still be available.
Passkeys and secure-access capabilities #
Use BiometricStorage().getCapabilities() or the newer capability helpers when
you need to distinguish:
- biometric-backed local secret storage
- passkey authentication support
- passkey PRF-backed local secret storage
final biometricStorage = BiometricStorage();
final capabilities = await biometricStorage.getCapabilities();
if (capabilities.isCapabilityAvailable(
SecureAccessCapability.passkeyAuthentication,
)) {
// Offer passkey login.
}
if (capabilities.isCapabilityAvailable(
SecureAccessCapability.passkeyPrfStorage,
)) {
// Offer passkey-backed local secret unlock.
} else if (capabilities.isCapabilityAvailable(
SecureAccessCapability.biometricStorage,
)) {
// Fall back to biometric local secret unlock.
}
For passkey challenge-response flows, pass the standard server options straight through the package and post the results back to your server:
final registration = await biometricStorage.registerPasskey(serverOptions);
final assertion = await biometricStorage.authenticateWithPasskey(requestOptions);
The DTOs are standards-based and designed to round-trip cleanly with server platforms such as ASP.NET Core or Next.js ecosystems without bespoke field mapping.
What this means in practice #
- Web support is capability-gated, not assumed.
- A storage handle can still be created without prompting the user.
- The first
write()creates the WebAuthn credential when the user opts in. - A later
read()prompts the platform authenticator only if a stored credential already exists.
Recommended approach for web #
For web applications, prefer:
- server-backed session tokens with short lifetimes
- passkeys / WebAuthn for authentication
- keeping the most sensitive bearer secrets off the browser when possible
If you need a browser login flow, this package can participate through the passkey APIs when WebAuthn is available. For local secret storage or unlock, PRF support is still required. When PRF is not available, the intended fallback remains your regular web login/session design or a non-PRF secure access path.
Getting Started #
Supported vs available right now #
This package intentionally distinguishes between:
- supported: the platform can use biometric-backed storage APIs at all
- available right now: the platform authenticator can actually be used at this moment
That distinction matters in real apps.
For example on macOS, Touch ID storage can still be supported while biometrics are temporarily unavailable because:
- the Mac is in closed clamshell mode
- biometrics are locked out temporarily
- no biometric enrollment is present
- the device requires passcode / credential setup first
In those cases:
isSupported()may still returntruecanAuthenticate()reports the current runtime statecanAuthenticateWithBiometrics()returns whether you should offer biometric login right now
Use this rule of thumb:
- use
isSupported()to decide whether the app can ever offer biometric-backed storage on this platform - use
canAuthenticate(),canAuthenticateWithBiometrics(), orgetCapabilities().isBiometricStorageAvailableto decide whether to show or auto-start biometric login right now
Do not gate your biometric-login button only on isSupported() or on
whether getStorageIfSupported() returns a handle. A storage handle can exist
even when the authenticator is temporarily unavailable.
Installation and project configuration #
This package is federated. Your Flutter app usually depends only on
webauthn_secure_storage, but the project hosting it must still be configured
correctly per platform.
Platform quick-reference #
| Platform | Local secret storage | Biometric gate | Passkey APIs in this release | Project setup required |
|---|---|---|---|---|
| Android | Yes | Yes | No | Yes |
| iOS | Yes | Yes | No | Yes |
| macOS | Yes | Yes | No | Yes |
| Linux | Yes | No | No | Sometimes |
| Windows | Yes | No current biometric gate | No | Minimal |
| Web | PRF-capable browsers only | WebAuthn user verification | Yes | Yes |
Android #
Requirements:
- Android API level >= 23 (
minSdkVersion 23) - a current Kotlin/AGP/Flutter toolchain
MainActivitymust extendFlutterFragmentActivity- the hosting activity theme should inherit from an AppCompat theme on older Android versions
The example app uses:
Theme.AppCompat.NoActionBarinandroid/app/src/main/res/values/styles.xmlFlutterFragmentActivityinMainActivity.kt
Typical checklist:
- Ensure
minSdkVersionis at least23. - Make your activity extend
FlutterFragmentActivityinstead ofFlutterActivity. - Use an AppCompat-based launch/activity theme.
- Keep release signing configured normally for your app; this package does not
require special Android permissions in
AndroidManifest.xml.
Example activity base class:
class MainActivity : FlutterFragmentActivity()
Example theme inheritance:
<style name="LaunchTheme" parent="Theme.AppCompat.NoActionBar">
What to expect:
isSupported()can betrueon supported Android hardwarecanAuthenticate()reports runtime state such as no enrollment or temporary hardware unavailability- passkey APIs are not currently implemented natively on Android in this first release
Helpful references:
- https://developer.android.com/topic/security/data
- https://developer.android.com/topic/security/best-practices
iOS #
Current plugin baseline:
- iOS deployment target:
13.0+
Required project settings:
- Add
NSFaceIDUsageDescriptiontoios/Runner/Info.plistwith a user-facing explanation. - Run normal CocoaPods installation (
flutter pub get, thenpod installas needed by Flutter tooling). - Sign the app normally for device testing and release builds.
Example Info.plist entry:
<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to unlock your encrypted local secrets.</string>
Notes:
- iOS uses LocalAuthentication + Keychain.
- No extra entitlement is required just to use this package’s biometric-backed storage in a standard iOS app.
- On iOS, apps should not expect a separate Keychain access consent dialog; protected operations use the normal LocalAuthentication prompt instead.
- Since iOS 15, simulator local-auth behavior is limited and often not representative of real devices.
- passkey APIs are not currently implemented natively on iOS in this first release, so you do not need to add Associated Domains just for this package today.
Apple reference:
- https://developer.apple.com/documentation/localauthentication/logging_a_user_into_your_app_with_face_id_or_touch_id
- https://developer.apple.com/forums/thread/685773
macOS #
Current plugin baseline:
- macOS deployment target:
10.15+
Required project settings:
- Add
NSFaceIDUsageDescriptiontomacos/Runner/Info.plist. - Enable code signing for normal app builds.
- If your app uses the App Sandbox, add a
keychain-access-groupsentitlement. - Ensure your entitlements are present in both debug/profile and release configurations.
The example app demonstrates this in:
macos/Runner/Info.plistmacos/Runner/DebugProfile.entitlementsmacos/Runner/Release.entitlements
Example Info.plist entry:
<key>NSFaceIDUsageDescription</key>
<string>Use Touch ID / Face ID to unlock encrypted local secrets.</string>
Example entitlement shape when sandboxed:
<key>com.apple.security.app-sandbox</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
Important macOS notes:
- this package uses LocalAuthentication + Keychain
- a signed app with correct entitlements is the reliable path for device testing and distribution
- closed clamshell mode can make Touch ID unavailable temporarily even when biometric-backed storage is still supported on that Mac
- gate UI on
canAuthenticate()/canAuthenticateWithBiometrics(), not only onisSupported() - passkey APIs are not currently implemented natively on macOS in this first release, so Associated Domains are not required for this package today
What to do if you get a prompt for Keychain access
On macOS, the intended experience for protected items is the system authentication prompt (Touch ID / password), not the classic Keychain Access dialog asking whether to allow the app to access an item.
If you see the classic Keychain Access prompt, it usually means macOS does not see your app as the same trusted/signed app identity that originally created the item.
Most common fixes:
- Make sure the app is signed consistently
- in Xcode, select the macOS Runner target
- enable automatic signing or otherwise use a stable signing identity
- keep the same Team ID between runs
- Keep the bundle identifier stable
- changing the bundle ID can make a later build look like a different app to Keychain
- Keep entitlements aligned across configurations
- if you use the App Sandbox, include
keychain-access-groups - make sure both debug/profile and release builds point at valid entitlements files
- the example app includes this in both
macos/Runner/DebugProfile.entitlementsandmacos/Runner/Release.entitlements
- if you use the App Sandbox, include
- Clean out stale development state after signing changes
- if you previously ran unsigned builds, changed teams, or changed bundle identifiers, delete the old app from your machine
- remove old test items for the app from Keychain Access if they were created under the wrong identity
- then rebuild and run the newly signed app
What your entitlements should look like in a sandboxed app:
com.apple.security.app-sandbox = truekeychain-access-groupscontaining your app identifier prefix + bundle ID
Example shape:
<key>com.apple.security.app-sandbox</key>
<true/>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
If you still get the classic Keychain dialog after fixing signing and entitlements, treat that as an app configuration problem first, not as the expected runtime UX of this package.
Linux #
Linux support is intentionally narrower:
- secure local storage uses
libsecret/ the desktop keyring - biometric authentication is not supported on Linux in this package
Project/runtime expectations:
- The target system needs a working Secret Service / keyring implementation.
- Snap-packaged apps may need the password-manager interface connected.
- Treat Linux as secure storage without a biometric gate.
For Snap environments, the example app surfaces the common fix:
snap connect <your-snap-name>:password-manager-service
If you want to diagnose that case at runtime, use:
final denied = await BiometricStorage().linuxCheckAppArmorError();
Windows #
Windows support in this first release uses Windows Credential Manager for local secret storage.
What this means:
- local secret storage works
- biometric prompting is not currently implemented as a runtime gate in the Windows federated implementation
- passkey APIs are not currently implemented natively on Windows in this first release
Project setup is minimal:
- no extra
Info.plist/entitlement-style step exists on Windows - normal Flutter Windows build setup is sufficient
- you should still gate any biometric-specific UI on
canAuthenticateWithBiometrics(), which is expected to befalseon the current Windows implementation
Web #
Web support is the most capability-sensitive platform.
Requirements:
- Serve the app from a secure context (
https:or localhost secure context). - Use browsers with WebAuthn support for passkey APIs.
- Use browsers that expose the PRF extension if you want local secure-storage support on the web.
- Ensure your server-provided WebAuthn options use an RP ID / origin that actually matches your deployed site.
Important behavior:
- passkey authentication is implemented on web in this release
- PRF-backed local secret storage is stricter than plain browser storage and is only available when runtime PRF support is proven
isSupported()may befalsefor storage on browsers where passkey login is still available
Practical checklist:
- Deploy over HTTPS.
- Configure your backend WebAuthn relying-party settings to match your real host name.
- Use
getCapabilities()if you need to distinguish passkey login from PRF-backed local secret storage. - Expect local development on
localhostto behave differently from random insecure LAN origins.
Flutter / CocoaPods / generated files #
For Apple platforms, let Flutter manage generated pod settings unless you have a strong reason not to.
Typical recovery steps when platform integration gets weird:
flutter pub get- let Flutter regenerate iOS/macOS ephemeral files
- run CocoaPods install/update through the normal Flutter workflow
- clean/rebuild if Xcode still shows stale signing or entitlement state
This package does not require custom pod edits beyond the normal Flutter iOS and macOS project setup.
Usage #
You basically only need 4 methods.
- Check whether the package is supported on this platform
final storage = BiometricStorage();
if (!await storage.isSupported()) {
// Skip biometrics entirely and fall back to regular login.
return;
}
- Check whether biometric authentication is available right now
final response = await storage.canAuthenticate();
if (response.shouldFallbackToRegularLogin) {
// Show regular login. You can still inspect response to see if the user
// needs to enroll biometrics, enable a passcode, etc.
return;
}
If you only need a bool for startup auth, you can use:
final canAutoPrompt = await storage.canAuthenticateWithBiometrics();
- Create the access object
final store = await storage.getStorageIfSupported('mystorage');
if (store == null) {
// Unsupported platform: skip without throwing.
return;
}
getStorageIfSupported() only checks platform/package support. It does not
mean biometric authentication is currently available. Use it after you have
already decided which login path to offer.
- Read data
final data = await store.read();
- Write data
const myNewData = 'Hello World';
await store.write(myNewData);
Suggested login flow #
For the UX described above:
- On app start:
- call
isSupported() - if
false, go straight to regular login - if
true, callcanAuthenticate()orcanAuthenticateWithBiometrics() - if biometrics are available, read from the protected store and let the platform prompt automatically
- otherwise, fall back to regular login without error
- call
- After regular login success:
- if
isSupported()istrue, show a “Use biometrics next time” toggle - when enabled, write the login token / small secret to a biometric-backed store for next launch
- if
Example: correct startup gating #
final storage = BiometricStorage();
if (!await storage.isSupported(options: authOptions)) {
// This platform cannot use biometric-backed storage at all.
await showRegularLogin();
return;
}
final authState = await storage.canAuthenticate(options: authOptions);
if (!authState.canAuthenticateWithBiometrics) {
// Supported, but unavailable right now.
// Example: macOS closed clamshell mode can land here.
await showRegularLogin();
return;
}
final store = await storage.getStorageIfSupported(
'auth-token',
options: authOptions,
);
final token = await store?.read();
if (token != null) {
await signInWithStoredToken(token);
} else {
await showRegularLogin();
}
Example: opt-in after password login #
final storage = BiometricStorage();
if (await storage.isSupported(options: authOptions)) {
// Show a toggle such as "Use Touch ID / Face ID next time".
final enabled = await askUserToEnableBiometricLogin();
if (enabled) {
final store = await storage.getStorage(
'auth-token',
options: authOptions,
);
await store.write(tokenFromSuccessfulPasswordLogin);
}
}
This workflow avoids a common integration bug:
isSupported() == truemeans “this device/platform can use the feature”canAuthenticateWithBiometrics() == truemeans “you can offer the biometric login path right now”
See also the API documentation: https://pub.dev/documentation/webauthn_secure_storage/latest/webauthn_secure_storage/BiometricStorageFile-class.html#instance-methods