From 5824df28cbaa3c2506c7be0fd1ce0afa75a98520 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 31 Dec 2025 21:38:30 +0000 Subject: [PATCH] Add aeg.cipher(alg) for loading modules by name in str. Added aeg.CIPHERS mapping of the nominal names to module names and Cipher for typing literals. --- src/aeg/__init__.py | 16 ++++++ src/aeg/_ciphers.py | 20 ++++++++ src/aeg/_typing.py | 122 ++++++++++++++++++++++++++++++++++++++++++++ tools/generate.py | 32 +++++++++++- 4 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 src/aeg/_ciphers.py create mode 100644 src/aeg/_typing.py diff --git a/src/aeg/__init__.py b/src/aeg/__init__.py index e69de29..4c918f7 100644 --- a/src/aeg/__init__.py +++ b/src/aeg/__init__.py @@ -0,0 +1,16 @@ +import importlib + +from ._ciphers import CIPHERS, CipherName +from ._typing import Cipher + +__all__ = ["cipher", "CIPHERS", "Cipher", "CipherName"] + + +def cipher(alg: CipherName) -> Cipher: + """Acquire a cipher module by name.""" + name = alg.lower().replace("-", "") + if name == "aegis128": + name = "aegis128l" # AEGIS-128 is dead, the user meant AEGIS-128L + if name in CIPHERS.values(): + return importlib.import_module(f".{name}", __package__) # type: ignore[return-value] + raise ValueError(f"Unknown algorithm {alg!r}. Valid options: {', '.join(CIPHERS)}") diff --git a/src/aeg/_ciphers.py b/src/aeg/_ciphers.py new file mode 100644 index 0000000..aac553d --- /dev/null +++ b/src/aeg/_ciphers.py @@ -0,0 +1,20 @@ +# This file is generated by tools/generate.py. Do not edit. +from typing import Literal + +CipherName = Literal[ + "AEGIS-128L", + "AEGIS-128X2", + "AEGIS-128X4", + "AEGIS-256", + "AEGIS-256X2", + "AEGIS-256X4", +] + +CIPHERS: dict[CipherName, str] = { + "AEGIS-128L": "aegis128l", + "AEGIS-128X2": "aegis128x2", + "AEGIS-128X4": "aegis128x4", + "AEGIS-256": "aegis256", + "AEGIS-256X2": "aegis256x2", + "AEGIS-256X4": "aegis256x4", +} diff --git a/src/aeg/_typing.py b/src/aeg/_typing.py new file mode 100644 index 0000000..ba05c49 --- /dev/null +++ b/src/aeg/_typing.py @@ -0,0 +1,122 @@ +from typing import TYPE_CHECKING, Protocol + +if TYPE_CHECKING: + from .util import Buffer + +__all__ = ["Cipher"] + + +class _Mac(Protocol): + def reset(self) -> None: ... + def clone(self) -> "_Mac": ... + def update(self, data: "Buffer") -> None: ... + def final(self, into: "Buffer | None" = None) -> bytearray | memoryview: ... + def digest(self) -> bytes: ... + def hexdigest(self) -> str: ... + def verify(self, mac: "Buffer") -> None: ... + + +class _Encryptor(Protocol): + def update( + self, message: "Buffer", into: "Buffer | None" = None + ) -> bytearray | memoryview: ... + def final(self, into: "Buffer | None" = None) -> bytearray | memoryview: ... + + +class _Decryptor(Protocol): + def update( + self, ct: "Buffer", into: "Buffer | None" = None + ) -> bytearray | memoryview: ... + def final(self, mac: "Buffer") -> None: ... + + +class Cipher(Protocol): + NAME: str + KEYBYTES: int + NONCEBYTES: int + MACBYTES: int + MACBYTES_LONG: int + ALIGNMENT: int + RATE: int + + Mac: type[_Mac] + Encryptor: type[_Encryptor] + Decryptor: type[_Decryptor] + + @staticmethod + def random_key() -> bytearray: ... + @staticmethod + def random_nonce() -> bytearray: ... + @staticmethod + def encrypt_detached( + key: "Buffer", + nonce: "Buffer", + message: "Buffer", + ad: "Buffer | None" = None, + *, + maclen: int = ..., + ct_into: "Buffer | None" = None, + mac_into: "Buffer | None" = None, + ) -> tuple[bytearray | memoryview, bytearray | memoryview]: ... + @staticmethod + def decrypt_detached( + key: "Buffer", + nonce: "Buffer", + ct: "Buffer", + mac: "Buffer", + ad: "Buffer | None" = None, + *, + into: "Buffer | None" = None, + ) -> bytearray | memoryview: ... + @staticmethod + def encrypt( + key: "Buffer", + nonce: "Buffer", + message: "Buffer", + ad: "Buffer | None" = None, + *, + maclen: int = ..., + into: "Buffer | None" = None, + ) -> bytearray | memoryview: ... + @staticmethod + def decrypt( + key: "Buffer", + nonce: "Buffer", + ct: "Buffer", + ad: "Buffer | None" = None, + *, + maclen: int = ..., + into: "Buffer | None" = None, + ) -> bytearray | memoryview: ... + @staticmethod + def stream( + key: "Buffer", + nonce: "Buffer | None", + length: int | None = None, + *, + into: "Buffer | None" = None, + ) -> "bytearray | Buffer": ... + @staticmethod + def encrypt_unauthenticated( + key: "Buffer", + nonce: "Buffer", + message: "Buffer", + *, + into: "Buffer | None" = None, + ) -> bytearray | memoryview: ... + @staticmethod + def decrypt_unauthenticated( + key: "Buffer", + nonce: "Buffer", + ct: "Buffer", + *, + into: "Buffer | None" = None, + ) -> bytearray | memoryview: ... + @staticmethod + def mac( + key: "Buffer", + nonce: "Buffer", + data: "Buffer", + maclen: int = ..., + into: "Buffer | None" = None, + ) -> bytearray | memoryview: ... diff --git a/tools/generate.py b/tools/generate.py index 0934724..7a8887b 100644 --- a/tools/generate.py +++ b/tools/generate.py @@ -273,6 +273,23 @@ def generate_python_modules( return updated, unchanged +def generate_ciphers_module(constants: Dict[str, Dict[str, int]]) -> str: + labels = [algo_label(variant) for variant in constants] + literal_items = ", ".join(f'"{label}"' for label in labels) + lines = [ + "# This file is generated by tools/generate.py. Do not edit.", + "from typing import Literal", + "", + f"CipherName = Literal[{literal_items}]", + "", + "CIPHERS: dict[CipherName, str] = {", + ] + for variant in constants: + lines.append(f' "{algo_label(variant)}": "{variant}",') + lines.append("}") + return "\n".join(lines) + "\n" + + def main() -> int: root = pathlib.Path(__file__).parent.parent libaegis_src_dir = root / "libaegis" / "src" @@ -304,7 +321,20 @@ def main() -> int: cdef_path.write_bytes(cdef_content.encode()) print(f" - Updated {cdef_path}", file=sys.stderr) - print("Step 3: Generating Python modules...", file=sys.stderr) + print("Step 3: Generating _ciphers.py...", file=sys.stderr) + ciphers_path = pyaegis_dir / "_ciphers.py" + ciphers_content = generate_ciphers_module(constants) + + if ( + ciphers_path.exists() + and ciphers_path.read_text(encoding="utf-8") == ciphers_content + ): + print(f" - No changes to {ciphers_path.name}", file=sys.stderr) + else: + ciphers_path.write_bytes(ciphers_content.encode()) + print(f" - Updated {ciphers_path.name}", file=sys.stderr) + + print("Step 4: Generating Python modules...", file=sys.stderr) try: updated, unchanged = generate_python_modules( pyaegis_dir / "aegis256x4.py", pyaegis_dir, constants