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