159 lines
4.3 KiB
Dart
159 lines
4.3 KiB
Dart
import 'dart:convert';
|
|
import 'dart:math';
|
|
|
|
import 'package:cryptography/cryptography.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
|
|
class AppLockService extends ChangeNotifier {
|
|
AppLockService._();
|
|
|
|
static final AppLockService instance = AppLockService._();
|
|
|
|
static const _enabledKey = 'app_lock_enabled_v1';
|
|
static const _saltKey = 'app_lock_salt_v1';
|
|
static const _hashKey = 'app_lock_hash_v1';
|
|
static const _pbkdf2Iterations = 120000;
|
|
|
|
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
|
final Pbkdf2 _kdf = Pbkdf2(
|
|
macAlgorithm: Hmac.sha256(),
|
|
iterations: _pbkdf2Iterations,
|
|
bits: 256,
|
|
);
|
|
|
|
bool _initialized = false;
|
|
bool _enabled = false;
|
|
bool _locked = false;
|
|
|
|
bool get initialized => _initialized;
|
|
bool get isEnabled => _enabled;
|
|
bool get isLocked => _enabled && _locked;
|
|
|
|
Future<void> init() async {
|
|
if (_initialized) return;
|
|
await _reloadFromStorage(lockIfEnabled: true);
|
|
}
|
|
|
|
Future<void> refresh() async {
|
|
await _reloadFromStorage(lockIfEnabled: false);
|
|
}
|
|
|
|
Future<void> setPasscode(String passcode) async {
|
|
final normalized = _normalizePasscode(passcode);
|
|
final salt = _generateSalt();
|
|
final hash = await _derivePasscodeHash(normalized, salt);
|
|
|
|
await _storage.write(key: _saltKey, value: salt);
|
|
await _storage.write(key: _hashKey, value: hash);
|
|
await _storage.write(key: _enabledKey, value: '1');
|
|
|
|
_enabled = true;
|
|
_locked = false;
|
|
_initialized = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<bool> verifyPasscode(String passcode) async {
|
|
final isValid = await _matchesStoredPasscode(passcode);
|
|
if (!isValid) return false;
|
|
|
|
_enabled = true;
|
|
_locked = false;
|
|
_initialized = true;
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
|
|
Future<bool> changePasscode({
|
|
required String currentPasscode,
|
|
required String newPasscode,
|
|
}) async {
|
|
final isCurrentValid = await _matchesStoredPasscode(currentPasscode);
|
|
if (!isCurrentValid) return false;
|
|
|
|
await setPasscode(newPasscode);
|
|
return true;
|
|
}
|
|
|
|
Future<bool> disablePasscode(String currentPasscode) async {
|
|
final isCurrentValid = await _matchesStoredPasscode(currentPasscode);
|
|
if (!isCurrentValid) return false;
|
|
|
|
await clear();
|
|
return true;
|
|
}
|
|
|
|
Future<void> clear() async {
|
|
await _storage.delete(key: _enabledKey);
|
|
await _storage.delete(key: _saltKey);
|
|
await _storage.delete(key: _hashKey);
|
|
|
|
_enabled = false;
|
|
_locked = false;
|
|
_initialized = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
void lock() {
|
|
if (!_enabled || _locked) return;
|
|
_locked = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> _reloadFromStorage({required bool lockIfEnabled}) async {
|
|
try {
|
|
final enabledValue = await _storage.read(key: _enabledKey);
|
|
_enabled = enabledValue == '1';
|
|
_locked = _enabled && lockIfEnabled ? true : (_enabled && _locked);
|
|
} catch (e) {
|
|
debugPrint('[APP_LOCK] Failed to load state: $e');
|
|
_enabled = false;
|
|
_locked = false;
|
|
}
|
|
|
|
if (!_enabled) {
|
|
_locked = false;
|
|
}
|
|
|
|
_initialized = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<bool> _matchesStoredPasscode(String passcode) async {
|
|
try {
|
|
final normalized = _normalizePasscode(passcode);
|
|
final salt = await _storage.read(key: _saltKey);
|
|
final storedHash = await _storage.read(key: _hashKey);
|
|
final enabledValue = await _storage.read(key: _enabledKey);
|
|
|
|
if (enabledValue != '1' || salt == null || storedHash == null) {
|
|
return false;
|
|
}
|
|
|
|
final computedHash = await _derivePasscodeHash(normalized, salt);
|
|
return computedHash == storedHash;
|
|
} catch (e) {
|
|
debugPrint('[APP_LOCK] Failed to verify passcode: $e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<String> _derivePasscodeHash(String passcode, String salt) async {
|
|
final secretKey = await _kdf.deriveKey(
|
|
secretKey: SecretKey(utf8.encode(passcode)),
|
|
nonce: utf8.encode(salt),
|
|
);
|
|
final bytes = await secretKey.extractBytes();
|
|
return base64UrlEncode(bytes);
|
|
}
|
|
|
|
String _generateSalt() {
|
|
final random = Random.secure();
|
|
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
|
|
return base64UrlEncode(bytes);
|
|
}
|
|
|
|
String _normalizePasscode(String input) => input.trim();
|
|
}
|