Initial commit
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user