import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'secure_store.dart'; /// Stores a salted PBKDF2-HMAC-SHA256 hash of the four-digit PIN in /// Keystore-backed secure storage. The PIN itself is never persisted. class PinService { PinService(this._store, {this.iterations = 12000}); final SecureStore _store; final int iterations; static const _saltKey = 'cardash.pin.salt'; static const _hashKey = 'cardash.pin.hash'; static const _iterationsKey = 'cardash.pin.iterations'; static final RegExp _validPin = RegExp(r'^\d{4}$'); Future isPinSet() async { final hash = await _store.read(_hashKey); return hash != null && hash.isNotEmpty; } Future setPin(String pin) async { if (!_validPin.hasMatch(pin)) { throw ArgumentError('The PIN must be exactly four digits.'); } final salt = _randomBytes(16); final derived = _derive(pin, salt, iterations); await _store.write(_saltKey, base64Encode(salt)); await _store.write(_hashKey, base64Encode(derived)); await _store.write(_iterationsKey, '$iterations'); } Future verify(String pin) async { if (!_validPin.hasMatch(pin)) return false; final saltRaw = await _store.read(_saltKey); final hashRaw = await _store.read(_hashKey); final iterationsRaw = await _store.read(_iterationsKey); if (saltRaw == null || hashRaw == null) return false; final salt = base64Decode(saltRaw); final expected = base64Decode(hashRaw); final rounds = int.tryParse(iterationsRaw ?? '') ?? iterations; final actual = _derive(pin, salt, rounds); return _constantTimeEquals(expected, actual); } Future clearPin() async { await _store.delete(_saltKey); await _store.delete(_hashKey); await _store.delete(_iterationsKey); } Uint8List _derive(String pin, List salt, int rounds) { final hmac = Hmac(sha256, utf8.encode(pin)); var block = Uint8List.fromList([...salt, 0, 0, 0, 1]); var u = Uint8List.fromList(hmac.convert(block).bytes); final result = Uint8List.fromList(u); for (var round = 1; round < rounds; round++) { u = Uint8List.fromList(hmac.convert(u).bytes); for (var i = 0; i < result.length; i++) { result[i] ^= u[i]; } } return result; } Uint8List _randomBytes(int length) { final random = Random.secure(); return Uint8List.fromList( List.generate(length, (_) => random.nextInt(256)), ); } bool _constantTimeEquals(List a, List b) { if (a.length != b.length) return false; var difference = 0; for (var i = 0; i < a.length; i++) { difference |= a[i] ^ b[i]; } return difference == 0; } }