4 Commits
v0.3.0 ... main

Author SHA1 Message Date
Leo Vasanko
88efc4cabc Update pyproject, bump version. 2025-11-09 20:41:40 -06:00
Leo Vasanko
04b11e9925 README tuning. New benchmark results (a bit slower than initial versions were). 2025-11-09 20:40:04 -06:00
Leo Vasanko
d8a9a7ee9d Use a much faster method to wipe buffers. 2025-11-09 20:38:28 -06:00
Leo Vasanko
f5430a6ad4 Cleanup. 2025-11-09 20:00:12 -06:00
4 changed files with 38 additions and 47 deletions

View File

@@ -49,7 +49,7 @@ Common parameters and returns (applies to all items below):
- 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. The inputs can be any Buffer supporting len() (e.g. `bytes`, `bytearray`, `memoryview`).
Only the first few can be positional arguments that are always provided in this order. All arguments can be passed as kwargs. The inputs can be any Buffer (e.g. `bytes`, `bytearray`, `memoryview`).
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.
@@ -181,16 +181,17 @@ dec.final(mac) # raises ValueError on failure
It is often practical to split larger messages into frames that can be individually decrypted and verified. Because every frame needs a different key, we employ the `nonce_increment` utility function to produce sequential nonces for each frame. As for the AEGIS algorithm, each frame is a completely independent invocation. The program will each time produce a completely different random-looking encrypted.bin file.
```python
# Encryption settings
from pyaegis import aegis128x4 as ciph
message = bytearray(30 * b"Attack at dawn! ")
key = b"sixteenbyte key!" # 16 bytes secret key for aegis128* algorithms
nonce = ciph.random_nonce()
framebytes = 80 # In real applications 1 MiB or more is practical
maclen = ciph.MACBYTES # 16
message = bytearray(30 * b"Attack at dawn! ")
with open("encrypted.bin", "wb") as f:
f.write(nonce) # Public initial nonce sent with the ciphertext
# Public initial nonce sent with the ciphertext
nonce = ciph.random_nonce()
f.write(nonce)
while message:
chunk = message[:framebytes - maclen]
del message[:len(chunk)]
@@ -200,9 +201,8 @@ with open("encrypted.bin", "wb") as f:
```
```python
from pyaegis import aegis128x4 as ciph
# Decryption needs same values as encryption
from pyaegis import aegis128x4 as ciph
key = b"sixteenbyte key!"
framebytes = 80
maclen = ciph.MACBYTES
@@ -239,7 +239,7 @@ Note: this is seekable by converting the block number to nonce with `idx.to_byte
### Preallocated output buffers (into=)
For advanced use cases, the output buffer can be supplied with `into` kwarg. Any type of writable buffer with len() >= space required can be used. This includes bytearrays, memoryviews, mmap files, numpy.getbuffer etc.
For advanced use cases, the output buffer can be supplied with `into` kwarg. Any type of writable buffer with a sufficient number of bytes can be used. This includes bytearrays, memoryviews, mmap files, numpy arrays etc.
A `TypeError` is raised if the buffer is too small. For convenience, the functions return a memoryview showing only the bytes actually written.
@@ -249,12 +249,12 @@ Foreign arrays can be used. This example fills a Numpy array with random integer
import numpy as np
from pyaegis import aegis128x4 as ciph
key, nonce = ciph.random_key(), ciph.random_nonce()
arr = np.empty(10, dtype=np.uint64) # Uninitialised integer array
ciph.stream(key, nonce, into=arr) # Fill with random bytes
print(arr)
```
In-place operations are supported when the input and the output point to the same location in memory. When using attached MAC tag, the input buffer needs to be sliced to correct length:
```python
@@ -276,28 +276,22 @@ Detached and unauthenticated modes can use same size input and output (no MAC ad
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.
Run the built-in benchmark to see which variant is fastest on your machine:
Benchmarks using the included benchmark module, run on Intel i7-14700, linux, single core (the software is not multithreaded). Note that the results are in megabits per second, not bytes. The CPU lacks AVX-512 that makes the X4 variants faster on AMD hardware.
```fish
uv run -m pyaegis.benchmark
```
Benchmarks of the Python module and the C library run on Intel i7-14700, linux, single core (the software is not multithreaded). Note that the results are in megabits per second, not bytes. The CPU lacks AVX-512 that makes the X4 variants faster on AMD hardware.
```fish
$ python -m pyaegis.benchmark
AEGIS-256 107666.56 Mb/s
AEGIS-256X2 191314.53 Mb/s
AEGIS-256X4 211537.44 Mb/s
AEGIS-128L 159074.08 Mb/s
AEGIS-128X2 307332.53 Mb/s
AEGIS-128X4 230106.70 Mb/s
AEGIS-128L MAC 206082.24 Mb/s
AEGIS-128X2 MAC 366401.20 Mb/s
AEGIS-128X4 MAC 375011.51 Mb/s
AEGIS-256 MAC 110187.03 Mb/s
AEGIS-256X2 MAC 210063.51 Mb/s
AEGIS-256X4 MAC 347406.96 Mb/s
$ uv run -m pyaegis.benchmark
AEGIS-256 103166.24 Mb/s
AEGIS-256X2 184225.50 Mb/s
AEGIS-256X4 194018.26 Mb/s
AEGIS-128L 161551.73 Mb/s
AEGIS-128X2 281987.80 Mb/s
AEGIS-128X4 217997.37 Mb/s
AEGIS-128L MAC 188886.40 Mb/s
AEGIS-128X2 MAC 306457.97 Mb/s
AEGIS-128X4 MAC 299576.59 Mb/s
AEGIS-256 MAC 100914.04 Mb/s
AEGIS-256X2 MAC 190208.20 Mb/s
AEGIS-256X4 MAC 315919.87 Mb/s
```
The Python library performance is similar to that of the C library:

View File

@@ -295,9 +295,7 @@ def decrypt(
out = bytearray(expected_out)
else:
if into.nbytes < expected_out:
raise TypeError(
"into length must be at least ct.nbytes - maclen"
)
raise TypeError("into length must be at least ct.nbytes - maclen")
out = into
rc = _lib.aegis256x4_decrypt(
@@ -582,7 +580,9 @@ class Mac:
out = into
clone = self.clone()
rc = _lib.aegis256x4_mac_final(clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).nbytes)
rc = _lib.aegis256x4_mac_final(
clone._proxy.ptr, ffi.from_buffer(out), memoryview(out).nbytes
)
if rc != 0:
err_num = ffi.errno
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
@@ -851,7 +851,9 @@ class Decryptor:
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
raise RuntimeError(f"state decrypt update failed: {err_name}")
w = int(written[0])
assert w == expected_out, f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}"
assert w == expected_out, (
f"got {w}, expected {expected_out}, ct.nbytes={ct.nbytes}"
)
return out if into is None else memoryview(out)[:w] # type: ignore
def final(self, mac: Buffer) -> None:

View File

@@ -11,12 +11,9 @@ from ._loader import ffi
__all__ = ["new_aligned_struct", "aligned_address", "Buffer", "nonce_increment", "wipe"]
try:
from collections.abc import Buffer as _Buffer # type: ignore[misc]
class Buffer(_Buffer, Protocol): # type: ignore[misc]
pass
from collections.abc import Buffer # type: ignore
except ImportError:
# Fallback for Python < 3.12
class Buffer(Protocol):
def __buffer__(self, flags: int) -> memoryview: ...
@@ -75,13 +72,11 @@ def nonce_increment(nonce: Buffer) -> None:
def wipe(buffer: Buffer) -> None:
"""Set all bytes of the input buffer to zero.
Useful for securely clearing sensitive data from memory.
"""Securely clearing sensitive data from memory. Sets all bytes of the buffer to 0xFF.
Args:
buffer: The buffer to wipe (modified in place).
"""
n = memoryview(buffer)
for i in range(len(n)):
n[i] = 0
# This is the fastest method I have found in Python
n = memoryview(buffer).cast("B")
n[:] = b"\xff" * len(n)

View File

@@ -5,7 +5,7 @@ backend-path = ["tools"]
[project]
name = "pyaegis"
version = "0.3.0"
version = "0.3.1"
description = "Python bindings for libaegis"
requires-python = ">=3.10"
classifiers = [
@@ -20,7 +20,7 @@ dependencies = [
]
[project.urls]
Homepage = "https://github.com/aegis-aead/libaegis"
Homepage = "https://github.com/LeoVasanko/pyaegis"
[dependency-groups]
dev = [