Files
DriverVault/Phone App/lib/biometric.dart
T
2026-07-06 08:50:52 +02:00

115 lines
4.0 KiB
Dart

import "package:flutter/services.dart";
import "package:flutter_secure_storage/flutter_secure_storage.dart";
import "package:local_auth/local_auth.dart";
import "package:local_auth_android/local_auth_android.dart";
/// Biometric ("Face recognition" / "Fingerprint") sign-in.
///
/// The app already persists a JWT in SharedPreferences, so a valid session
/// auto-restores on boot and the login screen is only shown after logout or
/// token expiry. Biometric login covers that case: the user's credentials are
/// kept in Android Keystore-backed secure storage and released only after the
/// system BiometricPrompt succeeds, then replayed against the normal login API
/// (so each biometric sign-in mints a fresh session/token).
class BiometricAuth {
final LocalAuthentication _auth = LocalAuthentication();
final FlutterSecureStorage _store = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
static const _emailKey = "cc_bio_email";
static const _passwordKey = "cc_bio_password";
/// Synchronous mirror of [isEnabled], kept fresh by the async calls below so
/// the app-lifecycle handler (which can't await) can decide whether to lock.
bool enabledCached = false;
/// Whether the device has usable biometric hardware with something enrolled.
Future<bool> isAvailable() async {
try {
if (!await _auth.isDeviceSupported()) return false;
return await _auth.canCheckBiometrics;
} on PlatformException {
return false;
}
}
/// Which biometric kinds are enrolled (used to label the sign-in buttons).
Future<List<BiometricType>> availableTypes() async {
try {
return await _auth.getAvailableBiometrics();
} on PlatformException {
return const [];
}
}
Future<bool> hasFace() async =>
(await availableTypes()).contains(BiometricType.face);
Future<bool> hasFingerprint() async =>
(await availableTypes()).contains(BiometricType.fingerprint);
/// The email biometric login was enabled for, or null if it's off.
Future<String?> enabledEmail() async {
final v = await _store.read(key: _emailKey);
enabledCached = v != null;
return v;
}
Future<bool> isEnabled() async => (await enabledEmail()) != null;
/// Remember credentials for biometric sign-in. Caller should only invoke this
/// after a successful password login so the stored creds are known-good.
Future<void> enable(String email, String password) async {
await _store.write(key: _emailKey, value: email);
await _store.write(key: _passwordKey, value: password);
enabledCached = true;
}
Future<void> disable() async {
await _store.delete(key: _emailKey);
await _store.delete(key: _passwordKey);
enabledCached = false;
}
/// Prompt for biometrics and, on success, return the stored (email, password).
/// Returns null if the user cancels or nothing is stored; throws
/// [BiometricException] with a friendly message on a hard error.
Future<(String, String)?> unlock({String? reason}) async {
bool ok;
try {
ok = await _auth.authenticate(
localizedReason: reason ?? "Sign in to Car Control",
options: const AuthenticationOptions(
biometricOnly: true,
stickyAuth: true,
),
authMessages: const [
AndroidAuthMessages(
signInTitle: "Car Control sign-in",
biometricHint: "",
cancelButton: "Use password",
),
],
);
} on PlatformException catch (e) {
throw BiometricException(e.message ?? "Biometric authentication failed.");
}
if (!ok) return null;
final email = await _store.read(key: _emailKey);
final password = await _store.read(key: _passwordKey);
if (email == null || password == null) return null;
return (email, password);
}
}
class BiometricException implements Exception {
final String message;
BiometricException(this.message);
@override
String toString() => message;
}
/// App-wide singleton (mirrors the other services in main.dart).
final biometricAuth = BiometricAuth();