| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889 |
- 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<bool> isPinSet() async {
- final hash = await _store.read(_hashKey);
- return hash != null && hash.isNotEmpty;
- }
- Future<void> 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<bool> 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<void> clearPin() async {
- await _store.delete(_saltKey);
- await _store.delete(_hashKey);
- await _store.delete(_iterationsKey);
- }
- Uint8List _derive(String pin, List<int> 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<int>.generate(length, (_) => random.nextInt(256)),
- );
- }
- bool _constantTimeEquals(List<int> a, List<int> 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;
- }
- }
|