flutter_mfa_core
Pure Dart core of flutter_mfa:
- TOTP/HOTP generation per RFC 6238 / RFC 4226, verified against every RFC test vector.
- Server-side verification with replay protection and attempt limiting.
otpauth://URI parsing and generation, with compatibility warnings.- Base32 and the encrypted backup format.
It has no Flutter dependency, so the same audited code runs in your Dart backend (shelf, dart_frog, Serverpod) and in Flutter apps. It runs on the VM, and compiled to JS and WASM.
Status: v0.2.0, the first public release. Until 1.0, a minor version may change the API; the CHANGELOG says what changed.
Install
dart pub add flutter_mfa_core
Who verifies?
Codes are generated on the user's device and verified by your server. A code checked on the same device that holds the secret is not a second factor, and client-side rate limits can be reset by whoever holds the device. Use TotpVerifier / HotpVerifier on your server; on-device verification is only for local step-up.
Enrol a user (relying party)
import 'package:flutter_mfa_core/flutter_mfa_core.dart';
final secret = OtpSecret.generate(); // 20 random bytes, Random.secure()
final uri = OtpAuthUri(
type: OtpType.totp,
secret: secret,
accountName: 'alice@example.com',
issuer: 'Example',
);
showQrCode(uri.toUri().toString()); // contains the secret: never log it
// Store `secret.bytes` encrypted at rest, then confirm the first code with the verifier.
Verify a code on your server
final verifier = TotpVerifier(
totp: Totp(OtpSecret.fromBytes(user.secretBytes)),
limiter: AttemptLimiter(store: myRedisAttemptStore), // shared storage across instances
);
try {
final result = await verifier.verify(code, subject: user.id, lastUsedStep: user.lastStep);
switch (result) {
case Accepted(:final nextState):
await users.saveLastStep(user.id, nextState); // persist BEFORE reporting success
case Rejected(:final reason): // invalid, replayed or malformed
reject(reason);
}
} on VerificationRateLimited catch (e) {
tooManyAttempts(e.retryAfter, lockedOut: e.lockedOut);
}
A complete, runnable version is in example/main.dart. The Verify on a Dart server guide covers storage, shared attempt limits and a checklist.
What the verifier guarantees
- Throttling first. The limiter is consulted before any code is computed. Check → compute → record is atomic per subject in-process, so a burst of parallel guesses can't exceed the limit.
- Replay protection. A code for a step ≤
lastUsedStepis rejected asreplayed(RFC 6238 §5.2), but only if you persistnextState. - Constant-time comparison. Every candidate in the drift window is compared, with no early exit.
- Strict input handling. Spaces are stripped; anything other than exactly 6–8 ASCII digits is
malformedand still counts as an attempt.
Limits (LimiterPolicy)
Defaults:
- at most 5 failures per 5 minutes;
- exponential back-off from 1 s, capped at 15 min;
- hard lockout after 20 consecutive failures (
VerificationRateLimited.lockedOut). Waiting does not lift it; clear the subject in your store to unlock.
Implement AttemptStore on shared storage (Redis, SQL). AttemptState.toJson() / fromJson() give you a versioned format.
Interoperability notes
- Defaults: SHA-1, 6 digits and 30 s work with every authenticator app. Other values produce
compatibilityWarnings, because many apps silently ignore them. - Secret length: secrets imported from existing issuers may be 10–15 bytes (80-bit). They are accepted, but flagged by
OtpSecret.meetsRfcMinimumandCompatibilityWarning.shortSecret. Generated secrets are always at least 16 bytes (RFC 4226 R6). - Counter ceiling: HOTP counters are limited to 2^53 − 1 on every platform, the JavaScript exact-integer limit.
Security notes
OtpSecretkeeps a private copy, returns copies, has a redactedtoString, and compares in constant time. Dart's garbage collector means memory cannot be reliably wiped.- No exception or
toStringin this package ever contains a secret, a code or a URI. - Report vulnerabilities privately: see SECURITY.md.
Third-party data
PassphrasePolicy rejects the 10 000 most common passwords from zxcvbn 4.4.2 (MIT, Copyright (c) 2012-2016 Dan Wheeler and Dropbox, Inc.). The list and its licence notice are in lib/src/backup/common_passwords.g.dart, generated by tool/gen_common_passwords.dart.
Testing helpers
package:flutter_mfa_core/testing.dart provides FakeClock, SeededRandom (insecure, tests only) and the RFC 4226/6238 vectors.
Built and maintained by BreaktroughF1 LLP in collaboration with ARDC, CHRIST (Deemed to be University). MIT licensed.
Libraries
- flutter_mfa_core
- Pure Dart core of flutter_mfa.
- testing
- Test helpers for flutter_mfa_core: a controllable clock, a seeded (insecure) random generator and the frozen RFC 4226 / RFC 6238 vectors.