Files
notas/lib/data/local_vault_service.dart
T
2026-05-17 15:45:52 +02:00

215 lines
7.0 KiB
Dart

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<String?> 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:
'Toca el sensor de huellas digitales',
biometricOnly: true,
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?> 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(
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: 'Toca el sensor de huellas digitales',
biometricOnly: true,
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);
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: 'Toca el sensor de huellas digitales',
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<int> bytes = List<int>.generate(32, (_) => random.nextInt(256));
return bytes
.map((int byte) => byte.toRadixString(16).padLeft(2, '0'))
.join();
}
}