flutter_mfa 0.2.0
flutter_mfa: ^0.2.0 copied to clipboard
Multi-factor authentication for Flutter: a TOTP/HOTP authenticator with hardware-backed storage (Keystore, Keychain, WebCrypto), biometric protection and encrypted backup.
flutter_mfa #
The Flutter plugin of flutter_mfa: MfaClient, a secure vault (Android Keystore, iOS Keychain, WebCrypto), biometric-bound protection, sensitive clipboard and encrypted backup.
Status: v0.2.0, the first public release. Until 1.0, a minor version may change the API; the CHANGELOG says what changed.
Built and maintained by BreaktroughF1 LLP in collaboration with ARDC, CHRIST (Deemed to be University). MIT licensed.
Security model and design: see the project docs.
Install #
flutter pub add flutter_mfa
# optional: widgets and the QR scanner
flutter pub add flutter_mfa_ui flutter_mfa_qr
Android needs minSdk 24, iOS needs 15.0 (plus NSFaceIDUsageDescription for biometric protection) and the web needs a secure context (HTTPS or localhost). See Platform notes.
Quick start #
import 'package:flutter_mfa/flutter_mfa.dart';
final client = await MfaClient.create(); // fails with a VaultException if secrets can't be stored safely
// Add an account from a scanned or pasted otpauth:// URI, or from a typed setup key.
final account = await client.enrollFromUri(otpauthUri);
await client.enrollManual(secretBase32: setupKey, accountName: 'alice@example.com', issuer: 'Example');
// Live codes: the current code, then every second, and once at each change.
final subscription = client.codeStream(account.id).listen(showCode); // cancel() when off screen
// Copy with the sensitive clipboard (cleared after 30 s by default).
await client.copyCode(account.id);
| Capability | API |
|---|---|
| Enrol (URI or setup key), list, rename, remove, wipe | enrollFromUri, enrollManual, accounts, watchAccounts, rename, remove, wipeAll |
| Codes | codeStream (TOTP, one shared timer per client), currentCode (HOTP advances and persists the counter first) |
| Sensitive clipboard | copyCode, copySensitive |
| Screen-capture protection | setScreenCaptureProtection (Android FLAG_SECURE; iOS and the web cannot block screenshots) |
| Relying-party helper (demos and tests) | MfaClient.createEnrollment |
| Vault capabilities | capabilities (hardware backing, biometrics, secure context, persistent storage, supported protection levels) |
Secrets stay in the platform vault: Android Keystore (StrongBox when available), iOS Keychain (ThisDeviceOnly, excluded from backups) or, on the web, AES-256-GCM under a non-extractable WebCrypto key. Biometric-bound protection levels are available on Android and iOS (see below). Nothing is logged unless you pass an MfaLogger, and even then only event names and account ids.
All of flutter_mfa_core is re-exported, so one import is enough. Widgets are in flutter_mfa_ui and QR scanning is in flutter_mfa_qr.
Guides: Add an authenticator to your app · Enrol users (relying party) · Verify on a Dart server. A complete app is in example/.
Device integrity #
MfaClientOptions(integrityCheck: …, integrityPolicy: …) plugs in a root, jailbreak or tamper detector of your choice. MfaClient.create runs it once and gives the IntegrityReport to your policy. Return to continue, or throw to refuse to start. checkIntegrity() runs it again, for example when the app resumes. The default NoopIntegrityCheck reports unknown. This is detection, not prevention (MASVS resilience): on a device the attacker controls, any in-app check can be bypassed, so use it to warn users or raise the bar, never as your only protection.
Platform notes #
- Android biometric protection (
biometric,hardwareBound) needs Android 11 (API 30) or later, a strong (Class 3) biometric enrolled, and secure key hardware;capabilities.supportedLevelstells you whether this device has them. Any hostActivityworks (noFlutterFragmentActivityneeded); the plugin declaresUSE_BIOMETRIC. Each protected account has its own Keystore key that unlocks only after a fingerprint or face check, for every code. Saving an account never prompts. Enrolling a new fingerprint or face invalidates these keys: reading then throwsVaultKeyInvalidated, and the user must restore from an encrypted backup or enrol the account again.hardwareBounduses StrongBox when the device has it. - iOS biometric protection.
biometricstores the account in a Keychain item that only Face ID or Touch ID can release (biometryCurrentSet, no passcode fallback).hardwareBoundalso wraps it with a Secure Enclave key; the simulator has no Secure Enclave. AddNSFaceIDUsageDescriptiontoios/Runner/Info.plist, or Face ID cannot be used. Adding or removing a face or fingerprint invalidates protected accounts (VaultKeyInvalidated): restore them from an encrypted backup or enrol them again. A passcode must be set on the device. - Uninstall removes accounts on every platform. Android deletes app data on uninstall. iOS keeps Keychain items, so the plugin wipes anything left from an earlier installation before its first vault operation (ADR-0012). Users who reinstall need an encrypted backup to restore their accounts.
Web: security limits #
On the web the vault is best-effort. Prefer the mobile apps for high-value accounts.
- Secrets are encrypted with AES-256-GCM under a non-extractable WebCrypto key. Both the key and the ciphertext live in IndexedDB (
flutter_mfa_v1). Page script cannot export the key. The browser still stores the key material in the profile's files, so anyone who can read the browser profile on disk (malware, a stolen unencrypted laptop) can recover the secrets. The web vault has no hardware binding. - Script running in your origin (XSS, a compromised dependency, a malicious extension with page access) can still ask the browser to decrypt, so it can read codes. This cannot be fixed inside a web page. Serve the app with a strict Content-Security-Policy and Trusted Types, and keep third-party script off the page.
- The vault works only in a secure context (HTTPS or
localhost). Anywhere else, every call throwsVaultUnavailable(insecureContext). - Only
ProtectionLevel.standardexists on the web. Requests forbiometricorhardwareBoundthrowVaultHardwareUnavailable, and the account is never stored at a weaker level. - Browsers may evict site data. The vault asks for persistent storage and reports the browser's answer in
capabilities.persistentStorage. When it isfalse, prompt users to keep an encrypted backup. Clearing site data deletes all accounts. - Copied codes are cleared after the expiry only when the page can already read the clipboard without a permission prompt. Otherwise the code stays on the clipboard until something else replaces it.