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); }