215 lines
7.0 KiB
Dart
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();
|
|
}
|
|
} |