11 Commits
26 changed files with 328 additions and 80 deletions

No files matched your search

+3 -3
View File
@@ -3,9 +3,9 @@
*.egg-info *.egg-info
/dist /dist
/build /build
/pyaegis/build /src/aeg/build
/pyaegis/_aegis*.so /src/aeg/_aegis*.so
/pyaegis/_aegis*.pyd /src/aeg/_aegis*.pyd
__pycache__ __pycache__
!.gitignore !.gitignore
!.gitmodules !.gitmodules
+7 -7
View File
@@ -1,6 +1,6 @@
# Building pyaegis # Building aeg
This document contains instructions for developers who want to build pyaegis from source. This document contains instructions for developers who want to build aeg from source.
## Prerequisites ## Prerequisites
@@ -11,7 +11,7 @@ This document contains instructions for developers who want to build pyaegis fro
### Installing Zig ### Installing Zig
pyaegis uses Zig to build the underlying libaegis C library. Install Zig from [ziglang.org/download](https://ziglang.org/download/) or using your package manager: aeg uses Zig to build the underlying libaegis C library. Install Zig from [ziglang.org/download](https://ziglang.org/download/) or using your package manager:
- **macOS**: `brew install zig` - **macOS**: `brew install zig`
- **Linux**: See [Zig installation guide](https://github.com/ziglang/zig/wiki/Install-Zig-from-a-Package-Manager) - **Linux**: See [Zig installation guide](https://github.com/ziglang/zig/wiki/Install-Zig-from-a-Package-Manager)
@@ -22,8 +22,8 @@ pyaegis uses Zig to build the underlying libaegis C library. Install Zig from [z
Clone the repository with submodules: Clone the repository with submodules:
```fish ```fish
git clone --recursive https://github.com/LeoVasanko/pyaegis.git git clone --recursive https://github.com/LeoVasanko/aeg.git
cd pyaegis cd aeg
``` ```
If you already cloned without `--recursive`, initialize submodules: If you already cloned without `--recursive`, initialize submodules:
@@ -74,7 +74,7 @@ This creates files in the `dist/` directory.
## Code Generation ## Code Generation
The Python modules and CFFI definitions are generated from C sources and templates. If you modify the core implementation in `pyaegis/aegis256x4.py` or update libaegis headers, regenerate all files: The Python modules and CFFI definitions are generated from C sources and templates. If you modify the core implementation in `src/aeg/aegis256x4.py` or update libaegis headers, regenerate all files:
```fish ```fish
python tools/generate.py python tools/generate.py
@@ -100,7 +100,7 @@ If you cannot install Zig, you may manually compile in the libaegis folder (Zig,
## Project Structure ## Project Structure
- `pyaegis/` - Python package source - `src/aeg/` - Python package source
- `libaegis/` - C library source (submodule) - `libaegis/` - C library source (submodule)
- `tests/` - Test suite - `tests/` - Test suite
- `tools/` - Code generation scripts and `build_backend.py` used to build libaegis - `tools/` - Code generation scripts and `build_backend.py` used to build libaegis
+1 -1
View File
@@ -1,4 +1,4 @@
include pyaegis/aegis_cdef.h include src/aeg/aegis_cdef.h
include setup.py include setup.py
include tools/build_backend.py include tools/build_backend.py
include BUILD.md include BUILD.md
+42 -30
View File
@@ -1,33 +1,41 @@
# pyaegis # AEGIS Cipher Python Binding
[![PyPI version](https://badge.fury.io/py/pyaegis.svg)](https://badge.fury.io/py/pyaegis) [![PyPI version](https://badge.fury.io/py/aeg.svg)](https://badge.fury.io/py/aeg)
Safe Python bindings for the AEGIS family of very fast authenticated encryption algorithms (via libaegis). Safe Python bindings for the AEGIS family of very fast authenticated encryption algorithms via libaegis. The module runs without compilation required on Windows, Mac and Linux (has precompiled wheels). For other platforms compilation is performed at install time.
AEGIS enables extremely fast Encryption, MAC and CSPRNG - many times faster than AES, ChaCha20 or traditional random number generators. Authenticated Encryption with Additional Data is supported with the MAC derived from the cipher state at the end, making it different from other AEADs like AES-GCM and ChaCha20-Poly1305. The whole internal state thus depends on the prior data, and it is neither Encrypt-Then-Mac nor Mac-The-Encrypt scheme when both features are used together.
## Install ## Install
Using [uv](https://docs.astral.sh/uv/getting-started/installation/): ```sh
```fish pip install aeg
uv pip install git+https://github.com/LeoVasanko/pyaegis.git
``` ```
For development builds, see BUILD.md. Or add to your project using [UV](https://docs.astral.sh/uv/getting-started/installation/):
```sh
uv add aeg
```
## Variants ## Variants
All submodules expose the same API; pick one for your key/nonce size and platform: All submodules expose the same API; pick one for your needs. The 256 bit variants offer maximal security and use larger key and nonce, while the 128 bit variants run slightly faster and use smaller key and nonce while still providing strong security. The MAC length does not depend on the variant. Note that the x2 and x4 variants are typically the fastest (depending on CPU) by utilizing SIMD multi-lane processing for the highest throughput.
- aegis128l (16-byte key, 16-byte nonce) | Variant | Key/Nonce Bytes | Notes |
- aegis256 (32-byte key, 32-byte nonce) |----------------|----------------:|-------------------------|
- aegis128x2 / aegis128x4 (multi-lane 128-bit; best throughput on SIMD-capable CPUs) | **aegis128l** | 16 | |
- aegis256x2 / aegis256x4 (multi-lane 256-bit) | **aegis128x2** | 16 | Fastest on Intel Core |
| **aegis128x4** | 16 | Fastest on AMD and Xeon |
| **aegis256** | 32 | |
| **aegis256x2** | 32 | Fast on Intel Core |
| **aegis256x4** | 32 | Fast on AMD and Xeon |
## Quick start ## Quick start
Normal authenticated encryption using the AEGIS-128X4 algorithm: Normal authenticated encryption using the AEGIS-128X4 algorithm:
```python ```python
from pyaegis import aegis128x4 as ciph from aeg import aegis128x4 as ciph
key = ciph.random_key() # Secret key (stored securely) key = ciph.random_key() # Secret key (stored securely)
nonce = ciph.random_nonce() # Public nonce (recreated for each message) nonce = ciph.random_nonce() # Public nonce (recreated for each message)
@@ -83,13 +91,13 @@ The object releases its state and becomes unusable after final has been called.
No encryption, but prevents changes to the data without the correct key. No encryption, but prevents changes to the data without the correct key.
- mac(key, nonce, data, maclen=16, into=None) -> mac - mac(key, nonce, data, maclen=16, into=None) -> mac bytes
- Mac(key, nonce, maclen=16) - Mac(key, nonce, maclen=16)
- update(data) - update(data)
- final([into]) -> mac - final([into]) -> mac bytes
- verify(mac) -> raises ValueError on failure - verify(mac) -> raises ValueError on failure
- digest() -> bytes - digest() -> mac bytes
- hexdigest() -> str - hexdigest() -> mac str
- reset() - reset()
- clone() -> Mac - clone() -> Mac
@@ -123,7 +131,7 @@ Constants (per module): NAME, KEYBYTES, NONCEBYTES, MACBYTES, MACBYTES_LONG, RAT
A cryptographically secure keyed hash is produced. The example uses all zeroes for the nonce to always produce the same hash for the same key: A cryptographically secure keyed hash is produced. The example uses all zeroes for the nonce to always produce the same hash for the same key:
```python ```python
from pyaegis import aegis256x4 as ciph from aeg import aegis256x4 as ciph
key, nonce = ciph.random_key(), bytes(ciph.NONCEBYTES) key, nonce = ciph.random_key(), bytes(ciph.NONCEBYTES)
mac = ciph.mac(key, nonce, b"message", maclen=32) mac = ciph.mac(key, nonce, b"message", maclen=32)
@@ -146,7 +154,7 @@ b.verify(mac) # Raises ValueError
Keeping the ciphertext, mac and ad separate. The ad represents a file header that needs to be tamper proofed. Keeping the ciphertext, mac and ad separate. The ad represents a file header that needs to be tamper proofed.
```python ```python
from pyaegis import aegis256x4 as ciph from aeg import aegis256x4 as ciph
key, nonce = ciph.random_key(), ciph.random_nonce() key, nonce = ciph.random_key(), ciph.random_nonce()
ct, mac = ciph.encrypt_detached(key, nonce, b"secret", ad=b"header") ct, mac = ciph.encrypt_detached(key, nonce, b"secret", ad=b"header")
@@ -162,7 +170,7 @@ ciph.wipe(pt)
Class-based interface for incremental updates is an alternative to the one-shot functions. Not to be confused with separately verified ciphertext frames (see the next example). Class-based interface for incremental updates is an alternative to the one-shot functions. Not to be confused with separately verified ciphertext frames (see the next example).
```python ```python
from pyaegis import aegis256x4 as ciph from aeg import aegis256x4 as ciph
key, nonce = ciph.random_key(), ciph.random_nonce() key, nonce = ciph.random_key(), ciph.random_nonce()
enc = ciph.Encryptor(key, nonce, ad=b"header", maclen=16) enc = ciph.Encryptor(key, nonce, ad=b"header", maclen=16)
@@ -182,7 +190,7 @@ It is often practical to split larger messages into frames that can be individua
```python ```python
# Encryption settings # Encryption settings
from pyaegis import aegis128x4 as ciph from aeg import aegis128x4 as ciph
key = b"sixteenbyte key!" # 16 bytes secret key for aegis128* algorithms key = b"sixteenbyte key!" # 16 bytes secret key for aegis128* algorithms
framebytes = 80 # In real applications 1 MiB or more is practical framebytes = 80 # In real applications 1 MiB or more is practical
maclen = ciph.MACBYTES # 16 maclen = ciph.MACBYTES # 16
@@ -202,7 +210,7 @@ with open("encrypted.bin", "wb") as f:
```python ```python
# Decryption needs same values as encryption # Decryption needs same values as encryption
from pyaegis import aegis128x4 as ciph from aeg import aegis128x4 as ciph
key = b"sixteenbyte key!" key = b"sixteenbyte key!"
framebytes = 80 framebytes = 80
maclen = ciph.MACBYTES maclen = ciph.MACBYTES
@@ -223,7 +231,7 @@ with open("encrypted.bin", "rb") as f:
The stream generator is much faster than any traditional random number generator, cryptographically secure and seekable. Use `random_key()` for unpredictable output. The stream generator is much faster than any traditional random number generator, cryptographically secure and seekable. Use `random_key()` for unpredictable output.
```python ```python
from pyaegis import aegis128x4 as ciph from aeg import aegis128x4 as ciph
key = b"SeedForReplay001" # A non-random deterministic seed (16 bytes) key = b"SeedForReplay001" # A non-random deterministic seed (16 bytes)
nonce = bytearray(ciph.NONCEBYTES) # All-zeroes nonce nonce = bytearray(ciph.NONCEBYTES) # All-zeroes nonce
@@ -247,7 +255,7 @@ Foreign arrays can be used. This example fills a Numpy array with random integer
```python ```python
import numpy as np import numpy as np
from pyaegis import aegis128x4 as ciph from aeg import aegis128x4 as ciph
key, nonce = ciph.random_key(), ciph.random_nonce() key, nonce = ciph.random_key(), ciph.random_nonce()
arr = np.empty(10, dtype=np.uint64) # Uninitialised integer array arr = np.empty(10, dtype=np.uint64) # Uninitialised integer array
@@ -258,7 +266,7 @@ print(arr)
In-place operations are supported when the input and the output point to the same location in memory. When using attached MAC tag, the input buffer needs to be sliced to correct length: In-place operations are supported when the input and the output point to the same location in memory. When using attached MAC tag, the input buffer needs to be sliced to correct length:
```python ```python
from pyaegis import aegis256x4 as ciph from aeg import aegis256x4 as ciph
key, nonce = ciph.random_key(), ciph.random_nonce() key, nonce = ciph.random_key(), ciph.random_nonce()
buf = memoryview(bytearray(1000)) # memoryview[:len] is still in the same buffer (no copy) buf = memoryview(bytearray(1000)) # memoryview[:len] is still in the same buffer (no copy)
buf[:7] = b"message" buf[:7] = b"message"
@@ -276,10 +284,10 @@ Detached and unauthenticated modes can use same size input and output (no MAC ad
Runtime CPU feature detection selects optimized code paths (AES-NI, ARM Crypto, AVX2/AVX-512). Multi-lane variants (x2/x4) offer higher throughput on suitable CPUs. Runtime CPU feature detection selects optimized code paths (AES-NI, ARM Crypto, AVX2/AVX-512). Multi-lane variants (x2/x4) offer higher throughput on suitable CPUs.
Benchmarks using the included benchmark module, run on Intel i7-14700, linux, single core (the software is not multithreaded). Note that the results are in megabits per second, not bytes. The CPU lacks AVX-512 that makes the X4 variants faster on AMD hardware. Benchmarks using the included benchmark module, run on Intel i7-14700, linux, single core (the software is not multithreaded). Note that the results are in megabits per second, not bytes. The CPU lacks AVX-512 that makes the X4 variants faster on processors supporting it (most AMD, Xeon).
```fish ```sh
$ uv run -m pyaegis.benchmark uv run -m aeg.benchmark
AEGIS-256 103166.24 Mb/s AEGIS-256 103166.24 Mb/s
AEGIS-256X2 184225.50 Mb/s AEGIS-256X2 184225.50 Mb/s
AEGIS-256X4 194018.26 Mb/s AEGIS-256X4 194018.26 Mb/s
@@ -295,8 +303,8 @@ AEGIS-256X4 MAC 315919.87 Mb/s
``` ```
The Python library performance is similar to that of the C library: The Python library performance is similar to that of the C library:
```fish ```sh
$ ./libaegis/zig-out/bin/benchmark ./libaegis/zig-out/bin/benchmark
AEGIS-256 107820.86 Mb/s AEGIS-256 107820.86 Mb/s
AEGIS-256X2 205025.57 Mb/s AEGIS-256X2 205025.57 Mb/s
AEGIS-256X4 223361.81 Mb/s AEGIS-256X4 223361.81 Mb/s
@@ -310,3 +318,7 @@ AEGIS-256 MAC 116776.62 Mb/s
AEGIS-256X2 MAC 224150.04 Mb/s AEGIS-256X2 MAC 224150.04 Mb/s
AEGIS-256X4 MAC 392088.05 Mb/s AEGIS-256X4 MAC 392088.05 Mb/s
``` ```
## Alternatives
There is also a package named [pyaegis](https://github.com/jedisct1/pyaegis) on PyPI that is unrelated to this module, but that also binds to the libaegis C library. There are also a number of modules named aegis from different packages not at all related to the encryption algorithm.
View File
Whitespace-only changes.
+9 -7
View File
@@ -4,13 +4,12 @@ build-backend = "build_backend"
backend-path = ["tools"] backend-path = ["tools"]
[project] [project]
name = "pyaegis" name = "aeg"
dynamic = ["version"] dynamic = ["version"]
description = "Python bindings for libaegis" 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 :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Topic :: Security :: Cryptography", "Topic :: Security :: Cryptography",
@@ -20,10 +19,12 @@ dependencies = [
] ]
[project.urls] [project.urls]
Homepage = "https://github.com/LeoVasanko/pyaegis" Homepage = "https://git.zi.fi/LeoVasanko/aegis-python"
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",
@@ -31,9 +32,10 @@ dev = [
] ]
[tool.setuptools] [tool.setuptools]
packages = ["pyaegis"] package-dir = {"" = "src"}
packages = ["aeg"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
pyaegis = ["*.h", "*.so", "*.pyd"] aeg = ["*.h", "*.so", "*.pyd"]
[tool.setuptools_scm] [tool.setuptools_scm]
+3 -3
View File
@@ -1,4 +1,4 @@
"""Setup script for pyaegis - builds CFFI extension with libaegis C library.""" """Setup script for aeg - builds CFFI extension with libaegis C library."""
import sys import sys
from pathlib import Path from pathlib import Path
@@ -20,7 +20,7 @@ if not libaegis_include.exists():
include_dirs = [str(libaegis_include)] include_dirs = [str(libaegis_include)]
# Read the CDEF header # Read the CDEF header
cdef_path = Path(__file__).parent / "pyaegis" / "aegis_cdef.h" cdef_path = Path(__file__).parent / "src" / "aeg" / "aegis_cdef.h"
cdef_content = cdef_path.read_text(encoding="utf-8") cdef_content = cdef_path.read_text(encoding="utf-8")
# Create CFFI builder # Create CFFI builder
@@ -29,7 +29,7 @@ ffibuilder.cdef(cdef_content)
# Set the source # Set the source
ffibuilder.set_source( ffibuilder.set_source(
"pyaegis._aegis", # module name "aeg._aegis", # module name
""" """
#include "aegis.h" #include "aegis.h"
#include "aegis128l.h" #include "aegis128l.h"
+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",
}
+1 -1
View File
@@ -1,6 +1,6 @@
"""Loader for libaegis CFFI extension module.""" """Loader for libaegis CFFI extension module."""
from pyaegis._aegis import ffi, lib from aeg._aegis import ffi, lib
__all__ = ["ffi", "lib"] __all__ = ["ffi", "lib"]
+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: ...
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -12,7 +12,7 @@ Output format and throughput units mirror the Zig benchmark (Mb/s).
import secrets import secrets
import time import time
from pyaegis import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4 from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
MSG_LEN = 16384000 # 16 000 KiB MSG_LEN = 16384000 # 16 000 KiB
ITERATIONS = 100 ITERATIONS = 100
+1 -1
View File
@@ -1,4 +1,4 @@
"""Utility helpers for pyaegis. """Utility helpers for aeg.
Currently provides Python-side aligned allocation helpers that avoid relying Currently provides Python-side aligned allocation helpers that avoid relying
on libc/posix_memalign. Memory is owned by Python; C code only borrows it. on libc/posix_memalign. Memory is owned by Python; C code only borrows it.
+1 -1
View File
@@ -3,7 +3,7 @@ from pathlib import Path
import pytest import pytest
from pyaegis import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4 from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
from .util import random_split_bytes from .util import random_split_bytes
+1 -1
View File
@@ -3,7 +3,7 @@ from pathlib import Path
import pytest import pytest
from pyaegis import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4 from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
from .util import random_split_bytes from .util import random_split_bytes
+1 -1
View File
@@ -6,7 +6,7 @@ after calling final(), preventing accidental misuse.
import pytest import pytest
from pyaegis import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4 from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
# All AEGIS algorithm modules # All AEGIS algorithm modules
ALL_ALGORITHMS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4] ALL_ALGORITHMS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4]
+1 -1
View File
@@ -15,7 +15,7 @@ def _check_zig_available():
"\n" + "=" * 70 + "\n" "\n" + "=" * 70 + "\n"
"ERROR: Zig compiler not found!\n" "ERROR: Zig compiler not found!\n"
"\n" "\n"
"Building pyaegis requires the Zig compiler to build the libaegis\n" "Building aeg requires the Zig compiler to build the libaegis\n"
"static library. Please install Zig before building this package.\n" "static library. Please install Zig before building this package.\n"
"\n" "\n"
"Installation instructions:\n" "Installation instructions:\n"
Regular → Executable
+33 -2
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,11 +274,28 @@ 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"
include_dir = libaegis_src_dir / "include" include_dir = libaegis_src_dir / "include"
pyaegis_dir = root / "pyaegis" pyaegis_dir = root / "src" / "aeg"
if not include_dir.exists(): if not include_dir.exists():
print(f"Include directory not found: {include_dir}", file=sys.stderr) print(f"Include directory not found: {include_dir}", file=sys.stderr)
@@ -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
+65 -20
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}")
@@ -112,8 +113,8 @@ def normalize_line_endings(repo_root: Path):
"""Normalize all text files to LF line endings.""" """Normalize all text files to LF line endings."""
# Patterns for files to normalize # Patterns for files to normalize
patterns = [ patterns = [
"pyaegis/**/*.py", "src/aeg/**/*.py",
"pyaegis/**/*.h", "src/aeg/**/*.h",
"tests/**/*.py", "tests/**/*.py",
"tools/**/*.py", "tools/**/*.py",
"*.py", "*.py",
@@ -168,7 +169,7 @@ def main():
# Main header for the packaging process # Main header for the packaging process
print(f"\n{'=' * 70}") print(f"\n{'=' * 70}")
print( print(
f"Packaging pyaegis-{version}" f"Packaging aeg-{version}"
+ (" for release" if is_release else " (not release)") + (" for release" if is_release else " (not release)")
) )
print(f"Building wheels for Python versions: {', '.join(PYTHON_VERSIONS)}") print(f"Building wheels for Python versions: {', '.join(PYTHON_VERSIONS)}")
@@ -202,7 +203,12 @@ def main():
continue continue
# Find the wheel for this version # Find the wheel for this version
wheel_pattern = f"pyaegis-*-cp{py_version.replace('.', '')}-*.whl" if py_version.startswith("pypy"):
# PyPy wheels use pp3XX format
version_tag = f"pp{py_version.replace('pypy', '').replace('.', '')}"
else:
version_tag = f"cp{py_version.replace('.', '').replace('t', '')}"
wheel_pattern = f"aeg-*-{version_tag}-*.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 +217,45 @@ 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-*-{version_tag}-*.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",
@@ -241,7 +286,7 @@ def main():
"--with", "--with",
str(wheel), str(wheel),
"-m", "-m",
"pyaegis.benchmark", "aeg.benchmark",
] ]
if not run_command(bench_cmd, f"Running benchmark for Python {py_version}"): if not run_command(bench_cmd, f"Running benchmark for Python {py_version}"):
print(f"✗ Benchmark failed for Python {py_version}", file=sys.stderr) print(f"✗ Benchmark failed for Python {py_version}", file=sys.stderr)