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:
2026-05-17 13:48:09 +02:00
parent 2141009d36
commit 2160478fa7
13 changed files with 910 additions and 63 deletions
+169 -10
View File
@@ -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() {