Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1bfeb2434 | ||
|
|
a26ce6a9fb | ||
|
|
7e6a5bf52a | ||
|
|
8018e68274 | ||
|
|
ccccfd7918 | ||
|
|
4de66b6bc6 | ||
|
|
66338ef416 | ||
|
|
509898ac73 | ||
|
|
67ec091bc5 | ||
|
|
4977a7e79d | ||
|
|
d7acf7a315 | ||
|
|
7f01de8327 | ||
|
|
9294fba86d | ||
|
|
348812e750 | ||
|
|
22dff32b22 | ||
|
|
178b689a35 | ||
|
|
bd8b183e32 | ||
|
|
daba717ebb | ||
|
|
5b74bcb683 | ||
|
|
d9a18f43d5 | ||
|
|
98ee4ab7b3 | ||
|
|
c6148806dd | ||
|
|
6e6bc49466 | ||
|
|
c2ac2e2790 | ||
|
|
c6e77cac66 | ||
|
|
df1acadb1f | ||
|
|
cb0f9956c3 | ||
|
|
a29405455a | ||
|
|
0d63844201 | ||
|
|
7682d5bc8b | ||
|
|
26b883ecbc | ||
|
|
87d9a07868 | ||
|
|
eed36e2463 | ||
|
|
46bc05f547 | ||
|
|
56d304a82e | ||
|
|
28ee0e1314 | ||
|
|
1f506806d4 | ||
|
|
17fba3c2f2 | ||
|
|
d04c593766 | ||
|
|
fce6324ae5 | ||
|
|
b35dc75513 | ||
|
|
1da3000a22 | ||
|
|
f983cbe4c5 | ||
|
|
f0b4373c0e | ||
|
|
0efc2595d8 | ||
|
|
a78f5c4ea3 | ||
|
|
fc1025f56d | ||
|
|
3d75cc7e55 | ||
|
|
5b49422cb7 | ||
|
|
cca78f4224 | ||
|
|
2e460b5796 | ||
|
|
407cedc7a9 | ||
|
|
b5b939cffe | ||
|
|
5c04940525 | ||
|
|
e644f1b2ca | ||
|
|
56b451ac0c | ||
|
|
93bdbc26a2 | ||
|
|
2612dc8187 | ||
|
|
302429b7a4 | ||
|
|
b83120187c | ||
|
|
e3508025f5 | ||
|
|
3767b34e2c | ||
|
|
0ae8752666 | ||
|
|
d51736f5ba | ||
|
|
df6b1b2832 | ||
|
|
c16e785203 | ||
|
|
2ffe7b6c0e | ||
|
|
618d9e0967 | ||
|
|
299586e6ee | ||
|
|
22299a1676 |
+3
-3
@@ -3,9 +3,9 @@
|
|||||||
*.egg-info
|
*.egg-info
|
||||||
/dist
|
/dist
|
||||||
/build
|
/build
|
||||||
/pyaegis/build
|
/src/aeg/build
|
||||||
/pyaegis/_aegis.*.so
|
/src/aeg/_aegis*.so
|
||||||
/pyaegis/_aegis.*.pyd
|
/src/aeg/_aegis*.pyd
|
||||||
__pycache__
|
__pycache__
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!.gitmodules
|
!.gitmodules
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Building pyaegis
|
# Building aeg
|
||||||
|
|
||||||
This document contains instructions for developers who want to build pyaegis from source.
|
This document contains instructions for developers who want to build aeg from source.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ This document contains instructions for developers who want to build pyaegis fro
|
|||||||
|
|
||||||
### Installing Zig
|
### Installing Zig
|
||||||
|
|
||||||
pyaegis uses Zig to build the underlying libaegis C library. Install Zig from [ziglang.org/download](https://ziglang.org/download/) or using your package manager:
|
aeg uses Zig to build the underlying libaegis C library. Install Zig from [ziglang.org/download](https://ziglang.org/download/) or using your package manager:
|
||||||
|
|
||||||
- **macOS**: `brew install zig`
|
- **macOS**: `brew install zig`
|
||||||
- **Linux**: See [Zig installation guide](https://github.com/ziglang/zig/wiki/Install-Zig-from-a-Package-Manager)
|
- **Linux**: See [Zig installation guide](https://github.com/ziglang/zig/wiki/Install-Zig-from-a-Package-Manager)
|
||||||
@@ -22,8 +22,8 @@ pyaegis uses Zig to build the underlying libaegis C library. Install Zig from [z
|
|||||||
Clone the repository with submodules:
|
Clone the repository with submodules:
|
||||||
|
|
||||||
```fish
|
```fish
|
||||||
git clone --recursive https://github.com/LeoVasanko/pyaegis.git
|
git clone --recursive https://github.com/LeoVasanko/aeg.git
|
||||||
cd pyaegis
|
cd aeg
|
||||||
```
|
```
|
||||||
|
|
||||||
If you already cloned without `--recursive`, initialize submodules:
|
If you already cloned without `--recursive`, initialize submodules:
|
||||||
@@ -74,16 +74,10 @@ This creates files in the `dist/` directory.
|
|||||||
|
|
||||||
## Code Generation
|
## Code Generation
|
||||||
|
|
||||||
The Python modules are generated from templates. If you modify the core implementation in `pyaegis/aegis256x4.py`, regenerate the other variants:
|
The Python modules and CFFI definitions are generated from C sources and templates. If you modify the core implementation in `src/aeg/aegis256x4.py` or update libaegis headers, regenerate all files:
|
||||||
|
|
||||||
```fish
|
```fish
|
||||||
python tools/gen_modules.py
|
python tools/generate.py
|
||||||
```
|
|
||||||
|
|
||||||
If you update libaegis headers, regenerate the CFFI definitions:
|
|
||||||
|
|
||||||
```fish
|
|
||||||
python tools/gen_cdef.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
@@ -106,7 +100,7 @@ If you cannot install Zig, you may manually compile in the libaegis folder (Zig,
|
|||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
- `pyaegis/` - Python package source
|
- `src/aeg/` - Python package source
|
||||||
- `libaegis/` - C library source (submodule)
|
- `libaegis/` - C library source (submodule)
|
||||||
- `tests/` - Test suite
|
- `tests/` - Test suite
|
||||||
- `tools/` - Code generation scripts and `build_backend.py` used to build libaegis
|
- `tools/` - Code generation scripts and `build_backend.py` used to build libaegis
|
||||||
|
|||||||
+1
-3
@@ -1,13 +1,11 @@
|
|||||||
include pyaegis/aegis_cdef.h
|
include src/aeg/aegis_cdef.h
|
||||||
include setup.py
|
include setup.py
|
||||||
include tools/build_backend.py
|
include tools/build_backend.py
|
||||||
include BUILD.md
|
include BUILD.md
|
||||||
include README.md
|
include README.md
|
||||||
recursive-include libaegis *.c *.h *.zig *.zon
|
recursive-include libaegis *.c *.h *.zig *.zon
|
||||||
include libaegis/CMakeLists.txt
|
|
||||||
include libaegis/LICENSE
|
include libaegis/LICENSE
|
||||||
include libaegis/README.md
|
include libaegis/README.md
|
||||||
recursive-include libaegis/cmake *.cmake *.cmake.in
|
|
||||||
graft libaegis/src
|
graft libaegis/src
|
||||||
include libaegis/build.zig
|
include libaegis/build.zig
|
||||||
include libaegis/build.zig.zon
|
include libaegis/build.zig.zon
|
||||||
|
|||||||
@@ -1,33 +1,28 @@
|
|||||||
# pyaegis
|
# AEGIS Cipher Python Binding
|
||||||
|
|
||||||
[](https://badge.fury.io/py/pyaegis)
|
[](https://badge.fury.io/py/aeg)
|
||||||
|
|
||||||
Safe Python bindings for the AEGIS family of very fast authenticated encryption algorithms (via libaegis).
|
Safe Python bindings for the AEGIS family of very fast authenticated encryption algorithms via libaegis. The module runs without compilation required on Windows, Mac and Linux (has precompiled wheels). For other platforms compilation is performed at install time.
|
||||||
|
|
||||||
|
AEGIS enables extremely fast Encryption, MAC and CSPRNG - many times faster than AES, ChaCha20 or traditional random number generators. Authenticated Encryption with Additional Data is supported with the MAC derived from the cipher state at the end, making it different from other AEADs like AES-GCM and ChaCha20-Poly1305. The whole internal state thus depends on the prior data, and it is neither Encrypt-Then-Mac nor Mac-The-Encrypt scheme when both features are used together.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Using [uv](https://docs.astral.sh/uv/getting-started/installation/):
|
```sh
|
||||||
```fish
|
pip install aeg
|
||||||
uv pip install git+https://github.com/LeoVasanko/pyaegis.git
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For development builds, see BUILD.md.
|
Or add to your project using [UV](https://docs.astral.sh/uv/getting-started/installation/):
|
||||||
|
```sh
|
||||||
## Variants
|
uv add aeg
|
||||||
|
```
|
||||||
All submodules expose the same API; pick one for your key/nonce size and platform:
|
|
||||||
|
|
||||||
- aegis128l (16-byte key, 16-byte nonce)
|
|
||||||
- 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)
|
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Normal authenticated encryption using the AEGIS-128X4 algorithm:
|
Normal authenticated encryption using the AEGIS-128X4 algorithm:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pyaegis import aegis128x4 as ciph
|
from aeg import aegis128x4 as ciph
|
||||||
|
|
||||||
key = ciph.random_key() # Secret key (stored securely)
|
key = ciph.random_key() # Secret key (stored securely)
|
||||||
nonce = ciph.random_nonce() # Public nonce (recreated for each message)
|
nonce = ciph.random_nonce() # Public nonce (recreated for each message)
|
||||||
@@ -38,18 +33,39 @@ pt = ciph.decrypt(key, nonce, ct) # Raises ValueError if anything was tampered
|
|||||||
assert pt == msg
|
assert pt == msg
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Variants
|
||||||
|
|
||||||
|
All submodules expose the same API; pick one for your needs. The 256 bit variants offer maximal security and use larger key and nonce, while the 128 bit variants run slightly faster and use smaller key and nonce while still providing strong security. The MAC length does not depend on the variant. Note that the x2 and x4 variants are typically the fastest (depending on CPU) by utilizing SIMD multi-lane processing for the highest throughput.
|
||||||
|
|
||||||
|
| Variant | Key/Nonce Bytes | Notes |
|
||||||
|
|----------------|----------------:|-------------------------|
|
||||||
|
| **aegis128l** | 16 | |
|
||||||
|
| **aegis128x2** | 16 | Fastest on Intel Core |
|
||||||
|
| **aegis128x4** | 16 | Fastest on AMD and Xeon |
|
||||||
|
| **aegis256** | 32 | |
|
||||||
|
| **aegis256x2** | 32 | Fast on Intel Core |
|
||||||
|
| **aegis256x4** | 32 | Fast on AMD and Xeon |
|
||||||
|
|
||||||
|
Instead of importing the submodules, you can obtain one by its name string:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import aeg
|
||||||
|
|
||||||
|
ciph = aeg.cipher("AEGIS-128X2") # Also accepts "aegis128x2" and other forms
|
||||||
|
```
|
||||||
|
|
||||||
## API overview
|
## API overview
|
||||||
|
|
||||||
Common parameters and returns (applies to all items below):
|
Common parameters and returns (applies to all items below):
|
||||||
|
|
||||||
- key: bytes of length a.KEYBYTES
|
- key: bytes of length ciph.KEYBYTES
|
||||||
- nonce: bytes of length a.NPUBBYTES (must be unique per (key, message))
|
- nonce: bytes of length ciph.NONCEBYTES (must be unique per message)
|
||||||
- message/ct: plain text or ciphertext
|
- message/ct: plain text or ciphertext
|
||||||
- ad: optional associated data (authenticated, not encrypted)
|
- ad: optional associated data (authenticated, not encrypted)
|
||||||
- into: optional output buffer (see below)
|
- into: optional output buffer (see below)
|
||||||
- maclen: MAC tag length 16 or 32 bytes (default 16)
|
- 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.
|
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.
|
||||||
|
|
||||||
@@ -70,22 +86,30 @@ No MAC tag, vulnerable to alterations:
|
|||||||
### Incremental AEAD
|
### Incremental AEAD
|
||||||
|
|
||||||
Stateful classes that can be used for processing the data in separate chunks:
|
Stateful classes that can be used for processing the data in separate chunks:
|
||||||
- Encryptor(key, nonce, ad=None)
|
- Encryptor(key, nonce, ad=None, maclen=16)
|
||||||
- update(message[, into]) -> ciphertext_chunk
|
- update(message[, into]) -> ciphertext_chunk
|
||||||
- final([into], maclen=16) -> mac_tag
|
- final([into]) -> mac_tag
|
||||||
- Decryptor(key, nonce, ad=None)
|
- Decryptor(key, nonce, ad=None, maclen=16)
|
||||||
- update(ct_chunk[, into]) -> plaintext_chunk
|
- update(ct_chunk[, into]) -> plaintext_chunk
|
||||||
- final(mac) -> None (raises ValueError on failure)
|
- final(mac) -> raises ValueError on failure
|
||||||
|
|
||||||
|
The object releases its state and becomes unusable after final has been called.
|
||||||
|
|
||||||
### Message Authentication Code
|
### Message Authentication Code
|
||||||
|
|
||||||
No encryption, but prevents changes to the data without the correct key.
|
No encryption, but prevents changes to the data without the correct key.
|
||||||
|
|
||||||
- mac(key, nonce, data, maclen=16, into=None) -> mac
|
- mac(key, nonce, data, maclen=16, into=None) -> mac bytes
|
||||||
- Mac(key, nonce)
|
- Mac(key, nonce, maclen=16)
|
||||||
- update(data)
|
- update(data)
|
||||||
- final(maclen=16[, into]) -> mac
|
- final([into]) -> mac bytes
|
||||||
- verify(mac) -> bool (True on success; raises ValueError on failure)
|
- verify(mac) -> raises ValueError on failure
|
||||||
|
- digest() -> mac bytes
|
||||||
|
- hexdigest() -> mac str
|
||||||
|
- reset()
|
||||||
|
- clone() -> Mac
|
||||||
|
|
||||||
|
The `Mac` class follows the Python hashlib API for compatibility with code expecting hash objects. After calling `final()`, `digest()`, or `hexdigest()`, the Mac object becomes unusable for further `update()` operations. However, `digest()` and `hexdigest()` cache their results and can be called multiple times. Use `reset()` to clear the state and start over, or `clone()` to create a copy before finalizing.
|
||||||
|
|
||||||
### Keystream generation
|
### Keystream generation
|
||||||
|
|
||||||
@@ -95,10 +119,10 @@ Useful for creating pseudo random bytes as rapidly as possible. Reuse of the sam
|
|||||||
|
|
||||||
### Miscellaneous
|
### Miscellaneous
|
||||||
|
|
||||||
Constants (per module): KEYBYTES, NPUBBYTES, ABYTES_MIN, ABYTES_MAX, RATE, ALIGNMENT
|
Constants (per module): NAME, KEYBYTES, NONCEBYTES, MACBYTES, MACBYTES_LONG, RATE, ALIGNMENT
|
||||||
|
|
||||||
- random_key() -> bytearray (length KEYBYTES)
|
- random_key() -> bytearray (length KEYBYTES)
|
||||||
- random_nonce() -> bytearray (length NPUBBYTES)
|
- random_nonce() -> bytearray (length NONCEBYTES)
|
||||||
- nonce_increment(nonce)
|
- nonce_increment(nonce)
|
||||||
- wipe(buffer)
|
- wipe(buffer)
|
||||||
|
|
||||||
@@ -115,16 +139,22 @@ Constants (per module): KEYBYTES, NPUBBYTES, ABYTES_MIN, ABYTES_MAX, RATE, ALIGN
|
|||||||
|
|
||||||
A cryptographically secure keyed hash is produced. The example uses all zeroes for the nonce to always produce the same hash for the same key:
|
A cryptographically secure keyed hash is produced. The example uses all zeroes for the nonce to always produce the same hash for the same key:
|
||||||
```python
|
```python
|
||||||
from pyaegis import aegis256x4 as ciph
|
from aeg import aegis256x4 as ciph
|
||||||
key, nonce = ciph.random_key(), bytes(ciph.NPUBBYTES)
|
key, nonce = ciph.random_key(), bytes(ciph.NONCEBYTES)
|
||||||
|
|
||||||
mac = ciph.mac(key, nonce, b"message", maclen=32)
|
mac = ciph.mac(key, nonce, b"message", maclen=32)
|
||||||
print(mac)
|
print(mac.hex())
|
||||||
|
|
||||||
st = ciph.Mac(key, nonce)
|
# Alternative class-based API
|
||||||
st.update(b"message")
|
a = ciph.Mac(key, nonce, maclen=32)
|
||||||
st.update(b"Mallory Says Hello!")
|
a.update(b"message")
|
||||||
st.verify(mac) # Raises ValueError
|
print(a.hexdigest())
|
||||||
|
|
||||||
|
# Verification
|
||||||
|
b = ciph.Mac(key, nonce, maclen=32)
|
||||||
|
b.update(b"message")
|
||||||
|
b.update(b"Mallory Says Hello!")
|
||||||
|
b.verify(mac) # Raises ValueError
|
||||||
```
|
```
|
||||||
|
|
||||||
### Detached mode encryption and decryption
|
### Detached mode encryption and decryption
|
||||||
@@ -132,7 +162,7 @@ st.verify(mac) # Raises ValueError
|
|||||||
Keeping the ciphertext, mac and ad separate. The ad represents a file header that needs to be tamper proofed.
|
Keeping the ciphertext, mac and ad separate. The ad represents a file header that needs to be tamper proofed.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pyaegis import aegis256x4 as ciph
|
from aeg import aegis256x4 as ciph
|
||||||
key, nonce = ciph.random_key(), ciph.random_nonce()
|
key, nonce = ciph.random_key(), ciph.random_nonce()
|
||||||
|
|
||||||
ct, mac = ciph.encrypt_detached(key, nonce, b"secret", ad=b"header")
|
ct, mac = ciph.encrypt_detached(key, nonce, b"secret", ad=b"header")
|
||||||
@@ -148,15 +178,15 @@ ciph.wipe(pt)
|
|||||||
Class-based interface for incremental updates is an alternative to the one-shot functions. Not to be confused with separately verified ciphertext frames (see the next example).
|
Class-based interface for incremental updates is an alternative to the one-shot functions. Not to be confused with separately verified ciphertext frames (see the next example).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pyaegis import aegis256x4 as ciph
|
from aeg import aegis256x4 as ciph
|
||||||
key, nonce = ciph.random_key(), ciph.random_nonce()
|
key, nonce = ciph.random_key(), ciph.random_nonce()
|
||||||
|
|
||||||
enc = a.Encryptor(key, nonce, ad=b"header")
|
enc = ciph.Encryptor(key, nonce, ad=b"header", maclen=16)
|
||||||
c1 = enc.update(b"chunk1")
|
c1 = enc.update(b"chunk1")
|
||||||
c2 = enc.update(b"chunk2")
|
c2 = enc.update(b"chunk2")
|
||||||
mac = enc.final(maclen=16)
|
mac = enc.final()
|
||||||
|
|
||||||
dec = a.Decryptor(key, nonce, ad=b"header")
|
dec = ciph.Decryptor(key, nonce, ad=b"header", maclen=16)
|
||||||
p1 = dec.update(c1)
|
p1 = dec.update(c1)
|
||||||
p2 = dec.update(c2)
|
p2 = dec.update(c2)
|
||||||
dec.final(mac) # raises ValueError on failure
|
dec.final(mac) # raises ValueError on failure
|
||||||
@@ -167,16 +197,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.
|
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
|
```python
|
||||||
from pyaegis import aegis128x4 as ciph
|
# Encryption settings
|
||||||
|
from aeg import aegis128x4 as ciph
|
||||||
|
key = b"sixteenbyte key!" # 16 bytes secret key for aegis128* algorithms
|
||||||
|
framebytes = 80 # In real applications 1 MiB or more is practical
|
||||||
|
maclen = ciph.MACBYTES # 16
|
||||||
|
|
||||||
message = bytearray(30 * b"Attack at dawn! ")
|
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.ABYTES_MIN # 16
|
|
||||||
|
|
||||||
with open("encrypted.bin", "wb") as f:
|
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:
|
while message:
|
||||||
chunk = message[:framebytes - maclen]
|
chunk = message[:framebytes - maclen]
|
||||||
del message[:len(chunk)]
|
del message[:len(chunk)]
|
||||||
@@ -186,15 +217,14 @@ with open("encrypted.bin", "wb") as f:
|
|||||||
```
|
```
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pyaegis import aegis128x4 as ciph
|
|
||||||
|
|
||||||
# Decryption needs same values as encryption
|
# Decryption needs same values as encryption
|
||||||
|
from aeg import aegis128x4 as ciph
|
||||||
key = b"sixteenbyte key!"
|
key = b"sixteenbyte key!"
|
||||||
framebytes = 80
|
framebytes = 80
|
||||||
maclen = ciph.ABYTES_MIN
|
maclen = ciph.MACBYTES
|
||||||
|
|
||||||
with open("encrypted.bin", "rb") as f:
|
with open("encrypted.bin", "rb") as f:
|
||||||
nonce = bytearray(f.read(ciph.NPUBBYTES))
|
nonce = bytearray(f.read(ciph.NONCEBYTES))
|
||||||
while True:
|
while True:
|
||||||
frame = f.read(framebytes)
|
frame = f.read(framebytes)
|
||||||
if not frame:
|
if not frame:
|
||||||
@@ -204,16 +234,47 @@ with open("encrypted.bin", "rb") as f:
|
|||||||
print(pt)
|
print(pt)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Random generator
|
||||||
|
|
||||||
|
The stream generator is much faster than any traditional random number generator, cryptographically secure and seekable. Use `random_key()` for unpredictable output.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from aeg import aegis128x4 as ciph
|
||||||
|
|
||||||
|
key = b"SeedForReplay001" # A non-random deterministic seed (16 bytes)
|
||||||
|
nonce = bytearray(ciph.NONCEBYTES) # All-zeroes nonce
|
||||||
|
|
||||||
|
# Generate multiple blocks of pseudorandom data
|
||||||
|
for i in range(5):
|
||||||
|
rand = ciph.stream(key, nonce, 10)
|
||||||
|
print(f"Block {int.from_bytes(nonce, "little")}: {rand.hex()}")
|
||||||
|
ciph.nonce_increment(nonce)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: this is seekable by converting the block number to nonce with `idx.to_bytes(ciph.NONCEBYTES, "little")`, given some fixed block size (e.g. 1 MiB).
|
||||||
|
|
||||||
### Preallocated output buffers (into=)
|
### 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.
|
A `TypeError` is raised if the buffer is too small. For convenience, the functions return a memoryview showing only the bytes actually written.
|
||||||
|
|
||||||
|
Foreign arrays can be used. This example fills a Numpy array with random integers.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import numpy as np
|
||||||
|
from aeg 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:
|
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
|
```python
|
||||||
from pyaegis import aegis256x4 as ciph
|
from aeg import aegis256x4 as ciph
|
||||||
key, nonce = ciph.random_key(), ciph.random_nonce()
|
key, nonce = ciph.random_key(), ciph.random_nonce()
|
||||||
buf = memoryview(bytearray(1000)) # memoryview[:len] is still in the same buffer (no copy)
|
buf = memoryview(bytearray(1000)) # memoryview[:len] is still in the same buffer (no copy)
|
||||||
buf[:7] = b"message"
|
buf[:7] = b"message"
|
||||||
@@ -231,33 +292,27 @@ 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.
|
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 processors supporting it (most AMD, Xeon).
|
||||||
|
|
||||||
```fish
|
```sh
|
||||||
uv run -m pyaegis.benchmark
|
uv run -m aeg.benchmark
|
||||||
```
|
AEGIS-256 103166.24 Mb/s
|
||||||
|
AEGIS-256X2 184225.50 Mb/s
|
||||||
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.
|
AEGIS-256X4 194018.26 Mb/s
|
||||||
|
AEGIS-128L 161551.73 Mb/s
|
||||||
```fish
|
AEGIS-128X2 281987.80 Mb/s
|
||||||
$ python -m pyaegis.benchmark
|
AEGIS-128X4 217997.37 Mb/s
|
||||||
AEGIS-256 107666.56 Mb/s
|
AEGIS-128L MAC 188886.40 Mb/s
|
||||||
AEGIS-256X2 191314.53 Mb/s
|
AEGIS-128X2 MAC 306457.97 Mb/s
|
||||||
AEGIS-256X4 211537.44 Mb/s
|
AEGIS-128X4 MAC 299576.59 Mb/s
|
||||||
AEGIS-128L 159074.08 Mb/s
|
AEGIS-256 MAC 100914.04 Mb/s
|
||||||
AEGIS-128X2 307332.53 Mb/s
|
AEGIS-256X2 MAC 190208.20 Mb/s
|
||||||
AEGIS-128X4 230106.70 Mb/s
|
AEGIS-256X4 MAC 315919.87 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The Python library performance is similar to that of the C library:
|
The Python library performance is similar to that of the C library:
|
||||||
```fish
|
```sh
|
||||||
$ ./libaegis/zig-out/bin/benchmark
|
./libaegis/zig-out/bin/benchmark
|
||||||
AEGIS-256 107820.86 Mb/s
|
AEGIS-256 107820.86 Mb/s
|
||||||
AEGIS-256X2 205025.57 Mb/s
|
AEGIS-256X2 205025.57 Mb/s
|
||||||
AEGIS-256X4 223361.81 Mb/s
|
AEGIS-256X4 223361.81 Mb/s
|
||||||
@@ -271,3 +326,7 @@ AEGIS-256 MAC 116776.62 Mb/s
|
|||||||
AEGIS-256X2 MAC 224150.04 Mb/s
|
AEGIS-256X2 MAC 224150.04 Mb/s
|
||||||
AEGIS-256X4 MAC 392088.05 Mb/s
|
AEGIS-256X4 MAC 392088.05 Mb/s
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
There is also a package named [pyaegis](https://github.com/jedisct1/pyaegis) on PyPI that is unrelated to this module, but that also binds to the libaegis C library. There are also a number of modules named aegis from different packages not at all related to the encryption algorithm.
|
||||||
|
|||||||
+1
-1
Submodule libaegis updated: 4a234009c9...7b667dd883
@@ -1,823 +0,0 @@
|
|||||||
"""AEGIS-128L"""
|
|
||||||
# All modules are generated from aegis128l.py by tools/gen_modules.py!
|
|
||||||
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
from ._loader import ffi
|
|
||||||
from ._loader import lib as _lib
|
|
||||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
|
||||||
|
|
||||||
# Constants exposed as functions in C; mirror them as integers at module import time
|
|
||||||
KEYBYTES = _lib.aegis128l_keybytes()
|
|
||||||
NPUBBYTES = _lib.aegis128l_npubbytes()
|
|
||||||
ABYTES_MIN = _lib.aegis128l_abytes_min()
|
|
||||||
ABYTES_MAX = _lib.aegis128l_abytes_max()
|
|
||||||
TAILBYTES_MAX = _lib.aegis128l_tailbytes_max()
|
|
||||||
ALIGNMENT = 32
|
|
||||||
RATE = 32
|
|
||||||
|
|
||||||
|
|
||||||
def random_key() -> bytearray:
|
|
||||||
"""Generate a random key using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(KEYBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def random_nonce() -> bytearray:
|
|
||||||
"""Generate a random nonce using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(NPUBBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def _ptr(buf):
|
|
||||||
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
ct_into: Buffer | None = None,
|
|
||||||
mac_into: Buffer | None = None,
|
|
||||||
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
mac_into: Buffer to write MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (ciphertext, mac)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
|
|
||||||
if ct_into is None:
|
|
||||||
c = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(ct_into) < len(message):
|
|
||||||
raise TypeError("ct_into length must be at least len(message)")
|
|
||||||
c = ct_into
|
|
||||||
if mac_into is None:
|
|
||||||
mac = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(mac_into) < maclen:
|
|
||||||
raise TypeError("mac_into length must be at least maclen")
|
|
||||||
mac = mac_into
|
|
||||||
|
|
||||||
rc = _lib.aegis128l_encrypt_detached(
|
|
||||||
ffi.from_buffer(c),
|
|
||||||
ffi.from_buffer(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
|
||||||
return (
|
|
||||||
c if ct_into is None else memoryview(c)[: len(message)],
|
|
||||||
mac if mac_into is None else memoryview(mac)[:maclen],
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
mac: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with detached MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
mac: The MAC to verify.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis128l_decrypt_detached(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message) + maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < len(message) + maclen:
|
|
||||||
raise TypeError("into length must be at least len(message)+maclen")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis128l_encrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with appended MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext with MAC to decrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if len(ct) < maclen:
|
|
||||||
raise TypeError("ciphertext too short for tag")
|
|
||||||
expected_out = len(ct) - maclen
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(expected_out)
|
|
||||||
else:
|
|
||||||
if len(into) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be at least len(ciphertext_with_tag)-maclen"
|
|
||||||
)
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis128l_decrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def stream(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer | None,
|
|
||||||
length: int | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | Buffer:
|
|
||||||
f"""Generate a stream of pseudorandom bytes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}, uses zeroes for nonce if None).
|
|
||||||
length: Number of bytes to generate (required if into is None).
|
|
||||||
into: Buffer to write stream into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Pseudorandom bytes as bytearray, or into returned directly.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid or neither length nor into provided.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if nonce is not None and len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
if length is None:
|
|
||||||
raise TypeError("provide either into or length")
|
|
||||||
out = bytearray(length)
|
|
||||||
else:
|
|
||||||
if length is not None and len(into) < length:
|
|
||||||
raise TypeError("into length must be at least length")
|
|
||||||
out = into
|
|
||||||
_lib.aegis128l_stream(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: length or len(out)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(into) < len(message):
|
|
||||||
raise TypeError("into length must be at least len(message)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis128l_encrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(message)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis128l_decrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
# This is missing from C API but convenient to have here
|
|
||||||
def mac(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
data: Buffer,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Compute a MAC for the given data in one shot.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=})
|
|
||||||
nonce: Nonce ({NPUBBYTES=})
|
|
||||||
data: Data to MAC
|
|
||||||
maclen: MAC length (16 or 32, default 16)
|
|
||||||
into: Buffer to write MAC into (default: bytearray created)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
|
||||||
"""
|
|
||||||
mac_state = Mac(key, nonce)
|
|
||||||
mac_state.update(data)
|
|
||||||
return mac_state.final(maclen, into)
|
|
||||||
|
|
||||||
|
|
||||||
class Mac:
|
|
||||||
"""AEGIS-128L MAC state wrapper.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
mac = Mac(key, nonce)
|
|
||||||
mac.update(data)
|
|
||||||
tag = mac.final() # defaults to 16-byte MAC
|
|
||||||
# or verify:
|
|
||||||
mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner")
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
_other=None,
|
|
||||||
) -> None:
|
|
||||||
f"""Initialize a MAC state with a nonce and key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
st, owner = new_aligned_struct("aegis128l_mac_state", ALIGNMENT)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
if _other is not None: # clone path
|
|
||||||
_lib.aegis128l_mac_state_clone(self._st, _other._st)
|
|
||||||
return
|
|
||||||
# Normal init path
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
_lib.aegis128l_mac_init(self._st, _ptr(key), _ptr(nonce))
|
|
||||||
|
|
||||||
def __deepcopy__(self) -> "Mac":
|
|
||||||
"""Return a clone of current MAC state."""
|
|
||||||
return Mac(b"", b"", _other=self)
|
|
||||||
|
|
||||||
clone = __deepcopy__
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
"""Reset the MAC state so it can be reused with the same nonce and key."""
|
|
||||||
_lib.aegis128l_mac_reset(self._st)
|
|
||||||
|
|
||||||
def update(self, data: Buffer) -> None:
|
|
||||||
"""Absorb data into the MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Bytes-like object to authenticate.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If the underlying C function reports an error.
|
|
||||||
"""
|
|
||||||
rc = _lib.aegis128l_mac_update(self._st, _ptr(data), len(data))
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac update failed: {err_name}")
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize and return the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
maclen: Tag length in bytes (16 or 32). Defaults to 16.
|
|
||||||
into: Optional buffer to write the tag into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The tag as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If finalization fails in the C library.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < maclen:
|
|
||||||
raise TypeError("into length must be at least maclen")
|
|
||||||
out = into
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
rc = _lib.aegis128l_mac_final(self._st, ffi.from_buffer(out_mv), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac final failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
|
||||||
|
|
||||||
def verify(self, mac: Buffer):
|
|
||||||
"""Verify a tag for the current MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: The tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Only if verification succeeds.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If verification fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis128l_mac_verify(self._st, _ptr(mac), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("mac verification failed")
|
|
||||||
|
|
||||||
|
|
||||||
class Encryptor:
|
|
||||||
"""Incremental encryptor.
|
|
||||||
|
|
||||||
- update(message[, into]) -> returns produced ciphertext bytes
|
|
||||||
- final([into], maclen=16) -> returns tail+tag bytes
|
|
||||||
- final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental encryptor.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data to bind to the encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis128l_state", ALIGNMENT)
|
|
||||||
_lib.aegis128l_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total plaintext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total ciphertext bytes produced so far.
|
|
||||||
|
|
||||||
Includes update() and final() output.
|
|
||||||
"""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self, message: Buffer, into: Buffer | None = None
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Encrypt a chunk of the message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: Plaintext bytes to encrypt.
|
|
||||||
into: Optional destination buffer; must be >= len(message).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(message)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be >= expected output size for this update"
|
|
||||||
)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis128l_state_encrypt_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(
|
|
||||||
f"state encrypt update failed: {err_name} written {written[0]}"
|
|
||||||
)
|
|
||||||
w = int(written[0])
|
|
||||||
assert w == expected_out
|
|
||||||
self._bytes_in += len(message)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self, into: Buffer | None = None, maclen: int = ABYTES_MIN
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize encryption, writing any remaining bytes and the tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
into: Optional destination buffer for the tail and tag.
|
|
||||||
maclen: Tag length (16 or 32). Defaults to 16.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If maclen is invalid.
|
|
||||||
RuntimeError: If the C final call fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
# Only the authentication tag is produced here; allocate exactly maclen
|
|
||||||
out = into if into is not None else bytearray(maclen)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis128l_state_encrypt_final(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
written,
|
|
||||||
maclen,
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
|
||||||
w = int(written[0])
|
|
||||||
if into is None:
|
|
||||||
# Only the tag bytes are returned when we allocate the buffer
|
|
||||||
assert w == maclen
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class Decryptor:
|
|
||||||
"""Incremental decryptor.
|
|
||||||
|
|
||||||
- update(ciphertext[, into]) -> returns plaintext bytes
|
|
||||||
- final(mac) -> verifies the MAC tag
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental decryptor for detached tags.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data used during encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis128l_state", ALIGNMENT)
|
|
||||||
_lib.aegis128l_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total ciphertext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total plaintext bytes produced so far."""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
|
||||||
"""Process a chunk of ciphertext.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ct: Ciphertext bytes (without MAC).
|
|
||||||
into: Optional destination buffer; must be >= len(ciphertext).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(ct)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError("into length must be >= required capacity for this update")
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis128l_state_decrypt_detached_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
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}, len(ct)={len(ct)}"
|
|
||||||
self._bytes_in += len(ct)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
|
||||||
"""Finalize decryption by verifying the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: Tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis128l_state_decrypt_detached_final(
|
|
||||||
self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
|
|
||||||
|
|
||||||
def new_state():
|
|
||||||
"""Allocate and return a new aegis128l_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis128l_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
def new_mac_state():
|
|
||||||
"""Allocate and return a new aegis128l_mac_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis128l_mac_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# constants
|
|
||||||
"KEYBYTES",
|
|
||||||
"NPUBBYTES",
|
|
||||||
"ABYTES_MIN",
|
|
||||||
"ABYTES_MAX",
|
|
||||||
"TAILBYTES_MAX",
|
|
||||||
"ALIGNMENT",
|
|
||||||
"RATE",
|
|
||||||
# utility functions
|
|
||||||
"random_key",
|
|
||||||
"random_nonce",
|
|
||||||
"nonce_increment",
|
|
||||||
"wipe",
|
|
||||||
# one-shot functions
|
|
||||||
"encrypt_detached",
|
|
||||||
"decrypt_detached",
|
|
||||||
"encrypt",
|
|
||||||
"decrypt",
|
|
||||||
"stream",
|
|
||||||
"encrypt_unauthenticated",
|
|
||||||
"decrypt_unauthenticated",
|
|
||||||
"mac",
|
|
||||||
# incremental classes
|
|
||||||
"Encryptor",
|
|
||||||
"Decryptor",
|
|
||||||
"Mac",
|
|
||||||
]
|
|
||||||
@@ -1,823 +0,0 @@
|
|||||||
"""AEGIS-128X2"""
|
|
||||||
# All modules are generated from aegis128x2.py by tools/gen_modules.py!
|
|
||||||
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
from ._loader import ffi
|
|
||||||
from ._loader import lib as _lib
|
|
||||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
|
||||||
|
|
||||||
# Constants exposed as functions in C; mirror them as integers at module import time
|
|
||||||
KEYBYTES = _lib.aegis128x2_keybytes()
|
|
||||||
NPUBBYTES = _lib.aegis128x2_npubbytes()
|
|
||||||
ABYTES_MIN = _lib.aegis128x2_abytes_min()
|
|
||||||
ABYTES_MAX = _lib.aegis128x2_abytes_max()
|
|
||||||
TAILBYTES_MAX = _lib.aegis128x2_tailbytes_max()
|
|
||||||
ALIGNMENT = 64
|
|
||||||
RATE = 64
|
|
||||||
|
|
||||||
|
|
||||||
def random_key() -> bytearray:
|
|
||||||
"""Generate a random key using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(KEYBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def random_nonce() -> bytearray:
|
|
||||||
"""Generate a random nonce using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(NPUBBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def _ptr(buf):
|
|
||||||
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
ct_into: Buffer | None = None,
|
|
||||||
mac_into: Buffer | None = None,
|
|
||||||
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
mac_into: Buffer to write MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (ciphertext, mac)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
|
|
||||||
if ct_into is None:
|
|
||||||
c = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(ct_into) < len(message):
|
|
||||||
raise TypeError("ct_into length must be at least len(message)")
|
|
||||||
c = ct_into
|
|
||||||
if mac_into is None:
|
|
||||||
mac = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(mac_into) < maclen:
|
|
||||||
raise TypeError("mac_into length must be at least maclen")
|
|
||||||
mac = mac_into
|
|
||||||
|
|
||||||
rc = _lib.aegis128x2_encrypt_detached(
|
|
||||||
ffi.from_buffer(c),
|
|
||||||
ffi.from_buffer(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
|
||||||
return (
|
|
||||||
c if ct_into is None else memoryview(c)[: len(message)],
|
|
||||||
mac if mac_into is None else memoryview(mac)[:maclen],
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
mac: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with detached MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
mac: The MAC to verify.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis128x2_decrypt_detached(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message) + maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < len(message) + maclen:
|
|
||||||
raise TypeError("into length must be at least len(message)+maclen")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis128x2_encrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with appended MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext with MAC to decrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if len(ct) < maclen:
|
|
||||||
raise TypeError("ciphertext too short for tag")
|
|
||||||
expected_out = len(ct) - maclen
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(expected_out)
|
|
||||||
else:
|
|
||||||
if len(into) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be at least len(ciphertext_with_tag)-maclen"
|
|
||||||
)
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis128x2_decrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def stream(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer | None,
|
|
||||||
length: int | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | Buffer:
|
|
||||||
f"""Generate a stream of pseudorandom bytes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}, uses zeroes for nonce if None).
|
|
||||||
length: Number of bytes to generate (required if into is None).
|
|
||||||
into: Buffer to write stream into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Pseudorandom bytes as bytearray, or into returned directly.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid or neither length nor into provided.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if nonce is not None and len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
if length is None:
|
|
||||||
raise TypeError("provide either into or length")
|
|
||||||
out = bytearray(length)
|
|
||||||
else:
|
|
||||||
if length is not None and len(into) < length:
|
|
||||||
raise TypeError("into length must be at least length")
|
|
||||||
out = into
|
|
||||||
_lib.aegis128x2_stream(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: length or len(out)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(into) < len(message):
|
|
||||||
raise TypeError("into length must be at least len(message)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis128x2_encrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(message)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis128x2_decrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
# This is missing from C API but convenient to have here
|
|
||||||
def mac(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
data: Buffer,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Compute a MAC for the given data in one shot.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=})
|
|
||||||
nonce: Nonce ({NPUBBYTES=})
|
|
||||||
data: Data to MAC
|
|
||||||
maclen: MAC length (16 or 32, default 16)
|
|
||||||
into: Buffer to write MAC into (default: bytearray created)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
|
||||||
"""
|
|
||||||
mac_state = Mac(key, nonce)
|
|
||||||
mac_state.update(data)
|
|
||||||
return mac_state.final(maclen, into)
|
|
||||||
|
|
||||||
|
|
||||||
class Mac:
|
|
||||||
"""AEGIS-128X2 MAC state wrapper.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
mac = Mac(key, nonce)
|
|
||||||
mac.update(data)
|
|
||||||
tag = mac.final() # defaults to 16-byte MAC
|
|
||||||
# or verify:
|
|
||||||
mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner")
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
_other=None,
|
|
||||||
) -> None:
|
|
||||||
f"""Initialize a MAC state with a nonce and key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
st, owner = new_aligned_struct("aegis128x2_mac_state", ALIGNMENT)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
if _other is not None: # clone path
|
|
||||||
_lib.aegis128x2_mac_state_clone(self._st, _other._st)
|
|
||||||
return
|
|
||||||
# Normal init path
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
_lib.aegis128x2_mac_init(self._st, _ptr(key), _ptr(nonce))
|
|
||||||
|
|
||||||
def __deepcopy__(self) -> "Mac":
|
|
||||||
"""Return a clone of current MAC state."""
|
|
||||||
return Mac(b"", b"", _other=self)
|
|
||||||
|
|
||||||
clone = __deepcopy__
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
"""Reset the MAC state so it can be reused with the same nonce and key."""
|
|
||||||
_lib.aegis128x2_mac_reset(self._st)
|
|
||||||
|
|
||||||
def update(self, data: Buffer) -> None:
|
|
||||||
"""Absorb data into the MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Bytes-like object to authenticate.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If the underlying C function reports an error.
|
|
||||||
"""
|
|
||||||
rc = _lib.aegis128x2_mac_update(self._st, _ptr(data), len(data))
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac update failed: {err_name}")
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize and return the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
maclen: Tag length in bytes (16 or 32). Defaults to 16.
|
|
||||||
into: Optional buffer to write the tag into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The tag as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If finalization fails in the C library.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < maclen:
|
|
||||||
raise TypeError("into length must be at least maclen")
|
|
||||||
out = into
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
rc = _lib.aegis128x2_mac_final(self._st, ffi.from_buffer(out_mv), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac final failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
|
||||||
|
|
||||||
def verify(self, mac: Buffer):
|
|
||||||
"""Verify a tag for the current MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: The tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Only if verification succeeds.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If verification fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis128x2_mac_verify(self._st, _ptr(mac), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("mac verification failed")
|
|
||||||
|
|
||||||
|
|
||||||
class Encryptor:
|
|
||||||
"""Incremental encryptor.
|
|
||||||
|
|
||||||
- update(message[, into]) -> returns produced ciphertext bytes
|
|
||||||
- final([into], maclen=16) -> returns tail+tag bytes
|
|
||||||
- final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental encryptor.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data to bind to the encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis128x2_state", ALIGNMENT)
|
|
||||||
_lib.aegis128x2_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total plaintext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total ciphertext bytes produced so far.
|
|
||||||
|
|
||||||
Includes update() and final() output.
|
|
||||||
"""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self, message: Buffer, into: Buffer | None = None
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Encrypt a chunk of the message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: Plaintext bytes to encrypt.
|
|
||||||
into: Optional destination buffer; must be >= len(message).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(message)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be >= expected output size for this update"
|
|
||||||
)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis128x2_state_encrypt_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(
|
|
||||||
f"state encrypt update failed: {err_name} written {written[0]}"
|
|
||||||
)
|
|
||||||
w = int(written[0])
|
|
||||||
assert w == expected_out
|
|
||||||
self._bytes_in += len(message)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self, into: Buffer | None = None, maclen: int = ABYTES_MIN
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize encryption, writing any remaining bytes and the tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
into: Optional destination buffer for the tail and tag.
|
|
||||||
maclen: Tag length (16 or 32). Defaults to 16.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If maclen is invalid.
|
|
||||||
RuntimeError: If the C final call fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
# Only the authentication tag is produced here; allocate exactly maclen
|
|
||||||
out = into if into is not None else bytearray(maclen)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis128x2_state_encrypt_final(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
written,
|
|
||||||
maclen,
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
|
||||||
w = int(written[0])
|
|
||||||
if into is None:
|
|
||||||
# Only the tag bytes are returned when we allocate the buffer
|
|
||||||
assert w == maclen
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class Decryptor:
|
|
||||||
"""Incremental decryptor.
|
|
||||||
|
|
||||||
- update(ciphertext[, into]) -> returns plaintext bytes
|
|
||||||
- final(mac) -> verifies the MAC tag
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental decryptor for detached tags.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data used during encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis128x2_state", ALIGNMENT)
|
|
||||||
_lib.aegis128x2_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total ciphertext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total plaintext bytes produced so far."""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
|
||||||
"""Process a chunk of ciphertext.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ct: Ciphertext bytes (without MAC).
|
|
||||||
into: Optional destination buffer; must be >= len(ciphertext).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(ct)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError("into length must be >= required capacity for this update")
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis128x2_state_decrypt_detached_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
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}, len(ct)={len(ct)}"
|
|
||||||
self._bytes_in += len(ct)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
|
||||||
"""Finalize decryption by verifying the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: Tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis128x2_state_decrypt_detached_final(
|
|
||||||
self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
|
|
||||||
|
|
||||||
def new_state():
|
|
||||||
"""Allocate and return a new aegis128x2_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis128x2_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
def new_mac_state():
|
|
||||||
"""Allocate and return a new aegis128x2_mac_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis128x2_mac_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# constants
|
|
||||||
"KEYBYTES",
|
|
||||||
"NPUBBYTES",
|
|
||||||
"ABYTES_MIN",
|
|
||||||
"ABYTES_MAX",
|
|
||||||
"TAILBYTES_MAX",
|
|
||||||
"ALIGNMENT",
|
|
||||||
"RATE",
|
|
||||||
# utility functions
|
|
||||||
"random_key",
|
|
||||||
"random_nonce",
|
|
||||||
"nonce_increment",
|
|
||||||
"wipe",
|
|
||||||
# one-shot functions
|
|
||||||
"encrypt_detached",
|
|
||||||
"decrypt_detached",
|
|
||||||
"encrypt",
|
|
||||||
"decrypt",
|
|
||||||
"stream",
|
|
||||||
"encrypt_unauthenticated",
|
|
||||||
"decrypt_unauthenticated",
|
|
||||||
"mac",
|
|
||||||
# incremental classes
|
|
||||||
"Encryptor",
|
|
||||||
"Decryptor",
|
|
||||||
"Mac",
|
|
||||||
]
|
|
||||||
@@ -1,823 +0,0 @@
|
|||||||
"""AEGIS-128X4"""
|
|
||||||
# All modules are generated from aegis128x4.py by tools/gen_modules.py!
|
|
||||||
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
from ._loader import ffi
|
|
||||||
from ._loader import lib as _lib
|
|
||||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
|
||||||
|
|
||||||
# Constants exposed as functions in C; mirror them as integers at module import time
|
|
||||||
KEYBYTES = _lib.aegis128x4_keybytes()
|
|
||||||
NPUBBYTES = _lib.aegis128x4_npubbytes()
|
|
||||||
ABYTES_MIN = _lib.aegis128x4_abytes_min()
|
|
||||||
ABYTES_MAX = _lib.aegis128x4_abytes_max()
|
|
||||||
TAILBYTES_MAX = _lib.aegis128x4_tailbytes_max()
|
|
||||||
ALIGNMENT = 64
|
|
||||||
RATE = 128
|
|
||||||
|
|
||||||
|
|
||||||
def random_key() -> bytearray:
|
|
||||||
"""Generate a random key using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(KEYBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def random_nonce() -> bytearray:
|
|
||||||
"""Generate a random nonce using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(NPUBBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def _ptr(buf):
|
|
||||||
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
ct_into: Buffer | None = None,
|
|
||||||
mac_into: Buffer | None = None,
|
|
||||||
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
mac_into: Buffer to write MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (ciphertext, mac)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
|
|
||||||
if ct_into is None:
|
|
||||||
c = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(ct_into) < len(message):
|
|
||||||
raise TypeError("ct_into length must be at least len(message)")
|
|
||||||
c = ct_into
|
|
||||||
if mac_into is None:
|
|
||||||
mac = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(mac_into) < maclen:
|
|
||||||
raise TypeError("mac_into length must be at least maclen")
|
|
||||||
mac = mac_into
|
|
||||||
|
|
||||||
rc = _lib.aegis128x4_encrypt_detached(
|
|
||||||
ffi.from_buffer(c),
|
|
||||||
ffi.from_buffer(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
|
||||||
return (
|
|
||||||
c if ct_into is None else memoryview(c)[: len(message)],
|
|
||||||
mac if mac_into is None else memoryview(mac)[:maclen],
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
mac: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with detached MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
mac: The MAC to verify.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis128x4_decrypt_detached(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message) + maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < len(message) + maclen:
|
|
||||||
raise TypeError("into length must be at least len(message)+maclen")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis128x4_encrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with appended MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext with MAC to decrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if len(ct) < maclen:
|
|
||||||
raise TypeError("ciphertext too short for tag")
|
|
||||||
expected_out = len(ct) - maclen
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(expected_out)
|
|
||||||
else:
|
|
||||||
if len(into) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be at least len(ciphertext_with_tag)-maclen"
|
|
||||||
)
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis128x4_decrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def stream(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer | None,
|
|
||||||
length: int | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | Buffer:
|
|
||||||
f"""Generate a stream of pseudorandom bytes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}, uses zeroes for nonce if None).
|
|
||||||
length: Number of bytes to generate (required if into is None).
|
|
||||||
into: Buffer to write stream into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Pseudorandom bytes as bytearray, or into returned directly.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid or neither length nor into provided.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if nonce is not None and len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
if length is None:
|
|
||||||
raise TypeError("provide either into or length")
|
|
||||||
out = bytearray(length)
|
|
||||||
else:
|
|
||||||
if length is not None and len(into) < length:
|
|
||||||
raise TypeError("into length must be at least length")
|
|
||||||
out = into
|
|
||||||
_lib.aegis128x4_stream(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: length or len(out)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(into) < len(message):
|
|
||||||
raise TypeError("into length must be at least len(message)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis128x4_encrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(message)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis128x4_decrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
# This is missing from C API but convenient to have here
|
|
||||||
def mac(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
data: Buffer,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Compute a MAC for the given data in one shot.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=})
|
|
||||||
nonce: Nonce ({NPUBBYTES=})
|
|
||||||
data: Data to MAC
|
|
||||||
maclen: MAC length (16 or 32, default 16)
|
|
||||||
into: Buffer to write MAC into (default: bytearray created)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
|
||||||
"""
|
|
||||||
mac_state = Mac(key, nonce)
|
|
||||||
mac_state.update(data)
|
|
||||||
return mac_state.final(maclen, into)
|
|
||||||
|
|
||||||
|
|
||||||
class Mac:
|
|
||||||
"""AEGIS-128X4 MAC state wrapper.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
mac = Mac(key, nonce)
|
|
||||||
mac.update(data)
|
|
||||||
tag = mac.final() # defaults to 16-byte MAC
|
|
||||||
# or verify:
|
|
||||||
mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner")
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
_other=None,
|
|
||||||
) -> None:
|
|
||||||
f"""Initialize a MAC state with a nonce and key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
st, owner = new_aligned_struct("aegis128x4_mac_state", ALIGNMENT)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
if _other is not None: # clone path
|
|
||||||
_lib.aegis128x4_mac_state_clone(self._st, _other._st)
|
|
||||||
return
|
|
||||||
# Normal init path
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
_lib.aegis128x4_mac_init(self._st, _ptr(key), _ptr(nonce))
|
|
||||||
|
|
||||||
def __deepcopy__(self) -> "Mac":
|
|
||||||
"""Return a clone of current MAC state."""
|
|
||||||
return Mac(b"", b"", _other=self)
|
|
||||||
|
|
||||||
clone = __deepcopy__
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
"""Reset the MAC state so it can be reused with the same nonce and key."""
|
|
||||||
_lib.aegis128x4_mac_reset(self._st)
|
|
||||||
|
|
||||||
def update(self, data: Buffer) -> None:
|
|
||||||
"""Absorb data into the MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Bytes-like object to authenticate.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If the underlying C function reports an error.
|
|
||||||
"""
|
|
||||||
rc = _lib.aegis128x4_mac_update(self._st, _ptr(data), len(data))
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac update failed: {err_name}")
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize and return the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
maclen: Tag length in bytes (16 or 32). Defaults to 16.
|
|
||||||
into: Optional buffer to write the tag into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The tag as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If finalization fails in the C library.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < maclen:
|
|
||||||
raise TypeError("into length must be at least maclen")
|
|
||||||
out = into
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
rc = _lib.aegis128x4_mac_final(self._st, ffi.from_buffer(out_mv), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac final failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
|
||||||
|
|
||||||
def verify(self, mac: Buffer):
|
|
||||||
"""Verify a tag for the current MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: The tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Only if verification succeeds.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If verification fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis128x4_mac_verify(self._st, _ptr(mac), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("mac verification failed")
|
|
||||||
|
|
||||||
|
|
||||||
class Encryptor:
|
|
||||||
"""Incremental encryptor.
|
|
||||||
|
|
||||||
- update(message[, into]) -> returns produced ciphertext bytes
|
|
||||||
- final([into], maclen=16) -> returns tail+tag bytes
|
|
||||||
- final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental encryptor.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data to bind to the encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis128x4_state", ALIGNMENT)
|
|
||||||
_lib.aegis128x4_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total plaintext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total ciphertext bytes produced so far.
|
|
||||||
|
|
||||||
Includes update() and final() output.
|
|
||||||
"""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self, message: Buffer, into: Buffer | None = None
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Encrypt a chunk of the message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: Plaintext bytes to encrypt.
|
|
||||||
into: Optional destination buffer; must be >= len(message).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(message)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be >= expected output size for this update"
|
|
||||||
)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis128x4_state_encrypt_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(
|
|
||||||
f"state encrypt update failed: {err_name} written {written[0]}"
|
|
||||||
)
|
|
||||||
w = int(written[0])
|
|
||||||
assert w == expected_out
|
|
||||||
self._bytes_in += len(message)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self, into: Buffer | None = None, maclen: int = ABYTES_MIN
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize encryption, writing any remaining bytes and the tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
into: Optional destination buffer for the tail and tag.
|
|
||||||
maclen: Tag length (16 or 32). Defaults to 16.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If maclen is invalid.
|
|
||||||
RuntimeError: If the C final call fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
# Only the authentication tag is produced here; allocate exactly maclen
|
|
||||||
out = into if into is not None else bytearray(maclen)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis128x4_state_encrypt_final(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
written,
|
|
||||||
maclen,
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
|
||||||
w = int(written[0])
|
|
||||||
if into is None:
|
|
||||||
# Only the tag bytes are returned when we allocate the buffer
|
|
||||||
assert w == maclen
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class Decryptor:
|
|
||||||
"""Incremental decryptor.
|
|
||||||
|
|
||||||
- update(ciphertext[, into]) -> returns plaintext bytes
|
|
||||||
- final(mac) -> verifies the MAC tag
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental decryptor for detached tags.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data used during encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis128x4_state", ALIGNMENT)
|
|
||||||
_lib.aegis128x4_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total ciphertext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total plaintext bytes produced so far."""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
|
||||||
"""Process a chunk of ciphertext.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ct: Ciphertext bytes (without MAC).
|
|
||||||
into: Optional destination buffer; must be >= len(ciphertext).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(ct)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError("into length must be >= required capacity for this update")
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis128x4_state_decrypt_detached_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
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}, len(ct)={len(ct)}"
|
|
||||||
self._bytes_in += len(ct)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
|
||||||
"""Finalize decryption by verifying the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: Tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis128x4_state_decrypt_detached_final(
|
|
||||||
self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
|
|
||||||
|
|
||||||
def new_state():
|
|
||||||
"""Allocate and return a new aegis128x4_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis128x4_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
def new_mac_state():
|
|
||||||
"""Allocate and return a new aegis128x4_mac_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis128x4_mac_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# constants
|
|
||||||
"KEYBYTES",
|
|
||||||
"NPUBBYTES",
|
|
||||||
"ABYTES_MIN",
|
|
||||||
"ABYTES_MAX",
|
|
||||||
"TAILBYTES_MAX",
|
|
||||||
"ALIGNMENT",
|
|
||||||
"RATE",
|
|
||||||
# utility functions
|
|
||||||
"random_key",
|
|
||||||
"random_nonce",
|
|
||||||
"nonce_increment",
|
|
||||||
"wipe",
|
|
||||||
# one-shot functions
|
|
||||||
"encrypt_detached",
|
|
||||||
"decrypt_detached",
|
|
||||||
"encrypt",
|
|
||||||
"decrypt",
|
|
||||||
"stream",
|
|
||||||
"encrypt_unauthenticated",
|
|
||||||
"decrypt_unauthenticated",
|
|
||||||
"mac",
|
|
||||||
# incremental classes
|
|
||||||
"Encryptor",
|
|
||||||
"Decryptor",
|
|
||||||
"Mac",
|
|
||||||
]
|
|
||||||
@@ -1,823 +0,0 @@
|
|||||||
"""AEGIS-256"""
|
|
||||||
# All modules are generated from aegis256.py by tools/gen_modules.py!
|
|
||||||
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
from ._loader import ffi
|
|
||||||
from ._loader import lib as _lib
|
|
||||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
|
||||||
|
|
||||||
# Constants exposed as functions in C; mirror them as integers at module import time
|
|
||||||
KEYBYTES = _lib.aegis256_keybytes()
|
|
||||||
NPUBBYTES = _lib.aegis256_npubbytes()
|
|
||||||
ABYTES_MIN = _lib.aegis256_abytes_min()
|
|
||||||
ABYTES_MAX = _lib.aegis256_abytes_max()
|
|
||||||
TAILBYTES_MAX = _lib.aegis256_tailbytes_max()
|
|
||||||
ALIGNMENT = 16
|
|
||||||
RATE = 16
|
|
||||||
|
|
||||||
|
|
||||||
def random_key() -> bytearray:
|
|
||||||
"""Generate a random key using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(KEYBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def random_nonce() -> bytearray:
|
|
||||||
"""Generate a random nonce using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(NPUBBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def _ptr(buf):
|
|
||||||
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
ct_into: Buffer | None = None,
|
|
||||||
mac_into: Buffer | None = None,
|
|
||||||
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
mac_into: Buffer to write MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (ciphertext, mac)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
|
|
||||||
if ct_into is None:
|
|
||||||
c = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(ct_into) < len(message):
|
|
||||||
raise TypeError("ct_into length must be at least len(message)")
|
|
||||||
c = ct_into
|
|
||||||
if mac_into is None:
|
|
||||||
mac = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(mac_into) < maclen:
|
|
||||||
raise TypeError("mac_into length must be at least maclen")
|
|
||||||
mac = mac_into
|
|
||||||
|
|
||||||
rc = _lib.aegis256_encrypt_detached(
|
|
||||||
ffi.from_buffer(c),
|
|
||||||
ffi.from_buffer(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
|
||||||
return (
|
|
||||||
c if ct_into is None else memoryview(c)[: len(message)],
|
|
||||||
mac if mac_into is None else memoryview(mac)[:maclen],
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
mac: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with detached MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
mac: The MAC to verify.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis256_decrypt_detached(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message) + maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < len(message) + maclen:
|
|
||||||
raise TypeError("into length must be at least len(message)+maclen")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis256_encrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with appended MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext with MAC to decrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if len(ct) < maclen:
|
|
||||||
raise TypeError("ciphertext too short for tag")
|
|
||||||
expected_out = len(ct) - maclen
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(expected_out)
|
|
||||||
else:
|
|
||||||
if len(into) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be at least len(ciphertext_with_tag)-maclen"
|
|
||||||
)
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis256_decrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def stream(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer | None,
|
|
||||||
length: int | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | Buffer:
|
|
||||||
f"""Generate a stream of pseudorandom bytes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}, uses zeroes for nonce if None).
|
|
||||||
length: Number of bytes to generate (required if into is None).
|
|
||||||
into: Buffer to write stream into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Pseudorandom bytes as bytearray, or into returned directly.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid or neither length nor into provided.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if nonce is not None and len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
if length is None:
|
|
||||||
raise TypeError("provide either into or length")
|
|
||||||
out = bytearray(length)
|
|
||||||
else:
|
|
||||||
if length is not None and len(into) < length:
|
|
||||||
raise TypeError("into length must be at least length")
|
|
||||||
out = into
|
|
||||||
_lib.aegis256_stream(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: length or len(out)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(into) < len(message):
|
|
||||||
raise TypeError("into length must be at least len(message)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis256_encrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(message)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis256_decrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
# This is missing from C API but convenient to have here
|
|
||||||
def mac(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
data: Buffer,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Compute a MAC for the given data in one shot.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=})
|
|
||||||
nonce: Nonce ({NPUBBYTES=})
|
|
||||||
data: Data to MAC
|
|
||||||
maclen: MAC length (16 or 32, default 16)
|
|
||||||
into: Buffer to write MAC into (default: bytearray created)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
|
||||||
"""
|
|
||||||
mac_state = Mac(key, nonce)
|
|
||||||
mac_state.update(data)
|
|
||||||
return mac_state.final(maclen, into)
|
|
||||||
|
|
||||||
|
|
||||||
class Mac:
|
|
||||||
"""AEGIS-256 MAC state wrapper.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
mac = Mac(key, nonce)
|
|
||||||
mac.update(data)
|
|
||||||
tag = mac.final() # defaults to 16-byte MAC
|
|
||||||
# or verify:
|
|
||||||
mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner")
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
_other=None,
|
|
||||||
) -> None:
|
|
||||||
f"""Initialize a MAC state with a nonce and key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
st, owner = new_aligned_struct("aegis256_mac_state", ALIGNMENT)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
if _other is not None: # clone path
|
|
||||||
_lib.aegis256_mac_state_clone(self._st, _other._st)
|
|
||||||
return
|
|
||||||
# Normal init path
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
_lib.aegis256_mac_init(self._st, _ptr(key), _ptr(nonce))
|
|
||||||
|
|
||||||
def __deepcopy__(self) -> "Mac":
|
|
||||||
"""Return a clone of current MAC state."""
|
|
||||||
return Mac(b"", b"", _other=self)
|
|
||||||
|
|
||||||
clone = __deepcopy__
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
"""Reset the MAC state so it can be reused with the same nonce and key."""
|
|
||||||
_lib.aegis256_mac_reset(self._st)
|
|
||||||
|
|
||||||
def update(self, data: Buffer) -> None:
|
|
||||||
"""Absorb data into the MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Bytes-like object to authenticate.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If the underlying C function reports an error.
|
|
||||||
"""
|
|
||||||
rc = _lib.aegis256_mac_update(self._st, _ptr(data), len(data))
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac update failed: {err_name}")
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize and return the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
maclen: Tag length in bytes (16 or 32). Defaults to 16.
|
|
||||||
into: Optional buffer to write the tag into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The tag as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If finalization fails in the C library.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < maclen:
|
|
||||||
raise TypeError("into length must be at least maclen")
|
|
||||||
out = into
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
rc = _lib.aegis256_mac_final(self._st, ffi.from_buffer(out_mv), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac final failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
|
||||||
|
|
||||||
def verify(self, mac: Buffer):
|
|
||||||
"""Verify a tag for the current MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: The tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Only if verification succeeds.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If verification fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis256_mac_verify(self._st, _ptr(mac), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("mac verification failed")
|
|
||||||
|
|
||||||
|
|
||||||
class Encryptor:
|
|
||||||
"""Incremental encryptor.
|
|
||||||
|
|
||||||
- update(message[, into]) -> returns produced ciphertext bytes
|
|
||||||
- final([into], maclen=16) -> returns tail+tag bytes
|
|
||||||
- final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental encryptor.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data to bind to the encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis256_state", ALIGNMENT)
|
|
||||||
_lib.aegis256_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total plaintext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total ciphertext bytes produced so far.
|
|
||||||
|
|
||||||
Includes update() and final() output.
|
|
||||||
"""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self, message: Buffer, into: Buffer | None = None
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Encrypt a chunk of the message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: Plaintext bytes to encrypt.
|
|
||||||
into: Optional destination buffer; must be >= len(message).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(message)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be >= expected output size for this update"
|
|
||||||
)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis256_state_encrypt_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(
|
|
||||||
f"state encrypt update failed: {err_name} written {written[0]}"
|
|
||||||
)
|
|
||||||
w = int(written[0])
|
|
||||||
assert w == expected_out
|
|
||||||
self._bytes_in += len(message)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self, into: Buffer | None = None, maclen: int = ABYTES_MIN
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize encryption, writing any remaining bytes and the tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
into: Optional destination buffer for the tail and tag.
|
|
||||||
maclen: Tag length (16 or 32). Defaults to 16.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If maclen is invalid.
|
|
||||||
RuntimeError: If the C final call fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
# Only the authentication tag is produced here; allocate exactly maclen
|
|
||||||
out = into if into is not None else bytearray(maclen)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis256_state_encrypt_final(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
written,
|
|
||||||
maclen,
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
|
||||||
w = int(written[0])
|
|
||||||
if into is None:
|
|
||||||
# Only the tag bytes are returned when we allocate the buffer
|
|
||||||
assert w == maclen
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class Decryptor:
|
|
||||||
"""Incremental decryptor.
|
|
||||||
|
|
||||||
- update(ciphertext[, into]) -> returns plaintext bytes
|
|
||||||
- final(mac) -> verifies the MAC tag
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental decryptor for detached tags.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data used during encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis256_state", ALIGNMENT)
|
|
||||||
_lib.aegis256_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total ciphertext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total plaintext bytes produced so far."""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
|
||||||
"""Process a chunk of ciphertext.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ct: Ciphertext bytes (without MAC).
|
|
||||||
into: Optional destination buffer; must be >= len(ciphertext).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(ct)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError("into length must be >= required capacity for this update")
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis256_state_decrypt_detached_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
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}, len(ct)={len(ct)}"
|
|
||||||
self._bytes_in += len(ct)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
|
||||||
"""Finalize decryption by verifying the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: Tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis256_state_decrypt_detached_final(
|
|
||||||
self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
|
|
||||||
|
|
||||||
def new_state():
|
|
||||||
"""Allocate and return a new aegis256_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis256_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
def new_mac_state():
|
|
||||||
"""Allocate and return a new aegis256_mac_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis256_mac_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# constants
|
|
||||||
"KEYBYTES",
|
|
||||||
"NPUBBYTES",
|
|
||||||
"ABYTES_MIN",
|
|
||||||
"ABYTES_MAX",
|
|
||||||
"TAILBYTES_MAX",
|
|
||||||
"ALIGNMENT",
|
|
||||||
"RATE",
|
|
||||||
# utility functions
|
|
||||||
"random_key",
|
|
||||||
"random_nonce",
|
|
||||||
"nonce_increment",
|
|
||||||
"wipe",
|
|
||||||
# one-shot functions
|
|
||||||
"encrypt_detached",
|
|
||||||
"decrypt_detached",
|
|
||||||
"encrypt",
|
|
||||||
"decrypt",
|
|
||||||
"stream",
|
|
||||||
"encrypt_unauthenticated",
|
|
||||||
"decrypt_unauthenticated",
|
|
||||||
"mac",
|
|
||||||
# incremental classes
|
|
||||||
"Encryptor",
|
|
||||||
"Decryptor",
|
|
||||||
"Mac",
|
|
||||||
]
|
|
||||||
@@ -1,823 +0,0 @@
|
|||||||
"""AEGIS-256X2"""
|
|
||||||
# All modules are generated from aegis256x2.py by tools/gen_modules.py!
|
|
||||||
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
from ._loader import ffi
|
|
||||||
from ._loader import lib as _lib
|
|
||||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
|
||||||
|
|
||||||
# Constants exposed as functions in C; mirror them as integers at module import time
|
|
||||||
KEYBYTES = _lib.aegis256x2_keybytes()
|
|
||||||
NPUBBYTES = _lib.aegis256x2_npubbytes()
|
|
||||||
ABYTES_MIN = _lib.aegis256x2_abytes_min()
|
|
||||||
ABYTES_MAX = _lib.aegis256x2_abytes_max()
|
|
||||||
TAILBYTES_MAX = _lib.aegis256x2_tailbytes_max()
|
|
||||||
ALIGNMENT = 32
|
|
||||||
RATE = 32
|
|
||||||
|
|
||||||
|
|
||||||
def random_key() -> bytearray:
|
|
||||||
"""Generate a random key using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(KEYBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def random_nonce() -> bytearray:
|
|
||||||
"""Generate a random nonce using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(NPUBBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def _ptr(buf):
|
|
||||||
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
ct_into: Buffer | None = None,
|
|
||||||
mac_into: Buffer | None = None,
|
|
||||||
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
mac_into: Buffer to write MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (ciphertext, mac)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
|
|
||||||
if ct_into is None:
|
|
||||||
c = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(ct_into) < len(message):
|
|
||||||
raise TypeError("ct_into length must be at least len(message)")
|
|
||||||
c = ct_into
|
|
||||||
if mac_into is None:
|
|
||||||
mac = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(mac_into) < maclen:
|
|
||||||
raise TypeError("mac_into length must be at least maclen")
|
|
||||||
mac = mac_into
|
|
||||||
|
|
||||||
rc = _lib.aegis256x2_encrypt_detached(
|
|
||||||
ffi.from_buffer(c),
|
|
||||||
ffi.from_buffer(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
|
||||||
return (
|
|
||||||
c if ct_into is None else memoryview(c)[: len(message)],
|
|
||||||
mac if mac_into is None else memoryview(mac)[:maclen],
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
mac: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with detached MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
mac: The MAC to verify.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis256x2_decrypt_detached(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message) + maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < len(message) + maclen:
|
|
||||||
raise TypeError("into length must be at least len(message)+maclen")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis256x2_encrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with appended MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext with MAC to decrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if len(ct) < maclen:
|
|
||||||
raise TypeError("ciphertext too short for tag")
|
|
||||||
expected_out = len(ct) - maclen
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(expected_out)
|
|
||||||
else:
|
|
||||||
if len(into) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be at least len(ciphertext_with_tag)-maclen"
|
|
||||||
)
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis256x2_decrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def stream(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer | None,
|
|
||||||
length: int | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | Buffer:
|
|
||||||
f"""Generate a stream of pseudorandom bytes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}, uses zeroes for nonce if None).
|
|
||||||
length: Number of bytes to generate (required if into is None).
|
|
||||||
into: Buffer to write stream into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Pseudorandom bytes as bytearray, or into returned directly.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid or neither length nor into provided.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if nonce is not None and len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
if length is None:
|
|
||||||
raise TypeError("provide either into or length")
|
|
||||||
out = bytearray(length)
|
|
||||||
else:
|
|
||||||
if length is not None and len(into) < length:
|
|
||||||
raise TypeError("into length must be at least length")
|
|
||||||
out = into
|
|
||||||
_lib.aegis256x2_stream(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: length or len(out)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(into) < len(message):
|
|
||||||
raise TypeError("into length must be at least len(message)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis256x2_encrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(message)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis256x2_decrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
# This is missing from C API but convenient to have here
|
|
||||||
def mac(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
data: Buffer,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Compute a MAC for the given data in one shot.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=})
|
|
||||||
nonce: Nonce ({NPUBBYTES=})
|
|
||||||
data: Data to MAC
|
|
||||||
maclen: MAC length (16 or 32, default 16)
|
|
||||||
into: Buffer to write MAC into (default: bytearray created)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
|
||||||
"""
|
|
||||||
mac_state = Mac(key, nonce)
|
|
||||||
mac_state.update(data)
|
|
||||||
return mac_state.final(maclen, into)
|
|
||||||
|
|
||||||
|
|
||||||
class Mac:
|
|
||||||
"""AEGIS-256X2 MAC state wrapper.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
mac = Mac(key, nonce)
|
|
||||||
mac.update(data)
|
|
||||||
tag = mac.final() # defaults to 16-byte MAC
|
|
||||||
# or verify:
|
|
||||||
mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner")
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
_other=None,
|
|
||||||
) -> None:
|
|
||||||
f"""Initialize a MAC state with a nonce and key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
st, owner = new_aligned_struct("aegis256x2_mac_state", ALIGNMENT)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
if _other is not None: # clone path
|
|
||||||
_lib.aegis256x2_mac_state_clone(self._st, _other._st)
|
|
||||||
return
|
|
||||||
# Normal init path
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
_lib.aegis256x2_mac_init(self._st, _ptr(key), _ptr(nonce))
|
|
||||||
|
|
||||||
def __deepcopy__(self) -> "Mac":
|
|
||||||
"""Return a clone of current MAC state."""
|
|
||||||
return Mac(b"", b"", _other=self)
|
|
||||||
|
|
||||||
clone = __deepcopy__
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
"""Reset the MAC state so it can be reused with the same nonce and key."""
|
|
||||||
_lib.aegis256x2_mac_reset(self._st)
|
|
||||||
|
|
||||||
def update(self, data: Buffer) -> None:
|
|
||||||
"""Absorb data into the MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Bytes-like object to authenticate.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If the underlying C function reports an error.
|
|
||||||
"""
|
|
||||||
rc = _lib.aegis256x2_mac_update(self._st, _ptr(data), len(data))
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac update failed: {err_name}")
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize and return the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
maclen: Tag length in bytes (16 or 32). Defaults to 16.
|
|
||||||
into: Optional buffer to write the tag into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The tag as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If finalization fails in the C library.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < maclen:
|
|
||||||
raise TypeError("into length must be at least maclen")
|
|
||||||
out = into
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
rc = _lib.aegis256x2_mac_final(self._st, ffi.from_buffer(out_mv), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac final failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
|
||||||
|
|
||||||
def verify(self, mac: Buffer):
|
|
||||||
"""Verify a tag for the current MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: The tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Only if verification succeeds.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If verification fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis256x2_mac_verify(self._st, _ptr(mac), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("mac verification failed")
|
|
||||||
|
|
||||||
|
|
||||||
class Encryptor:
|
|
||||||
"""Incremental encryptor.
|
|
||||||
|
|
||||||
- update(message[, into]) -> returns produced ciphertext bytes
|
|
||||||
- final([into], maclen=16) -> returns tail+tag bytes
|
|
||||||
- final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental encryptor.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data to bind to the encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis256x2_state", ALIGNMENT)
|
|
||||||
_lib.aegis256x2_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total plaintext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total ciphertext bytes produced so far.
|
|
||||||
|
|
||||||
Includes update() and final() output.
|
|
||||||
"""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self, message: Buffer, into: Buffer | None = None
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Encrypt a chunk of the message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: Plaintext bytes to encrypt.
|
|
||||||
into: Optional destination buffer; must be >= len(message).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(message)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be >= expected output size for this update"
|
|
||||||
)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis256x2_state_encrypt_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(
|
|
||||||
f"state encrypt update failed: {err_name} written {written[0]}"
|
|
||||||
)
|
|
||||||
w = int(written[0])
|
|
||||||
assert w == expected_out
|
|
||||||
self._bytes_in += len(message)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self, into: Buffer | None = None, maclen: int = ABYTES_MIN
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize encryption, writing any remaining bytes and the tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
into: Optional destination buffer for the tail and tag.
|
|
||||||
maclen: Tag length (16 or 32). Defaults to 16.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If maclen is invalid.
|
|
||||||
RuntimeError: If the C final call fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
# Only the authentication tag is produced here; allocate exactly maclen
|
|
||||||
out = into if into is not None else bytearray(maclen)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis256x2_state_encrypt_final(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
written,
|
|
||||||
maclen,
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
|
||||||
w = int(written[0])
|
|
||||||
if into is None:
|
|
||||||
# Only the tag bytes are returned when we allocate the buffer
|
|
||||||
assert w == maclen
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class Decryptor:
|
|
||||||
"""Incremental decryptor.
|
|
||||||
|
|
||||||
- update(ciphertext[, into]) -> returns plaintext bytes
|
|
||||||
- final(mac) -> verifies the MAC tag
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental decryptor for detached tags.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data used during encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis256x2_state", ALIGNMENT)
|
|
||||||
_lib.aegis256x2_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total ciphertext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total plaintext bytes produced so far."""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
|
||||||
"""Process a chunk of ciphertext.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ct: Ciphertext bytes (without MAC).
|
|
||||||
into: Optional destination buffer; must be >= len(ciphertext).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(ct)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError("into length must be >= required capacity for this update")
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis256x2_state_decrypt_detached_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
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}, len(ct)={len(ct)}"
|
|
||||||
self._bytes_in += len(ct)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
|
||||||
"""Finalize decryption by verifying the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: Tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis256x2_state_decrypt_detached_final(
|
|
||||||
self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
|
|
||||||
|
|
||||||
def new_state():
|
|
||||||
"""Allocate and return a new aegis256x2_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis256x2_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
def new_mac_state():
|
|
||||||
"""Allocate and return a new aegis256x2_mac_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis256x2_mac_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# constants
|
|
||||||
"KEYBYTES",
|
|
||||||
"NPUBBYTES",
|
|
||||||
"ABYTES_MIN",
|
|
||||||
"ABYTES_MAX",
|
|
||||||
"TAILBYTES_MAX",
|
|
||||||
"ALIGNMENT",
|
|
||||||
"RATE",
|
|
||||||
# utility functions
|
|
||||||
"random_key",
|
|
||||||
"random_nonce",
|
|
||||||
"nonce_increment",
|
|
||||||
"wipe",
|
|
||||||
# one-shot functions
|
|
||||||
"encrypt_detached",
|
|
||||||
"decrypt_detached",
|
|
||||||
"encrypt",
|
|
||||||
"decrypt",
|
|
||||||
"stream",
|
|
||||||
"encrypt_unauthenticated",
|
|
||||||
"decrypt_unauthenticated",
|
|
||||||
"mac",
|
|
||||||
# incremental classes
|
|
||||||
"Encryptor",
|
|
||||||
"Decryptor",
|
|
||||||
"Mac",
|
|
||||||
]
|
|
||||||
@@ -1,823 +0,0 @@
|
|||||||
"""AEGIS-256X4"""
|
|
||||||
# All modules are generated from aegis256x4.py by tools/gen_modules.py!
|
|
||||||
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import secrets
|
|
||||||
|
|
||||||
from ._loader import ffi
|
|
||||||
from ._loader import lib as _lib
|
|
||||||
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
|
||||||
|
|
||||||
# Constants exposed as functions in C; mirror them as integers at module import time
|
|
||||||
KEYBYTES = _lib.aegis256x4_keybytes()
|
|
||||||
NPUBBYTES = _lib.aegis256x4_npubbytes()
|
|
||||||
ABYTES_MIN = _lib.aegis256x4_abytes_min()
|
|
||||||
ABYTES_MAX = _lib.aegis256x4_abytes_max()
|
|
||||||
TAILBYTES_MAX = _lib.aegis256x4_tailbytes_max()
|
|
||||||
ALIGNMENT = 64
|
|
||||||
RATE = 64
|
|
||||||
|
|
||||||
|
|
||||||
def random_key() -> bytearray:
|
|
||||||
"""Generate a random key using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(KEYBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def random_nonce() -> bytearray:
|
|
||||||
"""Generate a random nonce using cryptographically secure random bytes."""
|
|
||||||
return bytearray(secrets.token_bytes(NPUBBYTES))
|
|
||||||
|
|
||||||
|
|
||||||
def _ptr(buf):
|
|
||||||
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
ct_into: Buffer | None = None,
|
|
||||||
mac_into: Buffer | None = None,
|
|
||||||
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
mac_into: Buffer to write MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (ciphertext, mac)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
|
|
||||||
if ct_into is None:
|
|
||||||
c = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(ct_into) < len(message):
|
|
||||||
raise TypeError("ct_into length must be at least len(message)")
|
|
||||||
c = ct_into
|
|
||||||
if mac_into is None:
|
|
||||||
mac = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(mac_into) < maclen:
|
|
||||||
raise TypeError("mac_into length must be at least maclen")
|
|
||||||
mac = mac_into
|
|
||||||
|
|
||||||
rc = _lib.aegis256x4_encrypt_detached(
|
|
||||||
ffi.from_buffer(c),
|
|
||||||
ffi.from_buffer(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
|
||||||
return (
|
|
||||||
c if ct_into is None else memoryview(c)[: len(message)],
|
|
||||||
mac if mac_into is None else memoryview(mac)[:maclen],
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_detached(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
mac: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with detached MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
mac: The MAC to verify.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis256x4_decrypt_detached(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(mac),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If encryption fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message) + maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < len(message) + maclen:
|
|
||||||
raise TypeError("into length must be at least len(message)+maclen")
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis256x4_encrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
maclen,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"encrypt failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
ad: Buffer | None = None,
|
|
||||||
*,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext with appended MAC and associated data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext with MAC to decrypt.
|
|
||||||
ad: Associated data (optional).
|
|
||||||
maclen: MAC length (16 or 32, default 16).
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if len(ct) < maclen:
|
|
||||||
raise TypeError("ciphertext too short for tag")
|
|
||||||
expected_out = len(ct) - maclen
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(expected_out)
|
|
||||||
else:
|
|
||||||
if len(into) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be at least len(ciphertext_with_tag)-maclen"
|
|
||||||
)
|
|
||||||
out = into
|
|
||||||
|
|
||||||
rc = _lib.aegis256x4_decrypt(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
maclen,
|
|
||||||
_ptr(ad),
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def stream(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer | None,
|
|
||||||
length: int | None = None,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | Buffer:
|
|
||||||
f"""Generate a stream of pseudorandom bytes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}, uses zeroes for nonce if None).
|
|
||||||
length: Number of bytes to generate (required if into is None).
|
|
||||||
into: Buffer to write stream into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Pseudorandom bytes as bytearray, or into returned directly.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid or neither length nor into provided.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if nonce is not None and len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
if length is None:
|
|
||||||
raise TypeError("provide either into or length")
|
|
||||||
out = bytearray(length)
|
|
||||||
else:
|
|
||||||
if length is not None and len(into) < length:
|
|
||||||
raise TypeError("into length must be at least length")
|
|
||||||
out = into
|
|
||||||
_lib.aegis256x4_stream(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: length or len(out)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def encrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
message: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Encrypt message without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
message: The plaintext message to encrypt.
|
|
||||||
into: Buffer to write ciphertext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(message))
|
|
||||||
else:
|
|
||||||
if len(into) < len(message):
|
|
||||||
raise TypeError("into length must be at least len(message)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis256x4_encrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(message)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def decrypt_unauthenticated(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
ct: Buffer,
|
|
||||||
*,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Decrypt ciphertext without authentication (for testing/debugging).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ct: The ciphertext to decrypt.
|
|
||||||
into: Buffer to write plaintext into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(len(ct))
|
|
||||||
else:
|
|
||||||
if len(into) < len(ct):
|
|
||||||
raise TypeError("into length must be at least len(ciphertext)")
|
|
||||||
out = into
|
|
||||||
_lib.aegis256x4_decrypt_unauthenticated(
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
return out if into is None else memoryview(out)[: len(ct)] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
# This is missing from C API but convenient to have here
|
|
||||||
def mac(
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
data: Buffer,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
f"""Compute a MAC for the given data in one shot.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=})
|
|
||||||
nonce: Nonce ({NPUBBYTES=})
|
|
||||||
data: Data to MAC
|
|
||||||
maclen: MAC length (16 or 32, default 16)
|
|
||||||
into: Buffer to write MAC into (default: bytearray created)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
|
||||||
"""
|
|
||||||
mac_state = Mac(key, nonce)
|
|
||||||
mac_state.update(data)
|
|
||||||
return mac_state.final(maclen, into)
|
|
||||||
|
|
||||||
|
|
||||||
class Mac:
|
|
||||||
"""AEGIS-256X4 MAC state wrapper.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
mac = Mac(key, nonce)
|
|
||||||
mac.update(data)
|
|
||||||
tag = mac.final() # defaults to 16-byte MAC
|
|
||||||
# or verify:
|
|
||||||
mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner")
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
key: Buffer,
|
|
||||||
nonce: Buffer,
|
|
||||||
_other=None,
|
|
||||||
) -> None:
|
|
||||||
f"""Initialize a MAC state with a nonce and key.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
st, owner = new_aligned_struct("aegis256x4_mac_state", ALIGNMENT)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
if _other is not None: # clone path
|
|
||||||
_lib.aegis256x4_mac_state_clone(self._st, _other._st)
|
|
||||||
return
|
|
||||||
# Normal init path
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
_lib.aegis256x4_mac_init(self._st, _ptr(key), _ptr(nonce))
|
|
||||||
|
|
||||||
def __deepcopy__(self) -> "Mac":
|
|
||||||
"""Return a clone of current MAC state."""
|
|
||||||
return Mac(b"", b"", _other=self)
|
|
||||||
|
|
||||||
clone = __deepcopy__
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
"""Reset the MAC state so it can be reused with the same nonce and key."""
|
|
||||||
_lib.aegis256x4_mac_reset(self._st)
|
|
||||||
|
|
||||||
def update(self, data: Buffer) -> None:
|
|
||||||
"""Absorb data into the MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Bytes-like object to authenticate.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If the underlying C function reports an error.
|
|
||||||
"""
|
|
||||||
rc = _lib.aegis256x4_mac_update(self._st, _ptr(data), len(data))
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac update failed: {err_name}")
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self,
|
|
||||||
maclen: int = ABYTES_MIN,
|
|
||||||
into: Buffer | None = None,
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize and return the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
maclen: Tag length in bytes (16 or 32). Defaults to 16.
|
|
||||||
into: Optional buffer to write the tag into (default: bytearray created).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The tag as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If lengths are invalid.
|
|
||||||
RuntimeError: If finalization fails in the C library.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
if into is None:
|
|
||||||
out = bytearray(maclen)
|
|
||||||
else:
|
|
||||||
if len(into) < maclen:
|
|
||||||
raise TypeError("into length must be at least maclen")
|
|
||||||
out = into
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
rc = _lib.aegis256x4_mac_final(self._st, ffi.from_buffer(out_mv), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"mac final failed: {err_name}")
|
|
||||||
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
|
||||||
|
|
||||||
def verify(self, mac: Buffer):
|
|
||||||
"""Verify a tag for the current MAC state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: The tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Only if verification succeeds.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If verification fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis256x4_mac_verify(self._st, _ptr(mac), maclen)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("mac verification failed")
|
|
||||||
|
|
||||||
|
|
||||||
class Encryptor:
|
|
||||||
"""Incremental encryptor.
|
|
||||||
|
|
||||||
- update(message[, into]) -> returns produced ciphertext bytes
|
|
||||||
- final([into], maclen=16) -> returns tail+tag bytes
|
|
||||||
- final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac)
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental encryptor.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data to bind to the encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis256x4_state", ALIGNMENT)
|
|
||||||
_lib.aegis256x4_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total plaintext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total ciphertext bytes produced so far.
|
|
||||||
|
|
||||||
Includes update() and final() output.
|
|
||||||
"""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self, message: Buffer, into: Buffer | None = None
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Encrypt a chunk of the message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: Plaintext bytes to encrypt.
|
|
||||||
into: Optional destination buffer; must be >= len(message).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(message)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError(
|
|
||||||
"into length must be >= expected output size for this update"
|
|
||||||
)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis256x4_state_encrypt_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(message),
|
|
||||||
len(message),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(
|
|
||||||
f"state encrypt update failed: {err_name} written {written[0]}"
|
|
||||||
)
|
|
||||||
w = int(written[0])
|
|
||||||
assert w == expected_out
|
|
||||||
self._bytes_in += len(message)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(
|
|
||||||
self, into: Buffer | None = None, maclen: int = ABYTES_MIN
|
|
||||||
) -> bytearray | memoryview:
|
|
||||||
"""Finalize encryption, writing any remaining bytes and the tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
into: Optional destination buffer for the tail and tag.
|
|
||||||
maclen: Tag length (16 or 32). Defaults to 16.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If maclen is invalid.
|
|
||||||
RuntimeError: If the C final call fails.
|
|
||||||
"""
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("maclen must be 16 or 32")
|
|
||||||
# Only the authentication tag is produced here; allocate exactly maclen
|
|
||||||
out = into if into is not None else bytearray(maclen)
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis256x4_state_encrypt_final(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out),
|
|
||||||
len(out),
|
|
||||||
written,
|
|
||||||
maclen,
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
|
||||||
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
|
||||||
w = int(written[0])
|
|
||||||
if into is None:
|
|
||||||
# Only the tag bytes are returned when we allocate the buffer
|
|
||||||
assert w == maclen
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class Decryptor:
|
|
||||||
"""Incremental decryptor.
|
|
||||||
|
|
||||||
- update(ciphertext[, into]) -> returns plaintext bytes
|
|
||||||
- final(mac) -> verifies the MAC tag
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out")
|
|
||||||
|
|
||||||
def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None):
|
|
||||||
f"""Create an incremental decryptor for detached tags.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
key: Key ({KEYBYTES=}).
|
|
||||||
nonce: Nonce ({NPUBBYTES=}).
|
|
||||||
ad: Associated data used during encryption (optional).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If key or nonce lengths are invalid.
|
|
||||||
"""
|
|
||||||
if len(key) != KEYBYTES:
|
|
||||||
raise TypeError(f"key length must be {KEYBYTES}")
|
|
||||||
if len(nonce) != NPUBBYTES:
|
|
||||||
raise TypeError(f"nonce length must be {NPUBBYTES}")
|
|
||||||
st, owner = new_aligned_struct("aegis256x4_state", ALIGNMENT)
|
|
||||||
_lib.aegis256x4_state_init(
|
|
||||||
st,
|
|
||||||
_ptr(ad) if ad is not None else ffi.NULL,
|
|
||||||
0 if ad is None else len(ad),
|
|
||||||
_ptr(nonce),
|
|
||||||
_ptr(key),
|
|
||||||
)
|
|
||||||
self._st = st
|
|
||||||
self._owner = owner
|
|
||||||
self._bytes_in = 0
|
|
||||||
self._bytes_out = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_in(self) -> int:
|
|
||||||
"""Total ciphertext bytes fed to update() so far."""
|
|
||||||
return self._bytes_in
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bytes_out(self) -> int:
|
|
||||||
"""Total plaintext bytes produced so far."""
|
|
||||||
return self._bytes_out
|
|
||||||
|
|
||||||
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
|
||||||
"""Process a chunk of ciphertext.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ct: Ciphertext bytes (without MAC).
|
|
||||||
into: Optional destination buffer; must be >= len(ciphertext).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If destination buffer is too small.
|
|
||||||
RuntimeError: If the C update call fails.
|
|
||||||
"""
|
|
||||||
expected_out = len(ct)
|
|
||||||
out = into if into is not None else bytearray(expected_out)
|
|
||||||
out_mv = memoryview(out)
|
|
||||||
if len(out_mv) < expected_out:
|
|
||||||
raise TypeError("into length must be >= required capacity for this update")
|
|
||||||
written = ffi.new("size_t *")
|
|
||||||
rc = _lib.aegis256x4_state_decrypt_detached_update(
|
|
||||||
self._st,
|
|
||||||
ffi.from_buffer(out_mv),
|
|
||||||
len(out_mv),
|
|
||||||
written,
|
|
||||||
_ptr(ct),
|
|
||||||
len(ct),
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
err_num = ffi.errno
|
|
||||||
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}, len(ct)={len(ct)}"
|
|
||||||
self._bytes_in += len(ct)
|
|
||||||
self._bytes_out += w
|
|
||||||
return out if into is None else memoryview(out)[:w] # type: ignore
|
|
||||||
|
|
||||||
def final(self, mac: Buffer) -> None:
|
|
||||||
"""Finalize decryption by verifying the MAC tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mac: Tag to verify (16 or 32 bytes).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If tag length is invalid.
|
|
||||||
ValueError: If authentication fails.
|
|
||||||
"""
|
|
||||||
maclen = len(mac)
|
|
||||||
if maclen not in (16, 32):
|
|
||||||
raise TypeError("mac length must be 16 or 32")
|
|
||||||
rc = _lib.aegis256x4_state_decrypt_detached_final(
|
|
||||||
self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen
|
|
||||||
)
|
|
||||||
if rc != 0:
|
|
||||||
raise ValueError("authentication failed")
|
|
||||||
|
|
||||||
|
|
||||||
def new_state():
|
|
||||||
"""Allocate and return a new aegis256x4_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis256x4_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
def new_mac_state():
|
|
||||||
"""Allocate and return a new aegis256x4_mac_state* with proper alignment."""
|
|
||||||
return new_aligned_struct("aegis256x4_mac_state", ALIGNMENT)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
# constants
|
|
||||||
"KEYBYTES",
|
|
||||||
"NPUBBYTES",
|
|
||||||
"ABYTES_MIN",
|
|
||||||
"ABYTES_MAX",
|
|
||||||
"TAILBYTES_MAX",
|
|
||||||
"ALIGNMENT",
|
|
||||||
"RATE",
|
|
||||||
# utility functions
|
|
||||||
"random_key",
|
|
||||||
"random_nonce",
|
|
||||||
"nonce_increment",
|
|
||||||
"wipe",
|
|
||||||
# one-shot functions
|
|
||||||
"encrypt_detached",
|
|
||||||
"decrypt_detached",
|
|
||||||
"encrypt",
|
|
||||||
"decrypt",
|
|
||||||
"stream",
|
|
||||||
"encrypt_unauthenticated",
|
|
||||||
"decrypt_unauthenticated",
|
|
||||||
"mac",
|
|
||||||
# incremental classes
|
|
||||||
"Encryptor",
|
|
||||||
"Decryptor",
|
|
||||||
"Mac",
|
|
||||||
]
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Python benchmark matching src/test/benchmark.zig for all supported Aegis algorithms.
|
|
||||||
|
|
||||||
It performs two benchmarks with the same parameters as the Zig version:
|
|
||||||
- AEGIS encrypt (attached tag, maclen = ABYTES_MIN)
|
|
||||||
- AEGIS MAC (clone state pattern)
|
|
||||||
|
|
||||||
Output format and throughput units mirror the Zig benchmark (Mb/s).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
|
|
||||||
from pyaegis import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
|
|
||||||
|
|
||||||
MSG_LEN = 16384000 # 16 000 KiB
|
|
||||||
ITERATIONS = 100
|
|
||||||
|
|
||||||
ALGORITHMS = [
|
|
||||||
("AEGIS-128L", aegis128l),
|
|
||||||
("AEGIS-128X2", aegis128x2),
|
|
||||||
("AEGIS-128X4", aegis128x4),
|
|
||||||
("AEGIS-256", aegis256),
|
|
||||||
("AEGIS-256X2", aegis256x2),
|
|
||||||
("AEGIS-256X4", aegis256x4),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _random_bytes(n: int) -> bytes:
|
|
||||||
return os.urandom(n)
|
|
||||||
|
|
||||||
|
|
||||||
def bench_encrypt(alg_name: str, a) -> None:
|
|
||||||
key = _random_bytes(a.KEYBYTES)
|
|
||||||
nonce = _random_bytes(a.NPUBBYTES)
|
|
||||||
|
|
||||||
# Single buffer, as in Zig: c_out == m buffer, with tag appended
|
|
||||||
maclen = a.ABYTES_MIN
|
|
||||||
buf = bytearray(MSG_LEN + maclen)
|
|
||||||
# Initialize buffer with random data
|
|
||||||
buf[:] = _random_bytes(len(buf))
|
|
||||||
|
|
||||||
mview = memoryview(buf)[:MSG_LEN]
|
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
for _ in range(ITERATIONS):
|
|
||||||
a.encrypt(key, nonce, mview, None, maclen=maclen, into=buf)
|
|
||||||
t1 = time.perf_counter()
|
|
||||||
|
|
||||||
# Prevent any unrealistic optimization assumptions
|
|
||||||
_ = buf[0]
|
|
||||||
|
|
||||||
bits = MSG_LEN * ITERATIONS * 8
|
|
||||||
elapsed_s = t1 - t0
|
|
||||||
throughput_mbps = (
|
|
||||||
(bits / (elapsed_s * 1_000_000)) if elapsed_s > 0 else float("inf")
|
|
||||||
)
|
|
||||||
print(f"{alg_name}\t{throughput_mbps:10.2f} Mb/s")
|
|
||||||
|
|
||||||
|
|
||||||
def bench_mac(alg_name: str, a) -> None:
|
|
||||||
key = _random_bytes(a.KEYBYTES)
|
|
||||||
nonce = _random_bytes(a.NPUBBYTES)
|
|
||||||
|
|
||||||
buf = bytearray(MSG_LEN)
|
|
||||||
buf[:] = _random_bytes(len(buf))
|
|
||||||
|
|
||||||
mac0 = a.Mac(key, nonce)
|
|
||||||
mac_out = bytearray(a.ABYTES_MAX)
|
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
for _ in range(ITERATIONS):
|
|
||||||
mac = mac0.clone()
|
|
||||||
mac.update(buf)
|
|
||||||
mac.final(maclen=a.ABYTES_MAX, into=mac_out)
|
|
||||||
t1 = time.perf_counter()
|
|
||||||
|
|
||||||
_ = mac_out[0]
|
|
||||||
|
|
||||||
bits = MSG_LEN * ITERATIONS * 8
|
|
||||||
elapsed_s = t1 - t0
|
|
||||||
throughput_mbps = (
|
|
||||||
(bits / (elapsed_s * 1_000_000)) if elapsed_s > 0 else float("inf")
|
|
||||||
)
|
|
||||||
print(f"{alg_name} MAC\t{throughput_mbps:10.2f} Mb/s")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# aegis_init() is called in the loader at import time already
|
|
||||||
# Run encrypt benchmarks in order: 256, 256x2, 256x4, 128l, 128x2, 128x4
|
|
||||||
bench_encrypt("AEGIS-256", aegis256)
|
|
||||||
bench_encrypt("AEGIS-256X2", aegis256x2)
|
|
||||||
bench_encrypt("AEGIS-256X4", aegis256x4)
|
|
||||||
bench_encrypt("AEGIS-128L", aegis128l)
|
|
||||||
bench_encrypt("AEGIS-128X2", aegis128x2)
|
|
||||||
bench_encrypt("AEGIS-128X4", aegis128x4)
|
|
||||||
|
|
||||||
# Run MAC benchmarks in order: 128l, 128x2, 128x4, 256, 256x2, 256x4
|
|
||||||
bench_mac("AEGIS-128L", aegis128l)
|
|
||||||
bench_mac("AEGIS-128X2", aegis128x2)
|
|
||||||
bench_mac("AEGIS-128X4", aegis128x4)
|
|
||||||
bench_mac("AEGIS-256", aegis256)
|
|
||||||
bench_mac("AEGIS-256X2", aegis256x2)
|
|
||||||
bench_mac("AEGIS-256X4", aegis256x4)
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
"""Utility helpers for pyaegis.
|
|
||||||
|
|
||||||
Currently provides Python-side aligned allocation helpers that avoid relying
|
|
||||||
on libc/posix_memalign. Memory is owned by Python; C code only borrows it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Protocol
|
|
||||||
|
|
||||||
from ._loader import ffi
|
|
||||||
|
|
||||||
__all__ = ["new_aligned_struct", "aligned_address", "Buffer", "nonce_increment", "wipe"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
from collections.abc import Buffer as _Buffer
|
|
||||||
|
|
||||||
class Buffer(_Buffer, Protocol): # type: ignore[misc]
|
|
||||||
def __len__(self) -> int: ...
|
|
||||||
except ImportError:
|
|
||||||
|
|
||||||
class Buffer(Protocol):
|
|
||||||
def __len__(self) -> int: ...
|
|
||||||
def __buffer__(self, flags: int) -> memoryview: ...
|
|
||||||
|
|
||||||
|
|
||||||
def aligned_address(obj) -> int:
|
|
||||||
"""Return the integer address of the start of a cffi array object."""
|
|
||||||
return int(ffi.cast("uintptr_t", ffi.addressof(obj, 0)))
|
|
||||||
|
|
||||||
|
|
||||||
def new_aligned_struct(ctype: str, alignment: int) -> tuple[object, object]:
|
|
||||||
"""Allocate memory for one instance of ``ctype`` with requested alignment.
|
|
||||||
|
|
||||||
This allocates a Python-owned unsigned char[] buffer large enough to find
|
|
||||||
an aligned start address. Returns (ptr, owner) where ptr is a ``ctype *``
|
|
||||||
and owner is the buffer object keeping the memory alive.
|
|
||||||
"""
|
|
||||||
if alignment & (alignment - 1): # Not power of two
|
|
||||||
raise ValueError("alignment must be a power of two")
|
|
||||||
size = ffi.sizeof(ctype)
|
|
||||||
base = ffi.new("unsigned char[]", size + alignment - 1)
|
|
||||||
addr = aligned_address(base)
|
|
||||||
offset = (-addr) & (alignment - 1)
|
|
||||||
aligned_uc = ffi.addressof(base, offset)
|
|
||||||
ptr = ffi.cast(f"{ctype} *", aligned_uc)
|
|
||||||
return ptr, base
|
|
||||||
|
|
||||||
|
|
||||||
def nonce_increment(nonce: Buffer) -> None:
|
|
||||||
"""Increment the nonce in place using little-endian byte order.
|
|
||||||
|
|
||||||
Useful for generating unique nonces for each consecutive message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
nonce: The nonce buffer to increment (modified in place).
|
|
||||||
"""
|
|
||||||
n = memoryview(nonce)
|
|
||||||
for i in range(len(n)):
|
|
||||||
if n[i] < 255:
|
|
||||||
n[i] += 1
|
|
||||||
return
|
|
||||||
n[i] = 0
|
|
||||||
|
|
||||||
|
|
||||||
def wipe(buffer: Buffer) -> None:
|
|
||||||
"""Set all bytes of the input buffer to zero.
|
|
||||||
|
|
||||||
Useful for securely clearing sensitive data from memory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
buffer: The buffer to wipe (modified in place).
|
|
||||||
"""
|
|
||||||
n = memoryview(buffer)
|
|
||||||
for i in range(len(n)):
|
|
||||||
n[i] = 0
|
|
||||||
+16
-9
@@ -1,16 +1,16 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0", "cffi>=2.0.0"]
|
requires = ["setuptools>=61.0", "cffi>=2.0.0", "setuptools-scm>=8.0"]
|
||||||
build-backend = "build_backend"
|
build-backend = "build_backend"
|
||||||
backend-path = ["tools"]
|
backend-path = ["tools"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pyaegis"
|
name = "aeg"
|
||||||
version = "0.2.0"
|
dynamic = ["version"]
|
||||||
description = "Python bindings for libaegis"
|
description = "AEGIS encryption easy to use Python binding. Wheels for major platforms."
|
||||||
|
readme = {file = "README.md", content-type = "text/markdown"}
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"Topic :: Security :: Cryptography",
|
"Topic :: Security :: Cryptography",
|
||||||
@@ -20,16 +20,23 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/aegis-aead/libaegis"
|
Homepage = "https://git.zi.fi/LeoVasanko/aegis-python"
|
||||||
|
Repository = "https://github.com/LeoVasanko/aegis-python"
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"auditwheel>=6.5.0",
|
||||||
"pytest>=8.4.2",
|
"pytest>=8.4.2",
|
||||||
|
"ruff>=0.14.4",
|
||||||
"setuptools>=80.9.0",
|
"setuptools>=80.9.0",
|
||||||
|
"setuptools-scm>=9.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
packages = ["pyaegis"]
|
package-dir = {"" = "src"}
|
||||||
|
packages = ["aeg"]
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
pyaegis = ["*.h", "*.so", "*.pyd"]
|
aeg = ["*.h"]
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
|||||||
@@ -1,49 +1,24 @@
|
|||||||
"""Setup script for pyaegis - builds CFFI extension linking to libaegis.a"""
|
"""Setup script for aeg - builds CFFI extension with libaegis C library."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import sysconfig
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from cffi import FFI
|
from cffi import FFI
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
|
libaegis_static = Path("libaegis/zig-out/lib") / (
|
||||||
|
"aegis.lib" if sys.platform == "win32" else "libaegis.a"
|
||||||
|
)
|
||||||
|
|
||||||
def find_libaegis():
|
|
||||||
"""Locate libaegis.a - check common locations."""
|
|
||||||
libaegis_paths = [
|
|
||||||
Path("libaegis/zig-out/lib/libaegis.a"), # Zig build output (repo build)
|
|
||||||
Path("libaegis/build/libaegis.a"), # CMake build output (repo build)
|
|
||||||
Path("/usr/local/lib/libaegis.a"), # System install
|
|
||||||
Path("/usr/lib/libaegis.a"), # System install
|
|
||||||
]
|
|
||||||
|
|
||||||
for path in libaegis_paths:
|
|
||||||
if path.exists():
|
|
||||||
print(f"Found libaegis.a at: {path.resolve()}")
|
|
||||||
return str(path.resolve())
|
|
||||||
|
|
||||||
# Return None instead of raising - will be caught during build
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# Read the CDEF header
|
|
||||||
cdef_path = Path(__file__).parent / "pyaegis" / "aegis_cdef.h"
|
|
||||||
cdef_content = cdef_path.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
# Create CFFI builder
|
|
||||||
ffibuilder = FFI()
|
ffibuilder = FFI()
|
||||||
ffibuilder.cdef(cdef_content)
|
ffibuilder.cdef((Path(__file__).parent / "src/aeg/aegis_cdef.h").read_text())
|
||||||
|
|
||||||
# Include directory for headers
|
# Free-threaded Python does not support Limited API (abi3)
|
||||||
include_dirs = []
|
is_free_threaded = sysconfig.get_config_var("Py_GIL_DISABLED")
|
||||||
libaegis_include = Path("libaegis/src/include")
|
|
||||||
if libaegis_include.exists():
|
|
||||||
include_dirs.append(str(libaegis_include.resolve()))
|
|
||||||
|
|
||||||
# Try to find libaegis.a, but don't fail if not found (build backend will build it)
|
|
||||||
libaegis_static = find_libaegis()
|
|
||||||
|
|
||||||
# Set the source
|
|
||||||
ffibuilder.set_source(
|
ffibuilder.set_source(
|
||||||
"pyaegis._aegis", # module name
|
"aeg._aegis",
|
||||||
"""
|
"""
|
||||||
#include "aegis.h"
|
#include "aegis.h"
|
||||||
#include "aegis128l.h"
|
#include "aegis128l.h"
|
||||||
@@ -53,11 +28,15 @@ ffibuilder.set_source(
|
|||||||
#include "aegis256x2.h"
|
#include "aegis256x2.h"
|
||||||
#include "aegis256x4.h"
|
#include "aegis256x4.h"
|
||||||
""",
|
""",
|
||||||
include_dirs=include_dirs,
|
include_dirs=["libaegis/src/include"],
|
||||||
extra_objects=[libaegis_static] if libaegis_static else [],
|
extra_objects=[str(libaegis_static.resolve())],
|
||||||
|
py_limited_api=not is_free_threaded,
|
||||||
)
|
)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
setup(
|
setup(
|
||||||
cffi_modules=["setup.py:ffibuilder"],
|
cffi_modules=["setup.py:ffibuilder"],
|
||||||
|
options=(
|
||||||
|
{"bdist_wheel": {"py_limited_api": "cp310"}} if not is_free_threaded else {}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import importlib
|
||||||
|
|
||||||
|
from ._ciphers import CIPHERS, CipherName
|
||||||
|
from ._typing import Cipher
|
||||||
|
|
||||||
|
__all__ = ["cipher", "CIPHERS", "Cipher", "CipherName"]
|
||||||
|
|
||||||
|
|
||||||
|
def cipher(alg: CipherName) -> Cipher:
|
||||||
|
"""Acquire a cipher module by name."""
|
||||||
|
name = alg.lower().replace("-", "")
|
||||||
|
if name == "aegis128":
|
||||||
|
name = "aegis128l" # AEGIS-128 is dead, the user meant AEGIS-128L
|
||||||
|
if not name.startswith("aegis"):
|
||||||
|
name = "aegis" + name
|
||||||
|
if name in CIPHERS.values():
|
||||||
|
return importlib.import_module(f".{name}", __package__) # type: ignore[return-value]
|
||||||
|
raise ValueError(f"Unknown algorithm {alg!r}. Valid options: {', '.join(CIPHERS)}")
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# This file is generated by tools/generate.py. Do not edit.
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
CipherName = Literal[
|
||||||
|
"AEGIS-128L",
|
||||||
|
"AEGIS-128X2",
|
||||||
|
"AEGIS-128X4",
|
||||||
|
"AEGIS-256",
|
||||||
|
"AEGIS-256X2",
|
||||||
|
"AEGIS-256X4",
|
||||||
|
]
|
||||||
|
|
||||||
|
CIPHERS: dict[CipherName, str] = {
|
||||||
|
"AEGIS-128L": "aegis128l",
|
||||||
|
"AEGIS-128X2": "aegis128x2",
|
||||||
|
"AEGIS-128X4": "aegis128x4",
|
||||||
|
"AEGIS-256": "aegis256",
|
||||||
|
"AEGIS-256X2": "aegis256x2",
|
||||||
|
"AEGIS-256X4": "aegis256x4",
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Loader for libaegis CFFI extension module."""
|
"""Loader for libaegis CFFI extension module."""
|
||||||
|
|
||||||
from pyaegis._aegis import ffi, lib
|
from aeg._aegis import ffi, lib
|
||||||
|
|
||||||
__all__ = ["ffi", "lib"]
|
__all__ = ["ffi", "lib"]
|
||||||
|
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
from typing import TYPE_CHECKING, Protocol
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .util import Buffer
|
||||||
|
|
||||||
|
__all__ = ["Cipher"]
|
||||||
|
|
||||||
|
|
||||||
|
class _Mac(Protocol):
|
||||||
|
def reset(self) -> None: ...
|
||||||
|
def clone(self) -> "_Mac": ...
|
||||||
|
def update(self, data: "Buffer") -> None: ...
|
||||||
|
def final(self, into: "Buffer | None" = None) -> bytearray | memoryview: ...
|
||||||
|
def digest(self) -> bytes: ...
|
||||||
|
def hexdigest(self) -> str: ...
|
||||||
|
def verify(self, mac: "Buffer") -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _Encryptor(Protocol):
|
||||||
|
def update(
|
||||||
|
self, message: "Buffer", into: "Buffer | None" = None
|
||||||
|
) -> bytearray | memoryview: ...
|
||||||
|
def final(self, into: "Buffer | None" = None) -> bytearray | memoryview: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _Decryptor(Protocol):
|
||||||
|
def update(
|
||||||
|
self, ct: "Buffer", into: "Buffer | None" = None
|
||||||
|
) -> bytearray | memoryview: ...
|
||||||
|
def final(self, mac: "Buffer") -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class Cipher(Protocol):
|
||||||
|
NAME: str
|
||||||
|
KEYBYTES: int
|
||||||
|
NONCEBYTES: int
|
||||||
|
MACBYTES: int
|
||||||
|
MACBYTES_LONG: int
|
||||||
|
ALIGNMENT: int
|
||||||
|
RATE: int
|
||||||
|
|
||||||
|
Mac: type[_Mac]
|
||||||
|
Encryptor: type[_Encryptor]
|
||||||
|
Decryptor: type[_Decryptor]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def random_key() -> bytearray: ...
|
||||||
|
@staticmethod
|
||||||
|
def random_nonce() -> bytearray: ...
|
||||||
|
@staticmethod
|
||||||
|
def encrypt_detached(
|
||||||
|
key: "Buffer",
|
||||||
|
nonce: "Buffer",
|
||||||
|
message: "Buffer",
|
||||||
|
ad: "Buffer | None" = None,
|
||||||
|
*,
|
||||||
|
maclen: int = ...,
|
||||||
|
ct_into: "Buffer | None" = None,
|
||||||
|
mac_into: "Buffer | None" = None,
|
||||||
|
) -> tuple[bytearray | memoryview, bytearray | memoryview]: ...
|
||||||
|
@staticmethod
|
||||||
|
def decrypt_detached(
|
||||||
|
key: "Buffer",
|
||||||
|
nonce: "Buffer",
|
||||||
|
ct: "Buffer",
|
||||||
|
mac: "Buffer",
|
||||||
|
ad: "Buffer | None" = None,
|
||||||
|
*,
|
||||||
|
into: "Buffer | None" = None,
|
||||||
|
) -> bytearray | memoryview: ...
|
||||||
|
@staticmethod
|
||||||
|
def encrypt(
|
||||||
|
key: "Buffer",
|
||||||
|
nonce: "Buffer",
|
||||||
|
message: "Buffer",
|
||||||
|
ad: "Buffer | None" = None,
|
||||||
|
*,
|
||||||
|
maclen: int = ...,
|
||||||
|
into: "Buffer | None" = None,
|
||||||
|
) -> bytearray | memoryview: ...
|
||||||
|
@staticmethod
|
||||||
|
def decrypt(
|
||||||
|
key: "Buffer",
|
||||||
|
nonce: "Buffer",
|
||||||
|
ct: "Buffer",
|
||||||
|
ad: "Buffer | None" = None,
|
||||||
|
*,
|
||||||
|
maclen: int = ...,
|
||||||
|
into: "Buffer | None" = None,
|
||||||
|
) -> bytearray | memoryview: ...
|
||||||
|
@staticmethod
|
||||||
|
def stream(
|
||||||
|
key: "Buffer",
|
||||||
|
nonce: "Buffer | None",
|
||||||
|
length: int | None = None,
|
||||||
|
*,
|
||||||
|
into: "Buffer | None" = None,
|
||||||
|
) -> "bytearray | Buffer": ...
|
||||||
|
@staticmethod
|
||||||
|
def encrypt_unauthenticated(
|
||||||
|
key: "Buffer",
|
||||||
|
nonce: "Buffer",
|
||||||
|
message: "Buffer",
|
||||||
|
*,
|
||||||
|
into: "Buffer | None" = None,
|
||||||
|
) -> bytearray | memoryview: ...
|
||||||
|
@staticmethod
|
||||||
|
def decrypt_unauthenticated(
|
||||||
|
key: "Buffer",
|
||||||
|
nonce: "Buffer",
|
||||||
|
ct: "Buffer",
|
||||||
|
*,
|
||||||
|
into: "Buffer | None" = None,
|
||||||
|
) -> bytearray | memoryview: ...
|
||||||
|
@staticmethod
|
||||||
|
def mac(
|
||||||
|
key: "Buffer",
|
||||||
|
nonce: "Buffer",
|
||||||
|
data: "Buffer",
|
||||||
|
maclen: int = ...,
|
||||||
|
into: "Buffer | None" = None,
|
||||||
|
) -> bytearray | memoryview: ...
|
||||||
|
@staticmethod
|
||||||
|
def nonce_increment(nonce: "Buffer") -> None: ...
|
||||||
|
@staticmethod
|
||||||
|
def wipe(buffer: "Buffer") -> None: ...
|
||||||
@@ -0,0 +1,898 @@
|
|||||||
|
"""AEGIS-128L"""
|
||||||
|
# All modules are generated from aegis256x4.py by tools/generate.py!
|
||||||
|
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import secrets
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from ._loader import ffi
|
||||||
|
from ._loader import lib as _lib
|
||||||
|
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||||
|
|
||||||
|
NAME = "AEGIS-128L" #: Algorithm display name
|
||||||
|
KEYBYTES = 16 #: Key size in bytes (varies by algorithm)
|
||||||
|
NONCEBYTES = 16 #: Nonce size in bytes (varies by algorithm)
|
||||||
|
MACBYTES = 16 #: Normal MAC size (always 16)
|
||||||
|
MACBYTES_LONG = 32 #: Long MAC size (always 32)
|
||||||
|
ALIGNMENT = 64 #: Required alignment for internal structures
|
||||||
|
RATE = 64 #: Byte chunk size in internal processing
|
||||||
|
|
||||||
|
|
||||||
|
def random_key() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a secret key using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
It is recommended to wipe() the key after no longer needed.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(KEYBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def random_nonce() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a public nonce using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
Nonces (a number used once) are public data that may be sent together
|
||||||
|
with the ciphertext, but they need to be unique for each use.
|
||||||
|
|
||||||
|
See also: nonce_increment() can be used to derive sequential nonces.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(NONCEBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def _ptr(buf):
|
||||||
|
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
ct_into: Buffer | None = None,
|
||||||
|
mac_into: Buffer | None = None,
|
||||||
|
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
mac_into: Buffer to write MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (ciphertext, mac)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if ct_into is not None:
|
||||||
|
ct_into = memoryview(ct_into)
|
||||||
|
if mac_into is not None:
|
||||||
|
mac_into = memoryview(mac_into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
if ct_into is None:
|
||||||
|
c = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if ct_into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("ct_into length must be at least message.nbytes")
|
||||||
|
c = ct_into
|
||||||
|
if mac_into is None:
|
||||||
|
mac = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
if mac_into.nbytes < maclen:
|
||||||
|
raise TypeError("mac_into length must be at least maclen")
|
||||||
|
mac = mac_into
|
||||||
|
|
||||||
|
rc = _lib.aegis128l_encrypt_detached(
|
||||||
|
ffi.from_buffer(c),
|
||||||
|
ffi.from_buffer(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
||||||
|
return (
|
||||||
|
c if ct_into is None else memoryview(c)[: message.nbytes],
|
||||||
|
mac if mac_into is None else memoryview(mac)[:maclen],
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
mac: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with detached MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
mac: The MAC to verify.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis128l_decrypt_detached(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes + maclen)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes + maclen:
|
||||||
|
raise TypeError("into length must be at least message.nbytes + maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis128l_encrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes + maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with appended MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext with MAC to decrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if ct.nbytes < maclen:
|
||||||
|
raise TypeError("ciphertext too short for tag")
|
||||||
|
expected_out = ct.nbytes - maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(expected_out)
|
||||||
|
else:
|
||||||
|
if into.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis128l_decrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def stream(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer | None,
|
||||||
|
length: int | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | Buffer:
|
||||||
|
"""Generate a stream of pseudorandom bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None).
|
||||||
|
length: Number of bytes to generate (required if into is None).
|
||||||
|
into: Buffer to write stream into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pseudorandom bytes as bytearray, or into returned directly.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid or neither length nor into provided.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
if nonce is not None:
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce is not None and nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
if length is None:
|
||||||
|
raise TypeError("provide either into or length")
|
||||||
|
out = bytearray(length)
|
||||||
|
else:
|
||||||
|
if length is not None and into.nbytes < length:
|
||||||
|
raise TypeError("into length must be at least length")
|
||||||
|
out = into
|
||||||
|
_lib.aegis128l_stream(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
memoryview(out).nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("into length must be at least message.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis128l_encrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis128l_decrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# This is missing from C API but convenient to have here
|
||||||
|
def mac(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
data: Buffer,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Compute a MAC for the given data in one shot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key())
|
||||||
|
nonce: Public nonce (generate with random_nonce())
|
||||||
|
data: Data to MAC
|
||||||
|
maclen: MAC length (16 or 32, default 16)
|
||||||
|
into: Buffer to write MAC into (default: bytearray created)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
data = memoryview(data)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
mac_state = Mac(key, nonce, maclen)
|
||||||
|
mac_state.update(data)
|
||||||
|
return mac_state.final(into)
|
||||||
|
|
||||||
|
|
||||||
|
class Mac:
|
||||||
|
"""MAC calculation and verification with incremental updates.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
...
|
||||||
|
mac = a.final()
|
||||||
|
|
||||||
|
Hashlib compatible interface:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
bytes_mac = a.digest()
|
||||||
|
hex_mac = a.hexdigest()
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_proxy", "_maclen", "_cached_digest")
|
||||||
|
|
||||||
|
def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None:
|
||||||
|
"""Create a MAC with the given key, nonce, and tag length.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
self._maclen = maclen
|
||||||
|
self._proxy = new_aligned_struct("aegis128l_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis128l_mac_init(self._proxy.ptr, _ptr(key), _ptr(nonce))
|
||||||
|
self._cached_digest: None | Literal[False] | bytes = None
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset back to the original state, prior to any updates."""
|
||||||
|
_lib.aegis128l_mac_reset(self._proxy.ptr)
|
||||||
|
self._cached_digest = None
|
||||||
|
|
||||||
|
def clone(self) -> "Mac":
|
||||||
|
"""Return a clone of current MAC state."""
|
||||||
|
clone = object.__new__(Mac)
|
||||||
|
clone._maclen = self._maclen
|
||||||
|
clone._proxy = new_aligned_struct("aegis128l_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis128l_mac_state_clone(clone._proxy.ptr, self._proxy.ptr)
|
||||||
|
clone._cached_digest = self._cached_digest
|
||||||
|
return clone
|
||||||
|
|
||||||
|
__deepcopy__ = clone
|
||||||
|
|
||||||
|
def update(self, data: Buffer) -> None:
|
||||||
|
"""Update the MAC state with more data.
|
||||||
|
|
||||||
|
Repeated calls to update() are equivalent to a single call with the concatenated data.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError("Cannot update after final()")
|
||||||
|
data = memoryview(data)
|
||||||
|
rc = _lib.aegis128l_mac_update(self._proxy.ptr, _ptr(data), data.nbytes)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"mac update failed: {err_name}")
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Calculate and return the MAC tag for the currently input data.
|
||||||
|
|
||||||
|
This method can only be called once. After calling it, the MAC becomes unusable
|
||||||
|
for further updates or calls to final().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional buffer to write the tag into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If finalization fails in the C library or if already finalized.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The MAC can only be calculated once. Use reset() to start over, or clone() before finalizing to continue."
|
||||||
|
)
|
||||||
|
maclen = self._maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
into = memoryview(into)
|
||||||
|
if into.nbytes < maclen:
|
||||||
|
raise TypeError("into length must be at least maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
clone = self.clone()
|
||||||
|
rc = _lib.aegis128l_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}")
|
||||||
|
raise RuntimeError(f"mac final failed: {err_name}")
|
||||||
|
self._cached_digest = False
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
def digest(self) -> bytes:
|
||||||
|
"""Calculate and return the MAC tag as bytes.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
Can be called after final() to get the cached digest.
|
||||||
|
"""
|
||||||
|
if self._cached_digest:
|
||||||
|
return self._cached_digest
|
||||||
|
self._cached_digest = bytes(self.final()) # Overrides the False set by final()
|
||||||
|
return self._cached_digest
|
||||||
|
|
||||||
|
def hexdigest(self) -> str:
|
||||||
|
"""Calculate and return the MAC tag as a hex string.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
"""
|
||||||
|
return self.digest().hex()
|
||||||
|
|
||||||
|
def verify(self, mac: Buffer):
|
||||||
|
"""Verify that the data entered so far matches the given MAC tag.
|
||||||
|
|
||||||
|
Unlike the C library, this method does not alter the current state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: The tag to verify against (16 or 32 bytes).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length is invalid.
|
||||||
|
ValueError: If verification fails.
|
||||||
|
"""
|
||||||
|
mac = memoryview(mac)
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
|
||||||
|
cloned = self.clone()
|
||||||
|
rc = _lib.aegis128l_mac_verify(cloned._proxy.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("mac verification failed")
|
||||||
|
|
||||||
|
|
||||||
|
class Encryptor:
|
||||||
|
"""Incremental encryptor.
|
||||||
|
|
||||||
|
- update(message[, into]) -> returns produced ciphertext bytes
|
||||||
|
- final([into]) -> returns MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental encryptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
ad: Associated data to bind to the encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis128l_state", ALIGNMENT)
|
||||||
|
_lib.aegis128l_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, message: Buffer, into: Buffer | None = None
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt a chunk of the message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Plaintext bytes to encrypt.
|
||||||
|
into: Optional destination buffer; must be >= len(message).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = message.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError(
|
||||||
|
"into length must be >= expected output size for this update"
|
||||||
|
)
|
||||||
|
rc = _lib.aegis128l_state_encrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Finalize encryption and return the authentication tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional destination buffer for the tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The authentication tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the C final call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
# Only the authentication tag is produced here; allocate exactly maclen
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
out = into if into is not None else bytearray(maclen)
|
||||||
|
rc = _lib.aegis128l_state_encrypt_final(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
||||||
|
self._state = None
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Decryptor:
|
||||||
|
"""Incremental decryptor.
|
||||||
|
|
||||||
|
- update(ciphertext[, into]) -> returns plaintext bytes
|
||||||
|
- final(mac) -> verifies the MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental decryptor for detached tags.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ad: Associated data used during encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis128l_state", ALIGNMENT)
|
||||||
|
_lib.aegis128l_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Process a chunk of ciphertext.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ct: Ciphertext bytes (without MAC).
|
||||||
|
into: Optional destination buffer; must be >= len(ciphertext).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = ct.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be >= required capacity for this update")
|
||||||
|
rc = _lib.aegis128l_state_decrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state decrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, mac: Buffer) -> None:
|
||||||
|
"""Finalize decryption by verifying the MAC tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: Tag to verify.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length doesn't match the expected maclen.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
RuntimeError: If called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if mac.nbytes != maclen:
|
||||||
|
raise TypeError(f"mac length must be {maclen}")
|
||||||
|
rc = _lib.aegis128l_state_decrypt_final(self._state.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
|
||||||
|
def new_state():
|
||||||
|
"""Allocate and return a new aegis128l_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis128l_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
def new_mac_state():
|
||||||
|
"""Allocate and return a new aegis128l_mac_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis128l_mac_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# constants
|
||||||
|
"NAME",
|
||||||
|
"KEYBYTES",
|
||||||
|
"NONCEBYTES",
|
||||||
|
"MACBYTES",
|
||||||
|
"MACBYTES_LONG",
|
||||||
|
"ALIGNMENT",
|
||||||
|
"RATE",
|
||||||
|
# utility functions
|
||||||
|
"random_key",
|
||||||
|
"random_nonce",
|
||||||
|
"nonce_increment",
|
||||||
|
"wipe",
|
||||||
|
# one-shot functions
|
||||||
|
"encrypt_detached",
|
||||||
|
"decrypt_detached",
|
||||||
|
"encrypt",
|
||||||
|
"decrypt",
|
||||||
|
"stream",
|
||||||
|
"encrypt_unauthenticated",
|
||||||
|
"decrypt_unauthenticated",
|
||||||
|
"mac",
|
||||||
|
# incremental classes
|
||||||
|
"Encryptor",
|
||||||
|
"Decryptor",
|
||||||
|
"Mac",
|
||||||
|
]
|
||||||
@@ -0,0 +1,898 @@
|
|||||||
|
"""AEGIS-128X2"""
|
||||||
|
# All modules are generated from aegis256x4.py by tools/generate.py!
|
||||||
|
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import secrets
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from ._loader import ffi
|
||||||
|
from ._loader import lib as _lib
|
||||||
|
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||||
|
|
||||||
|
NAME = "AEGIS-128X2" #: Algorithm display name
|
||||||
|
KEYBYTES = 16 #: Key size in bytes (varies by algorithm)
|
||||||
|
NONCEBYTES = 16 #: Nonce size in bytes (varies by algorithm)
|
||||||
|
MACBYTES = 16 #: Normal MAC size (always 16)
|
||||||
|
MACBYTES_LONG = 32 #: Long MAC size (always 32)
|
||||||
|
ALIGNMENT = 64 #: Required alignment for internal structures
|
||||||
|
RATE = 64 #: Byte chunk size in internal processing
|
||||||
|
|
||||||
|
|
||||||
|
def random_key() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a secret key using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
It is recommended to wipe() the key after no longer needed.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(KEYBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def random_nonce() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a public nonce using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
Nonces (a number used once) are public data that may be sent together
|
||||||
|
with the ciphertext, but they need to be unique for each use.
|
||||||
|
|
||||||
|
See also: nonce_increment() can be used to derive sequential nonces.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(NONCEBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def _ptr(buf):
|
||||||
|
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
ct_into: Buffer | None = None,
|
||||||
|
mac_into: Buffer | None = None,
|
||||||
|
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
mac_into: Buffer to write MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (ciphertext, mac)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if ct_into is not None:
|
||||||
|
ct_into = memoryview(ct_into)
|
||||||
|
if mac_into is not None:
|
||||||
|
mac_into = memoryview(mac_into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
if ct_into is None:
|
||||||
|
c = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if ct_into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("ct_into length must be at least message.nbytes")
|
||||||
|
c = ct_into
|
||||||
|
if mac_into is None:
|
||||||
|
mac = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
if mac_into.nbytes < maclen:
|
||||||
|
raise TypeError("mac_into length must be at least maclen")
|
||||||
|
mac = mac_into
|
||||||
|
|
||||||
|
rc = _lib.aegis128x2_encrypt_detached(
|
||||||
|
ffi.from_buffer(c),
|
||||||
|
ffi.from_buffer(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
||||||
|
return (
|
||||||
|
c if ct_into is None else memoryview(c)[: message.nbytes],
|
||||||
|
mac if mac_into is None else memoryview(mac)[:maclen],
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
mac: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with detached MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
mac: The MAC to verify.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis128x2_decrypt_detached(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes + maclen)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes + maclen:
|
||||||
|
raise TypeError("into length must be at least message.nbytes + maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis128x2_encrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes + maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with appended MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext with MAC to decrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if ct.nbytes < maclen:
|
||||||
|
raise TypeError("ciphertext too short for tag")
|
||||||
|
expected_out = ct.nbytes - maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(expected_out)
|
||||||
|
else:
|
||||||
|
if into.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis128x2_decrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def stream(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer | None,
|
||||||
|
length: int | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | Buffer:
|
||||||
|
"""Generate a stream of pseudorandom bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None).
|
||||||
|
length: Number of bytes to generate (required if into is None).
|
||||||
|
into: Buffer to write stream into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pseudorandom bytes as bytearray, or into returned directly.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid or neither length nor into provided.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
if nonce is not None:
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce is not None and nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
if length is None:
|
||||||
|
raise TypeError("provide either into or length")
|
||||||
|
out = bytearray(length)
|
||||||
|
else:
|
||||||
|
if length is not None and into.nbytes < length:
|
||||||
|
raise TypeError("into length must be at least length")
|
||||||
|
out = into
|
||||||
|
_lib.aegis128x2_stream(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
memoryview(out).nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("into length must be at least message.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis128x2_encrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis128x2_decrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# This is missing from C API but convenient to have here
|
||||||
|
def mac(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
data: Buffer,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Compute a MAC for the given data in one shot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key())
|
||||||
|
nonce: Public nonce (generate with random_nonce())
|
||||||
|
data: Data to MAC
|
||||||
|
maclen: MAC length (16 or 32, default 16)
|
||||||
|
into: Buffer to write MAC into (default: bytearray created)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
data = memoryview(data)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
mac_state = Mac(key, nonce, maclen)
|
||||||
|
mac_state.update(data)
|
||||||
|
return mac_state.final(into)
|
||||||
|
|
||||||
|
|
||||||
|
class Mac:
|
||||||
|
"""MAC calculation and verification with incremental updates.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
...
|
||||||
|
mac = a.final()
|
||||||
|
|
||||||
|
Hashlib compatible interface:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
bytes_mac = a.digest()
|
||||||
|
hex_mac = a.hexdigest()
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_proxy", "_maclen", "_cached_digest")
|
||||||
|
|
||||||
|
def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None:
|
||||||
|
"""Create a MAC with the given key, nonce, and tag length.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
self._maclen = maclen
|
||||||
|
self._proxy = new_aligned_struct("aegis128x2_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis128x2_mac_init(self._proxy.ptr, _ptr(key), _ptr(nonce))
|
||||||
|
self._cached_digest: None | Literal[False] | bytes = None
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset back to the original state, prior to any updates."""
|
||||||
|
_lib.aegis128x2_mac_reset(self._proxy.ptr)
|
||||||
|
self._cached_digest = None
|
||||||
|
|
||||||
|
def clone(self) -> "Mac":
|
||||||
|
"""Return a clone of current MAC state."""
|
||||||
|
clone = object.__new__(Mac)
|
||||||
|
clone._maclen = self._maclen
|
||||||
|
clone._proxy = new_aligned_struct("aegis128x2_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis128x2_mac_state_clone(clone._proxy.ptr, self._proxy.ptr)
|
||||||
|
clone._cached_digest = self._cached_digest
|
||||||
|
return clone
|
||||||
|
|
||||||
|
__deepcopy__ = clone
|
||||||
|
|
||||||
|
def update(self, data: Buffer) -> None:
|
||||||
|
"""Update the MAC state with more data.
|
||||||
|
|
||||||
|
Repeated calls to update() are equivalent to a single call with the concatenated data.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError("Cannot update after final()")
|
||||||
|
data = memoryview(data)
|
||||||
|
rc = _lib.aegis128x2_mac_update(self._proxy.ptr, _ptr(data), data.nbytes)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"mac update failed: {err_name}")
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Calculate and return the MAC tag for the currently input data.
|
||||||
|
|
||||||
|
This method can only be called once. After calling it, the MAC becomes unusable
|
||||||
|
for further updates or calls to final().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional buffer to write the tag into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If finalization fails in the C library or if already finalized.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The MAC can only be calculated once. Use reset() to start over, or clone() before finalizing to continue."
|
||||||
|
)
|
||||||
|
maclen = self._maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
into = memoryview(into)
|
||||||
|
if into.nbytes < maclen:
|
||||||
|
raise TypeError("into length must be at least maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
clone = self.clone()
|
||||||
|
rc = _lib.aegis128x2_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}")
|
||||||
|
raise RuntimeError(f"mac final failed: {err_name}")
|
||||||
|
self._cached_digest = False
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
def digest(self) -> bytes:
|
||||||
|
"""Calculate and return the MAC tag as bytes.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
Can be called after final() to get the cached digest.
|
||||||
|
"""
|
||||||
|
if self._cached_digest:
|
||||||
|
return self._cached_digest
|
||||||
|
self._cached_digest = bytes(self.final()) # Overrides the False set by final()
|
||||||
|
return self._cached_digest
|
||||||
|
|
||||||
|
def hexdigest(self) -> str:
|
||||||
|
"""Calculate and return the MAC tag as a hex string.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
"""
|
||||||
|
return self.digest().hex()
|
||||||
|
|
||||||
|
def verify(self, mac: Buffer):
|
||||||
|
"""Verify that the data entered so far matches the given MAC tag.
|
||||||
|
|
||||||
|
Unlike the C library, this method does not alter the current state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: The tag to verify against (16 or 32 bytes).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length is invalid.
|
||||||
|
ValueError: If verification fails.
|
||||||
|
"""
|
||||||
|
mac = memoryview(mac)
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
|
||||||
|
cloned = self.clone()
|
||||||
|
rc = _lib.aegis128x2_mac_verify(cloned._proxy.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("mac verification failed")
|
||||||
|
|
||||||
|
|
||||||
|
class Encryptor:
|
||||||
|
"""Incremental encryptor.
|
||||||
|
|
||||||
|
- update(message[, into]) -> returns produced ciphertext bytes
|
||||||
|
- final([into]) -> returns MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental encryptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
ad: Associated data to bind to the encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis128x2_state", ALIGNMENT)
|
||||||
|
_lib.aegis128x2_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, message: Buffer, into: Buffer | None = None
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt a chunk of the message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Plaintext bytes to encrypt.
|
||||||
|
into: Optional destination buffer; must be >= len(message).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = message.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError(
|
||||||
|
"into length must be >= expected output size for this update"
|
||||||
|
)
|
||||||
|
rc = _lib.aegis128x2_state_encrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Finalize encryption and return the authentication tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional destination buffer for the tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The authentication tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the C final call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
# Only the authentication tag is produced here; allocate exactly maclen
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
out = into if into is not None else bytearray(maclen)
|
||||||
|
rc = _lib.aegis128x2_state_encrypt_final(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
||||||
|
self._state = None
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Decryptor:
|
||||||
|
"""Incremental decryptor.
|
||||||
|
|
||||||
|
- update(ciphertext[, into]) -> returns plaintext bytes
|
||||||
|
- final(mac) -> verifies the MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental decryptor for detached tags.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ad: Associated data used during encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis128x2_state", ALIGNMENT)
|
||||||
|
_lib.aegis128x2_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Process a chunk of ciphertext.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ct: Ciphertext bytes (without MAC).
|
||||||
|
into: Optional destination buffer; must be >= len(ciphertext).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = ct.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be >= required capacity for this update")
|
||||||
|
rc = _lib.aegis128x2_state_decrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state decrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, mac: Buffer) -> None:
|
||||||
|
"""Finalize decryption by verifying the MAC tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: Tag to verify.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length doesn't match the expected maclen.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
RuntimeError: If called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if mac.nbytes != maclen:
|
||||||
|
raise TypeError(f"mac length must be {maclen}")
|
||||||
|
rc = _lib.aegis128x2_state_decrypt_final(self._state.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
|
||||||
|
def new_state():
|
||||||
|
"""Allocate and return a new aegis128x2_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis128x2_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
def new_mac_state():
|
||||||
|
"""Allocate and return a new aegis128x2_mac_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis128x2_mac_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# constants
|
||||||
|
"NAME",
|
||||||
|
"KEYBYTES",
|
||||||
|
"NONCEBYTES",
|
||||||
|
"MACBYTES",
|
||||||
|
"MACBYTES_LONG",
|
||||||
|
"ALIGNMENT",
|
||||||
|
"RATE",
|
||||||
|
# utility functions
|
||||||
|
"random_key",
|
||||||
|
"random_nonce",
|
||||||
|
"nonce_increment",
|
||||||
|
"wipe",
|
||||||
|
# one-shot functions
|
||||||
|
"encrypt_detached",
|
||||||
|
"decrypt_detached",
|
||||||
|
"encrypt",
|
||||||
|
"decrypt",
|
||||||
|
"stream",
|
||||||
|
"encrypt_unauthenticated",
|
||||||
|
"decrypt_unauthenticated",
|
||||||
|
"mac",
|
||||||
|
# incremental classes
|
||||||
|
"Encryptor",
|
||||||
|
"Decryptor",
|
||||||
|
"Mac",
|
||||||
|
]
|
||||||
@@ -0,0 +1,898 @@
|
|||||||
|
"""AEGIS-128X4"""
|
||||||
|
# All modules are generated from aegis256x4.py by tools/generate.py!
|
||||||
|
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import secrets
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from ._loader import ffi
|
||||||
|
from ._loader import lib as _lib
|
||||||
|
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||||
|
|
||||||
|
NAME = "AEGIS-128X4" #: Algorithm display name
|
||||||
|
KEYBYTES = 16 #: Key size in bytes (varies by algorithm)
|
||||||
|
NONCEBYTES = 16 #: Nonce size in bytes (varies by algorithm)
|
||||||
|
MACBYTES = 16 #: Normal MAC size (always 16)
|
||||||
|
MACBYTES_LONG = 32 #: Long MAC size (always 32)
|
||||||
|
ALIGNMENT = 64 #: Required alignment for internal structures
|
||||||
|
RATE = 64 #: Byte chunk size in internal processing
|
||||||
|
|
||||||
|
|
||||||
|
def random_key() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a secret key using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
It is recommended to wipe() the key after no longer needed.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(KEYBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def random_nonce() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a public nonce using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
Nonces (a number used once) are public data that may be sent together
|
||||||
|
with the ciphertext, but they need to be unique for each use.
|
||||||
|
|
||||||
|
See also: nonce_increment() can be used to derive sequential nonces.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(NONCEBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def _ptr(buf):
|
||||||
|
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
ct_into: Buffer | None = None,
|
||||||
|
mac_into: Buffer | None = None,
|
||||||
|
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
mac_into: Buffer to write MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (ciphertext, mac)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if ct_into is not None:
|
||||||
|
ct_into = memoryview(ct_into)
|
||||||
|
if mac_into is not None:
|
||||||
|
mac_into = memoryview(mac_into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
if ct_into is None:
|
||||||
|
c = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if ct_into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("ct_into length must be at least message.nbytes")
|
||||||
|
c = ct_into
|
||||||
|
if mac_into is None:
|
||||||
|
mac = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
if mac_into.nbytes < maclen:
|
||||||
|
raise TypeError("mac_into length must be at least maclen")
|
||||||
|
mac = mac_into
|
||||||
|
|
||||||
|
rc = _lib.aegis128x4_encrypt_detached(
|
||||||
|
ffi.from_buffer(c),
|
||||||
|
ffi.from_buffer(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
||||||
|
return (
|
||||||
|
c if ct_into is None else memoryview(c)[: message.nbytes],
|
||||||
|
mac if mac_into is None else memoryview(mac)[:maclen],
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
mac: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with detached MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
mac: The MAC to verify.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis128x4_decrypt_detached(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes + maclen)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes + maclen:
|
||||||
|
raise TypeError("into length must be at least message.nbytes + maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis128x4_encrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes + maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with appended MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext with MAC to decrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if ct.nbytes < maclen:
|
||||||
|
raise TypeError("ciphertext too short for tag")
|
||||||
|
expected_out = ct.nbytes - maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(expected_out)
|
||||||
|
else:
|
||||||
|
if into.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis128x4_decrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def stream(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer | None,
|
||||||
|
length: int | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | Buffer:
|
||||||
|
"""Generate a stream of pseudorandom bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None).
|
||||||
|
length: Number of bytes to generate (required if into is None).
|
||||||
|
into: Buffer to write stream into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pseudorandom bytes as bytearray, or into returned directly.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid or neither length nor into provided.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
if nonce is not None:
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce is not None and nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
if length is None:
|
||||||
|
raise TypeError("provide either into or length")
|
||||||
|
out = bytearray(length)
|
||||||
|
else:
|
||||||
|
if length is not None and into.nbytes < length:
|
||||||
|
raise TypeError("into length must be at least length")
|
||||||
|
out = into
|
||||||
|
_lib.aegis128x4_stream(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
memoryview(out).nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("into length must be at least message.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis128x4_encrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis128x4_decrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# This is missing from C API but convenient to have here
|
||||||
|
def mac(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
data: Buffer,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Compute a MAC for the given data in one shot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key())
|
||||||
|
nonce: Public nonce (generate with random_nonce())
|
||||||
|
data: Data to MAC
|
||||||
|
maclen: MAC length (16 or 32, default 16)
|
||||||
|
into: Buffer to write MAC into (default: bytearray created)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
data = memoryview(data)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
mac_state = Mac(key, nonce, maclen)
|
||||||
|
mac_state.update(data)
|
||||||
|
return mac_state.final(into)
|
||||||
|
|
||||||
|
|
||||||
|
class Mac:
|
||||||
|
"""MAC calculation and verification with incremental updates.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
...
|
||||||
|
mac = a.final()
|
||||||
|
|
||||||
|
Hashlib compatible interface:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
bytes_mac = a.digest()
|
||||||
|
hex_mac = a.hexdigest()
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_proxy", "_maclen", "_cached_digest")
|
||||||
|
|
||||||
|
def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None:
|
||||||
|
"""Create a MAC with the given key, nonce, and tag length.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
self._maclen = maclen
|
||||||
|
self._proxy = new_aligned_struct("aegis128x4_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis128x4_mac_init(self._proxy.ptr, _ptr(key), _ptr(nonce))
|
||||||
|
self._cached_digest: None | Literal[False] | bytes = None
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset back to the original state, prior to any updates."""
|
||||||
|
_lib.aegis128x4_mac_reset(self._proxy.ptr)
|
||||||
|
self._cached_digest = None
|
||||||
|
|
||||||
|
def clone(self) -> "Mac":
|
||||||
|
"""Return a clone of current MAC state."""
|
||||||
|
clone = object.__new__(Mac)
|
||||||
|
clone._maclen = self._maclen
|
||||||
|
clone._proxy = new_aligned_struct("aegis128x4_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis128x4_mac_state_clone(clone._proxy.ptr, self._proxy.ptr)
|
||||||
|
clone._cached_digest = self._cached_digest
|
||||||
|
return clone
|
||||||
|
|
||||||
|
__deepcopy__ = clone
|
||||||
|
|
||||||
|
def update(self, data: Buffer) -> None:
|
||||||
|
"""Update the MAC state with more data.
|
||||||
|
|
||||||
|
Repeated calls to update() are equivalent to a single call with the concatenated data.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError("Cannot update after final()")
|
||||||
|
data = memoryview(data)
|
||||||
|
rc = _lib.aegis128x4_mac_update(self._proxy.ptr, _ptr(data), data.nbytes)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"mac update failed: {err_name}")
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Calculate and return the MAC tag for the currently input data.
|
||||||
|
|
||||||
|
This method can only be called once. After calling it, the MAC becomes unusable
|
||||||
|
for further updates or calls to final().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional buffer to write the tag into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If finalization fails in the C library or if already finalized.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The MAC can only be calculated once. Use reset() to start over, or clone() before finalizing to continue."
|
||||||
|
)
|
||||||
|
maclen = self._maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
into = memoryview(into)
|
||||||
|
if into.nbytes < maclen:
|
||||||
|
raise TypeError("into length must be at least maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
clone = self.clone()
|
||||||
|
rc = _lib.aegis128x4_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}")
|
||||||
|
raise RuntimeError(f"mac final failed: {err_name}")
|
||||||
|
self._cached_digest = False
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
def digest(self) -> bytes:
|
||||||
|
"""Calculate and return the MAC tag as bytes.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
Can be called after final() to get the cached digest.
|
||||||
|
"""
|
||||||
|
if self._cached_digest:
|
||||||
|
return self._cached_digest
|
||||||
|
self._cached_digest = bytes(self.final()) # Overrides the False set by final()
|
||||||
|
return self._cached_digest
|
||||||
|
|
||||||
|
def hexdigest(self) -> str:
|
||||||
|
"""Calculate and return the MAC tag as a hex string.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
"""
|
||||||
|
return self.digest().hex()
|
||||||
|
|
||||||
|
def verify(self, mac: Buffer):
|
||||||
|
"""Verify that the data entered so far matches the given MAC tag.
|
||||||
|
|
||||||
|
Unlike the C library, this method does not alter the current state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: The tag to verify against (16 or 32 bytes).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length is invalid.
|
||||||
|
ValueError: If verification fails.
|
||||||
|
"""
|
||||||
|
mac = memoryview(mac)
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
|
||||||
|
cloned = self.clone()
|
||||||
|
rc = _lib.aegis128x4_mac_verify(cloned._proxy.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("mac verification failed")
|
||||||
|
|
||||||
|
|
||||||
|
class Encryptor:
|
||||||
|
"""Incremental encryptor.
|
||||||
|
|
||||||
|
- update(message[, into]) -> returns produced ciphertext bytes
|
||||||
|
- final([into]) -> returns MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental encryptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
ad: Associated data to bind to the encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis128x4_state", ALIGNMENT)
|
||||||
|
_lib.aegis128x4_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, message: Buffer, into: Buffer | None = None
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt a chunk of the message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Plaintext bytes to encrypt.
|
||||||
|
into: Optional destination buffer; must be >= len(message).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = message.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError(
|
||||||
|
"into length must be >= expected output size for this update"
|
||||||
|
)
|
||||||
|
rc = _lib.aegis128x4_state_encrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Finalize encryption and return the authentication tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional destination buffer for the tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The authentication tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the C final call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
# Only the authentication tag is produced here; allocate exactly maclen
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
out = into if into is not None else bytearray(maclen)
|
||||||
|
rc = _lib.aegis128x4_state_encrypt_final(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
||||||
|
self._state = None
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Decryptor:
|
||||||
|
"""Incremental decryptor.
|
||||||
|
|
||||||
|
- update(ciphertext[, into]) -> returns plaintext bytes
|
||||||
|
- final(mac) -> verifies the MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental decryptor for detached tags.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ad: Associated data used during encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis128x4_state", ALIGNMENT)
|
||||||
|
_lib.aegis128x4_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Process a chunk of ciphertext.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ct: Ciphertext bytes (without MAC).
|
||||||
|
into: Optional destination buffer; must be >= len(ciphertext).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = ct.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be >= required capacity for this update")
|
||||||
|
rc = _lib.aegis128x4_state_decrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state decrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, mac: Buffer) -> None:
|
||||||
|
"""Finalize decryption by verifying the MAC tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: Tag to verify.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length doesn't match the expected maclen.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
RuntimeError: If called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if mac.nbytes != maclen:
|
||||||
|
raise TypeError(f"mac length must be {maclen}")
|
||||||
|
rc = _lib.aegis128x4_state_decrypt_final(self._state.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
|
||||||
|
def new_state():
|
||||||
|
"""Allocate and return a new aegis128x4_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis128x4_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
def new_mac_state():
|
||||||
|
"""Allocate and return a new aegis128x4_mac_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis128x4_mac_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# constants
|
||||||
|
"NAME",
|
||||||
|
"KEYBYTES",
|
||||||
|
"NONCEBYTES",
|
||||||
|
"MACBYTES",
|
||||||
|
"MACBYTES_LONG",
|
||||||
|
"ALIGNMENT",
|
||||||
|
"RATE",
|
||||||
|
# utility functions
|
||||||
|
"random_key",
|
||||||
|
"random_nonce",
|
||||||
|
"nonce_increment",
|
||||||
|
"wipe",
|
||||||
|
# one-shot functions
|
||||||
|
"encrypt_detached",
|
||||||
|
"decrypt_detached",
|
||||||
|
"encrypt",
|
||||||
|
"decrypt",
|
||||||
|
"stream",
|
||||||
|
"encrypt_unauthenticated",
|
||||||
|
"decrypt_unauthenticated",
|
||||||
|
"mac",
|
||||||
|
# incremental classes
|
||||||
|
"Encryptor",
|
||||||
|
"Decryptor",
|
||||||
|
"Mac",
|
||||||
|
]
|
||||||
@@ -0,0 +1,898 @@
|
|||||||
|
"""AEGIS-256"""
|
||||||
|
# All modules are generated from aegis256x4.py by tools/generate.py!
|
||||||
|
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import secrets
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from ._loader import ffi
|
||||||
|
from ._loader import lib as _lib
|
||||||
|
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||||
|
|
||||||
|
NAME = "AEGIS-256" #: Algorithm display name
|
||||||
|
KEYBYTES = 32 #: Key size in bytes (varies by algorithm)
|
||||||
|
NONCEBYTES = 32 #: Nonce size in bytes (varies by algorithm)
|
||||||
|
MACBYTES = 16 #: Normal MAC size (always 16)
|
||||||
|
MACBYTES_LONG = 32 #: Long MAC size (always 32)
|
||||||
|
ALIGNMENT = 64 #: Required alignment for internal structures
|
||||||
|
RATE = 64 #: Byte chunk size in internal processing
|
||||||
|
|
||||||
|
|
||||||
|
def random_key() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a secret key using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
It is recommended to wipe() the key after no longer needed.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(KEYBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def random_nonce() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a public nonce using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
Nonces (a number used once) are public data that may be sent together
|
||||||
|
with the ciphertext, but they need to be unique for each use.
|
||||||
|
|
||||||
|
See also: nonce_increment() can be used to derive sequential nonces.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(NONCEBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def _ptr(buf):
|
||||||
|
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
ct_into: Buffer | None = None,
|
||||||
|
mac_into: Buffer | None = None,
|
||||||
|
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
mac_into: Buffer to write MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (ciphertext, mac)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if ct_into is not None:
|
||||||
|
ct_into = memoryview(ct_into)
|
||||||
|
if mac_into is not None:
|
||||||
|
mac_into = memoryview(mac_into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
if ct_into is None:
|
||||||
|
c = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if ct_into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("ct_into length must be at least message.nbytes")
|
||||||
|
c = ct_into
|
||||||
|
if mac_into is None:
|
||||||
|
mac = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
if mac_into.nbytes < maclen:
|
||||||
|
raise TypeError("mac_into length must be at least maclen")
|
||||||
|
mac = mac_into
|
||||||
|
|
||||||
|
rc = _lib.aegis256_encrypt_detached(
|
||||||
|
ffi.from_buffer(c),
|
||||||
|
ffi.from_buffer(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
||||||
|
return (
|
||||||
|
c if ct_into is None else memoryview(c)[: message.nbytes],
|
||||||
|
mac if mac_into is None else memoryview(mac)[:maclen],
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
mac: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with detached MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
mac: The MAC to verify.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis256_decrypt_detached(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes + maclen)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes + maclen:
|
||||||
|
raise TypeError("into length must be at least message.nbytes + maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis256_encrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes + maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with appended MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext with MAC to decrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if ct.nbytes < maclen:
|
||||||
|
raise TypeError("ciphertext too short for tag")
|
||||||
|
expected_out = ct.nbytes - maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(expected_out)
|
||||||
|
else:
|
||||||
|
if into.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis256_decrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def stream(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer | None,
|
||||||
|
length: int | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | Buffer:
|
||||||
|
"""Generate a stream of pseudorandom bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None).
|
||||||
|
length: Number of bytes to generate (required if into is None).
|
||||||
|
into: Buffer to write stream into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pseudorandom bytes as bytearray, or into returned directly.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid or neither length nor into provided.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
if nonce is not None:
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce is not None and nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
if length is None:
|
||||||
|
raise TypeError("provide either into or length")
|
||||||
|
out = bytearray(length)
|
||||||
|
else:
|
||||||
|
if length is not None and into.nbytes < length:
|
||||||
|
raise TypeError("into length must be at least length")
|
||||||
|
out = into
|
||||||
|
_lib.aegis256_stream(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
memoryview(out).nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("into length must be at least message.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis256_encrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis256_decrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# This is missing from C API but convenient to have here
|
||||||
|
def mac(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
data: Buffer,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Compute a MAC for the given data in one shot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key())
|
||||||
|
nonce: Public nonce (generate with random_nonce())
|
||||||
|
data: Data to MAC
|
||||||
|
maclen: MAC length (16 or 32, default 16)
|
||||||
|
into: Buffer to write MAC into (default: bytearray created)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
data = memoryview(data)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
mac_state = Mac(key, nonce, maclen)
|
||||||
|
mac_state.update(data)
|
||||||
|
return mac_state.final(into)
|
||||||
|
|
||||||
|
|
||||||
|
class Mac:
|
||||||
|
"""MAC calculation and verification with incremental updates.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
...
|
||||||
|
mac = a.final()
|
||||||
|
|
||||||
|
Hashlib compatible interface:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
bytes_mac = a.digest()
|
||||||
|
hex_mac = a.hexdigest()
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_proxy", "_maclen", "_cached_digest")
|
||||||
|
|
||||||
|
def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None:
|
||||||
|
"""Create a MAC with the given key, nonce, and tag length.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
self._maclen = maclen
|
||||||
|
self._proxy = new_aligned_struct("aegis256_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis256_mac_init(self._proxy.ptr, _ptr(key), _ptr(nonce))
|
||||||
|
self._cached_digest: None | Literal[False] | bytes = None
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset back to the original state, prior to any updates."""
|
||||||
|
_lib.aegis256_mac_reset(self._proxy.ptr)
|
||||||
|
self._cached_digest = None
|
||||||
|
|
||||||
|
def clone(self) -> "Mac":
|
||||||
|
"""Return a clone of current MAC state."""
|
||||||
|
clone = object.__new__(Mac)
|
||||||
|
clone._maclen = self._maclen
|
||||||
|
clone._proxy = new_aligned_struct("aegis256_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis256_mac_state_clone(clone._proxy.ptr, self._proxy.ptr)
|
||||||
|
clone._cached_digest = self._cached_digest
|
||||||
|
return clone
|
||||||
|
|
||||||
|
__deepcopy__ = clone
|
||||||
|
|
||||||
|
def update(self, data: Buffer) -> None:
|
||||||
|
"""Update the MAC state with more data.
|
||||||
|
|
||||||
|
Repeated calls to update() are equivalent to a single call with the concatenated data.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError("Cannot update after final()")
|
||||||
|
data = memoryview(data)
|
||||||
|
rc = _lib.aegis256_mac_update(self._proxy.ptr, _ptr(data), data.nbytes)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"mac update failed: {err_name}")
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Calculate and return the MAC tag for the currently input data.
|
||||||
|
|
||||||
|
This method can only be called once. After calling it, the MAC becomes unusable
|
||||||
|
for further updates or calls to final().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional buffer to write the tag into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If finalization fails in the C library or if already finalized.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The MAC can only be calculated once. Use reset() to start over, or clone() before finalizing to continue."
|
||||||
|
)
|
||||||
|
maclen = self._maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
into = memoryview(into)
|
||||||
|
if into.nbytes < maclen:
|
||||||
|
raise TypeError("into length must be at least maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
clone = self.clone()
|
||||||
|
rc = _lib.aegis256_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}")
|
||||||
|
raise RuntimeError(f"mac final failed: {err_name}")
|
||||||
|
self._cached_digest = False
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
def digest(self) -> bytes:
|
||||||
|
"""Calculate and return the MAC tag as bytes.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
Can be called after final() to get the cached digest.
|
||||||
|
"""
|
||||||
|
if self._cached_digest:
|
||||||
|
return self._cached_digest
|
||||||
|
self._cached_digest = bytes(self.final()) # Overrides the False set by final()
|
||||||
|
return self._cached_digest
|
||||||
|
|
||||||
|
def hexdigest(self) -> str:
|
||||||
|
"""Calculate and return the MAC tag as a hex string.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
"""
|
||||||
|
return self.digest().hex()
|
||||||
|
|
||||||
|
def verify(self, mac: Buffer):
|
||||||
|
"""Verify that the data entered so far matches the given MAC tag.
|
||||||
|
|
||||||
|
Unlike the C library, this method does not alter the current state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: The tag to verify against (16 or 32 bytes).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length is invalid.
|
||||||
|
ValueError: If verification fails.
|
||||||
|
"""
|
||||||
|
mac = memoryview(mac)
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
|
||||||
|
cloned = self.clone()
|
||||||
|
rc = _lib.aegis256_mac_verify(cloned._proxy.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("mac verification failed")
|
||||||
|
|
||||||
|
|
||||||
|
class Encryptor:
|
||||||
|
"""Incremental encryptor.
|
||||||
|
|
||||||
|
- update(message[, into]) -> returns produced ciphertext bytes
|
||||||
|
- final([into]) -> returns MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental encryptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
ad: Associated data to bind to the encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis256_state", ALIGNMENT)
|
||||||
|
_lib.aegis256_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, message: Buffer, into: Buffer | None = None
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt a chunk of the message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Plaintext bytes to encrypt.
|
||||||
|
into: Optional destination buffer; must be >= len(message).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = message.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError(
|
||||||
|
"into length must be >= expected output size for this update"
|
||||||
|
)
|
||||||
|
rc = _lib.aegis256_state_encrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Finalize encryption and return the authentication tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional destination buffer for the tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The authentication tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the C final call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
# Only the authentication tag is produced here; allocate exactly maclen
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
out = into if into is not None else bytearray(maclen)
|
||||||
|
rc = _lib.aegis256_state_encrypt_final(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
||||||
|
self._state = None
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Decryptor:
|
||||||
|
"""Incremental decryptor.
|
||||||
|
|
||||||
|
- update(ciphertext[, into]) -> returns plaintext bytes
|
||||||
|
- final(mac) -> verifies the MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental decryptor for detached tags.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ad: Associated data used during encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis256_state", ALIGNMENT)
|
||||||
|
_lib.aegis256_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Process a chunk of ciphertext.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ct: Ciphertext bytes (without MAC).
|
||||||
|
into: Optional destination buffer; must be >= len(ciphertext).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = ct.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be >= required capacity for this update")
|
||||||
|
rc = _lib.aegis256_state_decrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state decrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, mac: Buffer) -> None:
|
||||||
|
"""Finalize decryption by verifying the MAC tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: Tag to verify.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length doesn't match the expected maclen.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
RuntimeError: If called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if mac.nbytes != maclen:
|
||||||
|
raise TypeError(f"mac length must be {maclen}")
|
||||||
|
rc = _lib.aegis256_state_decrypt_final(self._state.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
|
||||||
|
def new_state():
|
||||||
|
"""Allocate and return a new aegis256_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis256_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
def new_mac_state():
|
||||||
|
"""Allocate and return a new aegis256_mac_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis256_mac_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# constants
|
||||||
|
"NAME",
|
||||||
|
"KEYBYTES",
|
||||||
|
"NONCEBYTES",
|
||||||
|
"MACBYTES",
|
||||||
|
"MACBYTES_LONG",
|
||||||
|
"ALIGNMENT",
|
||||||
|
"RATE",
|
||||||
|
# utility functions
|
||||||
|
"random_key",
|
||||||
|
"random_nonce",
|
||||||
|
"nonce_increment",
|
||||||
|
"wipe",
|
||||||
|
# one-shot functions
|
||||||
|
"encrypt_detached",
|
||||||
|
"decrypt_detached",
|
||||||
|
"encrypt",
|
||||||
|
"decrypt",
|
||||||
|
"stream",
|
||||||
|
"encrypt_unauthenticated",
|
||||||
|
"decrypt_unauthenticated",
|
||||||
|
"mac",
|
||||||
|
# incremental classes
|
||||||
|
"Encryptor",
|
||||||
|
"Decryptor",
|
||||||
|
"Mac",
|
||||||
|
]
|
||||||
@@ -0,0 +1,898 @@
|
|||||||
|
"""AEGIS-256X2"""
|
||||||
|
# All modules are generated from aegis256x4.py by tools/generate.py!
|
||||||
|
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import secrets
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from ._loader import ffi
|
||||||
|
from ._loader import lib as _lib
|
||||||
|
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||||
|
|
||||||
|
NAME = "AEGIS-256X2" #: Algorithm display name
|
||||||
|
KEYBYTES = 32 #: Key size in bytes (varies by algorithm)
|
||||||
|
NONCEBYTES = 32 #: Nonce size in bytes (varies by algorithm)
|
||||||
|
MACBYTES = 16 #: Normal MAC size (always 16)
|
||||||
|
MACBYTES_LONG = 32 #: Long MAC size (always 32)
|
||||||
|
ALIGNMENT = 64 #: Required alignment for internal structures
|
||||||
|
RATE = 64 #: Byte chunk size in internal processing
|
||||||
|
|
||||||
|
|
||||||
|
def random_key() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a secret key using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
It is recommended to wipe() the key after no longer needed.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(KEYBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def random_nonce() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a public nonce using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
Nonces (a number used once) are public data that may be sent together
|
||||||
|
with the ciphertext, but they need to be unique for each use.
|
||||||
|
|
||||||
|
See also: nonce_increment() can be used to derive sequential nonces.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(NONCEBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def _ptr(buf):
|
||||||
|
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
ct_into: Buffer | None = None,
|
||||||
|
mac_into: Buffer | None = None,
|
||||||
|
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
mac_into: Buffer to write MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (ciphertext, mac)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if ct_into is not None:
|
||||||
|
ct_into = memoryview(ct_into)
|
||||||
|
if mac_into is not None:
|
||||||
|
mac_into = memoryview(mac_into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
if ct_into is None:
|
||||||
|
c = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if ct_into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("ct_into length must be at least message.nbytes")
|
||||||
|
c = ct_into
|
||||||
|
if mac_into is None:
|
||||||
|
mac = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
if mac_into.nbytes < maclen:
|
||||||
|
raise TypeError("mac_into length must be at least maclen")
|
||||||
|
mac = mac_into
|
||||||
|
|
||||||
|
rc = _lib.aegis256x2_encrypt_detached(
|
||||||
|
ffi.from_buffer(c),
|
||||||
|
ffi.from_buffer(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
||||||
|
return (
|
||||||
|
c if ct_into is None else memoryview(c)[: message.nbytes],
|
||||||
|
mac if mac_into is None else memoryview(mac)[:maclen],
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
mac: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with detached MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
mac: The MAC to verify.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis256x2_decrypt_detached(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes + maclen)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes + maclen:
|
||||||
|
raise TypeError("into length must be at least message.nbytes + maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis256x2_encrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes + maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with appended MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext with MAC to decrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if ct.nbytes < maclen:
|
||||||
|
raise TypeError("ciphertext too short for tag")
|
||||||
|
expected_out = ct.nbytes - maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(expected_out)
|
||||||
|
else:
|
||||||
|
if into.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis256x2_decrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def stream(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer | None,
|
||||||
|
length: int | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | Buffer:
|
||||||
|
"""Generate a stream of pseudorandom bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None).
|
||||||
|
length: Number of bytes to generate (required if into is None).
|
||||||
|
into: Buffer to write stream into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pseudorandom bytes as bytearray, or into returned directly.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid or neither length nor into provided.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
if nonce is not None:
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce is not None and nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
if length is None:
|
||||||
|
raise TypeError("provide either into or length")
|
||||||
|
out = bytearray(length)
|
||||||
|
else:
|
||||||
|
if length is not None and into.nbytes < length:
|
||||||
|
raise TypeError("into length must be at least length")
|
||||||
|
out = into
|
||||||
|
_lib.aegis256x2_stream(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
memoryview(out).nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("into length must be at least message.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis256x2_encrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis256x2_decrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# This is missing from C API but convenient to have here
|
||||||
|
def mac(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
data: Buffer,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Compute a MAC for the given data in one shot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key())
|
||||||
|
nonce: Public nonce (generate with random_nonce())
|
||||||
|
data: Data to MAC
|
||||||
|
maclen: MAC length (16 or 32, default 16)
|
||||||
|
into: Buffer to write MAC into (default: bytearray created)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
data = memoryview(data)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
mac_state = Mac(key, nonce, maclen)
|
||||||
|
mac_state.update(data)
|
||||||
|
return mac_state.final(into)
|
||||||
|
|
||||||
|
|
||||||
|
class Mac:
|
||||||
|
"""MAC calculation and verification with incremental updates.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
...
|
||||||
|
mac = a.final()
|
||||||
|
|
||||||
|
Hashlib compatible interface:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
bytes_mac = a.digest()
|
||||||
|
hex_mac = a.hexdigest()
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_proxy", "_maclen", "_cached_digest")
|
||||||
|
|
||||||
|
def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None:
|
||||||
|
"""Create a MAC with the given key, nonce, and tag length.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
self._maclen = maclen
|
||||||
|
self._proxy = new_aligned_struct("aegis256x2_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis256x2_mac_init(self._proxy.ptr, _ptr(key), _ptr(nonce))
|
||||||
|
self._cached_digest: None | Literal[False] | bytes = None
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset back to the original state, prior to any updates."""
|
||||||
|
_lib.aegis256x2_mac_reset(self._proxy.ptr)
|
||||||
|
self._cached_digest = None
|
||||||
|
|
||||||
|
def clone(self) -> "Mac":
|
||||||
|
"""Return a clone of current MAC state."""
|
||||||
|
clone = object.__new__(Mac)
|
||||||
|
clone._maclen = self._maclen
|
||||||
|
clone._proxy = new_aligned_struct("aegis256x2_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis256x2_mac_state_clone(clone._proxy.ptr, self._proxy.ptr)
|
||||||
|
clone._cached_digest = self._cached_digest
|
||||||
|
return clone
|
||||||
|
|
||||||
|
__deepcopy__ = clone
|
||||||
|
|
||||||
|
def update(self, data: Buffer) -> None:
|
||||||
|
"""Update the MAC state with more data.
|
||||||
|
|
||||||
|
Repeated calls to update() are equivalent to a single call with the concatenated data.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError("Cannot update after final()")
|
||||||
|
data = memoryview(data)
|
||||||
|
rc = _lib.aegis256x2_mac_update(self._proxy.ptr, _ptr(data), data.nbytes)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"mac update failed: {err_name}")
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Calculate and return the MAC tag for the currently input data.
|
||||||
|
|
||||||
|
This method can only be called once. After calling it, the MAC becomes unusable
|
||||||
|
for further updates or calls to final().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional buffer to write the tag into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If finalization fails in the C library or if already finalized.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The MAC can only be calculated once. Use reset() to start over, or clone() before finalizing to continue."
|
||||||
|
)
|
||||||
|
maclen = self._maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
into = memoryview(into)
|
||||||
|
if into.nbytes < maclen:
|
||||||
|
raise TypeError("into length must be at least maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
clone = self.clone()
|
||||||
|
rc = _lib.aegis256x2_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}")
|
||||||
|
raise RuntimeError(f"mac final failed: {err_name}")
|
||||||
|
self._cached_digest = False
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
def digest(self) -> bytes:
|
||||||
|
"""Calculate and return the MAC tag as bytes.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
Can be called after final() to get the cached digest.
|
||||||
|
"""
|
||||||
|
if self._cached_digest:
|
||||||
|
return self._cached_digest
|
||||||
|
self._cached_digest = bytes(self.final()) # Overrides the False set by final()
|
||||||
|
return self._cached_digest
|
||||||
|
|
||||||
|
def hexdigest(self) -> str:
|
||||||
|
"""Calculate and return the MAC tag as a hex string.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
"""
|
||||||
|
return self.digest().hex()
|
||||||
|
|
||||||
|
def verify(self, mac: Buffer):
|
||||||
|
"""Verify that the data entered so far matches the given MAC tag.
|
||||||
|
|
||||||
|
Unlike the C library, this method does not alter the current state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: The tag to verify against (16 or 32 bytes).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length is invalid.
|
||||||
|
ValueError: If verification fails.
|
||||||
|
"""
|
||||||
|
mac = memoryview(mac)
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
|
||||||
|
cloned = self.clone()
|
||||||
|
rc = _lib.aegis256x2_mac_verify(cloned._proxy.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("mac verification failed")
|
||||||
|
|
||||||
|
|
||||||
|
class Encryptor:
|
||||||
|
"""Incremental encryptor.
|
||||||
|
|
||||||
|
- update(message[, into]) -> returns produced ciphertext bytes
|
||||||
|
- final([into]) -> returns MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental encryptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
ad: Associated data to bind to the encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis256x2_state", ALIGNMENT)
|
||||||
|
_lib.aegis256x2_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, message: Buffer, into: Buffer | None = None
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt a chunk of the message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Plaintext bytes to encrypt.
|
||||||
|
into: Optional destination buffer; must be >= len(message).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = message.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError(
|
||||||
|
"into length must be >= expected output size for this update"
|
||||||
|
)
|
||||||
|
rc = _lib.aegis256x2_state_encrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Finalize encryption and return the authentication tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional destination buffer for the tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The authentication tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the C final call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
# Only the authentication tag is produced here; allocate exactly maclen
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
out = into if into is not None else bytearray(maclen)
|
||||||
|
rc = _lib.aegis256x2_state_encrypt_final(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
||||||
|
self._state = None
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Decryptor:
|
||||||
|
"""Incremental decryptor.
|
||||||
|
|
||||||
|
- update(ciphertext[, into]) -> returns plaintext bytes
|
||||||
|
- final(mac) -> verifies the MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental decryptor for detached tags.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ad: Associated data used during encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis256x2_state", ALIGNMENT)
|
||||||
|
_lib.aegis256x2_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Process a chunk of ciphertext.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ct: Ciphertext bytes (without MAC).
|
||||||
|
into: Optional destination buffer; must be >= len(ciphertext).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = ct.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be >= required capacity for this update")
|
||||||
|
rc = _lib.aegis256x2_state_decrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state decrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, mac: Buffer) -> None:
|
||||||
|
"""Finalize decryption by verifying the MAC tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: Tag to verify.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length doesn't match the expected maclen.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
RuntimeError: If called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if mac.nbytes != maclen:
|
||||||
|
raise TypeError(f"mac length must be {maclen}")
|
||||||
|
rc = _lib.aegis256x2_state_decrypt_final(self._state.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
|
||||||
|
def new_state():
|
||||||
|
"""Allocate and return a new aegis256x2_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis256x2_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
def new_mac_state():
|
||||||
|
"""Allocate and return a new aegis256x2_mac_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis256x2_mac_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# constants
|
||||||
|
"NAME",
|
||||||
|
"KEYBYTES",
|
||||||
|
"NONCEBYTES",
|
||||||
|
"MACBYTES",
|
||||||
|
"MACBYTES_LONG",
|
||||||
|
"ALIGNMENT",
|
||||||
|
"RATE",
|
||||||
|
# utility functions
|
||||||
|
"random_key",
|
||||||
|
"random_nonce",
|
||||||
|
"nonce_increment",
|
||||||
|
"wipe",
|
||||||
|
# one-shot functions
|
||||||
|
"encrypt_detached",
|
||||||
|
"decrypt_detached",
|
||||||
|
"encrypt",
|
||||||
|
"decrypt",
|
||||||
|
"stream",
|
||||||
|
"encrypt_unauthenticated",
|
||||||
|
"decrypt_unauthenticated",
|
||||||
|
"mac",
|
||||||
|
# incremental classes
|
||||||
|
"Encryptor",
|
||||||
|
"Decryptor",
|
||||||
|
"Mac",
|
||||||
|
]
|
||||||
@@ -0,0 +1,898 @@
|
|||||||
|
"""AEGIS-256X4"""
|
||||||
|
# All modules are generated from aegis256x4.py by tools/generate.py!
|
||||||
|
# DO NOT EDIT OTHER ALGORITHM FILES MANUALLY!
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import secrets
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from ._loader import ffi
|
||||||
|
from ._loader import lib as _lib
|
||||||
|
from .util import Buffer, new_aligned_struct, nonce_increment, wipe
|
||||||
|
|
||||||
|
NAME = "AEGIS-256X4" #: Algorithm display name
|
||||||
|
KEYBYTES = 32 #: Key size in bytes (varies by algorithm)
|
||||||
|
NONCEBYTES = 32 #: Nonce size in bytes (varies by algorithm)
|
||||||
|
MACBYTES = 16 #: Normal MAC size (always 16)
|
||||||
|
MACBYTES_LONG = 32 #: Long MAC size (always 32)
|
||||||
|
ALIGNMENT = 64 #: Required alignment for internal structures
|
||||||
|
RATE = 64 #: Byte chunk size in internal processing
|
||||||
|
|
||||||
|
|
||||||
|
def random_key() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a secret key using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
It is recommended to wipe() the key after no longer needed.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(KEYBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def random_nonce() -> bytearray:
|
||||||
|
"""
|
||||||
|
Generate a public nonce using cryptographically secure random bytes.
|
||||||
|
|
||||||
|
Nonces (a number used once) are public data that may be sent together
|
||||||
|
with the ciphertext, but they need to be unique for each use.
|
||||||
|
|
||||||
|
See also: nonce_increment() can be used to derive sequential nonces.
|
||||||
|
"""
|
||||||
|
return bytearray(secrets.token_bytes(NONCEBYTES))
|
||||||
|
|
||||||
|
|
||||||
|
def _ptr(buf):
|
||||||
|
return ffi.NULL if buf is None else ffi.from_buffer(buf)
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
ct_into: Buffer | None = None,
|
||||||
|
mac_into: Buffer | None = None,
|
||||||
|
) -> tuple[bytearray | memoryview, bytearray | memoryview]:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext and MAC separately.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
ct_into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
mac_into: Buffer to write MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (ciphertext, mac)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if ct_into is not None:
|
||||||
|
ct_into = memoryview(ct_into)
|
||||||
|
if mac_into is not None:
|
||||||
|
mac_into = memoryview(mac_into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
if ct_into is None:
|
||||||
|
c = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if ct_into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("ct_into length must be at least message.nbytes")
|
||||||
|
c = ct_into
|
||||||
|
if mac_into is None:
|
||||||
|
mac = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
if mac_into.nbytes < maclen:
|
||||||
|
raise TypeError("mac_into length must be at least maclen")
|
||||||
|
mac = mac_into
|
||||||
|
|
||||||
|
rc = _lib.aegis256x4_encrypt_detached(
|
||||||
|
ffi.from_buffer(c),
|
||||||
|
ffi.from_buffer(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt detached failed: {err_name}")
|
||||||
|
return (
|
||||||
|
c if ct_into is None else memoryview(c)[: message.nbytes],
|
||||||
|
mac if mac_into is None else memoryview(mac)[:maclen],
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_detached(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
mac: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with detached MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
mac: The MAC to verify.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis256x4_decrypt_detached(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(mac),
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message with associated data, returning ciphertext with appended MAC.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write ciphertext+MAC into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If encryption fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes + maclen)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes + maclen:
|
||||||
|
raise TypeError("into length must be at least message.nbytes + maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis256x4_encrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"encrypt failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes + maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
*,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext with appended MAC and associated data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext with MAC to decrypt.
|
||||||
|
ad: Associated data (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if ct.nbytes < maclen:
|
||||||
|
raise TypeError("ciphertext too short for tag")
|
||||||
|
expected_out = ct.nbytes - maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(expected_out)
|
||||||
|
else:
|
||||||
|
if into.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes - maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
rc = _lib.aegis256x4_decrypt(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
maclen,
|
||||||
|
_ptr(ad),
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def stream(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer | None,
|
||||||
|
length: int | None = None,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | Buffer:
|
||||||
|
"""Generate a stream of pseudorandom bytes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None).
|
||||||
|
length: Number of bytes to generate (required if into is None).
|
||||||
|
into: Buffer to write stream into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Pseudorandom bytes as bytearray, or into returned directly.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid or neither length nor into provided.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
if nonce is not None:
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce is not None and nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
if length is None:
|
||||||
|
raise TypeError("provide either into or length")
|
||||||
|
out = bytearray(length)
|
||||||
|
else:
|
||||||
|
if length is not None and into.nbytes < length:
|
||||||
|
raise TypeError("into length must be at least length")
|
||||||
|
out = into
|
||||||
|
_lib.aegis256x4_stream(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
memoryview(out).nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
message: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt message without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
message: The plaintext message to encrypt.
|
||||||
|
into: Buffer to write ciphertext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Ciphertext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(message.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < message.nbytes:
|
||||||
|
raise TypeError("into length must be at least message.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis256x4_encrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: message.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_unauthenticated(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ct: Buffer,
|
||||||
|
*,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Decrypt ciphertext without authentication (for testing/debugging).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ct: The ciphertext to decrypt.
|
||||||
|
into: Buffer to write plaintext into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plaintext as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(ct.nbytes)
|
||||||
|
else:
|
||||||
|
if into.nbytes < ct.nbytes:
|
||||||
|
raise TypeError("into length must be at least ct.nbytes")
|
||||||
|
out = into
|
||||||
|
_lib.aegis256x4_decrypt_unauthenticated(
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
return out if into is None else memoryview(out)[: ct.nbytes] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# This is missing from C API but convenient to have here
|
||||||
|
def mac(
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
data: Buffer,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
into: Buffer | None = None,
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Compute a MAC for the given data in one shot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key())
|
||||||
|
nonce: Public nonce (generate with random_nonce())
|
||||||
|
data: Data to MAC
|
||||||
|
maclen: MAC length (16 or 32, default 16)
|
||||||
|
into: Buffer to write MAC into (default: bytearray created)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MAC bytes as bytearray if into not provided, memoryview of into otherwise
|
||||||
|
"""
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
data = memoryview(data)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
mac_state = Mac(key, nonce, maclen)
|
||||||
|
mac_state.update(data)
|
||||||
|
return mac_state.final(into)
|
||||||
|
|
||||||
|
|
||||||
|
class Mac:
|
||||||
|
"""MAC calculation and verification with incremental updates.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
...
|
||||||
|
mac = a.final()
|
||||||
|
|
||||||
|
Hashlib compatible interface:
|
||||||
|
a = Mac(key, nonce)
|
||||||
|
a.update(data)
|
||||||
|
bytes_mac = a.digest()
|
||||||
|
hex_mac = a.hexdigest()
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_proxy", "_maclen", "_cached_digest")
|
||||||
|
|
||||||
|
def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None:
|
||||||
|
"""Create a MAC with the given key, nonce, and tag length.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
|
||||||
|
self._maclen = maclen
|
||||||
|
self._proxy = new_aligned_struct("aegis256x4_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis256x4_mac_init(self._proxy.ptr, _ptr(key), _ptr(nonce))
|
||||||
|
self._cached_digest: None | Literal[False] | bytes = None
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
"""Reset back to the original state, prior to any updates."""
|
||||||
|
_lib.aegis256x4_mac_reset(self._proxy.ptr)
|
||||||
|
self._cached_digest = None
|
||||||
|
|
||||||
|
def clone(self) -> "Mac":
|
||||||
|
"""Return a clone of current MAC state."""
|
||||||
|
clone = object.__new__(Mac)
|
||||||
|
clone._maclen = self._maclen
|
||||||
|
clone._proxy = new_aligned_struct("aegis256x4_mac_state", ALIGNMENT)
|
||||||
|
_lib.aegis256x4_mac_state_clone(clone._proxy.ptr, self._proxy.ptr)
|
||||||
|
clone._cached_digest = self._cached_digest
|
||||||
|
return clone
|
||||||
|
|
||||||
|
__deepcopy__ = clone
|
||||||
|
|
||||||
|
def update(self, data: Buffer) -> None:
|
||||||
|
"""Update the MAC state with more data.
|
||||||
|
|
||||||
|
Repeated calls to update() are equivalent to a single call with the concatenated data.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError("Cannot update after final()")
|
||||||
|
data = memoryview(data)
|
||||||
|
rc = _lib.aegis256x4_mac_update(self._proxy.ptr, _ptr(data), data.nbytes)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"mac update failed: {err_name}")
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Calculate and return the MAC tag for the currently input data.
|
||||||
|
|
||||||
|
This method can only be called once. After calling it, the MAC becomes unusable
|
||||||
|
for further updates or calls to final().
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional buffer to write the tag into (default: bytearray created).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If lengths are invalid.
|
||||||
|
RuntimeError: If finalization fails in the C library or if already finalized.
|
||||||
|
"""
|
||||||
|
if self._cached_digest is not None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The MAC can only be calculated once. Use reset() to start over, or clone() before finalizing to continue."
|
||||||
|
)
|
||||||
|
maclen = self._maclen
|
||||||
|
if into is None:
|
||||||
|
out = bytearray(maclen)
|
||||||
|
else:
|
||||||
|
into = memoryview(into)
|
||||||
|
if into.nbytes < maclen:
|
||||||
|
raise TypeError("into length must be at least maclen")
|
||||||
|
out = into
|
||||||
|
|
||||||
|
clone = self.clone()
|
||||||
|
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}")
|
||||||
|
raise RuntimeError(f"mac final failed: {err_name}")
|
||||||
|
self._cached_digest = False
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
def digest(self) -> bytes:
|
||||||
|
"""Calculate and return the MAC tag as bytes.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
Can be called after final() to get the cached digest.
|
||||||
|
"""
|
||||||
|
if self._cached_digest:
|
||||||
|
return self._cached_digest
|
||||||
|
self._cached_digest = bytes(self.final()) # Overrides the False set by final()
|
||||||
|
return self._cached_digest
|
||||||
|
|
||||||
|
def hexdigest(self) -> str:
|
||||||
|
"""Calculate and return the MAC tag as a hex string.
|
||||||
|
|
||||||
|
After calling this method, the MAC becomes unusable for further updates.
|
||||||
|
The result is cached and subsequent calls return the same value.
|
||||||
|
"""
|
||||||
|
return self.digest().hex()
|
||||||
|
|
||||||
|
def verify(self, mac: Buffer):
|
||||||
|
"""Verify that the data entered so far matches the given MAC tag.
|
||||||
|
|
||||||
|
Unlike the C library, this method does not alter the current state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: The tag to verify against (16 or 32 bytes).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length is invalid.
|
||||||
|
ValueError: If verification fails.
|
||||||
|
"""
|
||||||
|
mac = memoryview(mac)
|
||||||
|
maclen = mac.nbytes
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("mac length must be 16 or 32")
|
||||||
|
|
||||||
|
cloned = self.clone()
|
||||||
|
rc = _lib.aegis256x4_mac_verify(cloned._proxy.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("mac verification failed")
|
||||||
|
|
||||||
|
|
||||||
|
class Encryptor:
|
||||||
|
"""Incremental encryptor.
|
||||||
|
|
||||||
|
- update(message[, into]) -> returns produced ciphertext bytes
|
||||||
|
- final([into]) -> returns MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental encryptor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (generate with random_key()).
|
||||||
|
nonce: Public nonce (generate with random_nonce()).
|
||||||
|
ad: Associated data to bind to the encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis256x4_state", ALIGNMENT)
|
||||||
|
_lib.aegis256x4_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, message: Buffer, into: Buffer | None = None
|
||||||
|
) -> bytearray | memoryview:
|
||||||
|
"""Encrypt a chunk of the message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: Plaintext bytes to encrypt.
|
||||||
|
into: Optional destination buffer; must be >= len(message).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The ciphertext for this chunk as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
message = memoryview(message)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = message.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError(
|
||||||
|
"into length must be >= expected output size for this update"
|
||||||
|
)
|
||||||
|
rc = _lib.aegis256x4_state_encrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(message),
|
||||||
|
message.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Finalize encryption and return the authentication tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
into: Optional destination buffer for the tag.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The authentication tag as bytearray if into not provided, memoryview of into otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the C final call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
# Only the authentication tag is produced here; allocate exactly maclen
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
out = into if into is not None else bytearray(maclen)
|
||||||
|
rc = _lib.aegis256x4_state_encrypt_final(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out),
|
||||||
|
maclen,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state encrypt final failed: {err_name}")
|
||||||
|
self._state = None
|
||||||
|
return out if into is None else memoryview(out)[:maclen] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Decryptor:
|
||||||
|
"""Incremental decryptor.
|
||||||
|
|
||||||
|
- update(ciphertext[, into]) -> returns plaintext bytes
|
||||||
|
- final(mac) -> verifies the MAC tag
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_state", "_maclen")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
key: Buffer,
|
||||||
|
nonce: Buffer,
|
||||||
|
ad: Buffer | None = None,
|
||||||
|
maclen: int = MACBYTES,
|
||||||
|
):
|
||||||
|
"""Create an incremental decryptor for detached tags.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: Secret key (same key used during encryption).
|
||||||
|
nonce: Public nonce (same nonce used during encryption).
|
||||||
|
ad: Associated data used during encryption (optional).
|
||||||
|
maclen: MAC length (16 or 32, default 16).
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If key, nonce, or maclen are invalid.
|
||||||
|
"""
|
||||||
|
if maclen not in (16, 32):
|
||||||
|
raise TypeError("maclen must be 16 or 32")
|
||||||
|
key = memoryview(key)
|
||||||
|
nonce = memoryview(nonce)
|
||||||
|
if ad is not None:
|
||||||
|
ad = memoryview(ad)
|
||||||
|
if key.nbytes != KEYBYTES:
|
||||||
|
raise TypeError(f"key length must be {KEYBYTES}")
|
||||||
|
if nonce.nbytes != NONCEBYTES:
|
||||||
|
raise TypeError(f"nonce length must be {NONCEBYTES}")
|
||||||
|
self._state = new_aligned_struct("aegis256x4_state", ALIGNMENT)
|
||||||
|
_lib.aegis256x4_state_init(
|
||||||
|
self._state.ptr,
|
||||||
|
_ptr(ad) if ad is not None else ffi.NULL,
|
||||||
|
0 if ad is None else ad.nbytes,
|
||||||
|
_ptr(nonce),
|
||||||
|
_ptr(key),
|
||||||
|
)
|
||||||
|
self._maclen = maclen
|
||||||
|
|
||||||
|
def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview:
|
||||||
|
"""Process a chunk of ciphertext.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ct: Ciphertext bytes (without MAC).
|
||||||
|
into: Optional destination buffer; must be >= len(ciphertext).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A memoryview of the decrypted bytes for this chunk if into provided, bytearray otherwise.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If destination buffer is too small.
|
||||||
|
RuntimeError: If the C update call fails or if called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call update() after final()")
|
||||||
|
ct = memoryview(ct)
|
||||||
|
if into is not None:
|
||||||
|
into = memoryview(into)
|
||||||
|
expected_out = ct.nbytes
|
||||||
|
out = into if into is not None else bytearray(expected_out)
|
||||||
|
out_mv = memoryview(out)
|
||||||
|
if out_mv.nbytes < expected_out:
|
||||||
|
raise TypeError("into length must be >= required capacity for this update")
|
||||||
|
rc = _lib.aegis256x4_state_decrypt_update(
|
||||||
|
self._state.ptr,
|
||||||
|
ffi.from_buffer(out_mv),
|
||||||
|
_ptr(ct),
|
||||||
|
ct.nbytes,
|
||||||
|
)
|
||||||
|
if rc != 0:
|
||||||
|
err_num = ffi.errno
|
||||||
|
err_name = errno.errorcode.get(err_num, f"errno_{err_num}")
|
||||||
|
raise RuntimeError(f"state decrypt update failed: {err_name}")
|
||||||
|
return out if into is None else memoryview(out)[:expected_out] # type: ignore
|
||||||
|
|
||||||
|
def final(self, mac: Buffer) -> None:
|
||||||
|
"""Finalize decryption by verifying the MAC tag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac: Tag to verify.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError: If tag length doesn't match the expected maclen.
|
||||||
|
ValueError: If authentication fails.
|
||||||
|
RuntimeError: If called after final().
|
||||||
|
"""
|
||||||
|
if self._state is None:
|
||||||
|
raise RuntimeError("Cannot call final() after final()")
|
||||||
|
maclen = self._maclen
|
||||||
|
mac = memoryview(mac)
|
||||||
|
if mac.nbytes != maclen:
|
||||||
|
raise TypeError(f"mac length must be {maclen}")
|
||||||
|
rc = _lib.aegis256x4_state_decrypt_final(self._state.ptr, _ptr(mac), maclen)
|
||||||
|
if rc != 0:
|
||||||
|
raise ValueError("authentication failed")
|
||||||
|
self._state = None
|
||||||
|
|
||||||
|
|
||||||
|
def new_state():
|
||||||
|
"""Allocate and return a new aegis256x4_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis256x4_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
def new_mac_state():
|
||||||
|
"""Allocate and return a new aegis256x4_mac_state* with proper alignment."""
|
||||||
|
return new_aligned_struct("aegis256x4_mac_state", ALIGNMENT)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# constants
|
||||||
|
"NAME",
|
||||||
|
"KEYBYTES",
|
||||||
|
"NONCEBYTES",
|
||||||
|
"MACBYTES",
|
||||||
|
"MACBYTES_LONG",
|
||||||
|
"ALIGNMENT",
|
||||||
|
"RATE",
|
||||||
|
# utility functions
|
||||||
|
"random_key",
|
||||||
|
"random_nonce",
|
||||||
|
"nonce_increment",
|
||||||
|
"wipe",
|
||||||
|
# one-shot functions
|
||||||
|
"encrypt_detached",
|
||||||
|
"decrypt_detached",
|
||||||
|
"encrypt",
|
||||||
|
"decrypt",
|
||||||
|
"stream",
|
||||||
|
"encrypt_unauthenticated",
|
||||||
|
"decrypt_unauthenticated",
|
||||||
|
"mac",
|
||||||
|
# incremental classes
|
||||||
|
"Encryptor",
|
||||||
|
"Decryptor",
|
||||||
|
"Mac",
|
||||||
|
]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/* This file is generated with tools/gen_cdef.py. Do not edit. */
|
/* This file is generated with tools/generate.py. Do not edit. */
|
||||||
|
|
||||||
typedef unsigned char uint8_t;
|
typedef unsigned char uint8_t;
|
||||||
typedef unsigned long size_t;
|
typedef unsigned long size_t;
|
||||||
@@ -55,35 +55,10 @@ void aegis128l_state_init(aegis128l_state *st_,
|
|||||||
size_t adlen,
|
size_t adlen,
|
||||||
const uint8_t *npub,
|
const uint8_t *npub,
|
||||||
const uint8_t *k);
|
const uint8_t *k);
|
||||||
int aegis128l_state_encrypt_update(aegis128l_state *st_,
|
int aegis128l_state_encrypt_update(aegis128l_state *st_, uint8_t *c, const uint8_t *m, size_t mlen);
|
||||||
uint8_t *c,
|
int aegis128l_state_encrypt_final(aegis128l_state *st_, uint8_t *mac, size_t maclen);
|
||||||
size_t clen_max,
|
int aegis128l_state_decrypt_update(aegis128l_state *st_, uint8_t *m, const uint8_t *c, size_t clen) ;
|
||||||
size_t *written,
|
int aegis128l_state_decrypt_final(aegis128l_state *st_, const uint8_t *mac, size_t maclen) ;
|
||||||
const uint8_t *m,
|
|
||||||
size_t mlen);
|
|
||||||
int aegis128l_state_encrypt_detached_final(aegis128l_state *st_,
|
|
||||||
uint8_t *c,
|
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
uint8_t *mac,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis128l_state_encrypt_final(aegis128l_state *st_,
|
|
||||||
uint8_t *c,
|
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis128l_state_decrypt_detached_update(aegis128l_state *st_,
|
|
||||||
uint8_t *m,
|
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *c,
|
|
||||||
size_t clen) ;
|
|
||||||
int aegis128l_state_decrypt_detached_final(aegis128l_state *st_,
|
|
||||||
uint8_t *m,
|
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *mac,
|
|
||||||
size_t maclen) ;
|
|
||||||
void aegis128l_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
void aegis128l_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
||||||
void aegis128l_encrypt_unauthenticated(uint8_t *c,
|
void aegis128l_encrypt_unauthenticated(uint8_t *c,
|
||||||
const uint8_t *m,
|
const uint8_t *m,
|
||||||
@@ -151,33 +126,14 @@ void aegis128x2_state_init(aegis128x2_state *st_,
|
|||||||
const uint8_t *k);
|
const uint8_t *k);
|
||||||
int aegis128x2_state_encrypt_update(aegis128x2_state *st_,
|
int aegis128x2_state_encrypt_update(aegis128x2_state *st_,
|
||||||
uint8_t *c,
|
uint8_t *c,
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *m,
|
const uint8_t *m,
|
||||||
size_t mlen);
|
size_t mlen);
|
||||||
int aegis128x2_state_encrypt_detached_final(aegis128x2_state *st_,
|
int aegis128x2_state_encrypt_final(aegis128x2_state *st_, uint8_t *mac, size_t maclen);
|
||||||
uint8_t *c,
|
int aegis128x2_state_decrypt_update(aegis128x2_state *st_,
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
uint8_t *mac,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis128x2_state_encrypt_final(aegis128x2_state *st_,
|
|
||||||
uint8_t *c,
|
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis128x2_state_decrypt_detached_update(aegis128x2_state *st_,
|
|
||||||
uint8_t *m,
|
uint8_t *m,
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *c,
|
const uint8_t *c,
|
||||||
size_t clen) ;
|
size_t clen) ;
|
||||||
int aegis128x2_state_decrypt_detached_final(aegis128x2_state *st_,
|
int aegis128x2_state_decrypt_final(aegis128x2_state *st_, const uint8_t *mac, size_t maclen) ;
|
||||||
uint8_t *m,
|
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *mac,
|
|
||||||
size_t maclen) ;
|
|
||||||
void aegis128x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
void aegis128x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
||||||
void aegis128x2_encrypt_unauthenticated(uint8_t *c,
|
void aegis128x2_encrypt_unauthenticated(uint8_t *c,
|
||||||
const uint8_t *m,
|
const uint8_t *m,
|
||||||
@@ -245,33 +201,14 @@ void aegis128x4_state_init(aegis128x4_state *st_,
|
|||||||
const uint8_t *k);
|
const uint8_t *k);
|
||||||
int aegis128x4_state_encrypt_update(aegis128x4_state *st_,
|
int aegis128x4_state_encrypt_update(aegis128x4_state *st_,
|
||||||
uint8_t *c,
|
uint8_t *c,
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *m,
|
const uint8_t *m,
|
||||||
size_t mlen);
|
size_t mlen);
|
||||||
int aegis128x4_state_encrypt_detached_final(aegis128x4_state *st_,
|
int aegis128x4_state_encrypt_final(aegis128x4_state *st_, uint8_t *mac, size_t maclen);
|
||||||
uint8_t *c,
|
int aegis128x4_state_decrypt_update(aegis128x4_state *st_,
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
uint8_t *mac,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis128x4_state_encrypt_final(aegis128x4_state *st_,
|
|
||||||
uint8_t *c,
|
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis128x4_state_decrypt_detached_update(aegis128x4_state *st_,
|
|
||||||
uint8_t *m,
|
uint8_t *m,
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *c,
|
const uint8_t *c,
|
||||||
size_t clen) ;
|
size_t clen) ;
|
||||||
int aegis128x4_state_decrypt_detached_final(aegis128x4_state *st_,
|
int aegis128x4_state_decrypt_final(aegis128x4_state *st_, const uint8_t *mac, size_t maclen) ;
|
||||||
uint8_t *m,
|
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *mac,
|
|
||||||
size_t maclen) ;
|
|
||||||
void aegis128x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
void aegis128x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
||||||
void aegis128x4_encrypt_unauthenticated(uint8_t *c,
|
void aegis128x4_encrypt_unauthenticated(uint8_t *c,
|
||||||
const uint8_t *m,
|
const uint8_t *m,
|
||||||
@@ -337,35 +274,10 @@ void aegis256_state_init(aegis256_state *st_,
|
|||||||
size_t adlen,
|
size_t adlen,
|
||||||
const uint8_t *npub,
|
const uint8_t *npub,
|
||||||
const uint8_t *k);
|
const uint8_t *k);
|
||||||
int aegis256_state_encrypt_update(aegis256_state *st_,
|
int aegis256_state_encrypt_update(aegis256_state *st_, uint8_t *c, const uint8_t *m, size_t mlen);
|
||||||
uint8_t *c,
|
int aegis256_state_encrypt_final(aegis256_state *st_, uint8_t *mac, size_t maclen);
|
||||||
size_t clen_max,
|
int aegis256_state_decrypt_update(aegis256_state *st_, uint8_t *m, const uint8_t *c, size_t clen) ;
|
||||||
size_t *written,
|
int aegis256_state_decrypt_final(aegis256_state *st_, const uint8_t *mac, size_t maclen) ;
|
||||||
const uint8_t *m,
|
|
||||||
size_t mlen);
|
|
||||||
int aegis256_state_encrypt_detached_final(aegis256_state *st_,
|
|
||||||
uint8_t *c,
|
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
uint8_t *mac,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis256_state_encrypt_final(aegis256_state *st_,
|
|
||||||
uint8_t *c,
|
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis256_state_decrypt_detached_update(aegis256_state *st_,
|
|
||||||
uint8_t *m,
|
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *c,
|
|
||||||
size_t clen) ;
|
|
||||||
int aegis256_state_decrypt_detached_final(aegis256_state *st_,
|
|
||||||
uint8_t *m,
|
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *mac,
|
|
||||||
size_t maclen) ;
|
|
||||||
void aegis256_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
void aegis256_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
||||||
void aegis256_encrypt_unauthenticated(uint8_t *c,
|
void aegis256_encrypt_unauthenticated(uint8_t *c,
|
||||||
const uint8_t *m,
|
const uint8_t *m,
|
||||||
@@ -433,33 +345,14 @@ void aegis256x2_state_init(aegis256x2_state *st_,
|
|||||||
const uint8_t *k);
|
const uint8_t *k);
|
||||||
int aegis256x2_state_encrypt_update(aegis256x2_state *st_,
|
int aegis256x2_state_encrypt_update(aegis256x2_state *st_,
|
||||||
uint8_t *c,
|
uint8_t *c,
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *m,
|
const uint8_t *m,
|
||||||
size_t mlen);
|
size_t mlen);
|
||||||
int aegis256x2_state_encrypt_detached_final(aegis256x2_state *st_,
|
int aegis256x2_state_encrypt_final(aegis256x2_state *st_, uint8_t *mac, size_t maclen);
|
||||||
uint8_t *c,
|
int aegis256x2_state_decrypt_update(aegis256x2_state *st_,
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
uint8_t *mac,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis256x2_state_encrypt_final(aegis256x2_state *st_,
|
|
||||||
uint8_t *c,
|
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis256x2_state_decrypt_detached_update(aegis256x2_state *st_,
|
|
||||||
uint8_t *m,
|
uint8_t *m,
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *c,
|
const uint8_t *c,
|
||||||
size_t clen) ;
|
size_t clen) ;
|
||||||
int aegis256x2_state_decrypt_detached_final(aegis256x2_state *st_,
|
int aegis256x2_state_decrypt_final(aegis256x2_state *st_, const uint8_t *mac, size_t maclen) ;
|
||||||
uint8_t *m,
|
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *mac,
|
|
||||||
size_t maclen) ;
|
|
||||||
void aegis256x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
void aegis256x2_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
||||||
void aegis256x2_encrypt_unauthenticated(uint8_t *c,
|
void aegis256x2_encrypt_unauthenticated(uint8_t *c,
|
||||||
const uint8_t *m,
|
const uint8_t *m,
|
||||||
@@ -527,33 +420,14 @@ void aegis256x4_state_init(aegis256x4_state *st_,
|
|||||||
const uint8_t *k);
|
const uint8_t *k);
|
||||||
int aegis256x4_state_encrypt_update(aegis256x4_state *st_,
|
int aegis256x4_state_encrypt_update(aegis256x4_state *st_,
|
||||||
uint8_t *c,
|
uint8_t *c,
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *m,
|
const uint8_t *m,
|
||||||
size_t mlen);
|
size_t mlen);
|
||||||
int aegis256x4_state_encrypt_detached_final(aegis256x4_state *st_,
|
int aegis256x4_state_encrypt_final(aegis256x4_state *st_, uint8_t *mac, size_t maclen);
|
||||||
uint8_t *c,
|
int aegis256x4_state_decrypt_update(aegis256x4_state *st_,
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
uint8_t *mac,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis256x4_state_encrypt_final(aegis256x4_state *st_,
|
|
||||||
uint8_t *c,
|
|
||||||
size_t clen_max,
|
|
||||||
size_t *written,
|
|
||||||
size_t maclen);
|
|
||||||
int aegis256x4_state_decrypt_detached_update(aegis256x4_state *st_,
|
|
||||||
uint8_t *m,
|
uint8_t *m,
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *c,
|
const uint8_t *c,
|
||||||
size_t clen) ;
|
size_t clen) ;
|
||||||
int aegis256x4_state_decrypt_detached_final(aegis256x4_state *st_,
|
int aegis256x4_state_decrypt_final(aegis256x4_state *st_, const uint8_t *mac, size_t maclen) ;
|
||||||
uint8_t *m,
|
|
||||||
size_t mlen_max,
|
|
||||||
size_t *written,
|
|
||||||
const uint8_t *mac,
|
|
||||||
size_t maclen) ;
|
|
||||||
void aegis256x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
void aegis256x4_stream(uint8_t *out, size_t len, const uint8_t *npub, const uint8_t *k);
|
||||||
void aegis256x4_encrypt_unauthenticated(uint8_t *c,
|
void aegis256x4_encrypt_unauthenticated(uint8_t *c,
|
||||||
const uint8_t *m,
|
const uint8_t *m,
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Python benchmark matching src/test/benchmark.zig for all supported Aegis algorithms.
|
||||||
|
|
||||||
|
It performs two benchmarks with the same parameters as the Zig version:
|
||||||
|
- AEGIS encrypt (attached tag, maclen = MACBYTES)
|
||||||
|
- AEGIS MAC (clone state pattern)
|
||||||
|
|
||||||
|
Output format and throughput units mirror the Zig benchmark (Mb/s).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import secrets
|
||||||
|
import time
|
||||||
|
|
||||||
|
from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
|
||||||
|
|
||||||
|
MSG_LEN = 16384000 # 16 000 KiB
|
||||||
|
ITERATIONS = 100
|
||||||
|
|
||||||
|
|
||||||
|
def bench_encrypt(ciph) -> None:
|
||||||
|
key = ciph.random_key()
|
||||||
|
nonce = ciph.random_nonce()
|
||||||
|
|
||||||
|
# Single buffer, as in Zig: c_out == m buffer, with tag appended
|
||||||
|
maclen = ciph.MACBYTES
|
||||||
|
buf = bytearray(MSG_LEN + maclen)
|
||||||
|
# Initialize buffer with random data
|
||||||
|
buf[:] = secrets.token_bytes(len(buf))
|
||||||
|
|
||||||
|
mview = memoryview(buf)[:MSG_LEN]
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
for _ in range(ITERATIONS):
|
||||||
|
ciph.encrypt(key, nonce, mview, None, maclen=maclen, into=buf)
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
|
||||||
|
# Prevent any unrealistic optimization assumptions
|
||||||
|
_ = buf[0]
|
||||||
|
|
||||||
|
bits = MSG_LEN * ITERATIONS * 8
|
||||||
|
elapsed_s = t1 - t0
|
||||||
|
throughput_mbps = (
|
||||||
|
(bits / (elapsed_s * 1_000_000)) if elapsed_s > 0 else float("inf")
|
||||||
|
)
|
||||||
|
print(f"{ciph.NAME}\t{throughput_mbps:10.2f} Mb/s")
|
||||||
|
|
||||||
|
|
||||||
|
def bench_mac(ciph) -> None:
|
||||||
|
key = ciph.random_key()
|
||||||
|
nonce = ciph.random_nonce()
|
||||||
|
|
||||||
|
buf = bytearray(MSG_LEN)
|
||||||
|
buf[:] = secrets.token_bytes(len(buf))
|
||||||
|
|
||||||
|
mac_out = bytearray(ciph.MACBYTES_LONG)
|
||||||
|
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
for _ in range(ITERATIONS):
|
||||||
|
ciph.mac(key, nonce, buf, maclen=ciph.MACBYTES_LONG, into=mac_out)
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
|
||||||
|
_ = mac_out[0]
|
||||||
|
|
||||||
|
bits = MSG_LEN * ITERATIONS * 8
|
||||||
|
elapsed_s = t1 - t0
|
||||||
|
throughput_mbps = (
|
||||||
|
(bits / (elapsed_s * 1_000_000)) if elapsed_s > 0 else float("inf")
|
||||||
|
)
|
||||||
|
print(f"{ciph.NAME} MAC\t{throughput_mbps:10.2f} Mb/s")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# aegis_init() is called in the loader at import time already
|
||||||
|
# Run encrypt benchmarks in order: 256, 256x2, 256x4, 128l, 128x2, 128x4
|
||||||
|
bench_encrypt(aegis256)
|
||||||
|
bench_encrypt(aegis256x2)
|
||||||
|
bench_encrypt(aegis256x4)
|
||||||
|
bench_encrypt(aegis128l)
|
||||||
|
bench_encrypt(aegis128x2)
|
||||||
|
bench_encrypt(aegis128x4)
|
||||||
|
|
||||||
|
# Run MAC benchmarks in order: 128l, 128x2, 128x4, 256, 256x2, 256x4
|
||||||
|
bench_mac(aegis128l)
|
||||||
|
bench_mac(aegis128x2)
|
||||||
|
bench_mac(aegis128x4)
|
||||||
|
bench_mac(aegis256)
|
||||||
|
bench_mac(aegis256x2)
|
||||||
|
bench_mac(aegis256x4)
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""Utility helpers for aeg.
|
||||||
|
|
||||||
|
Currently provides Python-side aligned allocation helpers that avoid relying
|
||||||
|
on libc/posix_memalign. Memory is owned by Python; C code only borrows it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from ._loader import ffi
|
||||||
|
|
||||||
|
__all__ = ["new_aligned_struct", "aligned_address", "Buffer", "nonce_increment", "wipe"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from collections.abc import Buffer # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
# Fallback for Python < 3.12
|
||||||
|
class Buffer(Protocol):
|
||||||
|
def __buffer__(self, flags: int) -> memoryview: ...
|
||||||
|
|
||||||
|
|
||||||
|
def aligned_address(obj) -> int:
|
||||||
|
"""Return the integer address of the start of a cffi array object."""
|
||||||
|
return int(ffi.cast("uintptr_t", ffi.addressof(obj, 0)))
|
||||||
|
|
||||||
|
|
||||||
|
class StructHolder:
|
||||||
|
"""Proxy object for aligned struct allocation.
|
||||||
|
|
||||||
|
Exposes the aligned pointer as a property and wipes the buffer on deletion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, ptr: object, view: memoryview):
|
||||||
|
self._ptr = ptr
|
||||||
|
self._view = view # Keep memoryview slice and its bytearray alive
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ptr(self) -> object:
|
||||||
|
"""The aligned pointer to the struct."""
|
||||||
|
return self._ptr
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
wipe(self._view)
|
||||||
|
del self._ptr, self._view
|
||||||
|
|
||||||
|
|
||||||
|
def new_aligned_struct(ctype: str, alignment: int) -> StructHolder:
|
||||||
|
"""Allocate memory for one instance of ``ctype`` with requested alignment."""
|
||||||
|
# Allocate backing storage with extra space for alignment
|
||||||
|
size = ffi.sizeof(ctype)
|
||||||
|
view = memoryview(bytearray(size + alignment - 1))
|
||||||
|
# Compute alignment offset from the base address
|
||||||
|
offset = (-aligned_address(ffi.from_buffer(view))) & (alignment - 1)
|
||||||
|
# Slice the memoryview to the aligned region (keeps bytearray alive)
|
||||||
|
view = view[offset : offset + size]
|
||||||
|
return StructHolder(ffi.from_buffer(f"{ctype} *", view), view)
|
||||||
|
|
||||||
|
|
||||||
|
def nonce_increment(nonce: Buffer) -> None:
|
||||||
|
"""Increment the nonce in place using little-endian byte order.
|
||||||
|
|
||||||
|
Useful for generating unique nonces for each consecutive message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nonce: The nonce buffer to increment (modified in place).
|
||||||
|
"""
|
||||||
|
n = memoryview(nonce)
|
||||||
|
for i in range(len(n)):
|
||||||
|
if n[i] < 255:
|
||||||
|
n[i] += 1
|
||||||
|
return
|
||||||
|
n[i] = 0
|
||||||
|
|
||||||
|
|
||||||
|
def wipe(buffer: Buffer) -> None:
|
||||||
|
"""Securely clearing sensitive data from memory. Sets all bytes of the buffer to 0xFF.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buffer: The buffer to wipe (modified in place).
|
||||||
|
"""
|
||||||
|
# This is the fastest method I have found in Python
|
||||||
|
n = memoryview(buffer).cast("B")
|
||||||
|
n[:] = b"\xff" * len(n)
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#! /usr/bin/env python3
|
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def tvdump(topic, tvs):
|
|
||||||
with open(filename(topic), "w") as f:
|
|
||||||
f.write(json.dumps(tvs, indent=2))
|
|
||||||
|
|
||||||
print(json.dumps(tvs, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
def filename(topic):
|
|
||||||
return re.sub(r"[^a-z0-9]+", "-", topic.lower()) + ".json"
|
|
||||||
|
|
||||||
|
|
||||||
header = True
|
|
||||||
in_tv = False
|
|
||||||
tv = {}
|
|
||||||
tvs = []
|
|
||||||
must_fail = False
|
|
||||||
with open("../draft-irtf-cfrg-aegis-aead.md") as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.strip()
|
|
||||||
if line == "":
|
|
||||||
continue
|
|
||||||
if line.startswith("# Test Vectors"):
|
|
||||||
header = False
|
|
||||||
continue
|
|
||||||
if header:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line.startswith("## "):
|
|
||||||
if len(tvs) > 0:
|
|
||||||
tvdump(topic, tvs)
|
|
||||||
topic = line[3:]
|
|
||||||
tv_name = topic
|
|
||||||
tvs = []
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line.startswith("### "):
|
|
||||||
tv_name = line[4:]
|
|
||||||
tv = {"test": tv_name}
|
|
||||||
in_tv = False
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line == "~~~ test-vectors":
|
|
||||||
in_tv = True
|
|
||||||
tv = {"name": tv_name}
|
|
||||||
if must_fail:
|
|
||||||
tv["error"] = "verification failed"
|
|
||||||
must_fail = False
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line == "~~~":
|
|
||||||
tvs.append(tv)
|
|
||||||
in_tv = False
|
|
||||||
current_key = None
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line.find("verification failed") != -1:
|
|
||||||
must_fail = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line == "After initialization:":
|
|
||||||
tv_name = tv_name + " (after initialization)"
|
|
||||||
|
|
||||||
if not in_tv:
|
|
||||||
continue
|
|
||||||
|
|
||||||
parts = line.split(":")
|
|
||||||
if len(parts) == 2:
|
|
||||||
key = parts[0].strip()
|
|
||||||
value = parts[1].strip()
|
|
||||||
if key == "After Update":
|
|
||||||
continue
|
|
||||||
if key in tv:
|
|
||||||
key = key + "_2"
|
|
||||||
tv[key] = value
|
|
||||||
current_key = key
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not current_key:
|
|
||||||
continue
|
|
||||||
|
|
||||||
tv[key] += line.strip()
|
|
||||||
|
|
||||||
tvdump(topic, tvs)
|
|
||||||
+9
-13
@@ -3,7 +3,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pyaegis import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
|
from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
|
||||||
|
|
||||||
from .util import random_split_bytes
|
from .util import random_split_bytes
|
||||||
|
|
||||||
@@ -147,14 +147,12 @@ def test_encrypt_decrypt_incremental(vector):
|
|||||||
expected_tag128 = bytes.fromhex(vector["tag128"])
|
expected_tag128 = bytes.fromhex(vector["tag128"])
|
||||||
|
|
||||||
# Incremental encryption with random chunking
|
# Incremental encryption with random chunking
|
||||||
encryptor = alg.Encryptor(key, nonce, ad)
|
encryptor = alg.Encryptor(key, nonce, ad, maclen=16)
|
||||||
ct_chunks = []
|
ct_chunks = []
|
||||||
for chunk in random_split_bytes(msg):
|
for chunk in random_split_bytes(msg):
|
||||||
ct_result = encryptor.update(chunk)
|
ct_result = encryptor.update(chunk)
|
||||||
ct_chunks.append(bytes(ct_result))
|
ct_chunks.append(bytes(ct_result))
|
||||||
final_output = encryptor.final(maclen=16)
|
computed_mac = bytes(encryptor.final())
|
||||||
ct_chunks.append(bytes(final_output[:-16])) # ciphertext part
|
|
||||||
computed_mac = bytes(final_output[-16:]) # MAC part
|
|
||||||
|
|
||||||
# Combine ciphertext chunks
|
# Combine ciphertext chunks
|
||||||
computed_ct = b"".join(ct_chunks)
|
computed_ct = b"".join(ct_chunks)
|
||||||
@@ -170,7 +168,7 @@ def test_encrypt_decrypt_incremental(vector):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Incremental decryption with different random chunking
|
# Incremental decryption with different random chunking
|
||||||
decryptor = alg.Decryptor(key, nonce, ad)
|
decryptor = alg.Decryptor(key, nonce, ad, maclen=16)
|
||||||
pt_chunks = []
|
pt_chunks = []
|
||||||
for chunk in random_split_bytes(computed_ct):
|
for chunk in random_split_bytes(computed_ct):
|
||||||
pt_chunks.append(bytes(decryptor.update(chunk)))
|
pt_chunks.append(bytes(decryptor.update(chunk)))
|
||||||
@@ -187,14 +185,12 @@ def test_encrypt_decrypt_incremental(vector):
|
|||||||
expected_tag256 = bytes.fromhex(vector["tag256"])
|
expected_tag256 = bytes.fromhex(vector["tag256"])
|
||||||
|
|
||||||
# Incremental encryption with random chunking
|
# Incremental encryption with random chunking
|
||||||
encryptor = alg.Encryptor(key, nonce, ad)
|
encryptor = alg.Encryptor(key, nonce, ad, maclen=32)
|
||||||
ct_chunks = []
|
ct_chunks = []
|
||||||
for chunk in random_split_bytes(msg):
|
for chunk in random_split_bytes(msg):
|
||||||
ct_result = encryptor.update(chunk)
|
ct_result = encryptor.update(chunk)
|
||||||
ct_chunks.append(bytes(ct_result))
|
ct_chunks.append(bytes(ct_result))
|
||||||
final_output = encryptor.final(maclen=32)
|
computed_mac = bytes(encryptor.final())
|
||||||
ct_chunks.append(bytes(final_output[:-32])) # ciphertext part
|
|
||||||
computed_mac = bytes(final_output[-32:]) # MAC part
|
|
||||||
|
|
||||||
# Combine ciphertext chunks
|
# Combine ciphertext chunks
|
||||||
computed_ct = b"".join(ct_chunks)
|
computed_ct = b"".join(ct_chunks)
|
||||||
@@ -210,7 +206,7 @@ def test_encrypt_decrypt_incremental(vector):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Incremental decryption with different random chunking
|
# Incremental decryption with different random chunking
|
||||||
decryptor = alg.Decryptor(key, nonce, ad)
|
decryptor = alg.Decryptor(key, nonce, ad, maclen=32)
|
||||||
pt_chunks = []
|
pt_chunks = []
|
||||||
for chunk in random_split_bytes(computed_ct):
|
for chunk in random_split_bytes(computed_ct):
|
||||||
pt_chunks.append(bytes(decryptor.update(chunk)))
|
pt_chunks.append(bytes(decryptor.update(chunk)))
|
||||||
@@ -229,14 +225,14 @@ def test_encrypt_decrypt_incremental(vector):
|
|||||||
# Test that incremental decryption fails with the provided (invalid) MACs
|
# Test that incremental decryption fails with the provided (invalid) MACs
|
||||||
if "tag128" in vector:
|
if "tag128" in vector:
|
||||||
invalid_mac = bytes.fromhex(vector["tag128"])
|
invalid_mac = bytes.fromhex(vector["tag128"])
|
||||||
decryptor = alg.Decryptor(key, nonce, ad)
|
decryptor = alg.Decryptor(key, nonce, ad, maclen=16)
|
||||||
decryptor.update(ct) # This should succeed
|
decryptor.update(ct) # This should succeed
|
||||||
with pytest.raises(ValueError, match="authentication failed"):
|
with pytest.raises(ValueError, match="authentication failed"):
|
||||||
decryptor.final(invalid_mac)
|
decryptor.final(invalid_mac)
|
||||||
|
|
||||||
if "tag256" in vector:
|
if "tag256" in vector:
|
||||||
invalid_mac = bytes.fromhex(vector["tag256"])
|
invalid_mac = bytes.fromhex(vector["tag256"])
|
||||||
decryptor = alg.Decryptor(key, nonce, ad)
|
decryptor = alg.Decryptor(key, nonce, ad, maclen=32)
|
||||||
decryptor.update(ct) # This should succeed
|
decryptor.update(ct) # This should succeed
|
||||||
with pytest.raises(ValueError, match="authentication failed"):
|
with pytest.raises(ValueError, match="authentication failed"):
|
||||||
decryptor.final(invalid_mac)
|
decryptor.final(invalid_mac)
|
||||||
|
|||||||
+173
-5
@@ -2,10 +2,14 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pyaegis import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
|
|
||||||
|
from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
|
||||||
|
|
||||||
from .util import random_split_bytes
|
from .util import random_split_bytes
|
||||||
|
|
||||||
|
# All AEGIS algorithm modules
|
||||||
|
ALL_ALGORITHMS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4]
|
||||||
|
|
||||||
|
|
||||||
def load_mac_test_vectors():
|
def load_mac_test_vectors():
|
||||||
"""Load MAC test vectors from JSON file."""
|
"""Load MAC test vectors from JSON file."""
|
||||||
@@ -81,10 +85,10 @@ def test_mac_class(vector):
|
|||||||
# Test 128-bit MAC if present
|
# Test 128-bit MAC if present
|
||||||
if "tag128" in vector:
|
if "tag128" in vector:
|
||||||
expected_tag128 = bytes.fromhex(vector["tag128"])
|
expected_tag128 = bytes.fromhex(vector["tag128"])
|
||||||
mac_state = alg.Mac(key, nonce)
|
mac_state = alg.Mac(key, nonce, maclen=16)
|
||||||
for chunk in random_split_bytes(data):
|
for chunk in random_split_bytes(data):
|
||||||
mac_state.update(chunk)
|
mac_state.update(chunk)
|
||||||
computed_tag128 = mac_state.final(maclen=16)
|
computed_tag128 = mac_state.final()
|
||||||
assert computed_tag128 == expected_tag128, (
|
assert computed_tag128 == expected_tag128, (
|
||||||
f"128-bit MAC mismatch for {vector['name']}"
|
f"128-bit MAC mismatch for {vector['name']}"
|
||||||
)
|
)
|
||||||
@@ -92,10 +96,174 @@ def test_mac_class(vector):
|
|||||||
# Test 256-bit MAC if present
|
# Test 256-bit MAC if present
|
||||||
if "tag256" in vector:
|
if "tag256" in vector:
|
||||||
expected_tag256 = bytes.fromhex(vector["tag256"])
|
expected_tag256 = bytes.fromhex(vector["tag256"])
|
||||||
mac_state = alg.Mac(key, nonce)
|
mac_state = alg.Mac(key, nonce, maclen=32)
|
||||||
for chunk in random_split_bytes(data):
|
for chunk in random_split_bytes(data):
|
||||||
mac_state.update(chunk)
|
mac_state.update(chunk)
|
||||||
computed_tag256 = mac_state.final(maclen=32)
|
computed_tag256 = mac_state.final()
|
||||||
assert computed_tag256 == expected_tag256, (
|
assert computed_tag256 == expected_tag256, (
|
||||||
f"256-bit MAC mismatch for {vector['name']}"
|
f"256-bit MAC mismatch for {vector['name']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
|
||||||
|
def test_mac_class_with_digest(vector):
|
||||||
|
"""Test MAC computation using digest() and hexdigest() instead of final()."""
|
||||||
|
alg = get_algorithm_module(vector["name"])
|
||||||
|
|
||||||
|
key = bytes.fromhex(vector["key"])
|
||||||
|
nonce = bytes.fromhex(vector["nonce"])
|
||||||
|
data = bytes.fromhex(vector["data"])
|
||||||
|
|
||||||
|
# Test 128-bit MAC if present
|
||||||
|
if "tag128" in vector:
|
||||||
|
expected_tag128 = bytes.fromhex(vector["tag128"])
|
||||||
|
|
||||||
|
# Test with digest()
|
||||||
|
mac_state = alg.Mac(key, nonce, maclen=16)
|
||||||
|
for chunk in random_split_bytes(data):
|
||||||
|
mac_state.update(chunk)
|
||||||
|
computed_tag128 = mac_state.digest()
|
||||||
|
assert computed_tag128 == expected_tag128, (
|
||||||
|
f"128-bit MAC mismatch for {vector['name']} using digest()"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that digest() can be called multiple times
|
||||||
|
computed_tag128_again = mac_state.digest()
|
||||||
|
assert computed_tag128 == computed_tag128_again, (
|
||||||
|
"digest() should return the same value on repeated calls"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test hexdigest()
|
||||||
|
mac_state2 = alg.Mac(key, nonce, maclen=16)
|
||||||
|
for chunk in random_split_bytes(data):
|
||||||
|
mac_state2.update(chunk)
|
||||||
|
hex_tag = mac_state2.hexdigest()
|
||||||
|
assert hex_tag == expected_tag128.hex(), (
|
||||||
|
f"128-bit MAC hexdigest mismatch for {vector['name']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that hexdigest() can be called multiple times
|
||||||
|
hex_tag_again = mac_state2.hexdigest()
|
||||||
|
assert hex_tag == hex_tag_again, (
|
||||||
|
"hexdigest() should return the same value on repeated calls"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 256-bit MAC if present
|
||||||
|
if "tag256" in vector:
|
||||||
|
expected_tag256 = bytes.fromhex(vector["tag256"])
|
||||||
|
|
||||||
|
# Test with digest()
|
||||||
|
mac_state = alg.Mac(key, nonce, maclen=32)
|
||||||
|
for chunk in random_split_bytes(data):
|
||||||
|
mac_state.update(chunk)
|
||||||
|
computed_tag256 = mac_state.digest()
|
||||||
|
assert computed_tag256 == expected_tag256, (
|
||||||
|
f"256-bit MAC mismatch for {vector['name']} using digest()"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
|
||||||
|
def test_mac_clone(vector):
|
||||||
|
"""Test that cloning a Mac state works correctly."""
|
||||||
|
alg = get_algorithm_module(vector["name"])
|
||||||
|
|
||||||
|
key = bytes.fromhex(vector["key"])
|
||||||
|
nonce = bytes.fromhex(vector["nonce"])
|
||||||
|
data = bytes.fromhex(vector["data"])
|
||||||
|
|
||||||
|
# Test 128-bit MAC if present
|
||||||
|
if "tag128" in vector:
|
||||||
|
expected_tag128 = bytes.fromhex(vector["tag128"])
|
||||||
|
|
||||||
|
mac_state = alg.Mac(key, nonce, maclen=16)
|
||||||
|
for chunk in random_split_bytes(data):
|
||||||
|
mac_state.update(chunk)
|
||||||
|
|
||||||
|
# Clone the state
|
||||||
|
cloned_state = mac_state.clone()
|
||||||
|
|
||||||
|
# Both should produce the same tag
|
||||||
|
tag1 = mac_state.final()
|
||||||
|
tag2 = cloned_state.final()
|
||||||
|
|
||||||
|
assert tag1 == expected_tag128
|
||||||
|
assert tag2 == expected_tag128
|
||||||
|
assert tag1 == tag2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
|
||||||
|
def test_mac_reset(vector):
|
||||||
|
"""Test that resetting a Mac state works correctly."""
|
||||||
|
alg = get_algorithm_module(vector["name"])
|
||||||
|
|
||||||
|
key = bytes.fromhex(vector["key"])
|
||||||
|
nonce = bytes.fromhex(vector["nonce"])
|
||||||
|
data = bytes.fromhex(vector["data"])
|
||||||
|
|
||||||
|
# Test 128-bit MAC if present
|
||||||
|
if "tag128" in vector:
|
||||||
|
expected_tag128 = bytes.fromhex(vector["tag128"])
|
||||||
|
|
||||||
|
mac_state = alg.Mac(key, nonce, maclen=16)
|
||||||
|
for chunk in random_split_bytes(data):
|
||||||
|
mac_state.update(chunk)
|
||||||
|
tag1 = mac_state.final()
|
||||||
|
assert tag1 == expected_tag128
|
||||||
|
|
||||||
|
# Reset and compute again
|
||||||
|
mac_state.reset()
|
||||||
|
for chunk in random_split_bytes(data):
|
||||||
|
mac_state.update(chunk)
|
||||||
|
tag2 = mac_state.final()
|
||||||
|
|
||||||
|
assert tag2 == expected_tag128
|
||||||
|
assert tag1 == tag2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1])
|
||||||
|
def test_mac_reset_after_digest(alg):
|
||||||
|
"""Test that reset() clears the cached digest and allows reuse."""
|
||||||
|
key = alg.random_key()
|
||||||
|
nonce = alg.random_nonce()
|
||||||
|
|
||||||
|
mac_state = alg.Mac(key, nonce)
|
||||||
|
mac_state.update(b"Hello, world!")
|
||||||
|
tag1 = mac_state.digest()
|
||||||
|
|
||||||
|
# After digest(), update should fail
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
mac_state.update(b"More data")
|
||||||
|
|
||||||
|
# Reset should clear the cached digest
|
||||||
|
mac_state.reset()
|
||||||
|
|
||||||
|
# Now we should be able to update again
|
||||||
|
mac_state.update(b"Different data")
|
||||||
|
tag2 = mac_state.digest()
|
||||||
|
|
||||||
|
# Tags should be different since we used different data
|
||||||
|
assert tag1 != tag2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1])
|
||||||
|
def test_mac_clone_preserves_cached_digest(alg):
|
||||||
|
"""Test that cloning preserves the cached digest state."""
|
||||||
|
key = alg.random_key()
|
||||||
|
nonce = alg.random_nonce()
|
||||||
|
|
||||||
|
mac_state = alg.Mac(key, nonce)
|
||||||
|
mac_state.update(b"Hello, world!")
|
||||||
|
tag1 = mac_state.digest()
|
||||||
|
|
||||||
|
# Clone after digest
|
||||||
|
cloned_state = mac_state.clone()
|
||||||
|
|
||||||
|
# Both should return the same cached tag
|
||||||
|
tag2 = cloned_state.digest()
|
||||||
|
assert tag1 == tag2
|
||||||
|
|
||||||
|
# Both should be unable to update
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
mac_state.update(b"More data")
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
cloned_state.update(b"More data")
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
"""Tests for Encryptor and Decryptor finalization behavior.
|
||||||
|
|
||||||
|
This module verifies that Encryptor and Decryptor objects become unusable
|
||||||
|
after calling final(), preventing accidental misuse.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
|
||||||
|
|
||||||
|
# All AEGIS algorithm modules
|
||||||
|
ALL_ALGORITHMS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4]
|
||||||
|
|
||||||
|
|
||||||
|
class TestMacFinalization:
|
||||||
|
"""Test that Mac becomes unusable after final()."""
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1]
|
||||||
|
)
|
||||||
|
def test_update_after_final_raises(self, alg):
|
||||||
|
"""Test that calling update() after final() raises RuntimeError."""
|
||||||
|
key = alg.random_key()
|
||||||
|
nonce = alg.random_nonce()
|
||||||
|
|
||||||
|
mac = alg.Mac(key, nonce)
|
||||||
|
mac.update(b"Hello, world!")
|
||||||
|
mac.final()
|
||||||
|
|
||||||
|
# Attempting to update after final should raise RuntimeError
|
||||||
|
with pytest.raises(RuntimeError, match="Cannot update after final\\(\\)"):
|
||||||
|
mac.update(b"More data")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1]
|
||||||
|
)
|
||||||
|
def test_final_after_final_raises(self, alg):
|
||||||
|
"""Test that calling final() after final() raises RuntimeError."""
|
||||||
|
key = alg.random_key()
|
||||||
|
nonce = alg.random_nonce()
|
||||||
|
|
||||||
|
mac = alg.Mac(key, nonce)
|
||||||
|
mac.update(b"Hello, world!")
|
||||||
|
mac.final()
|
||||||
|
|
||||||
|
# Attempting to call final again should raise RuntimeError
|
||||||
|
with pytest.raises(RuntimeError, match="The MAC can only be calculated once"):
|
||||||
|
mac.final()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1]
|
||||||
|
)
|
||||||
|
def test_digest_after_final_raises(self, alg):
|
||||||
|
"""Test that digest() and hexdigest() raise after final()."""
|
||||||
|
key = alg.random_key()
|
||||||
|
nonce = alg.random_nonce()
|
||||||
|
|
||||||
|
mac = alg.Mac(key, nonce)
|
||||||
|
mac.update(b"Hello, world!")
|
||||||
|
mac.final()
|
||||||
|
|
||||||
|
# digest() should raise after final()
|
||||||
|
with pytest.raises(RuntimeError, match="The MAC can only be calculated once"):
|
||||||
|
mac.digest()
|
||||||
|
|
||||||
|
# hexdigest() should also raise after final()
|
||||||
|
with pytest.raises(RuntimeError, match="The MAC can only be calculated once"):
|
||||||
|
mac.hexdigest()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1]
|
||||||
|
)
|
||||||
|
def test_update_after_digest_raises(self, alg):
|
||||||
|
"""Test that calling update() after digest() raises RuntimeError."""
|
||||||
|
key = alg.random_key()
|
||||||
|
nonce = alg.random_nonce()
|
||||||
|
|
||||||
|
mac = alg.Mac(key, nonce)
|
||||||
|
mac.update(b"Hello, world!")
|
||||||
|
mac.digest()
|
||||||
|
|
||||||
|
# Attempting to update after digest should raise RuntimeError
|
||||||
|
with pytest.raises(RuntimeError, match="Cannot update after final\\(\\)"):
|
||||||
|
mac.update(b"More data")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1]
|
||||||
|
)
|
||||||
|
def test_final_after_digest_raises(self, alg):
|
||||||
|
"""Test that calling final() after digest() raises RuntimeError."""
|
||||||
|
key = alg.random_key()
|
||||||
|
nonce = alg.random_nonce()
|
||||||
|
|
||||||
|
mac = alg.Mac(key, nonce)
|
||||||
|
mac.update(b"Hello, world!")
|
||||||
|
mac.digest()
|
||||||
|
|
||||||
|
# Attempting to call final after digest should raise RuntimeError
|
||||||
|
with pytest.raises(RuntimeError, match="The MAC can only be calculated once"):
|
||||||
|
mac.final()
|
||||||
|
|
||||||
|
|
||||||
|
class TestEncryptorFinalization:
|
||||||
|
"""Test that Encryptor becomes unusable after final()."""
|
||||||
|
|
||||||
|
def test_update_after_final_raises(self):
|
||||||
|
"""Test that calling update() after final() raises RuntimeError."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
|
||||||
|
encryptor = aegis256x4.Encryptor(key, nonce)
|
||||||
|
|
||||||
|
# Encrypt some data and finalize
|
||||||
|
encryptor.update(b"Hello, world!")
|
||||||
|
encryptor.final()
|
||||||
|
|
||||||
|
# Attempting to update after final should raise RuntimeError
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
|
||||||
|
):
|
||||||
|
encryptor.update(b"More data")
|
||||||
|
|
||||||
|
def test_final_after_final_raises(self):
|
||||||
|
"""Test that calling final() after final() raises RuntimeError."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
|
||||||
|
encryptor = aegis256x4.Encryptor(key, nonce)
|
||||||
|
|
||||||
|
# Encrypt some data and finalize
|
||||||
|
encryptor.update(b"Hello, world!")
|
||||||
|
encryptor.final()
|
||||||
|
|
||||||
|
# Attempting to call final again should raise RuntimeError
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match="Cannot call final\\(\\) after final\\(\\)"
|
||||||
|
):
|
||||||
|
encryptor.final()
|
||||||
|
|
||||||
|
def test_update_then_final_after_final_raises(self):
|
||||||
|
"""Test that both update() and final() fail after final()."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
|
||||||
|
encryptor = aegis256x4.Encryptor(key, nonce)
|
||||||
|
|
||||||
|
# Encrypt and finalize
|
||||||
|
encryptor.update(b"Test data")
|
||||||
|
encryptor.final()
|
||||||
|
|
||||||
|
# Both operations should fail
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
|
||||||
|
):
|
||||||
|
encryptor.update(b"More data")
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match="Cannot call final\\(\\) after final\\(\\)"
|
||||||
|
):
|
||||||
|
encryptor.final()
|
||||||
|
|
||||||
|
def test_empty_encryption_finalization(self):
|
||||||
|
"""Test that finalization works correctly with no update() calls."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
|
||||||
|
encryptor = aegis256x4.Encryptor(key, nonce)
|
||||||
|
|
||||||
|
# Finalize without any updates
|
||||||
|
tag = encryptor.final()
|
||||||
|
assert len(tag) == aegis256x4.MACBYTES
|
||||||
|
|
||||||
|
# Should still be unusable after
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
|
||||||
|
):
|
||||||
|
encryptor.update(b"Data")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDecryptorFinalization:
|
||||||
|
"""Test that Decryptor becomes unusable after final()."""
|
||||||
|
|
||||||
|
def test_update_after_final_raises(self):
|
||||||
|
"""Test that calling update() after final() raises RuntimeError."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
message = b"Hello, world!"
|
||||||
|
|
||||||
|
# Encrypt first to get valid ciphertext and tag
|
||||||
|
ct, tag = aegis256x4.encrypt_detached(key, nonce, message)
|
||||||
|
|
||||||
|
# Now test decryption
|
||||||
|
decryptor = aegis256x4.Decryptor(key, nonce)
|
||||||
|
decryptor.update(ct)
|
||||||
|
decryptor.final(tag)
|
||||||
|
|
||||||
|
# Attempting to update after final should raise RuntimeError
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
|
||||||
|
):
|
||||||
|
decryptor.update(b"More ciphertext")
|
||||||
|
|
||||||
|
def test_final_after_final_raises(self):
|
||||||
|
"""Test that calling final() after final() raises RuntimeError."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
message = b"Hello, world!"
|
||||||
|
|
||||||
|
# Encrypt first to get valid ciphertext and tag
|
||||||
|
ct, tag = aegis256x4.encrypt_detached(key, nonce, message)
|
||||||
|
|
||||||
|
# Now test decryption
|
||||||
|
decryptor = aegis256x4.Decryptor(key, nonce)
|
||||||
|
decryptor.update(ct)
|
||||||
|
decryptor.final(tag)
|
||||||
|
|
||||||
|
# Attempting to call final again should raise RuntimeError
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match="Cannot call final\\(\\) after final\\(\\)"
|
||||||
|
):
|
||||||
|
decryptor.final(tag)
|
||||||
|
|
||||||
|
def test_update_then_final_after_final_raises(self):
|
||||||
|
"""Test that both update() and final() fail after final()."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
message = b"Test data"
|
||||||
|
|
||||||
|
# Encrypt first
|
||||||
|
ct, tag = aegis256x4.encrypt_detached(key, nonce, message)
|
||||||
|
|
||||||
|
# Decrypt and finalize
|
||||||
|
decryptor = aegis256x4.Decryptor(key, nonce)
|
||||||
|
decryptor.update(ct)
|
||||||
|
decryptor.final(tag)
|
||||||
|
|
||||||
|
# Both operations should fail
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
|
||||||
|
):
|
||||||
|
decryptor.update(b"More ciphertext")
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match="Cannot call final\\(\\) after final\\(\\)"
|
||||||
|
):
|
||||||
|
decryptor.final(tag)
|
||||||
|
|
||||||
|
def test_empty_decryption_finalization(self):
|
||||||
|
"""Test that finalization works correctly with no update() calls."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
|
||||||
|
# Encrypt empty message
|
||||||
|
ct, tag = aegis256x4.encrypt_detached(key, nonce, b"")
|
||||||
|
|
||||||
|
# Decrypt without any updates
|
||||||
|
decryptor = aegis256x4.Decryptor(key, nonce)
|
||||||
|
decryptor.final(tag) # Should work with empty ciphertext
|
||||||
|
|
||||||
|
# Should still be unusable after
|
||||||
|
with pytest.raises(
|
||||||
|
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
|
||||||
|
):
|
||||||
|
decryptor.update(b"Data")
|
||||||
|
|
||||||
|
def test_failed_verification_still_finalizes(self):
|
||||||
|
"""Test that even if verification fails, the object becomes unusable."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
message = b"Hello, world!"
|
||||||
|
|
||||||
|
# Encrypt first
|
||||||
|
ct, tag = aegis256x4.encrypt_detached(key, nonce, message)
|
||||||
|
|
||||||
|
# Decrypt but use wrong tag
|
||||||
|
decryptor = aegis256x4.Decryptor(key, nonce)
|
||||||
|
decryptor.update(ct)
|
||||||
|
|
||||||
|
# Try to finalize with invalid tag - should raise ValueError
|
||||||
|
bad_tag = bytes(len(tag)) # All zeros
|
||||||
|
with pytest.raises(ValueError, match="authentication failed"):
|
||||||
|
decryptor.final(bad_tag)
|
||||||
|
|
||||||
|
# Object should NOT be finalized on failure - should still be usable
|
||||||
|
# This is a design decision: failed verification shouldn't lock the object
|
||||||
|
# Let's verify current behavior
|
||||||
|
try:
|
||||||
|
decryptor.update(b"test")
|
||||||
|
# If this doesn't raise, the object is still usable after failed verification
|
||||||
|
# This might be the desired behavior
|
||||||
|
except RuntimeError:
|
||||||
|
# If this raises, failed verification also finalizes the object
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestMultipleChunksBeforeFinalization:
|
||||||
|
"""Test that multiple update() calls work before final()."""
|
||||||
|
|
||||||
|
def test_encryptor_multiple_updates(self):
|
||||||
|
"""Test that Encryptor can handle multiple update() calls before final()."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
|
||||||
|
encryptor = aegis256x4.Encryptor(key, nonce)
|
||||||
|
|
||||||
|
# Multiple updates
|
||||||
|
encryptor.update(b"Hello, ")
|
||||||
|
encryptor.update(b"world!")
|
||||||
|
encryptor.update(b" More data.")
|
||||||
|
|
||||||
|
# Should still work
|
||||||
|
tag = encryptor.final()
|
||||||
|
assert len(tag) == aegis256x4.MACBYTES
|
||||||
|
|
||||||
|
# Now unusable
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
encryptor.update(b"More")
|
||||||
|
|
||||||
|
def test_decryptor_multiple_updates(self):
|
||||||
|
"""Test that Decryptor can handle multiple update() calls before final()."""
|
||||||
|
key = aegis256x4.random_key()
|
||||||
|
nonce = aegis256x4.random_nonce()
|
||||||
|
|
||||||
|
# Encrypt in chunks
|
||||||
|
encryptor = aegis256x4.Encryptor(key, nonce)
|
||||||
|
ct1 = encryptor.update(b"Hello, ")
|
||||||
|
ct2 = encryptor.update(b"world!")
|
||||||
|
tag = encryptor.final()
|
||||||
|
|
||||||
|
# Decrypt in chunks
|
||||||
|
decryptor = aegis256x4.Decryptor(key, nonce)
|
||||||
|
decryptor.update(ct1)
|
||||||
|
decryptor.update(ct2)
|
||||||
|
decryptor.final(tag)
|
||||||
|
|
||||||
|
# Now unusable
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
decryptor.update(b"More")
|
||||||
+46
-78
@@ -1,100 +1,68 @@
|
|||||||
"""Custom build backend that builds libaegis with Zig before building the Python package."""
|
"""Custom build backend that builds libaegis with Zig before building the Python package."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from setuptools import build_meta as _orig
|
from setuptools import build_meta
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"build_sdist",
|
||||||
|
"build_wheel",
|
||||||
|
"build_editable",
|
||||||
|
"get_requires_for_build_sdist",
|
||||||
|
"get_requires_for_build_wheel",
|
||||||
|
"prepare_metadata_for_build_wheel",
|
||||||
|
]
|
||||||
|
|
||||||
|
_MACOS_TARGET = "11.0"
|
||||||
|
_prepared = False
|
||||||
|
|
||||||
|
|
||||||
def _check_zig_available():
|
def _prepare():
|
||||||
"""Check if Zig is installed and available."""
|
"""Prepare the build environment and build libaegis."""
|
||||||
|
global _prepared
|
||||||
|
if _prepared:
|
||||||
|
return
|
||||||
|
_prepared = True
|
||||||
|
|
||||||
|
# Set macOS deployment target
|
||||||
|
if sys.platform == "darwin" and "MACOSX_DEPLOYMENT_TARGET" not in os.environ:
|
||||||
|
os.environ["MACOSX_DEPLOYMENT_TARGET"] = _MACOS_TARGET
|
||||||
|
|
||||||
|
# Check Zig is available
|
||||||
if shutil.which("zig") is None:
|
if shutil.which("zig") is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"\n" + "=" * 70 + "\n"
|
"Zig compiler not found. Install from https://ziglang.org/download/"
|
||||||
"ERROR: Zig compiler not found!\n"
|
|
||||||
"\n"
|
|
||||||
"Building pyaegis requires the Zig compiler to build the libaegis\n"
|
|
||||||
"static library. Please install Zig before building this package.\n"
|
|
||||||
"\n"
|
|
||||||
"Installation instructions:\n"
|
|
||||||
" - Visit: https://ziglang.org/download/\n"
|
|
||||||
" - Or use a package manager:\n"
|
|
||||||
" * macOS: brew install zig\n"
|
|
||||||
" * Linux: See https://github.com/ziglang/zig/wiki/Install-Zig-from-a-Package-Manager\n"
|
|
||||||
" * Windows: choco install zig or scoop install zig\n"
|
|
||||||
"\n"
|
|
||||||
"After installing Zig, please try building again.\n" + "=" * 70 + "\n"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Build libaegis
|
||||||
def _build_libaegis():
|
|
||||||
"""Build libaegis static library with Zig."""
|
|
||||||
# Check Zig availability first
|
|
||||||
_check_zig_available()
|
|
||||||
|
|
||||||
libaegis_dir = Path(__file__).parent.parent / "libaegis"
|
libaegis_dir = Path(__file__).parent.parent / "libaegis"
|
||||||
if not libaegis_dir.exists():
|
cmd = ["zig", "build", "-Drelease"]
|
||||||
raise FileNotFoundError(
|
if sys.platform == "darwin":
|
||||||
f"libaegis directory not found at {libaegis_dir}. "
|
arch = {"arm64": "aarch64", "x86_64": "x86_64"}.get(platform.machine())
|
||||||
"Cannot build static library."
|
if arch:
|
||||||
)
|
cmd.append(f"-Dtarget={arch}-macos.{_MACOS_TARGET}")
|
||||||
|
subprocess.run(cmd, cwd=libaegis_dir, check=True)
|
||||||
print("Building libaegis static library with Zig...")
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
["zig", "build", "-Drelease"],
|
|
||||||
cwd=libaegis_dir,
|
|
||||||
check=True,
|
|
||||||
capture_output=False,
|
|
||||||
)
|
|
||||||
print("Successfully built libaegis static library")
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print(
|
|
||||||
f"\nError: Zig build failed with exit code {e.returncode}\n"
|
|
||||||
f"Command: {' '.join(e.cmd)}\n",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
# Expose all the standard build backend hooks
|
build_sdist = build_meta.build_sdist
|
||||||
def get_requires_for_build_wheel(config_settings=None):
|
get_requires_for_build_sdist = build_meta.get_requires_for_build_sdist
|
||||||
"""Return build requirements and ensure libaegis is built first."""
|
get_requires_for_build_wheel = build_meta.get_requires_for_build_wheel
|
||||||
_build_libaegis()
|
prepare_metadata_for_build_wheel = build_meta.prepare_metadata_for_build_wheel
|
||||||
return _orig.get_requires_for_build_wheel(config_settings)
|
|
||||||
|
|
||||||
|
|
||||||
def get_requires_for_build_sdist(config_settings=None):
|
|
||||||
"""Return build requirements for sdist and ensure libaegis is built first."""
|
|
||||||
_build_libaegis()
|
|
||||||
return _orig.get_requires_for_build_sdist(config_settings)
|
|
||||||
|
|
||||||
|
|
||||||
_orig_prepare_metadata_for_build_wheel = _orig.prepare_metadata_for_build_wheel
|
|
||||||
_orig_build_sdist = _orig.build_sdist
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
|
|
||||||
"""Prepare metadata and ensure libaegis is built (some frontends call this early)."""
|
|
||||||
_build_libaegis()
|
|
||||||
return _orig_prepare_metadata_for_build_wheel(metadata_directory, config_settings)
|
|
||||||
|
|
||||||
|
|
||||||
def build_sdist(sdist_directory, config_settings=None):
|
|
||||||
"""Build sdist, building libaegis first so the sdist can include built artifacts if needed."""
|
|
||||||
_build_libaegis()
|
|
||||||
return _orig_build_sdist(sdist_directory, config_settings)
|
|
||||||
|
|
||||||
|
|
||||||
|
# Wheel build hooks - need libaegis built first
|
||||||
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
||||||
"""Build wheel with libaegis built first."""
|
_prepare()
|
||||||
_build_libaegis()
|
return build_meta.build_wheel(wheel_directory, config_settings, metadata_directory)
|
||||||
return _orig.build_wheel(wheel_directory, config_settings, metadata_directory)
|
|
||||||
|
|
||||||
|
|
||||||
def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
|
def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
|
||||||
"""Build editable install with libaegis built first."""
|
_prepare()
|
||||||
_build_libaegis()
|
return build_meta.build_editable(
|
||||||
return _orig.build_editable(wheel_directory, config_settings, metadata_directory)
|
wheel_directory, config_settings, metadata_directory
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Generate CFFI cdef string from libaegis headers.
|
|
||||||
|
|
||||||
This script parses the C header files and extracts function declarations,
|
|
||||||
typedefs, and struct definitions to generate the cdef() string needed by CFFI.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pathlib
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def preprocess_content(content: str) -> str:
|
|
||||||
"""Remove comments, preprocessor directives, and extern "C" blocks."""
|
|
||||||
# Remove multi-line comments
|
|
||||||
content = re.sub(r"/\*.*?\*/", " ", content, flags=re.DOTALL)
|
|
||||||
# Remove line comments
|
|
||||||
content = re.sub(r"//.*$", "", content, flags=re.MULTILINE)
|
|
||||||
# Remove preprocessor directives
|
|
||||||
content = re.sub(r"^\s*#.*$", "", content, flags=re.MULTILINE)
|
|
||||||
# Remove extern "C" blocks
|
|
||||||
content = re.sub(r'extern\s+"C"\s*\{', "", content)
|
|
||||||
content = re.sub(r"(?:^|\n)\s*\}\s*(?:\n|$)", "\n", content, flags=re.MULTILINE)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def clean_declaration(text: str) -> str:
|
|
||||||
"""Clean up a C declaration for CFFI consumption."""
|
|
||||||
# Remove __attribute__(...) with proper nesting
|
|
||||||
while "__attribute__" in text:
|
|
||||||
old = text
|
|
||||||
text = re.sub(r"__attribute__\s*\(\([^()]*\)\)", "", text)
|
|
||||||
if text == old:
|
|
||||||
break
|
|
||||||
|
|
||||||
# For structs with CRYPTO_ALIGN, replace the field with "...;" to make it flexible
|
|
||||||
# This tells CFFI to use the C compiler's alignment instead of calculating it
|
|
||||||
if "CRYPTO_ALIGN" in text and "typedef struct" in text:
|
|
||||||
# Replace "CRYPTO_ALIGN(N) uint8_t opaque[SIZE];" with "...;"
|
|
||||||
text = re.sub(
|
|
||||||
r"CRYPTO_ALIGN\s*\(\s*\d+\s*\)\s+uint8_t\s+opaque\[\d+\];", "...;", text
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# For non-struct declarations, just remove CRYPTO_ALIGN
|
|
||||||
text = re.sub(r"CRYPTO_ALIGN\s*\(\s*\d+\s*\)", "", text)
|
|
||||||
|
|
||||||
# Normalize whitespace but preserve structure
|
|
||||||
lines = []
|
|
||||||
for line in text.split("\n"):
|
|
||||||
line = re.sub(r"\s+", " ", line).strip()
|
|
||||||
if line:
|
|
||||||
lines.append(line)
|
|
||||||
|
|
||||||
return " ".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def extract_declarations(header_path: pathlib.Path) -> list[str]:
|
|
||||||
"""Extract function declarations and typedefs from a header file."""
|
|
||||||
content = header_path.read_text(encoding="utf-8")
|
|
||||||
content = preprocess_content(content)
|
|
||||||
declarations = []
|
|
||||||
|
|
||||||
# Extract typedefs (including structs)
|
|
||||||
typedef_pattern = r"typedef\s+struct\s+\w+\s*\{[^}]+\}\s*\w+\s*;"
|
|
||||||
for match in re.finditer(typedef_pattern, content, re.DOTALL):
|
|
||||||
decl = clean_declaration(match.group(0))
|
|
||||||
if decl:
|
|
||||||
declarations.append(decl)
|
|
||||||
|
|
||||||
# Extract function declarations - more permissive pattern
|
|
||||||
func_pattern = r"((?:const\s+)?(?:int|void|size_t)\s+\w+\s*\([^;]+?\)\s*;)"
|
|
||||||
for match in re.finditer(func_pattern, content, re.DOTALL):
|
|
||||||
decl = clean_declaration(match.group(0))
|
|
||||||
if decl and "aegis" in decl.lower():
|
|
||||||
declarations.append(decl)
|
|
||||||
|
|
||||||
return declarations
|
|
||||||
|
|
||||||
|
|
||||||
def format_declaration(decl: str, max_width: int = 100) -> str:
|
|
||||||
"""Format a declaration for readability, with intelligent line breaking."""
|
|
||||||
# If it's short enough, return as-is
|
|
||||||
if len(decl) <= max_width:
|
|
||||||
return decl
|
|
||||||
|
|
||||||
# For function declarations, try to break at parameter boundaries
|
|
||||||
if "(" in decl and ")" in decl:
|
|
||||||
# Find the function name and opening paren
|
|
||||||
match = re.match(r"(.*?\s+\w+\s*)\((.*)\)(.*)", decl)
|
|
||||||
if match:
|
|
||||||
prefix, params, suffix = match.groups()
|
|
||||||
# Break parameters if they're too long
|
|
||||||
if len(prefix) + len(params) + 2 > max_width:
|
|
||||||
# Split parameters
|
|
||||||
param_list = [p.strip() for p in params.split(",")]
|
|
||||||
if len(param_list) > 1:
|
|
||||||
formatted_params = (",\n" + " " * (len(prefix) + 1)).join(
|
|
||||||
param_list
|
|
||||||
)
|
|
||||||
return f"{prefix}({formatted_params}){suffix}"
|
|
||||||
|
|
||||||
return decl
|
|
||||||
|
|
||||||
|
|
||||||
def generate_cdef(include_dir: pathlib.Path) -> str:
|
|
||||||
"""Generate the complete CFFI cdef string from all aegis headers."""
|
|
||||||
|
|
||||||
lines = [
|
|
||||||
"/* This file is generated with tools/gen_cdef.py. Do not edit. */",
|
|
||||||
"",
|
|
||||||
"typedef unsigned char uint8_t;",
|
|
||||||
"typedef unsigned long size_t;",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Header files in order, skipping aegis.h as it might be included elsewhere
|
|
||||||
headers = [
|
|
||||||
"aegis.h",
|
|
||||||
"aegis128l.h",
|
|
||||||
"aegis128x2.h",
|
|
||||||
"aegis128x4.h",
|
|
||||||
"aegis256.h",
|
|
||||||
"aegis256x2.h",
|
|
||||||
"aegis256x4.h",
|
|
||||||
]
|
|
||||||
|
|
||||||
for header_name in headers:
|
|
||||||
header_path = include_dir / header_name
|
|
||||||
if not header_path.exists():
|
|
||||||
print(f"Warning: {header_name} not found", file=sys.stderr)
|
|
||||||
continue
|
|
||||||
|
|
||||||
lines.append(f"/* {header_name} */")
|
|
||||||
declarations = extract_declarations(header_path)
|
|
||||||
|
|
||||||
for decl in declarations:
|
|
||||||
formatted = format_declaration(decl)
|
|
||||||
lines.append(formatted)
|
|
||||||
|
|
||||||
lines.append("")
|
|
||||||
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
# Find the include directory
|
|
||||||
root = pathlib.Path(__file__).parent.parent
|
|
||||||
include_dir = root / "libaegis" / "src" / "include"
|
|
||||||
|
|
||||||
if not include_dir.exists():
|
|
||||||
print(f"Include directory not found: {include_dir}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
cdef_string = generate_cdef(include_dir)
|
|
||||||
|
|
||||||
# Write to a file in the pyaegis directory
|
|
||||||
output_dir = root / "pyaegis"
|
|
||||||
output_dir.mkdir(exist_ok=True)
|
|
||||||
output_path = output_dir / "aegis_cdef.h"
|
|
||||||
output_path.write_text(cdef_string, encoding="utf-8")
|
|
||||||
print(f"Generated: {output_path}", file=sys.stderr)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Regenerate aegis*.py modules from the canonical template aegis256x4.py.
|
|
||||||
|
|
||||||
Changes per variant:
|
|
||||||
- Replace module name (aegis256x4 -> target)
|
|
||||||
- Replace label (AEGIS-256X4 -> target label like AEGIS-128L)
|
|
||||||
- Replace only the ALIGNMENT = <int> value
|
|
||||||
- Replace only the RATE = <int> value
|
|
||||||
|
|
||||||
We do not touch alloc_aligned(...) calls or any code formatting. Blank lines
|
|
||||||
after ALIGNMENT are preserved.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pathlib
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# Template and target locations
|
|
||||||
ROOT = pathlib.Path(__file__).parent.parent
|
|
||||||
AEGIS_DIR = ROOT / "pyaegis"
|
|
||||||
TEMPLATE = AEGIS_DIR / "aegis256x4.py"
|
|
||||||
|
|
||||||
# Variants to generate (template excluded) and their ALIGNMENT values
|
|
||||||
VARIANT_ALIGN = {
|
|
||||||
"aegis256": 16,
|
|
||||||
"aegis256x2": 32,
|
|
||||||
"aegis256x4": 64,
|
|
||||||
"aegis128l": 32,
|
|
||||||
"aegis128x2": 64,
|
|
||||||
"aegis128x4": 64,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Variants and their RATE values
|
|
||||||
VARIANT_RATE = {
|
|
||||||
"aegis256": 16,
|
|
||||||
"aegis256x2": 32,
|
|
||||||
"aegis256x4": 64,
|
|
||||||
"aegis128l": 32,
|
|
||||||
"aegis128x2": 64,
|
|
||||||
"aegis128x4": 128,
|
|
||||||
}
|
|
||||||
|
|
||||||
TEMPLATE_NAME = "aegis256x4"
|
|
||||||
TEMPLATE_LABEL = "AEGIS-256X4"
|
|
||||||
|
|
||||||
ALIGNMENT_LINE_RE = re.compile(r"^(ALIGNMENT\s*=\s*)(\d+)(\s*)$", re.MULTILINE)
|
|
||||||
RATE_LINE_RE = re.compile(r"^(RATE\s*=\s*)(\d+)(\s*)$", re.MULTILINE)
|
|
||||||
|
|
||||||
|
|
||||||
def set_alignment_only(text: str, value: int) -> str:
|
|
||||||
"""Replace only the numeric ALIGNMENT value, preserving surrounding whitespace and lines.
|
|
||||||
|
|
||||||
This preserves any empty lines following the ALIGNMENT assignment because
|
|
||||||
the line ending is not part of the match; we keep any trailing spaces too.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _sub(m: re.Match[str]) -> str:
|
|
||||||
prefix, _num, suffix = m.group(1), m.group(2), m.group(3)
|
|
||||||
return f"{prefix}{value}{suffix}"
|
|
||||||
|
|
||||||
return ALIGNMENT_LINE_RE.sub(_sub, text)
|
|
||||||
|
|
||||||
|
|
||||||
def set_rate_only(text: str, value: int) -> str:
|
|
||||||
"""Replace only the numeric RATE value, preserving surrounding whitespace and lines.
|
|
||||||
|
|
||||||
This preserves any empty lines following the RATE assignment because
|
|
||||||
the line ending is not part of the match; we keep any trailing spaces too.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _sub(m: re.Match[str]) -> str:
|
|
||||||
prefix, _num, suffix = m.group(1), m.group(2), m.group(3)
|
|
||||||
return f"{prefix}{value}{suffix}"
|
|
||||||
|
|
||||||
return RATE_LINE_RE.sub(_sub, text)
|
|
||||||
|
|
||||||
|
|
||||||
def algo_label(name: str) -> str:
|
|
||||||
"""Return the canonical label like AEGIS-256X4 for a module name like aegis256x4."""
|
|
||||||
if not name.startswith("aegis"):
|
|
||||||
raise ValueError(f"Unexpected algorithm name: {name}")
|
|
||||||
return "AEGIS-" + name[5:].upper()
|
|
||||||
|
|
||||||
|
|
||||||
def generate_variant(template_src: str, variant: str) -> str:
|
|
||||||
# 1) replace lowercase template name
|
|
||||||
s = template_src.replace(TEMPLATE_NAME, variant)
|
|
||||||
# 2) replace uppercase label
|
|
||||||
s = s.replace(TEMPLATE_LABEL, algo_label(variant))
|
|
||||||
# 3) set ALIGNMENT constant value using fallback map
|
|
||||||
align_value = VARIANT_ALIGN.get(variant, 64)
|
|
||||||
s = set_alignment_only(s, align_value)
|
|
||||||
# 4) set RATE constant value using fallback map
|
|
||||||
rate_value = VARIANT_RATE.get(variant, 64)
|
|
||||||
s = set_rate_only(s, rate_value)
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
if not TEMPLATE.exists():
|
|
||||||
print(f"Template not found: {TEMPLATE}", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
template_src = TEMPLATE.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
# Safety: ensure we are working from an up-to-date template that contains expected tokens
|
|
||||||
if TEMPLATE_NAME not in template_src or TEMPLATE_LABEL not in template_src:
|
|
||||||
print(
|
|
||||||
"Template file does not contain expected identifiers; aborting.",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return 3
|
|
||||||
|
|
||||||
wrote = []
|
|
||||||
for variant in VARIANT_ALIGN.keys():
|
|
||||||
# Skip the template itself; recreate all other modules
|
|
||||||
if variant == TEMPLATE_NAME:
|
|
||||||
continue
|
|
||||||
dst = AEGIS_DIR / f"{variant}.py"
|
|
||||||
content = generate_variant(template_src, variant)
|
|
||||||
dst.write_text(content, encoding="utf-8")
|
|
||||||
wrote.append(dst.relative_to(ROOT))
|
|
||||||
|
|
||||||
print("Generated modules:")
|
|
||||||
for p in wrote:
|
|
||||||
print(" -", p)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
Executable
+362
@@ -0,0 +1,362 @@
|
|||||||
|
#!/usr/bin/env -S uv run
|
||||||
|
"""Generate CFFI cdef and Python modules from libaegis C sources."""
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess_content(content: str) -> str:
|
||||||
|
content = re.sub(r"/\*.*?\*/", " ", content, flags=re.DOTALL)
|
||||||
|
content = re.sub(r"//.*$", "", content, flags=re.MULTILINE)
|
||||||
|
content = re.sub(r"^\s*#.*$", "", content, flags=re.MULTILINE)
|
||||||
|
content = re.sub(r'extern\s+"C"\s*\{', "", content)
|
||||||
|
content = re.sub(r"(?:^|\n)\s*\}\s*(?:\n|$)", "\n", content, flags=re.MULTILINE)
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def clean_declaration(text: str) -> str:
|
||||||
|
while "__attribute__" in text:
|
||||||
|
old = text
|
||||||
|
text = re.sub(r"__attribute__\s*\(\([^()]*\)\)", "", text)
|
||||||
|
if text == old:
|
||||||
|
break
|
||||||
|
|
||||||
|
if "CRYPTO_ALIGN" in text and "typedef struct" in text:
|
||||||
|
text = re.sub(
|
||||||
|
r"CRYPTO_ALIGN\s*\(\s*\d+\s*\)\s+uint8_t\s+opaque\[\d+\];", "...;", text
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
text = re.sub(r"CRYPTO_ALIGN\s*\(\s*\d+\s*\)", "", text)
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
re.sub(r"\s+", " ", line).strip() for line in text.split("\n") if line.strip()
|
||||||
|
]
|
||||||
|
return " ".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_declarations(header_path: pathlib.Path) -> list[str]:
|
||||||
|
content = preprocess_content(header_path.read_text(encoding="utf-8"))
|
||||||
|
declarations = []
|
||||||
|
|
||||||
|
typedef_pattern = r"typedef\s+struct\s+\w+\s*\{[^}]+\}\s*\w+\s*;"
|
||||||
|
for match in re.finditer(typedef_pattern, content, re.DOTALL):
|
||||||
|
if decl := clean_declaration(match.group(0)):
|
||||||
|
declarations.append(decl)
|
||||||
|
|
||||||
|
func_pattern = r"((?:const\s+)?(?:int|void|size_t)\s+\w+\s*\([^;]+?\)\s*;)"
|
||||||
|
for match in re.finditer(func_pattern, content, re.DOTALL):
|
||||||
|
if (decl := clean_declaration(match.group(0))) and "aegis" in decl.lower():
|
||||||
|
declarations.append(decl)
|
||||||
|
|
||||||
|
return declarations
|
||||||
|
|
||||||
|
|
||||||
|
def format_declaration(decl: str, max_width: int = 100) -> str:
|
||||||
|
if len(decl) <= max_width:
|
||||||
|
return decl
|
||||||
|
|
||||||
|
if "(" in decl and ")" in decl:
|
||||||
|
if match := re.match(r"(.*?\s+\w+\s*)\((.*)\)(.*)", decl):
|
||||||
|
prefix, params, suffix = match.groups()
|
||||||
|
if len(prefix) + len(params) + 2 > max_width:
|
||||||
|
param_list = [p.strip() for p in params.split(",")]
|
||||||
|
if len(param_list) > 1:
|
||||||
|
formatted_params = (",\n" + " " * (len(prefix) + 1)).join(
|
||||||
|
param_list
|
||||||
|
)
|
||||||
|
return f"{prefix}({formatted_params}){suffix}"
|
||||||
|
|
||||||
|
return decl
|
||||||
|
|
||||||
|
|
||||||
|
def generate_cdef(include_dir: pathlib.Path) -> str:
|
||||||
|
lines = [
|
||||||
|
"/* This file is generated with tools/generate.py. Do not edit. */",
|
||||||
|
"",
|
||||||
|
"typedef unsigned char uint8_t;",
|
||||||
|
"typedef unsigned long size_t;",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
headers = [
|
||||||
|
"aegis.h",
|
||||||
|
"aegis128l.h",
|
||||||
|
"aegis128x2.h",
|
||||||
|
"aegis128x4.h",
|
||||||
|
"aegis256.h",
|
||||||
|
"aegis256x2.h",
|
||||||
|
"aegis256x4.h",
|
||||||
|
]
|
||||||
|
|
||||||
|
for header_name in headers:
|
||||||
|
header_path = include_dir / header_name
|
||||||
|
if not header_path.exists():
|
||||||
|
print(f"Warning: {header_name} not found", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
lines.append(f"/* {header_name} */")
|
||||||
|
for decl in extract_declarations(header_path):
|
||||||
|
lines.append(format_declaration(decl))
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_constants(
|
||||||
|
common_h_path: pathlib.Path, header_path: pathlib.Path
|
||||||
|
) -> Dict[str, int]:
|
||||||
|
"""Extract constants from common.h (ALIGNMENT, RATE) and main header (KEYBYTES, NPUBBYTES, ABYTES_*)."""
|
||||||
|
constants = {}
|
||||||
|
|
||||||
|
# Extract from common.h
|
||||||
|
common_content = common_h_path.read_text(encoding="utf-8")
|
||||||
|
align_match = re.search(
|
||||||
|
r"^\s*#define\s+ALIGNMENT\s+(\d+)", common_content, re.MULTILINE
|
||||||
|
)
|
||||||
|
rate_match = re.search(r"^\s*#define\s+RATE\s+(\d+)", common_content, re.MULTILINE)
|
||||||
|
|
||||||
|
if not align_match or not rate_match:
|
||||||
|
raise ValueError(
|
||||||
|
f"Could not extract ALIGNMENT and/or RATE from {common_h_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
constants["ALIGNMENT"] = int(align_match.group(1))
|
||||||
|
constants["RATE"] = int(rate_match.group(1))
|
||||||
|
|
||||||
|
# Extract from main header
|
||||||
|
header_content = header_path.read_text(encoding="utf-8")
|
||||||
|
variant = header_path.stem # e.g., "aegis256x4"
|
||||||
|
|
||||||
|
for const_name in ["KEYBYTES", "NPUBBYTES", "ABYTES_MIN", "ABYTES_MAX"]:
|
||||||
|
pattern = rf"^\s*#define\s+{variant}_{const_name}\s+(\d+)"
|
||||||
|
match = re.search(pattern, header_content, re.MULTILINE)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Could not extract {const_name} from {header_path}")
|
||||||
|
constants[const_name] = int(match.group(1))
|
||||||
|
|
||||||
|
return constants
|
||||||
|
|
||||||
|
|
||||||
|
def extract_all_constants(
|
||||||
|
libaegis_src_dir: pathlib.Path, include_dir: pathlib.Path
|
||||||
|
) -> Dict[str, Dict[str, int]]:
|
||||||
|
variants = [
|
||||||
|
"aegis128l",
|
||||||
|
"aegis128x2",
|
||||||
|
"aegis128x4",
|
||||||
|
"aegis256",
|
||||||
|
"aegis256x2",
|
||||||
|
"aegis256x4",
|
||||||
|
]
|
||||||
|
constants = {}
|
||||||
|
|
||||||
|
for variant in variants:
|
||||||
|
common_h = libaegis_src_dir / variant / f"{variant}_common.h"
|
||||||
|
header_h = include_dir / f"{variant}.h"
|
||||||
|
|
||||||
|
if not common_h.exists():
|
||||||
|
print(f"Warning: {common_h} not found, skipping {variant}", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not header_h.exists():
|
||||||
|
print(f"Warning: {header_h} not found, skipping {variant}", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
constants[variant] = extract_constants(common_h, header_h)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error extracting constants from {variant}: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
return constants
|
||||||
|
|
||||||
|
|
||||||
|
ALIGNMENT_RE = re.compile(r"^(ALIGNMENT\s*=\s*)(\d+)(\s*)$", re.MULTILINE)
|
||||||
|
RATE_RE = re.compile(r"^(RATE\s*=\s*)(\d+)(\s*)$", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_constant(pattern: re.Pattern, text: str, value: int) -> str:
|
||||||
|
return pattern.sub(lambda m: f"{m.group(1)}{value}{m.group(3)}", text)
|
||||||
|
|
||||||
|
|
||||||
|
def algo_label(name: str) -> str:
|
||||||
|
return "AEGIS-" + name[5:].upper()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_variant(template_src: str, variant: str, constants: Dict[str, int]) -> str:
|
||||||
|
"""Generate a variant module from the template with substituted constants."""
|
||||||
|
s = template_src.replace("aegis256x4", variant).replace(
|
||||||
|
"AEGIS-256X4", algo_label(variant)
|
||||||
|
)
|
||||||
|
# Fix the comment to reference the template, not the variant itself
|
||||||
|
s = re.sub(
|
||||||
|
r"# All modules are generated from \w+\.py by tools/generate\.py!",
|
||||||
|
"# All modules are generated from aegis256x4.py by tools/generate.py!",
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
s = replace_constant(ALIGNMENT_RE, s, constants["ALIGNMENT"])
|
||||||
|
s = replace_constant(RATE_RE, s, constants["RATE"])
|
||||||
|
|
||||||
|
# Replace the constant assignments
|
||||||
|
s = re.sub(r"KEYBYTES = \d+", f"KEYBYTES = {constants['KEYBYTES']}", s)
|
||||||
|
s = re.sub(
|
||||||
|
r"NONCEBYTES = \d+",
|
||||||
|
f"NONCEBYTES = {constants['NPUBBYTES']}",
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
s = re.sub(
|
||||||
|
r"MACBYTES = \d+",
|
||||||
|
f"MACBYTES = {constants['ABYTES_MIN']}",
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
s = re.sub(
|
||||||
|
r"MACBYTES_LONG = \d+",
|
||||||
|
f"MACBYTES_LONG = {constants['ABYTES_MAX']}",
|
||||||
|
s,
|
||||||
|
)
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def generate_python_modules(
|
||||||
|
template_path: pathlib.Path,
|
||||||
|
output_dir: pathlib.Path,
|
||||||
|
constants: Dict[str, Dict[str, int]],
|
||||||
|
) -> Tuple[list[pathlib.Path], list[pathlib.Path]]:
|
||||||
|
if not template_path.exists():
|
||||||
|
raise FileNotFoundError(f"Template not found: {template_path}")
|
||||||
|
|
||||||
|
template_src = template_path.read_text(encoding="utf-8")
|
||||||
|
if "aegis256x4" not in template_src or "AEGIS-256X4" not in template_src:
|
||||||
|
raise ValueError("Template file does not contain expected identifiers")
|
||||||
|
|
||||||
|
updated = []
|
||||||
|
unchanged = []
|
||||||
|
for variant, const_dict in constants.items():
|
||||||
|
dst = output_dir / f"{variant}.py"
|
||||||
|
if variant == "aegis256x4":
|
||||||
|
# Update template in place with its own constants
|
||||||
|
new_content = replace_constant(
|
||||||
|
ALIGNMENT_RE, template_src, const_dict["ALIGNMENT"]
|
||||||
|
)
|
||||||
|
new_content = replace_constant(RATE_RE, new_content, const_dict["RATE"])
|
||||||
|
# Replace the constant assignments for the template itself
|
||||||
|
new_content = re.sub(
|
||||||
|
r"KEYBYTES = \d+",
|
||||||
|
f"KEYBYTES = {const_dict['KEYBYTES']}",
|
||||||
|
new_content,
|
||||||
|
)
|
||||||
|
new_content = re.sub(
|
||||||
|
r"NONCEBYTES = \d+",
|
||||||
|
f"NONCEBYTES = {const_dict['NPUBBYTES']}",
|
||||||
|
new_content,
|
||||||
|
)
|
||||||
|
new_content = re.sub(
|
||||||
|
r"MACBYTES = \d+",
|
||||||
|
f"MACBYTES = {const_dict['ABYTES_MIN']}",
|
||||||
|
new_content,
|
||||||
|
)
|
||||||
|
new_content = re.sub(
|
||||||
|
r"MACBYTES_LONG = \d+",
|
||||||
|
f"MACBYTES_LONG = {const_dict['ABYTES_MAX']}",
|
||||||
|
new_content,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
new_content = generate_variant(template_src, variant, const_dict)
|
||||||
|
|
||||||
|
if dst.exists() and dst.read_text(encoding="utf-8") == new_content:
|
||||||
|
unchanged.append(dst)
|
||||||
|
else:
|
||||||
|
dst.write_bytes(new_content.encode())
|
||||||
|
updated.append(dst)
|
||||||
|
|
||||||
|
return updated, unchanged
|
||||||
|
|
||||||
|
|
||||||
|
def generate_ciphers_module(constants: Dict[str, Dict[str, int]]) -> str:
|
||||||
|
labels = [algo_label(variant) for variant in constants]
|
||||||
|
literal_items = ", ".join(f'"{label}"' for label in labels)
|
||||||
|
lines = [
|
||||||
|
"# This file is generated by tools/generate.py. Do not edit.",
|
||||||
|
"from typing import Literal",
|
||||||
|
"",
|
||||||
|
f"CipherName = Literal[{literal_items}]",
|
||||||
|
"",
|
||||||
|
"CIPHERS: dict[CipherName, str] = {",
|
||||||
|
]
|
||||||
|
for variant in constants:
|
||||||
|
lines.append(f' "{algo_label(variant)}": "{variant}",')
|
||||||
|
lines.append("}")
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
root = pathlib.Path(__file__).parent.parent
|
||||||
|
libaegis_src_dir = root / "libaegis" / "src"
|
||||||
|
include_dir = libaegis_src_dir / "include"
|
||||||
|
pyaegis_dir = root / "src" / "aeg"
|
||||||
|
|
||||||
|
if not include_dir.exists():
|
||||||
|
print(f"Include directory not found: {include_dir}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if not libaegis_src_dir.exists():
|
||||||
|
print(f"Source directory not found: {libaegis_src_dir}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("Step 1: Extracting constants from C sources...", file=sys.stderr)
|
||||||
|
constants = extract_all_constants(libaegis_src_dir, include_dir)
|
||||||
|
if not constants:
|
||||||
|
print("Error: No constants extracted", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print("Step 2: Generating CFFI cdef header...", file=sys.stderr)
|
||||||
|
pyaegis_dir.mkdir(exist_ok=True)
|
||||||
|
cdef_path = pyaegis_dir / "aegis_cdef.h"
|
||||||
|
cdef_content = generate_cdef(include_dir)
|
||||||
|
|
||||||
|
if cdef_path.exists() and cdef_path.read_text(encoding="utf-8") == cdef_content:
|
||||||
|
print(f" - No changes to {cdef_path}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
cdef_path.write_bytes(cdef_content.encode())
|
||||||
|
print(f" - Updated {cdef_path}", file=sys.stderr)
|
||||||
|
|
||||||
|
print("Step 3: Generating _ciphers.py...", file=sys.stderr)
|
||||||
|
ciphers_path = pyaegis_dir / "_ciphers.py"
|
||||||
|
ciphers_content = generate_ciphers_module(constants)
|
||||||
|
|
||||||
|
if (
|
||||||
|
ciphers_path.exists()
|
||||||
|
and ciphers_path.read_text(encoding="utf-8") == ciphers_content
|
||||||
|
):
|
||||||
|
print(f" - No changes to {ciphers_path.name}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
ciphers_path.write_bytes(ciphers_content.encode())
|
||||||
|
print(f" - Updated {ciphers_path.name}", file=sys.stderr)
|
||||||
|
|
||||||
|
print("Step 4: Generating Python modules...", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
updated, unchanged = generate_python_modules(
|
||||||
|
pyaegis_dir / "aegis256x4.py", pyaegis_dir, constants
|
||||||
|
)
|
||||||
|
if updated:
|
||||||
|
for p in updated:
|
||||||
|
print(f" - {p.relative_to(root)}", file=sys.stderr)
|
||||||
|
if unchanged:
|
||||||
|
print(
|
||||||
|
" - No changes to",
|
||||||
|
f"{len(unchanged)} modules"
|
||||||
|
if len(unchanged) > 1
|
||||||
|
else unchanged[0].name,
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating Python modules: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Executable
+484
@@ -0,0 +1,484 @@
|
|||||||
|
#!/usr/bin/env -S uv run
|
||||||
|
"""Build wheels for all supported Python versions using uv."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from packaging.version import Version
|
||||||
|
|
||||||
|
# Import generate module from same directory
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
import generate
|
||||||
|
|
||||||
|
# Minimum macOS deployment target for compatibility
|
||||||
|
MACOS_DEPLOYMENT_TARGET = "11.0"
|
||||||
|
|
||||||
|
# ABI3 wheel: built once, works for all GIL-enabled Python versions
|
||||||
|
# We use a recent Python to build since it doesn't affect the wheel compatibility
|
||||||
|
ABI3_BUILD_VERSION = "3.14+gil"
|
||||||
|
|
||||||
|
# All GIL-enabled Python versions covered by the ABI3 wheel
|
||||||
|
ABI3_COVERED_VERSIONS = [
|
||||||
|
"3.10",
|
||||||
|
"3.11",
|
||||||
|
"3.12",
|
||||||
|
"3.13+gil",
|
||||||
|
"3.14+gil",
|
||||||
|
"3.15+gil",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Non-ABI3 wheels: each needs its own build (free-threaded and PyPy)
|
||||||
|
NON_ABI3_VERSIONS = [
|
||||||
|
"3.14t",
|
||||||
|
"3.15t",
|
||||||
|
"pypy3.10",
|
||||||
|
"pypy3.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
# All versions for testing and benchmarking
|
||||||
|
ALL_PYTHON_VERSIONS = ABI3_COVERED_VERSIONS + NON_ABI3_VERSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def get_version_from_scm():
|
||||||
|
"""Get version from setuptools-scm (git tags)."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["uv", "run", "-m", "setuptools_scm"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
cwd=Path(__file__).parent.parent,
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"✗ Error getting version from setuptools-scm: {e}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_release_version(version):
|
||||||
|
"""Check if version is a clean release (no dev/post/local identifiers)."""
|
||||||
|
# A release version is just x.y.z with optional alpha/beta/rc suffixes
|
||||||
|
# No +local or .devN or .postN
|
||||||
|
if not version:
|
||||||
|
return False
|
||||||
|
return not any(marker in version for marker in ["+", ".dev", ".post"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_version(current_version):
|
||||||
|
"""Get the next release version from a dev version."""
|
||||||
|
# Parse base version (strips dev/local parts)
|
||||||
|
try:
|
||||||
|
v = Version(current_version)
|
||||||
|
return f"{v.major}.{v.minor}.{v.micro}"
|
||||||
|
except Exception:
|
||||||
|
return current_version
|
||||||
|
|
||||||
|
|
||||||
|
def is_working_copy_clean():
|
||||||
|
"""Check if git working copy is clean."""
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "status", "--porcelain"], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
return result.returncode == 0 and not result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def make_release_message(version):
|
||||||
|
"""Generate message for making a release."""
|
||||||
|
next_version = get_next_version(version)
|
||||||
|
is_clean = is_working_copy_clean()
|
||||||
|
|
||||||
|
msg = "\n⚠️ This is not a clean release version; upload to PyPI skipped.\n\n"
|
||||||
|
msg += f"To create a release (e.g. {next_version}) and upload to PyPI:\n"
|
||||||
|
|
||||||
|
if not is_clean:
|
||||||
|
msg += " 1. Add and commit changes on the working copy\n"
|
||||||
|
msg += f" 2. Tag the commit: git tag v{next_version}\n"
|
||||||
|
msg += " 3. Run this script again\n"
|
||||||
|
msg += f" 4. Push the tag: git push origin v{next_version}\n"
|
||||||
|
else:
|
||||||
|
msg += f" 1. Tag the current commit: git tag v{next_version}\n"
|
||||||
|
msg += " 2. Run this script again\n"
|
||||||
|
msg += f" 3. Push the tag: git push origin v{next_version}\n"
|
||||||
|
|
||||||
|
msg += (
|
||||||
|
f"\nIf the build didn't work, delete the tag with git tag -d v{next_version}\n"
|
||||||
|
)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(cmd, description=None, env=None):
|
||||||
|
"""Run a command and handle errors. If description is None, only print the command."""
|
||||||
|
if description:
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print(f"{description}")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
print(f">>> {' '.join(cmd)}")
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=True, env=env)
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"✗ Command failed with exit code {e.returncode}", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_build_env():
|
||||||
|
"""Get environment variables for building wheels."""
|
||||||
|
env = os.environ.copy()
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
env["MACOSX_DEPLOYMENT_TARGET"] = MACOS_DEPLOYMENT_TARGET
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_line_endings(repo_root: Path):
|
||||||
|
"""Normalize all text files to LF line endings."""
|
||||||
|
# Patterns for files to normalize
|
||||||
|
patterns = [
|
||||||
|
"src/aeg/**/*.py",
|
||||||
|
"src/aeg/**/*.h",
|
||||||
|
"tests/**/*.py",
|
||||||
|
"tools/**/*.py",
|
||||||
|
"*.py",
|
||||||
|
"*.md",
|
||||||
|
"*.txt",
|
||||||
|
"*.toml",
|
||||||
|
"*.in",
|
||||||
|
]
|
||||||
|
for pattern in patterns:
|
||||||
|
for file_path in repo_root.glob(pattern):
|
||||||
|
if file_path.is_file():
|
||||||
|
content = file_path.read_bytes()
|
||||||
|
if b"\r\n" in content:
|
||||||
|
content = content.replace(b"\r\n", b"\n")
|
||||||
|
file_path.write_bytes(content)
|
||||||
|
|
||||||
|
|
||||||
|
def get_wheel_pattern(py_version: str, abi3: bool = False) -> str:
|
||||||
|
"""Get the glob pattern for finding a wheel file."""
|
||||||
|
if abi3:
|
||||||
|
# ABI3 wheels always use cp310-abi3 tag (minimum supported version)
|
||||||
|
# regardless of which Python version was used to build
|
||||||
|
return "aeg-*-cp310-abi3-*.whl"
|
||||||
|
elif py_version.startswith("pypy"):
|
||||||
|
# PyPy wheels use pp3XX format
|
||||||
|
return f"aeg-*-pp{py_version.replace('pypy', '').replace('.', '')}-*.whl"
|
||||||
|
elif py_version.endswith("t"):
|
||||||
|
# Free-threaded Python wheels use cpXXX-cpXXXt format (e.g., cp314-cp314t)
|
||||||
|
base_version = py_version.replace(".", "").replace("t", "")
|
||||||
|
return f"aeg-*-cp{base_version}-cp{base_version}t-*.whl"
|
||||||
|
else:
|
||||||
|
# Regular CPython wheels use cpXXX-cpXXX format
|
||||||
|
# Strip +gil suffix used to force non-free-threaded build
|
||||||
|
base_version = py_version.replace(".", "").replace("+gil", "")
|
||||||
|
return f"aeg-*-cp{base_version}-cp{base_version}-*.whl"
|
||||||
|
|
||||||
|
|
||||||
|
def build_abi3_wheel(dist_dir: Path, py_version: str) -> Path | None:
|
||||||
|
"""Build the ABI3 wheel using the specified Python version."""
|
||||||
|
cmd = ["uv", "build", "--python", py_version, "--wheel", "--quiet"]
|
||||||
|
|
||||||
|
if not run_command(cmd, env=get_build_env()):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find the ABI3 wheel (always tagged cp310-abi3 regardless of build Python version)
|
||||||
|
wheel_pattern = get_wheel_pattern(py_version, abi3=True)
|
||||||
|
wheels = list(dist_dir.glob(wheel_pattern))
|
||||||
|
if not wheels:
|
||||||
|
print(f"✗ Could not find ABI3 wheel matching {wheel_pattern}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
wheel = wheels[0]
|
||||||
|
|
||||||
|
# Repair wheel with auditwheel for manylinux compatibility (Linux only)
|
||||||
|
if platform.system() == "Linux":
|
||||||
|
wheel = repair_wheel_linux(dist_dir, wheel, py_version, abi3=True)
|
||||||
|
if not wheel:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wheel
|
||||||
|
|
||||||
|
|
||||||
|
def build_wheel_for_version(dist_dir: Path, py_version: str) -> Path | None:
|
||||||
|
"""Build a wheel for a specific Python version (non-ABI3)."""
|
||||||
|
cmd = ["uv", "build", "--python", py_version, "--wheel", "--quiet"]
|
||||||
|
|
||||||
|
if not run_command(cmd, env=get_build_env()):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find the wheel for this version
|
||||||
|
wheel_pattern = get_wheel_pattern(py_version, abi3=False)
|
||||||
|
wheels = list(dist_dir.glob(wheel_pattern))
|
||||||
|
if not wheels:
|
||||||
|
print(f"✗ Could not find wheel for Python {py_version}", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
wheel = wheels[0]
|
||||||
|
|
||||||
|
# Repair wheel with auditwheel for manylinux compatibility (Linux only)
|
||||||
|
if platform.system() == "Linux":
|
||||||
|
wheel = repair_wheel_linux(dist_dir, wheel, py_version, abi3=False)
|
||||||
|
if not wheel:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wheel
|
||||||
|
|
||||||
|
|
||||||
|
def repair_wheel_linux(
|
||||||
|
dist_dir: Path, wheel: Path, py_version: str, abi3: bool
|
||||||
|
) -> Path | None:
|
||||||
|
"""Repair a wheel with auditwheel for manylinux compatibility (Linux only)."""
|
||||||
|
repair_cmd = [
|
||||||
|
"uv",
|
||||||
|
"run",
|
||||||
|
"auditwheel",
|
||||||
|
"repair",
|
||||||
|
str(wheel),
|
||||||
|
"-w",
|
||||||
|
str(dist_dir),
|
||||||
|
]
|
||||||
|
if not run_command(repair_cmd):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find the repaired wheel (it will have a different name)
|
||||||
|
wheel_pattern = get_wheel_pattern(py_version, abi3=abi3)
|
||||||
|
all_wheels = list(dist_dir.glob(wheel_pattern))
|
||||||
|
repaired_wheels = [w for w in all_wheels if "linux_x86_64" not in str(w)]
|
||||||
|
if not repaired_wheels:
|
||||||
|
print(
|
||||||
|
f"✗ Could not find repaired (manylinux) wheel for Python {py_version}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
repaired_wheel = repaired_wheels[0]
|
||||||
|
|
||||||
|
# Remove the unrepaired linux_x86_64 wheels
|
||||||
|
for w in all_wheels:
|
||||||
|
if "linux_x86_64" in str(w):
|
||||||
|
w.unlink()
|
||||||
|
|
||||||
|
return repaired_wheel
|
||||||
|
|
||||||
|
|
||||||
|
def test_wheel(wheel: Path, py_version: str) -> bool:
|
||||||
|
"""Test a wheel with pytest."""
|
||||||
|
# --isolated: avoid .venv conflicts
|
||||||
|
# --no-project: don't build from source in current directory, use the wheel
|
||||||
|
# --refresh-package: force uv to not use cached old versions
|
||||||
|
test_cmd = [
|
||||||
|
"uv",
|
||||||
|
"run",
|
||||||
|
"--isolated",
|
||||||
|
"--no-project",
|
||||||
|
"--refresh-package",
|
||||||
|
"aeg",
|
||||||
|
"--python",
|
||||||
|
py_version,
|
||||||
|
"--with",
|
||||||
|
str(wheel),
|
||||||
|
"--with",
|
||||||
|
"pytest",
|
||||||
|
"pytest",
|
||||||
|
"tests/",
|
||||||
|
]
|
||||||
|
return run_command(test_cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def run_benchmark(wheel: Path, py_version: str) -> bool:
|
||||||
|
"""Run benchmark for a wheel."""
|
||||||
|
# --isolated: avoid .venv conflicts
|
||||||
|
# --no-project: don't build from source in current directory, use the wheel
|
||||||
|
# --refresh-package: force uv to not use cached old versions
|
||||||
|
bench_cmd = [
|
||||||
|
"uv",
|
||||||
|
"run",
|
||||||
|
"--isolated",
|
||||||
|
"--no-project",
|
||||||
|
"--refresh-package",
|
||||||
|
"aeg",
|
||||||
|
"--python",
|
||||||
|
py_version,
|
||||||
|
"--with",
|
||||||
|
str(wheel),
|
||||||
|
"-m",
|
||||||
|
"aeg.benchmark",
|
||||||
|
]
|
||||||
|
return run_command(bench_cmd)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Build wheels for all supported Python versions."""
|
||||||
|
repo_root = Path(__file__).parent.parent
|
||||||
|
dist_dir = repo_root / "dist"
|
||||||
|
|
||||||
|
# Generate CFFI definitions and Python modules
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("Code generation from C headers (tools/generate.py)")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
if generate.main() != 0:
|
||||||
|
print("✗ Code generation failed", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Run ruff to check and fix any issues
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("Linting and formatting")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
if not run_command(["uv", "run", "ruff", "check", "--fix", "."]):
|
||||||
|
print("✗ Ruff check failed", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Run ruff format
|
||||||
|
if not run_command(["uv", "run", "ruff", "format", "."]):
|
||||||
|
print("✗ Ruff format failed", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Normalize all line endings to LF (important for consistent builds)
|
||||||
|
normalize_line_endings(repo_root)
|
||||||
|
|
||||||
|
# Get version from git repo
|
||||||
|
version = get_version_from_scm()
|
||||||
|
if not version:
|
||||||
|
return 1
|
||||||
|
is_release = is_release_version(version)
|
||||||
|
|
||||||
|
# Main header for the packaging process
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print(
|
||||||
|
f"Packaging aeg-{version}"
|
||||||
|
+ (" for release" if is_release else " (not release)")
|
||||||
|
)
|
||||||
|
print(f"Building: 1 ABI3 wheel (for Python {', '.join(ABI3_COVERED_VERSIONS)})")
|
||||||
|
print(
|
||||||
|
f" + {len(NON_ABI3_VERSIONS)} non-ABI3 wheels ({', '.join(NON_ABI3_VERSIONS)})"
|
||||||
|
)
|
||||||
|
print(f"Testing/benchmarking: {len(ALL_PYTHON_VERSIONS)} Python versions")
|
||||||
|
print(f"Output directory: {dist_dir}", end=" ")
|
||||||
|
|
||||||
|
# Clean dist directory
|
||||||
|
if dist_dir.exists():
|
||||||
|
print("(wiped)")
|
||||||
|
shutil.rmtree(dist_dir)
|
||||||
|
else:
|
||||||
|
print("(created)")
|
||||||
|
|
||||||
|
# Clean build directory to remove stale CFFI-generated C code and .so files
|
||||||
|
build_dir = repo_root / "build"
|
||||||
|
if build_dir.exists():
|
||||||
|
print(f"Cleaning build directory: {build_dir}")
|
||||||
|
shutil.rmtree(build_dir)
|
||||||
|
|
||||||
|
# Build distributions
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("Building distributions")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
|
||||||
|
# Build source distribution first
|
||||||
|
if not run_command(["uv", "build", "--sdist", "--quiet"], env=get_build_env()):
|
||||||
|
print("✗ Source distribution build failed", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
failed_builds = []
|
||||||
|
failed_tests = []
|
||||||
|
successful_wheels = []
|
||||||
|
wheel_for_version = {} # Map Python version to wheel path
|
||||||
|
|
||||||
|
# Build ABI3 wheel (once, works for all GIL-enabled versions)
|
||||||
|
abi3_wheel = build_abi3_wheel(dist_dir, ABI3_BUILD_VERSION)
|
||||||
|
if abi3_wheel:
|
||||||
|
successful_wheels.append(abi3_wheel)
|
||||||
|
# This wheel works for all ABI3-covered versions
|
||||||
|
for py_version in ABI3_COVERED_VERSIONS:
|
||||||
|
wheel_for_version[py_version] = abi3_wheel
|
||||||
|
else:
|
||||||
|
failed_builds.append(f"abi3 (built with {ABI3_BUILD_VERSION})")
|
||||||
|
|
||||||
|
# Build non-ABI3 wheels (free-threaded and PyPy)
|
||||||
|
for py_version in NON_ABI3_VERSIONS:
|
||||||
|
wheel = build_wheel_for_version(dist_dir, py_version)
|
||||||
|
if wheel:
|
||||||
|
successful_wheels.append(wheel)
|
||||||
|
wheel_for_version[py_version] = wheel
|
||||||
|
else:
|
||||||
|
failed_builds.append(py_version)
|
||||||
|
|
||||||
|
# Test and benchmark each Python version with its appropriate wheel
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("Testing and benchmarking")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
|
||||||
|
for py_version in ALL_PYTHON_VERSIONS:
|
||||||
|
wheel = wheel_for_version.get(py_version)
|
||||||
|
if not wheel:
|
||||||
|
# No wheel available for this version (build failed)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Test the wheel with pytest
|
||||||
|
if not test_wheel(wheel, py_version):
|
||||||
|
failed_tests.append(py_version)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Run benchmark
|
||||||
|
if not run_benchmark(wheel, py_version):
|
||||||
|
failed_tests.append(py_version)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("BUILD SUMMARY")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
print(
|
||||||
|
f"Successful builds: sdist and {len(successful_wheels)} wheels "
|
||||||
|
f"(1 abi3 + {len(NON_ABI3_VERSIONS)} non-abi3)"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"Tests/benchmarks passed: {len(ALL_PYTHON_VERSIONS) - len(failed_tests) - len(failed_builds)}/{len(ALL_PYTHON_VERSIONS)} Python versions"
|
||||||
|
)
|
||||||
|
|
||||||
|
if failed_builds:
|
||||||
|
print(f"\nFailed builds: {len(failed_builds)}")
|
||||||
|
for failed_version in failed_builds:
|
||||||
|
print(f" ✗ {failed_version}")
|
||||||
|
|
||||||
|
if failed_tests:
|
||||||
|
print(f"\nFailed tests/benchmarks: {len(failed_tests)}")
|
||||||
|
for failed_version in failed_tests:
|
||||||
|
print(f" ✗ Python {failed_version}")
|
||||||
|
|
||||||
|
if not successful_wheels:
|
||||||
|
print("\n✗ No successful wheels to upload")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# List files to upload
|
||||||
|
sdist = list(dist_dir.glob("*.tar.gz"))
|
||||||
|
upload_files = sdist + successful_wheels
|
||||||
|
|
||||||
|
for file in upload_files:
|
||||||
|
print(f" - {file.name}")
|
||||||
|
|
||||||
|
# Only upload if this is a clean release version
|
||||||
|
if not is_release:
|
||||||
|
print(make_release_message(version))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Upload with twine
|
||||||
|
upload_cmd = ["uvx", "twine", "upload"] + [str(f) for f in upload_files]
|
||||||
|
if not run_command(upload_cmd, "Uploading to PyPI with twine"):
|
||||||
|
print("\n✗ Upload failed")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"\n{'=' * 70}")
|
||||||
|
print("All builds and upload completed successfully!")
|
||||||
|
print(f"{'=' * 70}")
|
||||||
|
print()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(1)
|
||||||
Reference in New Issue
Block a user