Metadata-Version: 2.4
Name: pyaegis
Version: 0.3.1
Summary: Python bindings for libaegis - high-performance AEGIS authenticated encryption
Author-email: Frank Denis <github@pureftpd.org>
License: MIT
Project-URL: Homepage, https://github.com/jedisct1/pyaegis
Project-URL: Repository, https://github.com/jedisct1/pyaegis
Project-URL: Issues, https://github.com/jedisct1/pyaegis/issues
Project-URL: Changelog, https://github.com/jedisct1/pyaegis/blob/main/CHANGELOG.md
Keywords: aegis,encryption,authenticated-encryption,aead,cryptography,cffi,security,crypto
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: C
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cffi>=2.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.4.2; extra == "dev"
Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
Requires-Dist: build>=1.3.0; extra == "dev"
Requires-Dist: twine>=6.2.0; extra == "dev"
Requires-Dist: ruff>=0.14.2; extra == "dev"
Requires-Dist: mypy>=1.18.2; extra == "dev"
Requires-Dist: pre-commit>=4.3.0; extra == "dev"
Provides-Extra: test
Requires-Dist: pytest>=8.4.2; extra == "test"
Requires-Dist: pytest-cov>=7.0.0; extra == "test"
Dynamic: license-file

# 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/jedisct1/pyaegis/blob/main/LICENSE)

Python bindings for libaegis - high-performance AEGIS authenticated encryption.

