122 lines
4.2 KiB
Dart
122 lines
4.2 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
import 'package:pointycastle/export.dart';
|
|
|
|
/// Derives and stores the 256-bit session key from a user-supplied passphrase.
|
|
///
|
|
/// Key derivation: PBKDF2-HMAC-SHA256 with a random salt.
|
|
/// The salt is kept in memory for the session; both peers must use the SAME
|
|
/// passphrase and DIFFERENT salts are handled by transmitting the salt
|
|
/// in the handshake packet before the session starts.
|
|
class KeyManager {
|
|
static const int _keyLen = 32; // AES-256
|
|
static const int _saltLen = 16;
|
|
static const int _iters = 100000;
|
|
|
|
Uint8List? _key;
|
|
Uint8List? _salt;
|
|
|
|
/// True once [deriveKey] has been called successfully.
|
|
bool get hasKey => _key != null;
|
|
|
|
/// The derived 32-byte key (throws if not yet derived).
|
|
Uint8List get key {
|
|
final k = _key;
|
|
if (k == null) throw StateError('Key not derived yet');
|
|
return k;
|
|
}
|
|
|
|
/// The 16-byte salt used during derivation (for sharing with the peer).
|
|
Uint8List get salt {
|
|
final s = _salt;
|
|
if (s == null) throw StateError('Salt not available');
|
|
return s;
|
|
}
|
|
|
|
// ── Derivation ─────────────────────────────────────────────────────
|
|
|
|
/// Derive a 256-bit key from [passphrase] using PBKDF2-HMAC-SHA256.
|
|
///
|
|
/// If [salt] is provided (received from the peer during handshake),
|
|
/// it is used directly. Otherwise a fresh random salt is generated.
|
|
///
|
|
/// Throws [ArgumentError] if the passphrase is empty.
|
|
void deriveKey(String passphrase, {Uint8List? salt}) {
|
|
if (passphrase.isEmpty) {
|
|
throw ArgumentError('Passphrase must not be empty');
|
|
}
|
|
|
|
// Use provided salt or generate one.
|
|
final usedSalt = salt ?? _generateSalt();
|
|
_salt = usedSalt;
|
|
|
|
final passwordBytes = Uint8List.fromList(utf8.encode(passphrase));
|
|
|
|
// PBKDF2-HMAC-SHA256 via PointyCastle.
|
|
final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64))
|
|
..init(Pbkdf2Parameters(usedSalt, _iters, _keyLen));
|
|
|
|
_key = pbkdf2.process(passwordBytes);
|
|
}
|
|
|
|
/// Load a raw 32-byte key directly (for testing or pre-shared key import).
|
|
void loadRawKey(Uint8List rawKey) {
|
|
if (rawKey.length != _keyLen) {
|
|
throw ArgumentError('Raw key must be $_keyLen bytes');
|
|
}
|
|
_key = Uint8List.fromList(rawKey);
|
|
_salt = Uint8List(_saltLen); // zero salt when using raw key
|
|
}
|
|
|
|
/// Clear the key from memory (call on session teardown).
|
|
void clear() {
|
|
if (_key != null) {
|
|
_key!.fillRange(0, _key!.length, 0);
|
|
_key = null;
|
|
}
|
|
_salt = null;
|
|
}
|
|
|
|
// ── Nonce construction ─────────────────────────────────────────────
|
|
|
|
/// Build a 16-byte AES-CTR nonce for a given packet sequence number.
|
|
///
|
|
/// Layout: [salt[0..7] ∥ sessionId[0..3] ∥ seqHigh ∥ seqLow ∥ 00 00 00 00]
|
|
/// (128-bit nonce; the counter part at the end starts at 0 per-block.)
|
|
///
|
|
/// Using the salt's first 8 bytes + session ID + seq ensures that
|
|
/// each packet has a unique nonce without transmitting the full nonce.
|
|
Uint8List buildNonce(Uint8List sessionId, int seq) {
|
|
final nonce = Uint8List(16);
|
|
// First 8 bytes from salt (or zeros if salt unavailable).
|
|
final s = _salt;
|
|
if (s != null) {
|
|
for (int i = 0; i < 8 && i < s.length; i++) { nonce[i] = s[i]; }
|
|
}
|
|
// Next 4 bytes: session ID.
|
|
for (int i = 0; i < 4 && i < sessionId.length; i++) {
|
|
nonce[8 + i] = sessionId[i];
|
|
}
|
|
// Last 4 bytes: packet sequence number (big-endian).
|
|
nonce[12] = (seq >> 24) & 0xFF;
|
|
nonce[13] = (seq >> 16) & 0xFF;
|
|
nonce[14] = (seq >> 8) & 0xFF;
|
|
nonce[15] = seq & 0xFF;
|
|
return nonce;
|
|
}
|
|
|
|
// ── Private helpers ────────────────────────────────────────────────
|
|
|
|
static Uint8List _generateSalt() {
|
|
final rng = FortunaRandom();
|
|
// Seed Fortuna with timestamp + hash-spread bytes.
|
|
final seed = Uint8List(32);
|
|
final ts = DateTime.now().microsecondsSinceEpoch;
|
|
for (int i = 0; i < 8; i++) {
|
|
seed[i] = (ts >> (i * 8)) & 0xFF;
|
|
}
|
|
rng.seed(KeyParameter(seed));
|
|
return rng.nextBytes(_saltLen);
|
|
}
|
|
}
|