feat: add biometric authentication support and related UI screens
- Updated AndroidManifest.xml to include permissions for biometric authentication. - Changed MainActivity to extend FlutterFragmentActivity for better compatibility. - Modified gradle.properties to optimize memory settings. - Enhanced app.dart to manage new app phases for biometric authentication. - Implemented LocalVaultService methods for handling biometric key protection. - Created BiometricChoiceScreen and BiometricGateScreen for user interaction. - Updated HomeScreen to handle vault invalidation scenarios. - Registered local_auth plugin for biometric functionality on macOS and Windows. - Updated pubspec.yaml and pubspec.lock to include local_auth dependency.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import 'dart:math';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
|
||||
class LocalVaultService {
|
||||
LocalVaultService._();
|
||||
@@ -9,26 +11,110 @@ class LocalVaultService {
|
||||
|
||||
static const String _encryptionKeyStorageKey =
|
||||
'notes_local_encryption_key_v1';
|
||||
static const String _vaultAccessCompletedKey =
|
||||
'notes_vault_access_completed_v1';
|
||||
static const String _biometricChoicePendingKey =
|
||||
'notes_vault_biometric_choice_pending_v1';
|
||||
static const String _biometricGateEnabledKey =
|
||||
'notes_vault_biometric_gate_enabled_v1';
|
||||
|
||||
final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
|
||||
final LocalAuthentication _localAuth = LocalAuthentication();
|
||||
String? _lastBiometricError;
|
||||
|
||||
String? _cachedEncryptionKey;
|
||||
/// Último error conocido al consultar/activar biometría. Útil para diagnóstico.
|
||||
String? getLastBiometricError() => _lastBiometricError;
|
||||
|
||||
Future<String?> readEncryptionKey() async {
|
||||
final String? cachedKey = _cachedEncryptionKey;
|
||||
if (cachedKey != null) {
|
||||
return cachedKey;
|
||||
}
|
||||
|
||||
final String? storedKey = await _secureStorage.read(
|
||||
key: _encryptionKeyStorageKey,
|
||||
);
|
||||
|
||||
_cachedEncryptionKey = storedKey;
|
||||
if (storedKey == null) return null;
|
||||
|
||||
// If biometric protection was enabled when the key was created, require
|
||||
// authentication before returning the key. We only enable biometric
|
||||
// protection on mobile (Android/iOS).
|
||||
if (await isBiometricGateEnabled()) {
|
||||
// Only attempt authentication on Android/iOS.
|
||||
if (!Platform.isAndroid && !Platform.isIOS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final bool supported = await _localAuth.isDeviceSupported();
|
||||
final bool canCheck = await _localAuth.canCheckBiometrics;
|
||||
|
||||
if (supported || canCheck) {
|
||||
final bool didAuthenticate = await _localAuth.authenticate(
|
||||
localizedReason:
|
||||
'Autentícate para acceder a la llave de encriptación',
|
||||
biometricOnly: false,
|
||||
sensitiveTransaction: true,
|
||||
persistAcrossBackgrounding: false,
|
||||
);
|
||||
|
||||
if (!didAuthenticate) return null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e, st) {
|
||||
_lastBiometricError = e.toString();
|
||||
// Also print stack for debugging when running locally.
|
||||
// ignore: avoid_print
|
||||
print('LocalVaultService.readEncryptionKey biometric error: $e\n$st');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return storedKey;
|
||||
}
|
||||
|
||||
Future<String> createEncryptionKey() async {
|
||||
Future<String?> readStoredEncryptionKeyRaw() {
|
||||
return _secureStorage.read(key: _encryptionKeyStorageKey);
|
||||
}
|
||||
|
||||
Future<bool> hasEncryptionKey() async {
|
||||
return (await readStoredEncryptionKeyRaw()) != null;
|
||||
}
|
||||
|
||||
Future<bool> isVaultAccessCompleted() async {
|
||||
return (await _secureStorage.read(key: _vaultAccessCompletedKey)) == '1';
|
||||
}
|
||||
|
||||
Future<void> setVaultAccessCompleted(bool value) async {
|
||||
if (value) {
|
||||
await _secureStorage.write(key: _vaultAccessCompletedKey, value: '1');
|
||||
} else {
|
||||
await _secureStorage.delete(key: _vaultAccessCompletedKey);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isBiometricChoicePending() async {
|
||||
return (await _secureStorage.read(key: _biometricChoicePendingKey)) == '1';
|
||||
}
|
||||
|
||||
Future<void> setBiometricChoicePending(bool value) async {
|
||||
if (value) {
|
||||
await _secureStorage.write(key: _biometricChoicePendingKey, value: '1');
|
||||
} else {
|
||||
await _secureStorage.delete(key: _biometricChoicePendingKey);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isBiometricGateEnabled() async {
|
||||
return (await _secureStorage.read(key: _biometricGateEnabledKey)) == '1';
|
||||
}
|
||||
|
||||
Future<void> setBiometricGateEnabled(bool value) async {
|
||||
if (value) {
|
||||
await _secureStorage.write(key: _biometricGateEnabledKey, value: '1');
|
||||
} else {
|
||||
await _secureStorage.delete(key: _biometricGateEnabledKey);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> createEncryptionKey({bool protectWithBiometrics = false}) async {
|
||||
final String encryptionKey = _generateEncryptionKey();
|
||||
|
||||
await _secureStorage.write(
|
||||
@@ -36,13 +122,86 @@ class LocalVaultService {
|
||||
value: encryptionKey,
|
||||
);
|
||||
|
||||
_cachedEncryptionKey = encryptionKey;
|
||||
// If requested, try to enable biometric protection. Only enable on mobile
|
||||
// platforms and only if the authentication succeeds.
|
||||
if (protectWithBiometrics && (Platform.isAndroid || Platform.isIOS)) {
|
||||
try {
|
||||
final bool supported = await _localAuth.isDeviceSupported();
|
||||
final bool canCheck = await _localAuth.canCheckBiometrics;
|
||||
|
||||
if (supported || canCheck) {
|
||||
final bool didAuthenticate = await _localAuth.authenticate(
|
||||
localizedReason: 'Configura biometría para proteger la llave',
|
||||
biometricOnly: false,
|
||||
sensitiveTransaction: true,
|
||||
persistAcrossBackgrounding: false,
|
||||
);
|
||||
|
||||
if (didAuthenticate) {
|
||||
await setBiometricGateEnabled(true);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_lastBiometricError = e.toString();
|
||||
// ignore: avoid_print
|
||||
print('LocalVaultService.createEncryptionKey biometric error: $e');
|
||||
// Ignore errors and leave biometric protection disabled.
|
||||
}
|
||||
}
|
||||
|
||||
return encryptionKey;
|
||||
}
|
||||
|
||||
Future<void> clearEncryptionKey() async {
|
||||
await _secureStorage.delete(key: _encryptionKeyStorageKey);
|
||||
_cachedEncryptionKey = null;
|
||||
await _secureStorage.delete(key: _vaultAccessCompletedKey);
|
||||
await _secureStorage.delete(key: _biometricChoicePendingKey);
|
||||
await _secureStorage.delete(key: _biometricGateEnabledKey);
|
||||
}
|
||||
|
||||
Future<bool> isBiometricAvailable() async {
|
||||
if (!Platform.isAndroid && !Platform.isIOS) return false;
|
||||
|
||||
try {
|
||||
final bool supported = await _localAuth.isDeviceSupported();
|
||||
final bool canCheck = await _localAuth.canCheckBiometrics;
|
||||
if (!supported && !canCheck) return false;
|
||||
|
||||
final List<BiometricType> types = await _localAuth.getAvailableBiometrics();
|
||||
_lastBiometricError = null;
|
||||
return types.isNotEmpty;
|
||||
} catch (e) {
|
||||
_lastBiometricError = e.toString();
|
||||
// ignore: avoid_print
|
||||
print('LocalVaultService.isBiometricAvailable error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> enableBiometricProtection() async {
|
||||
if (!await isBiometricAvailable()) return false;
|
||||
|
||||
try {
|
||||
// Prefer biometric-only authentication for activation to ensure the
|
||||
// user sets up biometric unlocking (no device credential fallback).
|
||||
final bool didAuthenticate = await _localAuth.authenticate(
|
||||
localizedReason: 'Autentícate para habilitar biometría',
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
persistAcrossBackgrounding: false,
|
||||
);
|
||||
|
||||
if (!didAuthenticate) return false;
|
||||
|
||||
await setBiometricGateEnabled(true);
|
||||
_lastBiometricError = null;
|
||||
return true;
|
||||
} catch (e) {
|
||||
_lastBiometricError = e.toString();
|
||||
// ignore: avoid_print
|
||||
print('LocalVaultService.enableBiometricProtection error: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String _generateEncryptionKey() {
|
||||
|
||||
Reference in New Issue
Block a user