Neda/call/lib/core/crypto/key_manager.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);
}
}