This commit is contained in:
Leo Vasanko
2025-11-06 21:11:38 -06:00
parent 1b4d43a448
commit 02eb4d7718
+151 -166
View File
@@ -3,220 +3,205 @@
[![PyPI version](https://badge.fury.io/py/pyaegis.svg)](https://badge.fury.io/py/pyaegis) [![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) [![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. - PyPI (recommended):
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:
```bash ```bash
pip install pyaegis 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 All submodules expose the same API; pick one for your key/nonce size and platform:
# Clone the repository
git clone https://github.com/LeoVasanko/pyaegis.git
cd pyaegis
# Install with uv (compiles C sources automatically) - aegis128l (16-byte key, 16-byte nonce)
uv pip install . - 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 ## Quick start
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
```python ```python
import pyaegis.aegis128l as a from pyaegis import aegis128x4 as ciph
key = b"K" * a.KEYBYTES # 16 bytes for aegis128l key = ciph.random_key()
nonce = b"N" * a.NPUBBYTES # 16 bytes for aegis128l nonce = ciph.random_nonce()
plaintext = b"Hello, World!" msg = b"hello"
ciphertext = a.encrypt(key, nonce, plaintext) ct = ciph.encrypt(key, nonce, msg)
decrypted = a.decrypt(key, nonce, ciphertext) pt = ciph.decrypt(key, nonce, ct)
assert decrypted == plaintext 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 ```python
ciphertext = a.encrypt(key, nonce, plaintext, ad=b"metadata") from pyaegis import aegis256x4 as a
plaintext = a.decrypt(key, nonce, ciphertext, ad=b"metadata") 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 ```python
ciphertext, mac = a.encrypt_detached(key, nonce, b"secret") from pyaegis import aegis256x4 as a
plaintext = a.decrypt_detached(key, nonce, ciphertext, mac) 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 MAC-only:
Use 16 or 32-byte MACs (default 16):
```python ```python
ciphertext, mac = a.encrypt_detached(key, nonce, message, maclen=32) from pyaegis import aegis256x4 as a
plaintext = a.decrypt_detached(key, nonce, ciphertext, mac, maclen=32) 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 ```python
buffer = bytearray(b"secret message") from pyaegis import aegis256x4 as a
mac = a.encrypt_unauthenticated_into(key, nonce, buffer) key, nonce = a.random_key(), a.random_nonce()
# buffer now contains ciphertext msg = b"data"
a.decrypt_unauthenticated_into(key, nonce, buffer, mac) out = bytearray(len(msg) + 16)
# buffer now contains plaintext again 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 ```python
# Pre-allocate output buffer from pyaegis import aegis256x4 as a
output = bytearray(len(plaintext) + 16) # +16 for MAC key, nonce = a.random_key(), a.random_nonce()
result = a.encrypt(key, nonce, plaintext, into=output)
# result is a memoryview of the output buffer
```
### 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 # Decrypt back in-place (plaintext written over the start region)
random_bytes = a.stream(key, nonce, 1024) a.decrypt(key, nonce, buf, into=m) # uses default maclen=16
``` assert bytes(m) == msg
### Incremental Encryption # Detached mode: ciphertext written back to the same buffer
buf2 = bytearray(len(msg))
```python buf2[:] = msg
encryptor = a.Encryptor(key, nonce, ad=b"header") m2 = memoryview(buf2)
ciphertext1 = encryptor.update(b"chunk1") ct_view, mac = a.encrypt_detached(key, nonce, m2, ct_into=buf2)
ciphertext2 = encryptor.update(b"chunk2") a.decrypt_detached(key, nonce, ct_view, mac, into=m2)
final_bytes = encryptor.final() # includes MAC assert bytes(m2) == msg
``` ```
## Performance ## 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 Run the built-in benchmark to see which variant is fastest on your machine:
- ARM Crypto Extensions on ARM processors
- AVX2 and AVX-512 for multi-lane variants
- Software fallback for other platforms
Multi-lane variants (X2, X4) provide higher throughput on systems with appropriate SIMD support. ```fish
python -m pyaegis.benchmark
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}")
``` ```
## 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 ## Security notes
- ARM Crypto Extensions on ARM processors
- AVX2 and AVX-512 for multi-lane variants
- Software fallback for other platforms
Multi-lane variants (X2, X4) provide higher throughput on systems with appropriate SIMD support. - 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.
## Security Considerations - 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.
- **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.