44 lines
1.9 KiB
Dart
44 lines
1.9 KiB
Dart
import 'dart:typed_data';
|
|
import 'package:pointycastle/export.dart';
|
|
|
|
/// AES-256-CTR symmetric cipher wrapper.
|
|
///
|
|
/// AES-CTR is a stream cipher: encryption and decryption use the same
|
|
/// operation (XOR with key-stream), and there is no padding. The output
|
|
/// length always equals the input length, making it ideal for our fixed-size
|
|
/// 11-byte voice payload.
|
|
///
|
|
/// Security notes:
|
|
/// • Never reuse the same (key, nonce) pair.
|
|
/// • Nonce uniqueness is guaranteed by including the packet sequence
|
|
/// number in the nonce (see [KeyManager.buildNonce]).
|
|
/// • The key is zeroed from memory when the session ends.
|
|
abstract final class AesCipher {
|
|
// ── Core cipher op (encrypt = decrypt for CTR mode) ───────────────
|
|
|
|
/// Encrypt or decrypt [data] with [key] (32 bytes) and [nonce] (16 bytes).
|
|
///
|
|
/// AES-256-CTR: keystream = AES_k(nonce ∥ counter), output = data ⊕ keystream.
|
|
/// [forEncryption] = true → encrypt; false → decrypt (same operation in CTR).
|
|
static Uint8List _ctr(Uint8List data, Uint8List key, Uint8List nonce,
|
|
{required bool forEncryption}) {
|
|
assert(key.length == 32, 'AES-256 requires a 32-byte key');
|
|
assert(nonce.length == 16, 'CTR nonce must be 16 bytes (one AES block)');
|
|
|
|
final cipher = StreamCipher('AES/CTR')
|
|
..init(forEncryption, ParametersWithIV(KeyParameter(key), nonce));
|
|
|
|
final out = Uint8List(data.length);
|
|
cipher.processBytes(data, 0, data.length, out, 0);
|
|
return out;
|
|
}
|
|
|
|
/// Encrypt [plaintext] → ciphertext (same length as input).
|
|
static Uint8List encrypt(Uint8List plaintext, Uint8List key, Uint8List nonce) =>
|
|
_ctr(plaintext, key, nonce, forEncryption: true);
|
|
|
|
/// Decrypt [ciphertext] → plaintext (same length as input).
|
|
static Uint8List decrypt(Uint8List ciphertext, Uint8List key, Uint8List nonce) =>
|
|
_ctr(ciphertext, key, nonce, forEncryption: false);
|
|
}
|