""" AES-256-CBC engine for I2P. Ported from: net.i2p.crypto.CryptixAESEngine (CBC encrypt/decrypt) net.i2p.crypto.AESEngine (base class) Wraps Python's ``cryptography`` library using AES-CBC with no padding. All data lengths must be multiples of 16 bytes. """ from __future__ import annotations from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes # AES block size in bytes _BLOCK_SIZE = 16 class AESEngine: """AES-256-CBC engine with no automatic padding. This mirrors I2P's ``CryptixAESEngine``: CBC mode, 16-byte IV, data length must be a multiple of 16. Single-block ECB helpers (``encrypt_block`` / ``decrypt_block``) are also provided for callers that implement their own chaining. All keys must be 16, 24, or 32 bytes (128/192/256-bit AES). The I2P network uses 32-byte (256-bit) session keys. """ # ------------------------------------------------------------------ # CBC bulk operations # ------------------------------------------------------------------ @staticmethod def encrypt(payload: bytes, key: bytes, iv: bytes) -> bytes: """Encrypt *payload* with AES-CBC using *key* and *iv*. Parameters ---------- payload: Plaintext whose length **must** be a positive multiple of 16. key: AES key (16, 24, or 32 bytes). iv: Initialisation vector, exactly 16 bytes. Returns ------- bytes Ciphertext of the same length as *payload*. Raises ------ ValueError If *payload* length is not a positive multiple of 16, or *iv* is not 16 bytes. """ AESEngine._validate(payload, key, iv) cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) encryptor = cipher.encryptor() return encryptor.update(payload) + encryptor.finalize() @staticmethod def decrypt(payload: bytes, key: bytes, iv: bytes) -> bytes: """Decrypt *payload* with AES-CBC using *key* and *iv*. Parameters ---------- payload: Ciphertext whose length **must** be a positive multiple of 16. key: AES key (16, 24, or 32 bytes). iv: Initialisation vector, exactly 16 bytes. Returns ------- bytes Plaintext of the same length as *payload*. Raises ------ ValueError If *payload* length is not a positive multiple of 16, or *iv* is not 16 bytes. """ AESEngine._validate(payload, key, iv) cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) decryptor = cipher.decryptor() return decryptor.update(payload) + decryptor.finalize() # ------------------------------------------------------------------ # Single-block (ECB) operations — 16 bytes exactly # ------------------------------------------------------------------ @staticmethod def encrypt_block(block: bytes, key: bytes) -> bytes: """Encrypt a single 16-byte block (ECB mode, no IV). Parameters ---------- block: Exactly 16 bytes of plaintext. key: AES key (16, 24, or 32 bytes). Returns ------- bytes 16 bytes of ciphertext. """ if len(block) != _BLOCK_SIZE: raise ValueError( f"Block must be exactly {_BLOCK_SIZE} bytes, got {len(block)}" ) cipher = Cipher(algorithms.AES(key), modes.ECB()) encryptor = cipher.encryptor() return encryptor.update(block) + encryptor.finalize() @staticmethod def decrypt_block(block: bytes, key: bytes) -> bytes: """Decrypt a single 16-byte block (ECB mode, no IV). Parameters ---------- block: Exactly 16 bytes of ciphertext. key: AES key (16, 24, or 32 bytes). Returns ------- bytes 16 bytes of plaintext. """ if len(block) != _BLOCK_SIZE: raise ValueError( f"Block must be exactly {_BLOCK_SIZE} bytes, got {len(block)}" ) cipher = Cipher(algorithms.AES(key), modes.ECB()) decryptor = cipher.decryptor() return decryptor.update(block) + decryptor.finalize() # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ @staticmethod def _validate(payload: bytes, key: bytes, iv: bytes) -> None: """Common pre-condition checks for CBC operations.""" if len(iv) != _BLOCK_SIZE: raise ValueError( f"IV must be exactly {_BLOCK_SIZE} bytes, got {len(iv)}" ) if len(payload) == 0: raise ValueError("Payload must not be empty") if len(payload) % _BLOCK_SIZE != 0: raise ValueError( f"Payload length must be a multiple of {_BLOCK_SIZE}, " f"got {len(payload)}" )