120 lines
2.8 KiB
Dart
120 lines
2.8 KiB
Dart
import 'dart:convert';
|
|
import 'dart:math';
|
|
|
|
import 'package:cryptography/cryptography.dart';
|
|
|
|
class PasswordHelper {
|
|
static const int _saltLength = 16;
|
|
static const int _hashLength = 32;
|
|
|
|
// 19 MiB, laut aktueller cryptography-Doku ein sinnvoller Startwert.
|
|
static const int _memory = 19 * 1024;
|
|
static const int _iterations = 2;
|
|
static const int _parallelism = 1;
|
|
|
|
static final Argon2id _algorithm = Argon2id(
|
|
memory: _memory,
|
|
iterations: _iterations,
|
|
parallelism: _parallelism,
|
|
hashLength: _hashLength,
|
|
);
|
|
|
|
/// Erstellt einen neuen Passwort-Hash inklusive zufälligem Salt.
|
|
///
|
|
/// Rückgabeformat:
|
|
/// argon2id$memory$iterations$parallelism$saltBase64$hashBase64
|
|
static Future<String> hashPassword(String password) async {
|
|
final salt = _generateSalt(_saltLength);
|
|
|
|
final secretKey = await _algorithm.deriveKeyFromPassword(
|
|
password: password,
|
|
nonce: salt,
|
|
);
|
|
|
|
final hashBytes = await secretKey.extractBytes();
|
|
|
|
final saltBase64 = base64UrlEncode(salt);
|
|
final hashBase64 = base64UrlEncode(hashBytes);
|
|
|
|
return 'argon2id'
|
|
'\$$_memory'
|
|
'\$$_iterations'
|
|
'\$$_parallelism'
|
|
'\$$saltBase64'
|
|
'\$$hashBase64';
|
|
}
|
|
|
|
/// Prüft ein Klartext-Passwort gegen den gespeicherten Hash.
|
|
static Future<bool> verifyPassword(
|
|
String password,
|
|
String storedHash,
|
|
) async {
|
|
try {
|
|
final parts = storedHash.split('\$');
|
|
|
|
if (parts.length != 6) {
|
|
return false;
|
|
}
|
|
|
|
if (parts[0] != 'argon2id') {
|
|
return false;
|
|
}
|
|
|
|
final memory = int.parse(parts[1]);
|
|
final iterations = int.parse(parts[2]);
|
|
final parallelism = int.parse(parts[3]);
|
|
|
|
final salt = base64Url.decode(parts[4]);
|
|
final expectedHash = base64Url.decode(parts[5]);
|
|
|
|
final algorithm = Argon2id(
|
|
memory: memory,
|
|
iterations: iterations,
|
|
parallelism: parallelism,
|
|
hashLength: expectedHash.length,
|
|
);
|
|
|
|
final secretKey = await algorithm.deriveKeyFromPassword(
|
|
password: password,
|
|
nonce: salt,
|
|
);
|
|
|
|
final calculatedHash = await secretKey.extractBytes();
|
|
|
|
return _constantTimeEquals(
|
|
calculatedHash,
|
|
expectedHash,
|
|
);
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Erzeugt kryptographisch zufälligen Salt.
|
|
static List<int> _generateSalt(int length) {
|
|
final random = Random.secure();
|
|
|
|
return List<int>.generate(
|
|
length,
|
|
(_) => random.nextInt(256),
|
|
);
|
|
}
|
|
|
|
/// Vergleich ohne vorzeitigen Abbruch.
|
|
static 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;
|
|
}
|
|
} |