169 lines
4.7 KiB
Dart
169 lines
4.7 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:kooltap/services/pin_lock_service.dart';
|
|
|
|
import 'helpers/memory_secure_storage.dart';
|
|
|
|
String _legacyHash(String pin, String salt) {
|
|
return sha256.convert(utf8.encode('$salt:$pin')).toString();
|
|
}
|
|
|
|
void main() {
|
|
late MemorySecureStorage storage;
|
|
late DateTime now;
|
|
late PinLockService service;
|
|
|
|
setUp(() {
|
|
storage = MemorySecureStorage();
|
|
now = DateTime(2026, 10, 7, 12);
|
|
service = PinLockService(storage: storage, clock: () => now);
|
|
});
|
|
|
|
Future<void> failTimes(int count) async {
|
|
for (var attempt = 0; attempt < count; attempt++) {
|
|
await service.verifyPin('0000');
|
|
}
|
|
}
|
|
|
|
group('PinLockService', () {
|
|
group('hasPin', () {
|
|
test('returns false when no hash stored', () async {
|
|
expect(await service.hasPin(), false);
|
|
});
|
|
|
|
test('returns false when hash is empty', () async {
|
|
storage.values['pin_hash'] = '';
|
|
|
|
expect(await service.hasPin(), false);
|
|
});
|
|
|
|
test('returns true once a pin is set', () async {
|
|
await service.setPin('1234');
|
|
|
|
expect(await service.hasPin(), true);
|
|
});
|
|
});
|
|
|
|
group('setPin', () {
|
|
test('stores a salted PBKDF2 hash', () async {
|
|
await service.setPin('1234');
|
|
|
|
expect(storage.values['pin_salt'], isNotEmpty);
|
|
expect(storage.values['pin_hash'], startsWith(r'pbkdf2-sha256$'));
|
|
expect(storage.values['pin_hash'], isNot(contains('1234')));
|
|
});
|
|
|
|
test('generates unique salts for different pins', () async {
|
|
await service.setPin('1234');
|
|
final firstSalt = storage.values['pin_salt'];
|
|
|
|
await service.setPin('5678');
|
|
|
|
expect(storage.values['pin_salt'], isNot(firstSalt));
|
|
});
|
|
});
|
|
|
|
group('verifyPin', () {
|
|
test('returns false when salt is missing', () async {
|
|
storage.values['pin_hash'] = 'somehash';
|
|
|
|
expect(await service.verifyPin('1234'), false);
|
|
});
|
|
|
|
test('returns false when hash is missing', () async {
|
|
storage.values['pin_salt'] = 'somesalt';
|
|
|
|
expect(await service.verifyPin('1234'), false);
|
|
});
|
|
|
|
test('returns true for correct pin', () async {
|
|
await service.setPin('1234');
|
|
|
|
expect(await service.verifyPin('1234'), true);
|
|
});
|
|
|
|
test('returns false for incorrect pin', () async {
|
|
await service.setPin('1234');
|
|
|
|
expect(await service.verifyPin('5678'), false);
|
|
});
|
|
|
|
test('accepts a legacy hash and upgrades it', () async {
|
|
storage.values['pin_salt'] = 'test_salt';
|
|
storage.values['pin_hash'] = _legacyHash('1234', 'test_salt');
|
|
|
|
expect(await service.verifyPin('1234'), true);
|
|
expect(storage.values['pin_hash'], startsWith(r'pbkdf2-sha256$'));
|
|
expect(await service.verifyPin('1234'), true);
|
|
});
|
|
|
|
test('rejects a wrong pin against a legacy hash', () async {
|
|
storage.values['pin_salt'] = 'test_salt';
|
|
storage.values['pin_hash'] = _legacyHash('1234', 'test_salt');
|
|
|
|
expect(await service.verifyPin('5678'), false);
|
|
expect(storage.values['pin_hash'], _legacyHash('1234', 'test_salt'));
|
|
});
|
|
});
|
|
|
|
group('lockout', () {
|
|
test('locks after five wrong attempts', () async {
|
|
await service.setPin('1234');
|
|
await failTimes(5);
|
|
|
|
await expectLater(
|
|
service.verifyPin('1234'),
|
|
throwsA(isA<PinLockedOutException>()),
|
|
);
|
|
});
|
|
|
|
test('unlocks once the lockout has passed', () async {
|
|
await service.setPin('1234');
|
|
await failTimes(5);
|
|
|
|
now = now.add(const Duration(seconds: 31));
|
|
|
|
expect(await service.verifyPin('1234'), true);
|
|
});
|
|
|
|
test('doubles the lockout for every further failure', () async {
|
|
await service.setPin('1234');
|
|
await failTimes(5);
|
|
now = now.add(const Duration(seconds: 31));
|
|
await failTimes(1);
|
|
|
|
now = now.add(const Duration(seconds: 31));
|
|
await expectLater(
|
|
service.verifyPin('1234'),
|
|
throwsA(isA<PinLockedOutException>()),
|
|
);
|
|
|
|
now = now.add(const Duration(seconds: 30));
|
|
expect(await service.verifyPin('1234'), true);
|
|
});
|
|
|
|
test('a correct pin resets the failure count', () async {
|
|
await service.setPin('1234');
|
|
await failTimes(4);
|
|
await service.verifyPin('1234');
|
|
await failTimes(4);
|
|
|
|
expect(await service.verifyPin('1234'), true);
|
|
});
|
|
});
|
|
|
|
group('clearPin', () {
|
|
test('deletes the pin and any lockout', () async {
|
|
await service.setPin('1234');
|
|
await failTimes(5);
|
|
|
|
await service.clearPin();
|
|
|
|
expect(storage.values, isEmpty);
|
|
});
|
|
});
|
|
});
|
|
}
|