- [pyaegis](#pyaegis)
  - [Overview](#overview)
    - [Supported Variants](#supported-variants)
      - [Authenticated Encryption (AEAD)](#authenticated-encryption-aead)
      - [Message Authentication Codes (MAC)](#message-authentication-codes-mac)
  - [Installation](#installation)
    - [From PyPI](#from-pypi)
    - [From Source](#from-source)
    - [Building a Distribution](#building-a-distribution)
  - [Usage](#usage)
    - [Basic Encryption/Decryption](#basic-encryptiondecryption)
    - [With Additional Authenticated Data (AAD)](#with-additional-authenticated-data-aad)
    - [Detached Tag Mode](#detached-tag-mode)
    - [Pre-allocated Buffers](#pre-allocated-buffers)
    - [Tag Size](#tag-size)
    - [In-Place Encryption/Decryption](#in-place-encryptiondecryption)
    - [Streaming Encryption/Decryption](#streaming-encryptiondecryption)
      - [Streaming Encryption](#streaming-encryption)
      - [Streaming Decryption](#streaming-decryption)
    - [Stream Generation](#stream-generation)
    - [Message Authentication Code (MAC)](#message-authentication-code-mac)
  - [Random Access Files (RAF)](#random-access-files-raf)
    - [Basic Usage](#basic-usage)
    - [Context-bound Keys](#context-bound-keys)
    - [File-based Storage](#file-based-storage)
    - [Random Access Operations](#random-access-operations)
    - [Auto-detecting Algorithm](#auto-detecting-algorithm)
    - [Available RAF Classes](#available-raf-classes)
    - [Merkle Tree Commitment](#merkle-tree-commitment)
    - [Storage Backends](#storage-backends)
  - [Error Handling](#error-handling)
  - [Performance](#performance)
  - [Security Considerations](#security-considerations)

## Overview

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.

### Supported Variants

#### Authenticated Encryption (AEAD)

- 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)

#### Message Authentication Codes (MAC)

All AEAD variants have corresponding MAC variants for authentication without encryption:

- AegisMac128L, AegisMac256
- AegisMac128X2, AegisMac128X4
- AegisMac256X2, AegisMac256X4

## Installation

### From PyPI

Using [uv](https://docs.astral.sh/uv/):

```bash
uv pip install pyaegis
```

Or using pip:

```bash
pip install pyaegis
```

### From Source

The package compiles the C library automatically using any installed C compiler:

```bash
# 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:

```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

### Basic Encryption/Decryption

```python
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 == plaintext
```

### With Additional Authenticated Data (AAD)

```python
from 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)
```

### Detached Tag Mode

```python
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)
```

### Pre-allocated Buffers

For performance-sensitive applications, you can provide pre-allocated buffers to avoid memory allocation:

```python
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
ciphertext = bytes(output_buffer)  # Convert to bytes for decrypt input
plaintext_buffer = bytearray(len(ciphertext) - cipher.tag_size)
cipher.decrypt(key, nonce, ciphertext, 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)
```

### Tag Size

By default, a 32-byte (256-bit) tag is used for maximum security. You can also use a 16-byte (128-bit) tag:

```python
cipher = Aegis128L(tag_size=16)
```

### In-Place Encryption/Decryption

For performance-critical applications, especially when working with large buffers (>10MB), in-place operations can provide 30-50% performance improvement by reducing memory bandwidth:

```python
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 again
```

In-place operations work with `bytearray` or `memoryview` objects and overwrite the input buffer directly. If decryption fails, the buffer is zeroed for security.

### Streaming Encryption/Decryption

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.

#### Streaming Encryption

```python
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 recipient
```

#### Streaming Decryption

```python
from 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` / `AegisStreamDecrypt128L`
- `AegisStreamEncrypt256` / `AegisStreamDecrypt256`
- `AegisStreamEncrypt128X2` / `AegisStreamDecrypt128X2`
- `AegisStreamEncrypt128X4` / `AegisStreamDecrypt128X4`
- `AegisStreamEncrypt256X2` / `AegisStreamDecrypt256X2`
- `AegisStreamEncrypt256X4` / `AegisStreamDecrypt256X4`

### Stream Generation

Generate a deterministic pseudo-random byte sequence (AEGIS-128L and AEGIS-256 only):

```python
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)
```

### Message Authentication Code (MAC)

Generate and verify authentication tags without encryption:

```python
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.

## Random Access Files (RAF)

The RAF API provides encrypted file storage with random access capabilities. Files are divided into fixed-size chunks, each independently encrypted, enabling efficient random access without decrypting the entire file.

### Basic Usage

```python
from pyaegis import AegisRaf128L, BytesIOStorage

# Create an in-memory storage backend
storage = BytesIOStorage()
key = AegisRaf128L.random_key()

# Create and write to an encrypted file
with AegisRaf128L(storage, key, create=True) as f:
    f.write(b"Hello, World!")
    f.write(b" More data.")

# Reopen and read
with AegisRaf128L(storage, key) as f:
    print(f.read())  # b'Hello, World! More data.'
```

### Context-bound Keys

Applications can derive separate RAF keys for different files or file families from one application master key.
The context identifies the file family and does not need to be secret.

```python
from pyaegis import AegisRaf128L, BytesIOStorage, raf_derive_master_key

app_master_key = AegisRaf128L.random_key()
context = b"customer-42/archive"
raf_key = raf_derive_master_key(app_master_key, context)

storage = BytesIOStorage()
with AegisRaf128L(storage, raf_key, create=True) as f:
    f.write(b"Secret data")

# Derive the same key with the same context when reopening the file.
raf_key = raf_derive_master_key(app_master_key, context)
with AegisRaf128L(storage, raf_key) as f:
    print(f.read())
```

Different contexts produce different keys.
An empty context is valid and still produces a RAF-specific key.
Contexts can contain up to 120 bytes with a 16-byte master key or 72 bytes with a 32-byte master key.

### File-based Storage

```python
from pyaegis import AegisRaf128L, FileStorage

key = AegisRaf128L.random_key()

# Create encrypted file on disk
with FileStorage("/path/to/file.raf", "w+b") as storage, \
        AegisRaf128L(storage, key, create=True) as f:
    f.write(b"Secret data")

# Read from encrypted file
with FileStorage("/path/to/file.raf", "r+b") as storage, \
        AegisRaf128L(storage, key) as f:
    print(f.read())
```

### Random Access Operations

```python
from pyaegis import AegisRaf128L, BytesIOStorage

storage = BytesIOStorage()
key = AegisRaf128L.random_key()

with AegisRaf128L(storage, key, create=True) as f:
    f.write(b"0123456789")

    # Seek and read
    f.seek(5)
    print(f.read(3))  # b'567'

    # pread - read without changing position
    print(f.pread(3, 0))  # b'012'
    print(f.tell())  # 8 (unchanged)

    # pwrite - write without changing position
    f.pwrite(b"ABC", 3)
    f.seek(0)
    print(f.read())  # b'012ABC6789'
```

### Auto-detecting Algorithm

```python
from pyaegis import raf_open, raf_probe, BytesIOStorage, AegisRaf256

storage = BytesIOStorage()
key = AegisRaf256.random_key()

# Create with AEGIS-256
with AegisRaf256(storage, key, create=True) as f:
    f.write(b"data")

# Probe to see file parameters (without key)
alg_id, chunk_size, file_size = raf_probe(storage)
print(f"Algorithm: {alg_id}, Chunk size: {chunk_size}, Size: {file_size}")

# Auto-detect and open with correct class
with raf_open(storage, key) as f:
    print(f.read())  # b'data'
```

### Available RAF Classes

- `AegisRaf128L`: 16-byte key
- `AegisRaf256`: 32-byte key
- `AegisRaf128X2`, `AegisRaf128X4`: 16-byte key, multi-lane
- `AegisRaf256X2`, `AegisRaf256X4`: 32-byte key, multi-lane

### Merkle Tree Commitment

Individual RAF chunks are already authenticated by their AEAD tags. Optionally, a Merkle hash tree can track whole-file commitment: every write automatically updates a binary hash tree in memory, maintaining a single root hash that represents the current plaintext content of the entire file. This root can be stored externally (e.g. in a database) and later used to detect any modification to the file.

```python
from pyaegis import AegisRaf128L, BytesIOStorage

storage = BytesIOStorage()
key = AegisRaf128L.random_key()

# Create with Merkle tree enabled (uses SHA-256 by default)
with AegisRaf128L(storage, key, create=True, merkle=True) as f:
    f.write(b"important data")
    root = f.root_hash  # 32-byte SHA-256 root hash
    print(f"Root hash: {root.hex()}")

# Later: reopen and verify integrity
with AegisRaf128L(storage, key, merkle=True) as f:
    f.verify_root(root)  # raises RAFAuthenticationError on mismatch
```

For more control, you can call `merkle_rebuild()` and `merkle_verify()` separately:

```python
with AegisRaf128L(storage, key, merkle=True) as f:
    # Rebuild tree from file contents (decrypts every chunk)
    f.merkle_rebuild()

    # Verify all chunks against the in-memory tree
    corrupted = f.merkle_verify()  # None if clean, chunk index if corrupted
```

The `merkle_max_chunks` parameter controls how many chunks the tree can track (default: 16384, covering ~1 GiB at 64 KB chunks). For larger files, pass a higher value:

```python
with AegisRaf128L(storage, key, create=True, merkle=True, merkle_max_chunks=100000) as f:
    ...
```

Custom hash functions can be used by passing a `MerkleHasher` instance instead of `True`:

```python
from pyaegis import SHA256MerkleHasher

# The default hasher (equivalent to merkle=True)
hasher = SHA256MerkleHasher()

# Or implement your own: any object with hash_len, hash_leaf(),
# hash_parent(), hash_empty(), and hash_commitment() methods works.

with AegisRaf128L(storage, key, create=True, merkle=hasher) as f:
    ...
```

Merkle support works with `raf_open()` too:

```python
from pyaegis import raf_open

with raf_open(storage, key, merkle=True) as f:
    f.verify_root(expected_root)
```

### Storage Backends

- `FileStorage`: File-based storage using `os.pread`/`os.pwrite` (Unix only)
- `BytesIOStorage`: In-memory storage for testing

Custom backends can be implemented by following the `RAFStorage` protocol.

## Error Handling

AEAD and streaming operations raise `DecryptionError` on authentication failure:

```python
from pyaegis import Aegis128L, DecryptionError

cipher = Aegis128L()
key = cipher.random_key()
nonce = cipher.random_nonce()

try:
    plaintext = cipher.decrypt(key, nonce, tampered_ciphertext)
except DecryptionError:
    print("Authentication failed - ciphertext was tampered with!")
```

RAF operations use a separate exception hierarchy:

- `RAFAuthenticationError` -- chunk authentication failed (corruption or tampering)
- `RAFIOError` -- I/O failure during read/write
- `RAFConfigError` -- invalid configuration (bad chunk size, Merkle overflow, etc.)

```python
from pyaegis import AegisRaf128L, BytesIOStorage, RAFAuthenticationError

storage = BytesIOStorage()
key = AegisRaf128L.random_key()

with AegisRaf128L(storage, key, create=True) as f:
    f.write(b"data")

try:
    wrong_key = AegisRaf128L.random_key()
    with AegisRaf128L(storage, wrong_key) as f:
        f.read()
except RAFAuthenticationError:
    print("Wrong key or corrupted file!")
```

All exceptions inherit from `AegisError`.

## Performance

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.

## Security Considerations

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