From a78f5c4ea312c306cb6fcb6c3054ed91e7464131 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 7 Nov 2025 09:11:38 +0000 Subject: [PATCH] README --- README.md | 317 ++++++++++++++++++++++++++---------------------------- 1 file changed, 151 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index c800d30..6c96787 100644 --- a/README.md +++ b/README.md @@ -3,220 +3,205 @@ [![PyPI version](https://badge.fury.io/py/pyaegis.svg)](https://badge.fury.io/py/pyaegis) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/LeoVasanko/pyaegis/blob/main/libaegis/LICENSE) -Python bindings for libaegis - high-performance AEGIS authenticated encryption. +Fast, safe Python bindings for the AEGIS family of authenticated encryption algorithms (via libaegis). -## Overview +## Install -pyaegis provides Python bindings to the AEGIS family of authenticated encryption algorithms implemented in the libaegis C library. - -AEGIS is a high-performance authenticated cipher that provides both confidentiality and authenticity guarantees. - -### Variant selection - -The following submodules are available: - -- **aegis128l**: 16-byte key, 16-byte nonce -- **aegis256**: 32-byte key, 32-byte nonce -- **aegis128x2**: 16-byte key, 16-byte nonce (recommended on most platforms) -- **aegis128x4**: 16-byte key, 16-byte nonce (recommended on high-end Intel CPUs) -- **aegis256x2**: 32-byte key, 32-byte nonce -- **aegis256x4**: 32-byte key, 32-byte nonce (recommended if a 256-bit nonce is required) - -```python -from pyaegis import aegis128x4 -``` - -### One-shot Functions - -- `encrypt(key, nonce, message, ad=None, maclen=16)` - Encrypt with attached MAC -- `decrypt(key, nonce, ciphertext, ad=None, maclen=16)` - Decrypt with attached MAC -- `encrypt_detached(key, nonce, message, ad=None, maclen=16)` - Encrypt with detached MAC -- `decrypt_detached(key, nonce, ciphertext, mac, ad=None, maclen=16)` - Decrypt with detached MAC -- `stream(key, nonce, length)` - Generate pseudo-random bytes -- `encrypt_unauthenticated(key, nonce, message)` - Encrypt without authentication (insecure) -- `decrypt_unauthenticated(key, nonce, ciphertext)` - Decrypt without authentication (insecure) - -**Note**: Functions that return buffers (like `encrypt`, `decrypt`, `stream`) return `memoryview` objects. Callers can optionally supply their own buffer via the `into=` keyword argument to avoid memory allocations. - -### Incremental Classes - -- `Encryptor(key, nonce, ad=None)` - For streaming encryption -- `Decryptor(key, nonce, ad=None)` - For streaming decryption - -## Installation - -### From PyPI - -Using [uv](https://docs.astral.sh/uv/): - -```bash -uv pip install pyaegis -``` - -Or using pip: +- PyPI (recommended): ```bash pip install pyaegis ``` -### From Source +For development builds, see BUILD.md. -The package compiles the C library automatically using any installed C compiler: +## Variants -```bash -# Clone the repository -git clone https://github.com/LeoVasanko/pyaegis.git -cd pyaegis +All submodules expose the same API; pick one for your key/nonce size and platform: -# Install with uv (compiles C sources automatically) -uv pip install . +- aegis128l (16-byte key, 16-byte nonce) +- aegis256 (32-byte key, 32-byte nonce) +- aegis128x2 / aegis128x4 (multi-lane 128-bit; best throughput on SIMD-capable CPUs) +- aegis256x2 / aegis256x4 (multi-lane 256-bit) -# Or for development -uv pip install -e . -``` - -Alternatively with pip: - -```bash -pip install . -# Or for development -pip install -e . -``` - -### Building a Distribution - -```bash -# With uv -uv run python -m build - -# Or with pip -python -m build -``` - -This creates both source and wheel distributions in the `dist/` directory. The C sources are bundled in the package and compiled during installation. - -## Usage - -All modules (`aegis128l`, `aegis256`, `aegis128x2`, `aegis128x4`, `aegis256x2`, `aegis256x4`) provide the exact same API. - -### Basic Encryption/Decryption +## Quick start ```python -import pyaegis.aegis128l as a +from pyaegis import aegis128x4 as ciph -key = b"K" * a.KEYBYTES # 16 bytes for aegis128l -nonce = b"N" * a.NPUBBYTES # 16 bytes for aegis128l -plaintext = b"Hello, World!" +key = ciph.random_key() +nonce = ciph.random_nonce() +msg = b"hello" -ciphertext = a.encrypt(key, nonce, plaintext) -decrypted = a.decrypt(key, nonce, ciphertext) -assert decrypted == plaintext +ct = ciph.encrypt(key, nonce, msg) +pt = ciph.decrypt(key, nonce, ct) +assert pt == msg ``` -### With Additional Authenticated Data (AAD) +## API overview + +Common parameters and returns (applies to all items below): + +- key: bytes of length a.KEYBYTES +- nonce: bytes of length a.NPUBBYTES (must be unique per (key, message)) +- message/ct: plain text or ciphertext +- ad: optional associated data (authenticated, not encrypted) +- into: optional output buffer (see below) +- maclen: MAC tag length 16 or 32 bytes (default 16) + +Only the first few can be positional arguments that are always provided in this order. All arguments can be passed as kwargs. + +Most functions return a buffer of bytes. By default a `bytearray` of the correct size is returned. An existing buffer can be provided by `into` argument, in which case the bytes of it that were written to are returned as a memoryview. + + +- random_key() -> bytes (correct length for the module) +- random_nonce() -> bytes (correct length for the module) + +Constants (per module): KEYBYTES, NPUBBYTES, ABYTES_MIN, ABYTES_MAX, RATE, ALIGNMENT + +### One-shot AEAD: + +Encrypt and decrypt messages with built-in authentication: +- encrypt(key, nonce, message, ad=None, maclen=16, into=None) -> ct_with_mac +- decrypt(key, nonce, ct_with_mac, ad=None, maclen=16, into=None) -> plaintext + +The MAC tag is handled separately of ciphertext: +- encrypt_detached(key, nonce, message, ad=None, maclen=16, ct_into=None, mac_into=None) -> (ct, mac) +- decrypt_detached(key, nonce, ct, mac, ad=None, into=None) -> plaintext + +No MAC tag, vulnerable to alterations: +- encrypt_unauthenticated(key, nonce, message, into=None) -> ciphertext (testing only) +- decrypt_unauthenticated(key, nonce, ct, into=None) -> plaintext (testing only) + +### Incremental AEAD: + +Stateful classes that can be used for processing the data in separate chunks: +- Encryptor(key, nonce, ad=None) + - update(message[, into]) -> ciphertext_chunk + - final([into], maclen=16) -> mac_tag +- Decryptor(key, nonce, ad=None) + - update(ct_chunk[, into]) -> plaintext_chunk + - final(mac) -> None (raises ValueError on failure) + +### Message Authentication Code: + +No encryption, but prevents changes to the data without the correct key. + +- mac(key, nonce, data, maclen=16, into=None) -> mac +- Mac(key, nonce) + - update(data) + - final(maclen=16[, into]) -> mac + - verify(mac) -> bool (True on success; raises ValueError on failure) + +### Keystream generation: + +Useful for creating pseudo random bytes as rapidly as possible. Reuse of the same (key, nonce) creates identical output. + +- stream(key, nonce=None, length=None, into=None) -> pseudorandom bytes (for tests/PRNG-like use) + + +## Examples + +Detached tag (ct, mac) ```python -ciphertext = a.encrypt(key, nonce, plaintext, ad=b"metadata") -plaintext = a.decrypt(key, nonce, ciphertext, ad=b"metadata") +from pyaegis import aegis256x4 as a +key, nonce = a.random_key(), a.random_nonce() +ct, mac = a.encrypt_detached(key, nonce, b"secret", ad=b"hdr", maclen=32) +pt = a.decrypt_detached(key, nonce, ct, mac, ad=b"hdr") ``` -### Detached Tag Mode +Incremental: ```python -ciphertext, mac = a.encrypt_detached(key, nonce, b"secret") -plaintext = a.decrypt_detached(key, nonce, ciphertext, mac) +from pyaegis import aegis256x4 as a +key, nonce = a.random_key(), a.random_nonce() + +enc = a.Encryptor(key, nonce, ad=b"hdr") +c1 = enc.update(b"chunk1") +c2 = enc.update(b"chunk2") +mac = enc.final(maclen=16) # returns only the tag + +dec = a.Decryptor(key, nonce, ad=b"hdr") +p1 = dec.update(c1) +p2 = dec.update(c2) +dec.final(mac) # raises ValueError on failure ``` -### Custom MAC Length - -Use 16 or 32-byte MACs (default 16): +MAC-only: ```python -ciphertext, mac = a.encrypt_detached(key, nonce, message, maclen=32) -plaintext = a.decrypt_detached(key, nonce, ciphertext, mac, maclen=32) +from pyaegis import aegis256x4 as a +key, nonce = a.random_key(), a.random_nonce() + +mac = a.mac(key, nonce, b"data", maclen=32) + +st = a.Mac(key, nonce) +st.update(b"data") +st.update(b"more data") +st.verify(mac) # True or raises ValueError ``` -### In-Place Operations +Pre-allocated buffers (avoid allocations): ```python -buffer = bytearray(b"secret message") -mac = a.encrypt_unauthenticated_into(key, nonce, buffer) -# buffer now contains ciphertext +from pyaegis import aegis256x4 as a +key, nonce = a.random_key(), a.random_nonce() +msg = b"data" -a.decrypt_unauthenticated_into(key, nonce, buffer, mac) -# buffer now contains plaintext again +out = bytearray(len(msg) + 16) +view = a.encrypt(key, nonce, msg, into=out) +assert bytes(view) == bytes(out) ``` -### Using Pre-allocated Buffers +In-place (same buffer for input and into): ```python -# Pre-allocate output buffer -output = bytearray(len(plaintext) + 16) # +16 for MAC -result = a.encrypt(key, nonce, plaintext, into=output) -# result is a memoryview of the output buffer -``` +from pyaegis import aegis256x4 as a +key, nonce = a.random_key(), a.random_nonce() -### Stream Generation +# Attached tag: place plaintext at the start of a buffer that has room for the tag +msg = b"secret" +maclen = 16 +buf = bytearray(len(msg) + maclen) +buf[: len(msg)] = msg +m = memoryview(buf)[: len(msg)] -Generate pseudo-random bytes: +# Encrypt in-place (ciphertext written back into buf, tag appended) +a.encrypt(key, nonce, m, into=buf) -```python -random_bytes = a.stream(key, nonce, 1024) -``` +# Decrypt back in-place (plaintext written over the start region) +a.decrypt(key, nonce, buf, into=m) # uses default maclen=16 +assert bytes(m) == msg -### Incremental Encryption - -```python -encryptor = a.Encryptor(key, nonce, ad=b"header") -ciphertext1 = encryptor.update(b"chunk1") -ciphertext2 = encryptor.update(b"chunk2") -final_bytes = encryptor.final() # includes MAC +# Detached mode: ciphertext written back to the same buffer +buf2 = bytearray(len(msg)) +buf2[:] = msg +m2 = memoryview(buf2) +ct_view, mac = a.encrypt_detached(key, nonce, m2, ct_into=buf2) +a.decrypt_detached(key, nonce, ct_view, mac, into=m2) +assert bytes(m2) == msg ``` ## Performance -The library automatically detects CPU features at runtime and uses the most optimized implementation available: +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. -- AES-NI on Intel/AMD processors -- ARM Crypto Extensions on ARM processors -- AVX2 and AVX-512 for multi-lane variants -- Software fallback for other platforms +Run the built-in benchmark to see which variant is fastest on your machine: -Multi-lane variants (X2, X4) provide higher throughput on systems with appropriate SIMD support. - -See `examples/benchmark.py` for performance measurements. - -## Error Handling - -Functions that can fail raise `ValueError` with descriptive messages: - -```python -import pyaegis.aegis128l as a - -key = b"K" * a.KEYBYTES -nonce = b"N" * a.NPUBBYTES - -try: - # This will raise ValueError if authentication fails - plaintext = a.decrypt(key, nonce, tampered_ciphertext) -except ValueError as e: - print(f"Decryption failed: {e}") +```fish +python -m pyaegis.benchmark ``` -## Performance +## Errors -The library automatically detects CPU features at runtime and uses the most optimized implementation available: +- Authentication failures raise ValueError. +- Invalid sizes/types raise TypeError. +- Unexpected errors from libaegis raise RuntimeError. -- AES-NI on Intel/AMD processors -- ARM Crypto Extensions on ARM processors -- AVX2 and AVX-512 for multi-lane variants -- Software fallback for other platforms +## Security notes -Multi-lane variants (X2, X4) provide higher throughput on systems with appropriate SIMD support. - -## Security Considerations - -- **Nonce Uniqueness**: Never reuse a nonce with the same key. If you can't maintain a counter, generate random nonces for each message. -- **Key Management**: Generate cryptographically secure keys. Keep keys secret. -- **AAD**: Additional authenticated data is not encrypted but is protected against tampering. -- **MAC vs AEAD**: Use AEAD for encryption needs. MAC-only variants are for authentication without confidentiality. +- Never reuse a nonce with the same key. Prefer a.random_nonce() per message. +- Keep keys secret; use a.random_key() to get correctly sized keys. +- AAD (ad=...) is authenticated but not encrypted. +- Do not use stream() or unauthenticated helpers for real data protection; they are for testing and specialized cases.