README
This commit is contained in:
@@ -3,220 +3,205 @@
|
|||||||
[](https://badge.fury.io/py/pyaegis)
|
[](https://badge.fury.io/py/pyaegis)
|
||||||
[](https://github.com/LeoVasanko/pyaegis/blob/main/libaegis/LICENSE)
|
[](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.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user