import 'dart:convert'; import 'dart:math'; import 'package:cryptography/cryptography.dart'; import '../../application/application.dart'; import '../../domain/domain.dart'; final class Pbkdf2PasswordHasher implements PasswordHasher { Pbkdf2PasswordHasher({ this.iterations = 210000, this.saltLength = 16, this.bits = 256, Random? random, }) : _random = random ?? Random.secure(); final int iterations; final int saltLength; final int bits; final Random _random; @override Future hash(String password) async { final salt = List.generate(saltLength, (_) => _random.nextInt(256)); final hash = await _derive(password, salt, iterations, bits); return [ 'pbkdf2_sha256', iterations.toString(), base64UrlEncode(salt), base64UrlEncode(hash), ].join(r'$'); } @override Future verify({ required String password, required String passwordHash, }) async { final parts = passwordHash.split(r'$'); if (parts.length != 4 || parts[0] != 'pbkdf2_sha256') { throw const ValidationException('Unsupported password hash format.'); } final parsedIterations = int.tryParse(parts[1]); if (parsedIterations == null || parsedIterations <= 0) { throw const ValidationException('Invalid password hash iterations.'); } final salt = base64Url.decode(parts[2]); final expectedHash = base64Url.decode(parts[3]); final actualHash = await _derive( password, salt, parsedIterations, expectedHash.length * 8, ); return _constantTimeEquals(actualHash, expectedHash); } Future> _derive( String password, List salt, int iterations, int bits, ) async { final algorithm = Pbkdf2( macAlgorithm: Hmac.sha256(), iterations: iterations, bits: bits, ); final key = await algorithm.deriveKey( secretKey: SecretKey(utf8.encode(password)), nonce: salt, ); return key.extractBytes(); } } bool _constantTimeEquals(List left, List right) { if (left.length != right.length) { return false; } var diff = 0; for (var index = 0; index < left.length; index++) { diff |= left[index] ^ right[index]; } return diff == 0; }