Python bindings for libaegis - high-performance AEGIS authenticated encryption.
pyaegis provides Pythonic interfaces to the AEGIS family of authenticated encryption algorithms.
AEGIS is a high-performance authenticated cipher that provides both confidentiality and authenticity guarantees.
- AEGIS-128L: 16-byte key, 16-byte nonce
- AEGIS-256: 32-byte key, 32-byte nonce
- AEGIS-128X2: 16-byte key, 16-byte nonce (recommended on most platforms)
- AEGIS-128X4: 16-byte key, 16-byte nonce (recommended on high-end Intel CPUs)
- AEGIS-256X2: 32-byte key, 32-byte nonce
- AEGIS-256X4: 32-byte key, 32-byte nonce (recommended if a 256-bit nonce is required)
All AEAD variants have corresponding MAC variants for authentication without encryption:
- AegisMac128L, AegisMac256
- AegisMac128X2, AegisMac128X4
- AegisMac256X2, AegisMac256X4
Using uv:
uv pip install pyaegisOr using pip:
pip install pyaegisThe package compiles the C library automatically using any installed C compiler:
# Clone the repository
git clone https://github.com/jedisct1/pyaegis.git
cd pyaegis
# Install with uv (compiles C sources automatically)
uv pip install .
# Or for development
uv pip install -e .Alternatively with pip:
pip install .
# Or for development
pip install -e .# With uv
uv run python -m build
# Or with pip
python -m buildThis creates both source and wheel distributions in the dist/ directory. The C sources are bundled in the package and compiled during installation.
from pyaegis import Aegis128L
# Create a cipher instance
cipher = Aegis128L()
# Generate random key and nonce
key = cipher.random_key()
nonce = cipher.random_nonce()
# Encrypt a message
plaintext = b"Hello, World!"
ciphertext = cipher.encrypt(key, nonce, plaintext)
# Decrypt the message
decrypted = cipher.decrypt(key, nonce, ciphertext)
assert decrypted == plaintextfrom pyaegis import Aegis256
cipher = Aegis256()
key = cipher.random_key()
nonce = cipher.random_nonce()
# AAD is authenticated but not encrypted
associated_data = b"metadata"
ciphertext = cipher.encrypt(key, nonce, b"secret", associated_data=associated_data)
plaintext = cipher.decrypt(key, nonce, ciphertext, associated_data=associated_data)from pyaegis import Aegis128L
cipher = Aegis128L()
key = cipher.random_key()
nonce = cipher.random_nonce()
# Encrypt with detached tag
ciphertext, tag = cipher.encrypt_detached(key, nonce, b"secret")
# Decrypt with detached tag
plaintext = cipher.decrypt_detached(key, nonce, ciphertext, tag)For performance-sensitive applications, you can provide pre-allocated buffers to avoid memory allocation:
from pyaegis import Aegis128L
cipher = Aegis128L()
key = cipher.random_key()
nonce = cipher.random_nonce()
plaintext = b"secret message"
# Pre-allocate output buffer for encryption
output_buffer = bytearray(len(plaintext) + cipher.tag_size)
cipher.encrypt(key, nonce, plaintext, into=output_buffer)
# Pre-allocate output buffer for decryption
plaintext_buffer = bytearray(len(output_buffer) - cipher.tag_size)
cipher.decrypt(key, nonce, output_buffer, into=plaintext_buffer)
# Also works with encrypt_detached
ciphertext_buffer = bytearray(len(plaintext))
ciphertext, tag = cipher.encrypt_detached(key, nonce, plaintext, ciphertext_into=ciphertext_buffer)By default, a 32-byte (256-bit) tag is used for maximum security. You can also use a 16-byte (128-bit) tag:
cipher = Aegis128L(tag_size=16)For performance-critical applications, especially when working with large buffers (>10MB), in-place operations can provide 30-50% performance improvement by reducing memory bandwidth:
from pyaegis import Aegis128X4
cipher = Aegis128X4()
key = cipher.random_key()
nonce = cipher.random_nonce()
# Encrypt in-place
buffer = bytearray(b"secret message")
tag = cipher.encrypt_inplace(key, nonce, buffer)
# buffer now contains ciphertext
# Decrypt in-place
cipher.decrypt_inplace(key, nonce, buffer, tag)
# buffer now contains plaintext againIn-place operations work with bytearray or memoryview objects and overwrite the input buffer directly. If decryption fails, the buffer is zeroed for security.
For processing large data in chunks or when data arrives incrementally, use the streaming encryption/decryption classes. These allow you to encrypt or decrypt data piece by piece without loading everything into memory at once.
from pyaegis import AegisStreamEncrypt128L
key = AegisStreamEncrypt128L.random_key()
nonce = AegisStreamEncrypt128L.random_nonce()
# Create a streaming encryption context
with AegisStreamEncrypt128L(key, nonce, associated_data=b"metadata") as enc:
# Encrypt data in chunks
ciphertext1 = enc.update(b"first chunk of data")
ciphertext2 = enc.update(b"second chunk of data")
# Finalize and get the authentication tag
tag = enc.final()
# Send ciphertext1 + ciphertext2 + tag to the recipientfrom pyaegis import AegisStreamDecrypt128L, DecryptionError
# Create a streaming decryption context
with AegisStreamDecrypt128L(key, nonce, associated_data=b"metadata") as dec:
# Decrypt data in chunks
dec.update(ciphertext1)
dec.update(ciphertext2)
# Verify the authentication tag and get all plaintext
try:
plaintext = dec.verify(tag)
# plaintext is only released after successful verification
except DecryptionError:
print("Authentication failed!")Security Note: The streaming decryption API buffers all plaintext internally and only releases it after successful tag verification. This prevents using unauthenticated data.
Available Streaming Classes:
AegisStreamEncrypt128L/AegisStreamDecrypt128LAegisStreamEncrypt256/AegisStreamDecrypt256AegisStreamEncrypt128X2/AegisStreamDecrypt128X2AegisStreamEncrypt128X4/AegisStreamDecrypt128X4AegisStreamEncrypt256X2/AegisStreamDecrypt256X2AegisStreamEncrypt256X4/AegisStreamDecrypt256X4
Generate a deterministic pseudo-random byte sequence (AEGIS-128L and AEGIS-256 only):
from pyaegis import Aegis128L
key = Aegis128L.random_key()
nonce = Aegis128L.random_nonce()
# Generate 1024 pseudo-random bytes
random_bytes = Aegis128L.stream(key, nonce, 1024)
# With pre-allocated buffer for better performance
buffer = bytearray(1024)
random_bytes = Aegis128L.stream(key, nonce, 1024, into=buffer)Generate and verify authentication tags without encryption:
from pyaegis import AegisMac128L, DecryptionError
key = AegisMac128L.random_key()
nonce = AegisMac128L.random_nonce()
# Generate MAC tag
mac = AegisMac128L(key, nonce)
mac.update(b"message part 1")
mac.update(b"message part 2")
tag = mac.final()
# Verify MAC tag
mac_verify = AegisMac128L(key, nonce)
mac_verify.update(b"message part 1message part 2")
try:
mac_verify.verify(tag)
print("Authentication successful!")
except DecryptionError:
print("Authentication failed!")Important: The same key must NOT be used for both MAC and encryption operations.
from pyaegis import Aegis128L, DecryptionError
cipher = Aegis128L()
key = cipher.random_key()
nonce = cipher.random_nonce()
try:
# This will raise DecryptionError if authentication fails
plaintext = cipher.decrypt(key, nonce, tampered_ciphertext)
except DecryptionError:
print("Authentication failed - ciphertext was tampered with!")The library automatically detects CPU features at runtime and uses the most optimized implementation available:
- 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
Multi-lane variants (X2, X4) provide higher throughput on systems with appropriate SIMD support.
- Nonce Uniqueness: Never reuse a nonce with the same key. If you can't maintain a counter, use
random_nonce()for each message. - Key Management: Use
random_key()to generate cryptographically secure keys. Keep keys secret. - AAD: Additional authenticated data is not encrypted but is protected against tampering.