300 lines
8.9 KiB
Dart
300 lines
8.9 KiB
Dart
import 'package:flutter/services.dart';
|
|
import 'package:local_auth/local_auth.dart';
|
|
import 'package:local_auth/error_codes.dart' as auth_error;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// Biometric authentication result
|
|
enum BiometricResult {
|
|
success,
|
|
failed,
|
|
cancelled,
|
|
notAvailable,
|
|
notEnrolled,
|
|
lockedOut,
|
|
permanentlyLockedOut,
|
|
error,
|
|
}
|
|
|
|
/// Lock requirement policy
|
|
enum LockPolicy {
|
|
always,
|
|
afterInactive, // After 5 min of inactivity
|
|
never,
|
|
}
|
|
|
|
/// Biometric authentication service with session state and preferences
|
|
class BiometricService {
|
|
static final BiometricService _instance = BiometricService._internal();
|
|
factory BiometricService() => _instance;
|
|
BiometricService._internal();
|
|
|
|
final LocalAuthentication _localAuth = LocalAuthentication();
|
|
|
|
// Session state
|
|
bool _isAuthenticated = false;
|
|
DateTime? _lastActivityTime;
|
|
|
|
// Constants
|
|
static const String _prefKeyEnabled = 'biometric_enabled';
|
|
static const String _prefKeyPolicy = 'biometric_lock_policy';
|
|
static const Duration _inactivityTimeout = Duration(minutes: 5);
|
|
|
|
/// Whether user is currently authenticated in this session
|
|
bool get isAuthenticated => _isAuthenticated;
|
|
|
|
/// Mark activity to track inactivity timeout
|
|
void recordActivity() {
|
|
_lastActivityTime = DateTime.now();
|
|
}
|
|
|
|
/// Check if device supports any biometrics
|
|
Future<bool> isDeviceSupported() async {
|
|
try {
|
|
return await _localAuth.isDeviceSupported();
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Check if biometrics can be used (hardware exists)
|
|
Future<bool> canCheckBiometrics() async {
|
|
try {
|
|
return await _localAuth.canCheckBiometrics;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Check if biometrics are available (supported + enrolled)
|
|
Future<bool> isBiometricsAvailable() async {
|
|
try {
|
|
final canCheck = await _localAuth.canCheckBiometrics;
|
|
final isSupported = await _localAuth.isDeviceSupported();
|
|
return canCheck && isSupported;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Get available biometric types on this device
|
|
Future<List<BiometricType>> getAvailableBiometrics() async {
|
|
try {
|
|
return await _localAuth.getAvailableBiometrics();
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// Get human-readable biometric type name
|
|
Future<String> getBiometricTypeName() async {
|
|
final types = await getAvailableBiometrics();
|
|
if (types.contains(BiometricType.face)) {
|
|
return 'Face ID';
|
|
} else if (types.contains(BiometricType.fingerprint)) {
|
|
return 'Fingerprint';
|
|
} else if (types.contains(BiometricType.iris)) {
|
|
return 'Iris';
|
|
} else if (types.contains(BiometricType.strong)) {
|
|
return 'Biometrics';
|
|
} else if (types.contains(BiometricType.weak)) {
|
|
return 'Biometrics';
|
|
}
|
|
return 'Biometrics';
|
|
}
|
|
|
|
/// Authenticate with biometrics, with fallback to device PIN/password
|
|
Future<BiometricResult> authenticate({
|
|
String reason = 'Please authenticate to access inou',
|
|
bool biometricOnly = false,
|
|
}) async {
|
|
// Check if already authenticated in session
|
|
if (_isAuthenticated && !_shouldRequireReauth()) {
|
|
return BiometricResult.success;
|
|
}
|
|
|
|
// Check availability
|
|
final available = await isBiometricsAvailable();
|
|
if (!available) {
|
|
final canCheck = await canCheckBiometrics();
|
|
if (!canCheck) {
|
|
return BiometricResult.notEnrolled;
|
|
}
|
|
return BiometricResult.notAvailable;
|
|
}
|
|
|
|
try {
|
|
final success = await _localAuth.authenticate(
|
|
localizedReason: reason,
|
|
options: AuthenticationOptions(
|
|
stickyAuth: true,
|
|
biometricOnly: biometricOnly,
|
|
useErrorDialogs: true,
|
|
sensitiveTransaction: true,
|
|
),
|
|
);
|
|
|
|
if (success) {
|
|
_isAuthenticated = true;
|
|
_lastActivityTime = DateTime.now();
|
|
return BiometricResult.success;
|
|
}
|
|
return BiometricResult.failed;
|
|
} on PlatformException catch (e) {
|
|
return _handlePlatformException(e);
|
|
} catch (e) {
|
|
return BiometricResult.error;
|
|
}
|
|
}
|
|
|
|
/// Handle platform-specific errors
|
|
BiometricResult _handlePlatformException(PlatformException e) {
|
|
switch (e.code) {
|
|
case auth_error.notAvailable:
|
|
return BiometricResult.notAvailable;
|
|
case auth_error.notEnrolled:
|
|
return BiometricResult.notEnrolled;
|
|
case auth_error.lockedOut:
|
|
return BiometricResult.lockedOut;
|
|
case auth_error.permanentlyLockedOut:
|
|
return BiometricResult.permanentlyLockedOut;
|
|
case auth_error.passcodeNotSet:
|
|
return BiometricResult.notEnrolled;
|
|
default:
|
|
// User cancelled or other error
|
|
if (e.message?.toLowerCase().contains('cancel') == true) {
|
|
return BiometricResult.cancelled;
|
|
}
|
|
return BiometricResult.error;
|
|
}
|
|
}
|
|
|
|
/// Check if re-authentication is needed based on policy
|
|
bool _shouldRequireReauth() {
|
|
final policy = _currentPolicyCache ?? LockPolicy.afterInactive;
|
|
|
|
switch (policy) {
|
|
case LockPolicy.always:
|
|
return true;
|
|
case LockPolicy.never:
|
|
return false;
|
|
case LockPolicy.afterInactive:
|
|
if (_lastActivityTime == null) return true;
|
|
return DateTime.now().difference(_lastActivityTime!) > _inactivityTimeout;
|
|
}
|
|
}
|
|
|
|
/// Check if authentication is required (for app resume scenarios)
|
|
Future<bool> isAuthenticationRequired() async {
|
|
final enabled = await isBiometricEnabled();
|
|
if (!enabled) return false;
|
|
|
|
final available = await isBiometricsAvailable();
|
|
if (!available) return false;
|
|
|
|
final policy = await getLockPolicy();
|
|
|
|
switch (policy) {
|
|
case LockPolicy.always:
|
|
return true;
|
|
case LockPolicy.never:
|
|
return false;
|
|
case LockPolicy.afterInactive:
|
|
if (!_isAuthenticated) return true;
|
|
if (_lastActivityTime == null) return true;
|
|
return DateTime.now().difference(_lastActivityTime!) > _inactivityTimeout;
|
|
}
|
|
}
|
|
|
|
/// Reset authentication state (for logout or app background)
|
|
void resetAuthState() {
|
|
_isAuthenticated = false;
|
|
_lastActivityTime = null;
|
|
}
|
|
|
|
/// Cancel any ongoing authentication
|
|
Future<void> cancelAuthentication() async {
|
|
try {
|
|
await _localAuth.stopAuthentication();
|
|
} catch (_) {}
|
|
}
|
|
|
|
// Preference management
|
|
LockPolicy? _currentPolicyCache;
|
|
|
|
/// Check if biometric authentication is enabled
|
|
Future<bool> isBiometricEnabled() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
return prefs.getBool(_prefKeyEnabled) ?? false;
|
|
}
|
|
|
|
/// Enable or disable biometric authentication
|
|
Future<void> setBiometricEnabled(bool enabled) async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setBool(_prefKeyEnabled, enabled);
|
|
|
|
if (!enabled) {
|
|
_isAuthenticated = true; // Don't require auth if disabled
|
|
}
|
|
}
|
|
|
|
/// Get current lock policy
|
|
Future<LockPolicy> getLockPolicy() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final value = prefs.getString(_prefKeyPolicy);
|
|
_currentPolicyCache = _policyFromString(value);
|
|
return _currentPolicyCache!;
|
|
}
|
|
|
|
/// Set lock policy
|
|
Future<void> setLockPolicy(LockPolicy policy) async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString(_prefKeyPolicy, _policyToString(policy));
|
|
_currentPolicyCache = policy;
|
|
}
|
|
|
|
String _policyToString(LockPolicy policy) {
|
|
switch (policy) {
|
|
case LockPolicy.always:
|
|
return 'always';
|
|
case LockPolicy.afterInactive:
|
|
return 'after_inactive';
|
|
case LockPolicy.never:
|
|
return 'never';
|
|
}
|
|
}
|
|
|
|
LockPolicy _policyFromString(String? value) {
|
|
switch (value) {
|
|
case 'always':
|
|
return LockPolicy.always;
|
|
case 'never':
|
|
return LockPolicy.never;
|
|
case 'after_inactive':
|
|
default:
|
|
return LockPolicy.afterInactive;
|
|
}
|
|
}
|
|
|
|
/// Get user-friendly error message for a result
|
|
String getErrorMessage(BiometricResult result) {
|
|
switch (result) {
|
|
case BiometricResult.success:
|
|
return 'Authentication successful';
|
|
case BiometricResult.failed:
|
|
return 'Authentication failed. Please try again.';
|
|
case BiometricResult.cancelled:
|
|
return 'Authentication was cancelled';
|
|
case BiometricResult.notAvailable:
|
|
return 'Biometric authentication is not available on this device';
|
|
case BiometricResult.notEnrolled:
|
|
return 'No biometrics enrolled. Please set up Face ID, Touch ID, or fingerprint in your device settings.';
|
|
case BiometricResult.lockedOut:
|
|
return 'Too many failed attempts. Please try again later or use your device PIN.';
|
|
case BiometricResult.permanentlyLockedOut:
|
|
return 'Biometrics are locked. Please unlock your device using PIN/password first.';
|
|
case BiometricResult.error:
|
|
return 'An error occurred. Please try again.';
|
|
}
|
|
}
|
|
}
|