Initial upload: fin65-mobile
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
|
||||
import 'offline_store.dart';
|
||||
|
||||
class SecurityService {
|
||||
SecurityService({
|
||||
OfflineStore? store,
|
||||
LocalAuthentication? localAuth,
|
||||
}) : _store = store ?? OfflineStore.instance,
|
||||
_localAuth = localAuth ?? LocalAuthentication();
|
||||
|
||||
final OfflineStore _store;
|
||||
final LocalAuthentication _localAuth;
|
||||
|
||||
static const String _pinEnabledKey = 'security_pin_enabled';
|
||||
static const String _pinHashKey = 'security_pin_hash';
|
||||
static const String _biometricEnabledKey = 'security_biometric_enabled';
|
||||
static const String _salt = 'FIN65_LOCAL_PIN_V1';
|
||||
|
||||
Future<bool> isPinEnabled() async {
|
||||
final enabled = await _store.getSetting(_pinEnabledKey);
|
||||
final hash = await _store.getSetting(_pinHashKey);
|
||||
return enabled == 'true' && hash != null && hash.isNotEmpty;
|
||||
}
|
||||
|
||||
Future<void> setPin(String pin) async {
|
||||
_validatePin(pin);
|
||||
await _store.setSetting(_pinHashKey, _hashPin(pin));
|
||||
await _store.setSetting(_pinEnabledKey, 'true');
|
||||
}
|
||||
|
||||
Future<bool> verifyPin(String pin) async {
|
||||
if (!_isValidPinShape(pin)) return false;
|
||||
final storedHash = await _store.getSetting(_pinHashKey);
|
||||
if (storedHash == null || storedHash.isEmpty) return false;
|
||||
return storedHash == _hashPin(pin);
|
||||
}
|
||||
|
||||
Future<void> disablePin() async {
|
||||
await _store.setSetting(_pinHashKey, '');
|
||||
await _store.setSetting(_pinEnabledKey, 'false');
|
||||
await setBiometricEnabled(false);
|
||||
}
|
||||
|
||||
Future<bool> isBiometricAvailable() async {
|
||||
try {
|
||||
final supported = await _localAuth.isDeviceSupported();
|
||||
final canCheck = await _localAuth.canCheckBiometrics;
|
||||
final biometrics = await _localAuth.getAvailableBiometrics();
|
||||
return supported && canCheck && biometrics.isNotEmpty;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<BiometricType>> getAvailableBiometrics() async {
|
||||
try {
|
||||
return await _localAuth.getAvailableBiometrics();
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> isBiometricEnabled() async {
|
||||
final enabled = await _store.getSetting(_biometricEnabledKey);
|
||||
final pinEnabled = await isPinEnabled();
|
||||
return pinEnabled && enabled == 'true';
|
||||
}
|
||||
|
||||
Future<void> setBiometricEnabled(bool enabled) async {
|
||||
if (enabled) {
|
||||
final pinEnabled = await isPinEnabled();
|
||||
if (!pinEnabled) {
|
||||
throw Exception('Active d’abord un PIN avant la biométrie.');
|
||||
}
|
||||
|
||||
final available = await isBiometricAvailable();
|
||||
if (!available) {
|
||||
throw Exception('Aucune empreinte/biométrie disponible sur cet appareil.');
|
||||
}
|
||||
}
|
||||
|
||||
await _store.setSetting(_biometricEnabledKey, enabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
Future<bool> authenticateWithBiometrics() async {
|
||||
final enabled = await isBiometricEnabled();
|
||||
if (!enabled) return false;
|
||||
|
||||
try {
|
||||
return await _localAuth.authenticate(
|
||||
localizedReason: 'Déverrouiller FIN65',
|
||||
biometricOnly: true,
|
||||
);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
String _hashPin(String pin) {
|
||||
return sha256.convert(utf8.encode('$_salt::$pin')).toString();
|
||||
}
|
||||
|
||||
void _validatePin(String pin) {
|
||||
if (!_isValidPinShape(pin)) {
|
||||
throw Exception('Le PIN doit contenir 4 Ã 8 chiffres.');
|
||||
}
|
||||
}
|
||||
|
||||
bool _isValidPinShape(String pin) {
|
||||
return RegExp(r'^\d{4,8}$').hasMatch(pin);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user