This commit is contained in:
Leo Vasanko
2025-11-06 21:11:38 -06:00
parent 1b4d43a448
commit 02eb4d7718

317
README.md
View File

@@ -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.