6 Commits
6 changed files with 258 additions and 17 deletions
+2
View File
@@ -7,6 +7,7 @@ backend-path = ["tools"]
name = "aeg" name = "aeg"
dynamic = ["version"] dynamic = ["version"]
description = "AEGIS encryption easy to use Python binding. Wheels for major platforms." description = "AEGIS encryption easy to use Python binding. Wheels for major platforms."
readme = {file = "README.md", content-type = "text/markdown"}
requires-python = ">=3.10" requires-python = ">=3.10"
classifiers = [ classifiers = [
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
@@ -23,6 +24,7 @@ Repository = "https://github.com/LeoVasanko/aegis-python"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"auditwheel>=6.5.0",
"pytest>=8.4.2", "pytest>=8.4.2",
"ruff>=0.14.4", "ruff>=0.14.4",
"setuptools>=80.9.0", "setuptools>=80.9.0",
+16
View File
@@ -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)}")
+20
View File
@@ -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",
}
+122
View File
@@ -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: ...
Regular → Executable
+32 -1
View File
@@ -1,3 +1,4 @@
#!/usr/bin/env -S uv run
"""Generate CFFI cdef and Python modules from libaegis C sources.""" """Generate CFFI cdef and Python modules from libaegis C sources."""
import pathlib import pathlib
@@ -273,6 +274,23 @@ def generate_python_modules(
return updated, unchanged 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: def main() -> int:
root = pathlib.Path(__file__).parent.parent root = pathlib.Path(__file__).parent.parent
libaegis_src_dir = root / "libaegis" / "src" libaegis_src_dir = root / "libaegis" / "src"
@@ -304,7 +322,20 @@ def main() -> int:
cdef_path.write_bytes(cdef_content.encode()) cdef_path.write_bytes(cdef_content.encode())
print(f" - Updated {cdef_path}", file=sys.stderr) 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: try:
updated, unchanged = generate_python_modules( updated, unchanged = generate_python_modules(
pyaegis_dir / "aegis256x4.py", pyaegis_dir, constants pyaegis_dir / "aegis256x4.py", pyaegis_dir, constants
Regular → Executable
+66 -16
View File
@@ -1,26 +1,30 @@
#!/usr/bin/env python3 #!/usr/bin/env -S uv run
"""Build wheels for all supported Python versions using uv.""" """Build wheels for all supported Python versions using uv."""
import platform
import shutil
import subprocess import subprocess
import sys import sys
import tomllib
from pathlib import Path from pathlib import Path
import shutil
from packaging.specifiers import SpecifierSet
from packaging.version import Version from packaging.version import Version
# Import generate module from same directory # Import generate module from same directory
sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent))
import generate import generate
PYTHON_VERSIONS = [
def get_python_versions(): "3.10",
"""Get supported Python versions.""" "3.11",
pyproject_toml = Path(__file__).parent.parent / "pyproject.toml" "3.12",
data = tomllib.loads(pyproject_toml.read_text(encoding="utf-8")) "3.13",
spec = SpecifierSet(data["project"]["requires-python"]) "3.14",
# Generate versions that match the specifier (up to Python 3.14) "3.14t",
return [f"3.{minor}" for minor in range(10, 15) if f"3.{minor}" in spec] "3.15",
"3.15t",
"pypy3.10",
"pypy3.11",
]
def get_version_from_scm(): def get_version_from_scm():
@@ -90,9 +94,6 @@ def make_release_message(version):
return msg return msg
PYTHON_VERSIONS = get_python_versions()
def run_command(cmd, description): def run_command(cmd, description):
"""Run a command and handle errors.""" """Run a command and handle errors."""
print(f"\n{'=' * 70}") print(f"\n{'=' * 70}")
@@ -202,7 +203,15 @@ def main():
continue continue
# Find the wheel for this version # Find the wheel for this version
wheel_pattern = f"aeg-*-cp{py_version.replace('.', '')}-*.whl" if py_version.startswith("pypy"):
# PyPy wheels use pp3XX format
wheel_pattern = (
f"aeg-*-pp{py_version.replace('pypy', '').replace('.', '')}-*.whl"
)
else:
wheel_pattern = (
f"aeg-*-cp{py_version.replace('.', '').replace('t', '')}-*.whl"
)
wheels = list(dist_dir.glob(wheel_pattern)) wheels = list(dist_dir.glob(wheel_pattern))
if not wheels: if not wheels:
print(f"✗ Could not find wheel for Python {py_version}", file=sys.stderr) print(f"✗ Could not find wheel for Python {py_version}", file=sys.stderr)
@@ -211,6 +220,47 @@ def main():
wheel = wheels[0] wheel = wheels[0]
# Repair wheel with auditwheel for manylinux compatibility (Linux only)
if platform.system() == "Linux":
repair_cmd = [
"uv",
"run",
"auditwheel",
"repair",
str(wheel),
"-w",
str(dist_dir),
]
if not run_command(
repair_cmd, f"Repairing wheel for Python {py_version} with auditwheel"
):
print(
f"✗ Auditwheel repair failed for Python {py_version}",
file=sys.stderr,
)
failed_builds.append(py_version)
continue
# Find the repaired wheel (it will have a different name)
all_wheels = list(
dist_dir.glob(f"aeg-*-cp{py_version.replace('.', '')}-*.whl")
)
repaired_wheels = [w for w in all_wheels if "linux_x86_64" not in str(w)]
if not repaired_wheels:
print(
f"✗ Could not find repaired (manylinux) wheel for Python {py_version}",
file=sys.stderr,
)
failed_builds.append(py_version)
continue
wheel = repaired_wheels[0] # Use the repaired wheel for testing
# Remove the unrepaired linux_x86_64 wheels
for w in all_wheels:
if "linux_x86_64" in str(w):
w.unlink()
# Test the wheel with pytest (use --isolated to avoid .venv conflicts) # Test the wheel with pytest (use --isolated to avoid .venv conflicts)
test_cmd = [ test_cmd = [
"uv", "uv",