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._(); static final LocalVaultService instance = 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; /// Último error conocido al consultar/activar biometría. Útil para diagnóstico. String? getLastBiometricError() => _lastBiometricError; Future readEncryptionKey() async { final String? storedKey = await _secureStorage.read( key: _encryptionKeyStorageKey, ); 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 readStoredEncryptionKeyRaw() { return _secureStorage.read(key: _encryptionKeyStorageKey); } Future hasEncryptionKey() async { return (await readStoredEncryptionKeyRaw()) != null; } Future isVaultAccessCompleted() async { return (await _secureStorage.read(key: _vaultAccessCompletedKey)) == '1'; } Future setVaultAccessCompleted(bool value) async { if (value) { await _secureStorage.write(key: _vaultAccessCompletedKey, value: '1'); } else { await _secureStorage.delete(key: _vaultAccessCompletedKey); } } Future isBiometricChoicePending() async { return (await _secureStorage.read(key: _biometricChoicePendingKey)) == '1'; } Future setBiometricChoicePending(bool value) async { if (value) { await _secureStorage.write(key: _biometricChoicePendingKey, value: '1'); } else { await _secureStorage.delete(key: _biometricChoicePendingKey); } } Future isBiometricGateEnabled() async { return (await _secureStorage.read(key: _biometricGateEnabledKey)) == '1'; } Future setBiometricGateEnabled(bool value) async { if (value) { await _secureStorage.write(key: _biometricGateEnabledKey, value: '1'); } else { await _secureStorage.delete(key: _biometricGateEnabledKey); } } Future createEncryptionKey({bool protectWithBiometrics = false}) async { final String encryptionKey = _generateEncryptionKey(); await _secureStorage.write( key: _encryptionKeyStorageKey, value: 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 clearEncryptionKey() async { await _secureStorage.delete(key: _encryptionKeyStorageKey); await _secureStorage.delete(key: _vaultAccessCompletedKey); await _secureStorage.delete(key: _biometricChoicePendingKey); await _secureStorage.delete(key: _biometricGateEnabledKey); } Future 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 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 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() { final Random random = Random.secure(); final List bytes = List.generate(32, (_) => random.nextInt(256)); return bytes .map((int byte) => byte.toRadixString(16).padLeft(2, '0')) .join(); } }