diff --git a/README.md b/README.md new file mode 100644 index 0000000..c800d30 --- /dev/null +++ b/README.md @@ -0,0 +1,222 @@ +# pyaegis + +[![PyPI version](https://badge.fury.io/py/pyaegis.svg)](https://badge.fury.io/py/pyaegis) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/LeoVasanko/pyaegis/blob/main/libaegis/LICENSE) + +Python bindings for libaegis - high-performance AEGIS authenticated encryption. + +## Overview + +pyaegis provides Python bindings to the AEGIS family of authenticated encryption algorithms implemented in the libaegis C library. + +AEGIS is a high-performance authenticated cipher that provides both confidentiality and authenticity guarantees. + +### Variant selection + +The following submodules are available: + +- **aegis128l**: 16-byte key, 16-byte nonce +- **aegis256**: 32-byte key, 32-byte nonce +- **aegis128x2**: 16-byte key, 16-byte nonce (recommended on most platforms) +- **aegis128x4**: 16-byte key, 16-byte nonce (recommended on high-end Intel CPUs) +- **aegis256x2**: 32-byte key, 32-byte nonce +- **aegis256x4**: 32-byte key, 32-byte nonce (recommended if a 256-bit nonce is required) + +```python +from pyaegis import aegis128x4 +``` + +### One-shot Functions + +- `encrypt(key, nonce, message, ad=None, maclen=16)` - Encrypt with attached MAC +- `decrypt(key, nonce, ciphertext, ad=None, maclen=16)` - Decrypt with attached MAC +- `encrypt_detached(key, nonce, message, ad=None, maclen=16)` - Encrypt with detached MAC +- `decrypt_detached(key, nonce, ciphertext, mac, ad=None, maclen=16)` - Decrypt with detached MAC +- `stream(key, nonce, length)` - Generate pseudo-random bytes +- `encrypt_unauthenticated(key, nonce, message)` - Encrypt without authentication (insecure) +- `decrypt_unauthenticated(key, nonce, ciphertext)` - Decrypt without authentication (insecure) + +**Note**: Functions that return buffers (like `encrypt`, `decrypt`, `stream`) return `memoryview` objects. Callers can optionally supply their own buffer via the `into=` keyword argument to avoid memory allocations. + +### Incremental Classes + +- `Encryptor(key, nonce, ad=None)` - For streaming encryption +- `Decryptor(key, nonce, ad=None)` - For streaming decryption + +## Installation + +### From PyPI + +Using [uv](https://docs.astral.sh/uv/): + +```bash +uv pip install pyaegis +``` + +Or using pip: + +```bash +pip install pyaegis +``` + +### From Source + +The package compiles the C library automatically using any installed C compiler: + +```bash +# Clone the repository +git clone https://github.com/LeoVasanko/pyaegis.git +cd pyaegis + +# Install with uv (compiles C sources automatically) +uv pip install . + +# Or for development +uv pip install -e . +``` + +Alternatively with pip: + +```bash +pip install . +# Or for development +pip install -e . +``` + +### Building a Distribution + +```bash +# With uv +uv run python -m build + +# Or with pip +python -m build +``` + +This creates both source and wheel distributions in the `dist/` directory. The C sources are bundled in the package and compiled during installation. + +## Usage + +All modules (`aegis128l`, `aegis256`, `aegis128x2`, `aegis128x4`, `aegis256x2`, `aegis256x4`) provide the exact same API. + +### Basic Encryption/Decryption + +```python +import pyaegis.aegis128l as a + +key = b"K" * a.KEYBYTES # 16 bytes for aegis128l +nonce = b"N" * a.NPUBBYTES # 16 bytes for aegis128l +plaintext = b"Hello, World!" + +ciphertext = a.encrypt(key, nonce, plaintext) +decrypted = a.decrypt(key, nonce, ciphertext) +assert decrypted == plaintext +``` + +### With Additional Authenticated Data (AAD) + +```python +ciphertext = a.encrypt(key, nonce, plaintext, ad=b"metadata") +plaintext = a.decrypt(key, nonce, ciphertext, ad=b"metadata") +``` + +### Detached Tag Mode + +```python +ciphertext, mac = a.encrypt_detached(key, nonce, b"secret") +plaintext = a.decrypt_detached(key, nonce, ciphertext, mac) +``` + +### Custom MAC Length + +Use 16 or 32-byte MACs (default 16): + +```python +ciphertext, mac = a.encrypt_detached(key, nonce, message, maclen=32) +plaintext = a.decrypt_detached(key, nonce, ciphertext, mac, maclen=32) +``` + +### In-Place Operations + +```python +buffer = bytearray(b"secret message") +mac = a.encrypt_unauthenticated_into(key, nonce, buffer) +# buffer now contains ciphertext + +a.decrypt_unauthenticated_into(key, nonce, buffer, mac) +# buffer now contains plaintext again +``` + +### Using Pre-allocated Buffers + +```python +# Pre-allocate output buffer +output = bytearray(len(plaintext) + 16) # +16 for MAC +result = a.encrypt(key, nonce, plaintext, into=output) +# result is a memoryview of the output buffer +``` + +### Stream Generation + +Generate pseudo-random bytes: + +```python +random_bytes = a.stream(key, nonce, 1024) +``` + +### Incremental Encryption + +```python +encryptor = a.Encryptor(key, nonce, ad=b"header") +ciphertext1 = encryptor.update(b"chunk1") +ciphertext2 = encryptor.update(b"chunk2") +final_bytes = encryptor.final() # includes MAC +``` + +## Performance + +The library automatically detects CPU features at runtime and uses the most optimized implementation available: + +- AES-NI on Intel/AMD processors +- ARM Crypto Extensions on ARM processors +- AVX2 and AVX-512 for multi-lane variants +- Software fallback for other platforms + +Multi-lane variants (X2, X4) provide higher throughput on systems with appropriate SIMD support. + +See `examples/benchmark.py` for performance measurements. + +## Error Handling + +Functions that can fail raise `ValueError` with descriptive messages: + +```python +import pyaegis.aegis128l as a + +key = b"K" * a.KEYBYTES +nonce = b"N" * a.NPUBBYTES + +try: + # This will raise ValueError if authentication fails + plaintext = a.decrypt(key, nonce, tampered_ciphertext) +except ValueError as e: + print(f"Decryption failed: {e}") +``` + +## Performance + +The library automatically detects CPU features at runtime and uses the most optimized implementation available: + +- AES-NI on Intel/AMD processors +- ARM Crypto Extensions on ARM processors +- AVX2 and AVX-512 for multi-lane variants +- Software fallback for other platforms + +Multi-lane variants (X2, X4) provide higher throughput on systems with appropriate SIMD support. + +## Security Considerations + +- **Nonce Uniqueness**: Never reuse a nonce with the same key. If you can't maintain a counter, generate random nonces for each message. +- **Key Management**: Generate cryptographically secure keys. Keep keys secret. +- **AAD**: Additional authenticated data is not encrypted but is protected against tampering. +- **MAC vs AEAD**: Use AEAD for encryption needs. MAC-only variants are for authentication without confidentiality. diff --git a/libaegis b/libaegis index 79dee09..4a23400 160000 --- a/libaegis +++ b/libaegis @@ -1 +1 @@ -Subproject commit 79dee09317bc43e5faaae034e4695369e5661bbb +Subproject commit 4a234009c94454fe818ed1b19091c0c41a1c63d7 diff --git a/pyaegis/aegis128l.py b/pyaegis/aegis128l.py index 9510863..0d00db8 100644 --- a/pyaegis/aegis128l.py +++ b/pyaegis/aegis128l.py @@ -10,11 +10,10 @@ Error return codes from the C library raise ValueError. # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! import errno -from collections.abc import Buffer from ._loader import ffi from ._loader import lib as _lib -from .util import new_aligned_struct +from .util import Buffer, new_aligned_struct # Constants exposed as functions in C; mirror them as integers at module import time KEYBYTES = _lib.aegis128l_keybytes() @@ -39,7 +38,7 @@ def encrypt_detached( maclen: int = ABYTES_MIN, ct_into: Buffer | None = None, mac_into: Buffer | None = None, -) -> tuple[memoryview, memoryview]: +) -> tuple[bytearray | memoryview, bytearray | memoryview]: """Encrypt message with associated data, returning ciphertext and MAC separately. Args: @@ -58,35 +57,34 @@ def encrypt_detached( TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - ct_into = memoryview(ct_into) if ct_into is not None else None - mac_into = memoryview(mac_into) if mac_into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) - mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) - if c.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") - if mac.nbytes != maclen: - raise TypeError("mac_into length must equal maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -94,7 +92,10 @@ def encrypt_detached( 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, mac + return ( + c if ct_into is None else memoryview(c)[: len(message)], + mac if mac_into is None else memoryview(mac)[:maclen], + ) def decrypt_detached( @@ -105,7 +106,7 @@ def decrypt_detached( ad: Buffer | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with detached MAC and associated data. Args: @@ -117,44 +118,40 @@ def decrypt_detached( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - mac = memoryview(mac) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if m_out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), _ptr(mac), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return memoryview(m_out) + return out if into is None else memoryview(out)[: len(ct)] # type: ignore def encrypt( @@ -165,7 +162,7 @@ def encrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message with associated data, returning ciphertext with appended MAC. Args: @@ -177,35 +174,32 @@ def encrypt( into: Buffer to write ciphertext+MAC into (default: bytearray created). Returns: - Ciphertext with appended MAC as bytearray if into not provided. + Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) - if out.nbytes != message.nbytes + maclen: - raise TypeError("into length must be len(message)+maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -213,7 +207,7 @@ def encrypt( err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"encrypt failed: {err_name}") - return out + return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore def decrypt( @@ -224,7 +218,7 @@ def decrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with appended MAC and associated data. Args: @@ -236,43 +230,43 @@ def decrypt( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if ct.nbytes < maclen: + if len(ct) < maclen: raise TypeError("ciphertext too short for tag") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) - if m_out.nbytes != ct.nbytes - maclen: - raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return m_out + return out if into is None else memoryview(out)[:expected_out] # type: ignore def stream( @@ -281,7 +275,7 @@ def stream( length: int | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | Buffer: """Generate a stream of pseudorandom bytes. Args: @@ -291,29 +285,30 @@ def stream( into: Buffer to write stream into (default: bytearray created). Returns: - Pseudorandom bytes as bytearray if into not provided. + Pseudorandom bytes as bytearray, or into returned directly. Raises: TypeError: If lengths are invalid or neither length nor into provided. """ - nonce = memoryview(nonce) if nonce is not None else None - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce is not None and nonce.nbytes != NPUBBYTES: + if nonce is not None and len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if into is None and length is None: - raise TypeError("provide either into or length") - out = into if into is not None else memoryview(bytearray(int(length or 0))) + 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), - out.nbytes, + len(out), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: length or len(out)] # type: ignore def encrypt_unauthenticated( @@ -322,7 +317,7 @@ def encrypt_unauthenticated( message: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). Args: @@ -332,31 +327,29 @@ def encrypt_unauthenticated( into: Buffer to write ciphertext into (default: bytearray created). Returns: - Ciphertext as bytearray if into not provided. + Ciphertext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - message = memoryview(message) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes)) - if out.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") + 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), - message.nbytes, + len(message), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(message)] # type: ignore def decrypt_unauthenticated( @@ -365,7 +358,7 @@ def decrypt_unauthenticated( ct: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). Args: @@ -375,31 +368,29 @@ def decrypt_unauthenticated( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - ct = memoryview(ct) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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), - ct.nbytes, + len(ct), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(ct)] # type: ignore # This is missing from C API but convenient to have here @@ -408,7 +399,8 @@ def mac( nonce: Buffer, data: Buffer, maclen: int = ABYTES_MIN, -) -> memoryview: + into: Buffer | None = None, +) -> bytearray | memoryview: """Compute a MAC for the given data in one shot. Args: @@ -416,13 +408,14 @@ def mac( nonce: Nonce (32 bytes) data: Data to MAC maclen: MAC length (16 or 32, default 16) + into: Buffer to write MAC into (default: bytearray created) Returns: - MAC bytes + 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) + return mac_state.final(maclen, into) class Mac: @@ -460,11 +453,9 @@ class Mac: _lib.aegis128l_mac_state_clone(self._st, _other._st) return # Normal init path - nonce = memoryview(nonce) - key = memoryview(key) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") _lib.aegis128l_mac_init(self._st, _ptr(key), _ptr(nonce)) @@ -487,8 +478,7 @@ class Mac: Raises: RuntimeError: If the underlying C function reports an error. """ - data = memoryview(data) - rc = _lib.aegis128l_mac_update(self._st, _ptr(data), data.nbytes) + 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}") @@ -498,7 +488,7 @@ class Mac: self, maclen: int = ABYTES_MIN, into: Buffer | None = None, - ) -> memoryview: + ) -> bytearray | memoryview: """Finalize and return the MAC tag. Args: @@ -506,7 +496,7 @@ class Mac: into: Optional buffer to write the tag into (default: bytearray created). Returns: - The tag as a memoryview; if ``into`` is provided, it views that buffer. + The tag as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. @@ -514,16 +504,19 @@ class Mac: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - out = into if into is not None else bytearray(maclen) - out = memoryview(out) - if out.nbytes != maclen: - raise TypeError("into length must equal maclen") - rc = _lib.aegis128l_mac_final(self._st, ffi.from_buffer(out), maclen) + 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 + return out if into is None else memoryview(out)[:maclen] # type: ignore def verify(self, mac: Buffer) -> bool: """Verify a tag for the current MAC state. @@ -538,8 +531,7 @@ class Mac: TypeError: If tag length is invalid. ValueError: If verification fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + 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) @@ -569,23 +561,20 @@ class Encryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total plaintext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -602,7 +591,9 @@ class Encryptor: """ return self._bytes_out - def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + def update( + self, message: Buffer, into: Buffer | None = None + ) -> bytearray | memoryview: """Encrypt a chunk of the message. Args: @@ -610,28 +601,27 @@ class Encryptor: into: Optional destination buffer; must be >= len(message). Returns: - The ciphertext for this chunk as a memoryview; when ``into`` is - provided, a view of that buffer up to the number of bytes written. + 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. """ - message = memoryview(message) - expected_out = message.nbytes - out = memoryview(into if into is not None else bytearray(expected_out)) - if out.nbytes < expected_out: + 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(message), - message.nbytes, + len(message), ) if rc != 0: err_num = ffi.errno @@ -641,11 +631,13 @@ class Encryptor: ) w = int(written[0]) assert w == expected_out - self._bytes_in += message.nbytes + self._bytes_in += len(message) self._bytes_out += w - return 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) -> memoryview: + def final( + self, into: Buffer | None = None, maclen: int = ABYTES_MIN + ) -> bytearray | memoryview: """Finalize encryption, writing any remaining bytes and the tag. Args: @@ -653,9 +645,7 @@ class Encryptor: maclen: Tag length (16 or 32). Defaults to 16. Returns: - A memoryview of the produced bytes (tail + tag). When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. Raises: TypeError: If maclen is invalid. @@ -663,14 +653,13 @@ class Encryptor: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag - out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) - out = memoryview(out) + # 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), - out.nbytes, + len(out), written, maclen, ) @@ -679,15 +668,18 @@ class Encryptor: 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[: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[, into]) -> returns any remaining plaintext bytes + - final(mac) -> verifies the MAC tag """ __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") @@ -703,23 +695,20 @@ class Decryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total ciphertext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -730,13 +719,10 @@ class Decryptor: @property def bytes_out(self) -> int: - """Total plaintext bytes produced so far. - - Includes bytes written by update() and by final(). - """ + """Total plaintext bytes produced so far.""" return self._bytes_out - def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview: """Process a chunk of ciphertext. Args: @@ -744,75 +730,54 @@ class Decryptor: into: Optional destination buffer; must be >= len(ciphertext). Returns: - A memoryview of the decrypted bytes for this chunk. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + 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. """ - ct = memoryview(ct) - expected_out = ct.nbytes + expected_out = len(ct) out = into if into is not None else bytearray(expected_out) - out = memoryview(out) - if out.nbytes < 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(ct), - ct.nbytes, + 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 - self._bytes_in += ct.nbytes + 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[:w] + return out if into is None else memoryview(out)[:w] # type: ignore - def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: - """Finalize decryption by verifying tag and flushing remaining bytes. + def final(self, mac: Buffer) -> None: + """Finalize decryption by verifying the MAC tag. Args: mac: Tag to verify (16 or 32 bytes). - into: Optional destination buffer for the remaining plaintext bytes. - - Returns: - A memoryview of the remaining plaintext bytes. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. Raises: TypeError: If tag length is invalid. ValueError: If authentication fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - out = into if into is not None else bytearray(TAILBYTES_MAX) - out = memoryview(out) - written = ffi.new("size_t *") rc = _lib.aegis128l_state_decrypt_detached_final( - self._st, - ffi.from_buffer(out), - out.nbytes, - written, - _ptr(mac), - maclen, + self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") - w = int(written[0]) - self._bytes_out += w - return out[:w] def new_state(): diff --git a/pyaegis/aegis128x2.py b/pyaegis/aegis128x2.py index 0c70f70..be0674f 100644 --- a/pyaegis/aegis128x2.py +++ b/pyaegis/aegis128x2.py @@ -10,11 +10,10 @@ Error return codes from the C library raise ValueError. # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! import errno -from collections.abc import Buffer from ._loader import ffi from ._loader import lib as _lib -from .util import new_aligned_struct +from .util import Buffer, new_aligned_struct # Constants exposed as functions in C; mirror them as integers at module import time KEYBYTES = _lib.aegis128x2_keybytes() @@ -39,7 +38,7 @@ def encrypt_detached( maclen: int = ABYTES_MIN, ct_into: Buffer | None = None, mac_into: Buffer | None = None, -) -> tuple[memoryview, memoryview]: +) -> tuple[bytearray | memoryview, bytearray | memoryview]: """Encrypt message with associated data, returning ciphertext and MAC separately. Args: @@ -58,35 +57,34 @@ def encrypt_detached( TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - ct_into = memoryview(ct_into) if ct_into is not None else None - mac_into = memoryview(mac_into) if mac_into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) - mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) - if c.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") - if mac.nbytes != maclen: - raise TypeError("mac_into length must equal maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -94,7 +92,10 @@ def encrypt_detached( 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, mac + return ( + c if ct_into is None else memoryview(c)[: len(message)], + mac if mac_into is None else memoryview(mac)[:maclen], + ) def decrypt_detached( @@ -105,7 +106,7 @@ def decrypt_detached( ad: Buffer | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with detached MAC and associated data. Args: @@ -117,44 +118,40 @@ def decrypt_detached( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - mac = memoryview(mac) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if m_out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), _ptr(mac), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return memoryview(m_out) + return out if into is None else memoryview(out)[: len(ct)] # type: ignore def encrypt( @@ -165,7 +162,7 @@ def encrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message with associated data, returning ciphertext with appended MAC. Args: @@ -177,35 +174,32 @@ def encrypt( into: Buffer to write ciphertext+MAC into (default: bytearray created). Returns: - Ciphertext with appended MAC as bytearray if into not provided. + Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) - if out.nbytes != message.nbytes + maclen: - raise TypeError("into length must be len(message)+maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -213,7 +207,7 @@ def encrypt( err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"encrypt failed: {err_name}") - return out + return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore def decrypt( @@ -224,7 +218,7 @@ def decrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with appended MAC and associated data. Args: @@ -236,43 +230,43 @@ def decrypt( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if ct.nbytes < maclen: + if len(ct) < maclen: raise TypeError("ciphertext too short for tag") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) - if m_out.nbytes != ct.nbytes - maclen: - raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return m_out + return out if into is None else memoryview(out)[:expected_out] # type: ignore def stream( @@ -281,7 +275,7 @@ def stream( length: int | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | Buffer: """Generate a stream of pseudorandom bytes. Args: @@ -291,29 +285,30 @@ def stream( into: Buffer to write stream into (default: bytearray created). Returns: - Pseudorandom bytes as bytearray if into not provided. + Pseudorandom bytes as bytearray, or into returned directly. Raises: TypeError: If lengths are invalid or neither length nor into provided. """ - nonce = memoryview(nonce) if nonce is not None else None - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce is not None and nonce.nbytes != NPUBBYTES: + if nonce is not None and len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if into is None and length is None: - raise TypeError("provide either into or length") - out = into if into is not None else memoryview(bytearray(int(length or 0))) + 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), - out.nbytes, + len(out), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: length or len(out)] # type: ignore def encrypt_unauthenticated( @@ -322,7 +317,7 @@ def encrypt_unauthenticated( message: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). Args: @@ -332,31 +327,29 @@ def encrypt_unauthenticated( into: Buffer to write ciphertext into (default: bytearray created). Returns: - Ciphertext as bytearray if into not provided. + Ciphertext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - message = memoryview(message) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes)) - if out.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") + 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), - message.nbytes, + len(message), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(message)] # type: ignore def decrypt_unauthenticated( @@ -365,7 +358,7 @@ def decrypt_unauthenticated( ct: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). Args: @@ -375,31 +368,29 @@ def decrypt_unauthenticated( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - ct = memoryview(ct) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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), - ct.nbytes, + len(ct), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(ct)] # type: ignore # This is missing from C API but convenient to have here @@ -408,7 +399,8 @@ def mac( nonce: Buffer, data: Buffer, maclen: int = ABYTES_MIN, -) -> memoryview: + into: Buffer | None = None, +) -> bytearray | memoryview: """Compute a MAC for the given data in one shot. Args: @@ -416,13 +408,14 @@ def mac( nonce: Nonce (32 bytes) data: Data to MAC maclen: MAC length (16 or 32, default 16) + into: Buffer to write MAC into (default: bytearray created) Returns: - MAC bytes + 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) + return mac_state.final(maclen, into) class Mac: @@ -460,11 +453,9 @@ class Mac: _lib.aegis128x2_mac_state_clone(self._st, _other._st) return # Normal init path - nonce = memoryview(nonce) - key = memoryview(key) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") _lib.aegis128x2_mac_init(self._st, _ptr(key), _ptr(nonce)) @@ -487,8 +478,7 @@ class Mac: Raises: RuntimeError: If the underlying C function reports an error. """ - data = memoryview(data) - rc = _lib.aegis128x2_mac_update(self._st, _ptr(data), data.nbytes) + 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}") @@ -498,7 +488,7 @@ class Mac: self, maclen: int = ABYTES_MIN, into: Buffer | None = None, - ) -> memoryview: + ) -> bytearray | memoryview: """Finalize and return the MAC tag. Args: @@ -506,7 +496,7 @@ class Mac: into: Optional buffer to write the tag into (default: bytearray created). Returns: - The tag as a memoryview; if ``into`` is provided, it views that buffer. + The tag as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. @@ -514,16 +504,19 @@ class Mac: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - out = into if into is not None else bytearray(maclen) - out = memoryview(out) - if out.nbytes != maclen: - raise TypeError("into length must equal maclen") - rc = _lib.aegis128x2_mac_final(self._st, ffi.from_buffer(out), maclen) + 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 + return out if into is None else memoryview(out)[:maclen] # type: ignore def verify(self, mac: Buffer) -> bool: """Verify a tag for the current MAC state. @@ -538,8 +531,7 @@ class Mac: TypeError: If tag length is invalid. ValueError: If verification fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + 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) @@ -569,23 +561,20 @@ class Encryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total plaintext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -602,7 +591,9 @@ class Encryptor: """ return self._bytes_out - def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + def update( + self, message: Buffer, into: Buffer | None = None + ) -> bytearray | memoryview: """Encrypt a chunk of the message. Args: @@ -610,28 +601,27 @@ class Encryptor: into: Optional destination buffer; must be >= len(message). Returns: - The ciphertext for this chunk as a memoryview; when ``into`` is - provided, a view of that buffer up to the number of bytes written. + 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. """ - message = memoryview(message) - expected_out = message.nbytes - out = memoryview(into if into is not None else bytearray(expected_out)) - if out.nbytes < expected_out: + 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(message), - message.nbytes, + len(message), ) if rc != 0: err_num = ffi.errno @@ -641,11 +631,13 @@ class Encryptor: ) w = int(written[0]) assert w == expected_out - self._bytes_in += message.nbytes + self._bytes_in += len(message) self._bytes_out += w - return 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) -> memoryview: + def final( + self, into: Buffer | None = None, maclen: int = ABYTES_MIN + ) -> bytearray | memoryview: """Finalize encryption, writing any remaining bytes and the tag. Args: @@ -653,9 +645,7 @@ class Encryptor: maclen: Tag length (16 or 32). Defaults to 16. Returns: - A memoryview of the produced bytes (tail + tag). When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. Raises: TypeError: If maclen is invalid. @@ -663,14 +653,13 @@ class Encryptor: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag - out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) - out = memoryview(out) + # 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), - out.nbytes, + len(out), written, maclen, ) @@ -679,15 +668,18 @@ class Encryptor: 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[: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[, into]) -> returns any remaining plaintext bytes + - final(mac) -> verifies the MAC tag """ __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") @@ -703,23 +695,20 @@ class Decryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total ciphertext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -730,13 +719,10 @@ class Decryptor: @property def bytes_out(self) -> int: - """Total plaintext bytes produced so far. - - Includes bytes written by update() and by final(). - """ + """Total plaintext bytes produced so far.""" return self._bytes_out - def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview: """Process a chunk of ciphertext. Args: @@ -744,75 +730,54 @@ class Decryptor: into: Optional destination buffer; must be >= len(ciphertext). Returns: - A memoryview of the decrypted bytes for this chunk. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + 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. """ - ct = memoryview(ct) - expected_out = ct.nbytes + expected_out = len(ct) out = into if into is not None else bytearray(expected_out) - out = memoryview(out) - if out.nbytes < 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(ct), - ct.nbytes, + 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 - self._bytes_in += ct.nbytes + 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[:w] + return out if into is None else memoryview(out)[:w] # type: ignore - def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: - """Finalize decryption by verifying tag and flushing remaining bytes. + def final(self, mac: Buffer) -> None: + """Finalize decryption by verifying the MAC tag. Args: mac: Tag to verify (16 or 32 bytes). - into: Optional destination buffer for the remaining plaintext bytes. - - Returns: - A memoryview of the remaining plaintext bytes. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. Raises: TypeError: If tag length is invalid. ValueError: If authentication fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - out = into if into is not None else bytearray(TAILBYTES_MAX) - out = memoryview(out) - written = ffi.new("size_t *") rc = _lib.aegis128x2_state_decrypt_detached_final( - self._st, - ffi.from_buffer(out), - out.nbytes, - written, - _ptr(mac), - maclen, + self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") - w = int(written[0]) - self._bytes_out += w - return out[:w] def new_state(): diff --git a/pyaegis/aegis128x4.py b/pyaegis/aegis128x4.py index e3ebfa6..fd65b57 100644 --- a/pyaegis/aegis128x4.py +++ b/pyaegis/aegis128x4.py @@ -10,11 +10,10 @@ Error return codes from the C library raise ValueError. # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! import errno -from collections.abc import Buffer from ._loader import ffi from ._loader import lib as _lib -from .util import new_aligned_struct +from .util import Buffer, new_aligned_struct # Constants exposed as functions in C; mirror them as integers at module import time KEYBYTES = _lib.aegis128x4_keybytes() @@ -39,7 +38,7 @@ def encrypt_detached( maclen: int = ABYTES_MIN, ct_into: Buffer | None = None, mac_into: Buffer | None = None, -) -> tuple[memoryview, memoryview]: +) -> tuple[bytearray | memoryview, bytearray | memoryview]: """Encrypt message with associated data, returning ciphertext and MAC separately. Args: @@ -58,35 +57,34 @@ def encrypt_detached( TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - ct_into = memoryview(ct_into) if ct_into is not None else None - mac_into = memoryview(mac_into) if mac_into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) - mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) - if c.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") - if mac.nbytes != maclen: - raise TypeError("mac_into length must equal maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -94,7 +92,10 @@ def encrypt_detached( 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, mac + return ( + c if ct_into is None else memoryview(c)[: len(message)], + mac if mac_into is None else memoryview(mac)[:maclen], + ) def decrypt_detached( @@ -105,7 +106,7 @@ def decrypt_detached( ad: Buffer | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with detached MAC and associated data. Args: @@ -117,44 +118,40 @@ def decrypt_detached( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - mac = memoryview(mac) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if m_out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), _ptr(mac), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return memoryview(m_out) + return out if into is None else memoryview(out)[: len(ct)] # type: ignore def encrypt( @@ -165,7 +162,7 @@ def encrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message with associated data, returning ciphertext with appended MAC. Args: @@ -177,35 +174,32 @@ def encrypt( into: Buffer to write ciphertext+MAC into (default: bytearray created). Returns: - Ciphertext with appended MAC as bytearray if into not provided. + Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) - if out.nbytes != message.nbytes + maclen: - raise TypeError("into length must be len(message)+maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -213,7 +207,7 @@ def encrypt( err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"encrypt failed: {err_name}") - return out + return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore def decrypt( @@ -224,7 +218,7 @@ def decrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with appended MAC and associated data. Args: @@ -236,43 +230,43 @@ def decrypt( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if ct.nbytes < maclen: + if len(ct) < maclen: raise TypeError("ciphertext too short for tag") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) - if m_out.nbytes != ct.nbytes - maclen: - raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return m_out + return out if into is None else memoryview(out)[:expected_out] # type: ignore def stream( @@ -281,7 +275,7 @@ def stream( length: int | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | Buffer: """Generate a stream of pseudorandom bytes. Args: @@ -291,29 +285,30 @@ def stream( into: Buffer to write stream into (default: bytearray created). Returns: - Pseudorandom bytes as bytearray if into not provided. + Pseudorandom bytes as bytearray, or into returned directly. Raises: TypeError: If lengths are invalid or neither length nor into provided. """ - nonce = memoryview(nonce) if nonce is not None else None - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce is not None and nonce.nbytes != NPUBBYTES: + if nonce is not None and len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if into is None and length is None: - raise TypeError("provide either into or length") - out = into if into is not None else memoryview(bytearray(int(length or 0))) + 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), - out.nbytes, + len(out), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: length or len(out)] # type: ignore def encrypt_unauthenticated( @@ -322,7 +317,7 @@ def encrypt_unauthenticated( message: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). Args: @@ -332,31 +327,29 @@ def encrypt_unauthenticated( into: Buffer to write ciphertext into (default: bytearray created). Returns: - Ciphertext as bytearray if into not provided. + Ciphertext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - message = memoryview(message) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes)) - if out.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") + 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), - message.nbytes, + len(message), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(message)] # type: ignore def decrypt_unauthenticated( @@ -365,7 +358,7 @@ def decrypt_unauthenticated( ct: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). Args: @@ -375,31 +368,29 @@ def decrypt_unauthenticated( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - ct = memoryview(ct) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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), - ct.nbytes, + len(ct), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(ct)] # type: ignore # This is missing from C API but convenient to have here @@ -408,7 +399,8 @@ def mac( nonce: Buffer, data: Buffer, maclen: int = ABYTES_MIN, -) -> memoryview: + into: Buffer | None = None, +) -> bytearray | memoryview: """Compute a MAC for the given data in one shot. Args: @@ -416,13 +408,14 @@ def mac( nonce: Nonce (32 bytes) data: Data to MAC maclen: MAC length (16 or 32, default 16) + into: Buffer to write MAC into (default: bytearray created) Returns: - MAC bytes + 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) + return mac_state.final(maclen, into) class Mac: @@ -460,11 +453,9 @@ class Mac: _lib.aegis128x4_mac_state_clone(self._st, _other._st) return # Normal init path - nonce = memoryview(nonce) - key = memoryview(key) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") _lib.aegis128x4_mac_init(self._st, _ptr(key), _ptr(nonce)) @@ -487,8 +478,7 @@ class Mac: Raises: RuntimeError: If the underlying C function reports an error. """ - data = memoryview(data) - rc = _lib.aegis128x4_mac_update(self._st, _ptr(data), data.nbytes) + 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}") @@ -498,7 +488,7 @@ class Mac: self, maclen: int = ABYTES_MIN, into: Buffer | None = None, - ) -> memoryview: + ) -> bytearray | memoryview: """Finalize and return the MAC tag. Args: @@ -506,7 +496,7 @@ class Mac: into: Optional buffer to write the tag into (default: bytearray created). Returns: - The tag as a memoryview; if ``into`` is provided, it views that buffer. + The tag as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. @@ -514,16 +504,19 @@ class Mac: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - out = into if into is not None else bytearray(maclen) - out = memoryview(out) - if out.nbytes != maclen: - raise TypeError("into length must equal maclen") - rc = _lib.aegis128x4_mac_final(self._st, ffi.from_buffer(out), maclen) + 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 + return out if into is None else memoryview(out)[:maclen] # type: ignore def verify(self, mac: Buffer) -> bool: """Verify a tag for the current MAC state. @@ -538,8 +531,7 @@ class Mac: TypeError: If tag length is invalid. ValueError: If verification fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + 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) @@ -569,23 +561,20 @@ class Encryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total plaintext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -602,7 +591,9 @@ class Encryptor: """ return self._bytes_out - def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + def update( + self, message: Buffer, into: Buffer | None = None + ) -> bytearray | memoryview: """Encrypt a chunk of the message. Args: @@ -610,28 +601,27 @@ class Encryptor: into: Optional destination buffer; must be >= len(message). Returns: - The ciphertext for this chunk as a memoryview; when ``into`` is - provided, a view of that buffer up to the number of bytes written. + 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. """ - message = memoryview(message) - expected_out = message.nbytes - out = memoryview(into if into is not None else bytearray(expected_out)) - if out.nbytes < expected_out: + 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(message), - message.nbytes, + len(message), ) if rc != 0: err_num = ffi.errno @@ -641,11 +631,13 @@ class Encryptor: ) w = int(written[0]) assert w == expected_out - self._bytes_in += message.nbytes + self._bytes_in += len(message) self._bytes_out += w - return 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) -> memoryview: + def final( + self, into: Buffer | None = None, maclen: int = ABYTES_MIN + ) -> bytearray | memoryview: """Finalize encryption, writing any remaining bytes and the tag. Args: @@ -653,9 +645,7 @@ class Encryptor: maclen: Tag length (16 or 32). Defaults to 16. Returns: - A memoryview of the produced bytes (tail + tag). When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. Raises: TypeError: If maclen is invalid. @@ -663,14 +653,13 @@ class Encryptor: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag - out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) - out = memoryview(out) + # 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), - out.nbytes, + len(out), written, maclen, ) @@ -679,15 +668,18 @@ class Encryptor: 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[: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[, into]) -> returns any remaining plaintext bytes + - final(mac) -> verifies the MAC tag """ __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") @@ -703,23 +695,20 @@ class Decryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total ciphertext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -730,13 +719,10 @@ class Decryptor: @property def bytes_out(self) -> int: - """Total plaintext bytes produced so far. - - Includes bytes written by update() and by final(). - """ + """Total plaintext bytes produced so far.""" return self._bytes_out - def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview: """Process a chunk of ciphertext. Args: @@ -744,75 +730,54 @@ class Decryptor: into: Optional destination buffer; must be >= len(ciphertext). Returns: - A memoryview of the decrypted bytes for this chunk. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + 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. """ - ct = memoryview(ct) - expected_out = ct.nbytes + expected_out = len(ct) out = into if into is not None else bytearray(expected_out) - out = memoryview(out) - if out.nbytes < 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(ct), - ct.nbytes, + 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 - self._bytes_in += ct.nbytes + 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[:w] + return out if into is None else memoryview(out)[:w] # type: ignore - def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: - """Finalize decryption by verifying tag and flushing remaining bytes. + def final(self, mac: Buffer) -> None: + """Finalize decryption by verifying the MAC tag. Args: mac: Tag to verify (16 or 32 bytes). - into: Optional destination buffer for the remaining plaintext bytes. - - Returns: - A memoryview of the remaining plaintext bytes. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. Raises: TypeError: If tag length is invalid. ValueError: If authentication fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - out = into if into is not None else bytearray(TAILBYTES_MAX) - out = memoryview(out) - written = ffi.new("size_t *") rc = _lib.aegis128x4_state_decrypt_detached_final( - self._st, - ffi.from_buffer(out), - out.nbytes, - written, - _ptr(mac), - maclen, + self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") - w = int(written[0]) - self._bytes_out += w - return out[:w] def new_state(): diff --git a/pyaegis/aegis256.py b/pyaegis/aegis256.py index 47bb2d2..380fa4d 100644 --- a/pyaegis/aegis256.py +++ b/pyaegis/aegis256.py @@ -10,11 +10,10 @@ Error return codes from the C library raise ValueError. # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! import errno -from collections.abc import Buffer from ._loader import ffi from ._loader import lib as _lib -from .util import new_aligned_struct +from .util import Buffer, new_aligned_struct # Constants exposed as functions in C; mirror them as integers at module import time KEYBYTES = _lib.aegis256_keybytes() @@ -39,7 +38,7 @@ def encrypt_detached( maclen: int = ABYTES_MIN, ct_into: Buffer | None = None, mac_into: Buffer | None = None, -) -> tuple[memoryview, memoryview]: +) -> tuple[bytearray | memoryview, bytearray | memoryview]: """Encrypt message with associated data, returning ciphertext and MAC separately. Args: @@ -58,35 +57,34 @@ def encrypt_detached( TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - ct_into = memoryview(ct_into) if ct_into is not None else None - mac_into = memoryview(mac_into) if mac_into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) - mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) - if c.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") - if mac.nbytes != maclen: - raise TypeError("mac_into length must equal maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -94,7 +92,10 @@ def encrypt_detached( 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, mac + return ( + c if ct_into is None else memoryview(c)[: len(message)], + mac if mac_into is None else memoryview(mac)[:maclen], + ) def decrypt_detached( @@ -105,7 +106,7 @@ def decrypt_detached( ad: Buffer | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with detached MAC and associated data. Args: @@ -117,44 +118,40 @@ def decrypt_detached( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - mac = memoryview(mac) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if m_out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), _ptr(mac), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return memoryview(m_out) + return out if into is None else memoryview(out)[: len(ct)] # type: ignore def encrypt( @@ -165,7 +162,7 @@ def encrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message with associated data, returning ciphertext with appended MAC. Args: @@ -177,35 +174,32 @@ def encrypt( into: Buffer to write ciphertext+MAC into (default: bytearray created). Returns: - Ciphertext with appended MAC as bytearray if into not provided. + Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) - if out.nbytes != message.nbytes + maclen: - raise TypeError("into length must be len(message)+maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -213,7 +207,7 @@ def encrypt( err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"encrypt failed: {err_name}") - return out + return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore def decrypt( @@ -224,7 +218,7 @@ def decrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with appended MAC and associated data. Args: @@ -236,43 +230,43 @@ def decrypt( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if ct.nbytes < maclen: + if len(ct) < maclen: raise TypeError("ciphertext too short for tag") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) - if m_out.nbytes != ct.nbytes - maclen: - raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return m_out + return out if into is None else memoryview(out)[:expected_out] # type: ignore def stream( @@ -281,7 +275,7 @@ def stream( length: int | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | Buffer: """Generate a stream of pseudorandom bytes. Args: @@ -291,29 +285,30 @@ def stream( into: Buffer to write stream into (default: bytearray created). Returns: - Pseudorandom bytes as bytearray if into not provided. + Pseudorandom bytes as bytearray, or into returned directly. Raises: TypeError: If lengths are invalid or neither length nor into provided. """ - nonce = memoryview(nonce) if nonce is not None else None - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce is not None and nonce.nbytes != NPUBBYTES: + if nonce is not None and len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if into is None and length is None: - raise TypeError("provide either into or length") - out = into if into is not None else memoryview(bytearray(int(length or 0))) + 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), - out.nbytes, + len(out), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: length or len(out)] # type: ignore def encrypt_unauthenticated( @@ -322,7 +317,7 @@ def encrypt_unauthenticated( message: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). Args: @@ -332,31 +327,29 @@ def encrypt_unauthenticated( into: Buffer to write ciphertext into (default: bytearray created). Returns: - Ciphertext as bytearray if into not provided. + Ciphertext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - message = memoryview(message) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes)) - if out.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") + 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), - message.nbytes, + len(message), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(message)] # type: ignore def decrypt_unauthenticated( @@ -365,7 +358,7 @@ def decrypt_unauthenticated( ct: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). Args: @@ -375,31 +368,29 @@ def decrypt_unauthenticated( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - ct = memoryview(ct) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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), - ct.nbytes, + len(ct), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(ct)] # type: ignore # This is missing from C API but convenient to have here @@ -408,7 +399,8 @@ def mac( nonce: Buffer, data: Buffer, maclen: int = ABYTES_MIN, -) -> memoryview: + into: Buffer | None = None, +) -> bytearray | memoryview: """Compute a MAC for the given data in one shot. Args: @@ -416,13 +408,14 @@ def mac( nonce: Nonce (32 bytes) data: Data to MAC maclen: MAC length (16 or 32, default 16) + into: Buffer to write MAC into (default: bytearray created) Returns: - MAC bytes + 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) + return mac_state.final(maclen, into) class Mac: @@ -460,11 +453,9 @@ class Mac: _lib.aegis256_mac_state_clone(self._st, _other._st) return # Normal init path - nonce = memoryview(nonce) - key = memoryview(key) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") _lib.aegis256_mac_init(self._st, _ptr(key), _ptr(nonce)) @@ -487,8 +478,7 @@ class Mac: Raises: RuntimeError: If the underlying C function reports an error. """ - data = memoryview(data) - rc = _lib.aegis256_mac_update(self._st, _ptr(data), data.nbytes) + 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}") @@ -498,7 +488,7 @@ class Mac: self, maclen: int = ABYTES_MIN, into: Buffer | None = None, - ) -> memoryview: + ) -> bytearray | memoryview: """Finalize and return the MAC tag. Args: @@ -506,7 +496,7 @@ class Mac: into: Optional buffer to write the tag into (default: bytearray created). Returns: - The tag as a memoryview; if ``into`` is provided, it views that buffer. + The tag as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. @@ -514,16 +504,19 @@ class Mac: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - out = into if into is not None else bytearray(maclen) - out = memoryview(out) - if out.nbytes != maclen: - raise TypeError("into length must equal maclen") - rc = _lib.aegis256_mac_final(self._st, ffi.from_buffer(out), maclen) + 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 + return out if into is None else memoryview(out)[:maclen] # type: ignore def verify(self, mac: Buffer) -> bool: """Verify a tag for the current MAC state. @@ -538,8 +531,7 @@ class Mac: TypeError: If tag length is invalid. ValueError: If verification fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + 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) @@ -569,23 +561,20 @@ class Encryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total plaintext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -602,7 +591,9 @@ class Encryptor: """ return self._bytes_out - def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + def update( + self, message: Buffer, into: Buffer | None = None + ) -> bytearray | memoryview: """Encrypt a chunk of the message. Args: @@ -610,28 +601,27 @@ class Encryptor: into: Optional destination buffer; must be >= len(message). Returns: - The ciphertext for this chunk as a memoryview; when ``into`` is - provided, a view of that buffer up to the number of bytes written. + 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. """ - message = memoryview(message) - expected_out = message.nbytes - out = memoryview(into if into is not None else bytearray(expected_out)) - if out.nbytes < expected_out: + 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(message), - message.nbytes, + len(message), ) if rc != 0: err_num = ffi.errno @@ -641,11 +631,13 @@ class Encryptor: ) w = int(written[0]) assert w == expected_out - self._bytes_in += message.nbytes + self._bytes_in += len(message) self._bytes_out += w - return 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) -> memoryview: + def final( + self, into: Buffer | None = None, maclen: int = ABYTES_MIN + ) -> bytearray | memoryview: """Finalize encryption, writing any remaining bytes and the tag. Args: @@ -653,9 +645,7 @@ class Encryptor: maclen: Tag length (16 or 32). Defaults to 16. Returns: - A memoryview of the produced bytes (tail + tag). When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. Raises: TypeError: If maclen is invalid. @@ -663,14 +653,13 @@ class Encryptor: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag - out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) - out = memoryview(out) + # 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), - out.nbytes, + len(out), written, maclen, ) @@ -679,15 +668,18 @@ class Encryptor: 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[: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[, into]) -> returns any remaining plaintext bytes + - final(mac) -> verifies the MAC tag """ __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") @@ -703,23 +695,20 @@ class Decryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total ciphertext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -730,13 +719,10 @@ class Decryptor: @property def bytes_out(self) -> int: - """Total plaintext bytes produced so far. - - Includes bytes written by update() and by final(). - """ + """Total plaintext bytes produced so far.""" return self._bytes_out - def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview: """Process a chunk of ciphertext. Args: @@ -744,75 +730,54 @@ class Decryptor: into: Optional destination buffer; must be >= len(ciphertext). Returns: - A memoryview of the decrypted bytes for this chunk. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + 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. """ - ct = memoryview(ct) - expected_out = ct.nbytes + expected_out = len(ct) out = into if into is not None else bytearray(expected_out) - out = memoryview(out) - if out.nbytes < 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(ct), - ct.nbytes, + 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 - self._bytes_in += ct.nbytes + 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[:w] + return out if into is None else memoryview(out)[:w] # type: ignore - def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: - """Finalize decryption by verifying tag and flushing remaining bytes. + def final(self, mac: Buffer) -> None: + """Finalize decryption by verifying the MAC tag. Args: mac: Tag to verify (16 or 32 bytes). - into: Optional destination buffer for the remaining plaintext bytes. - - Returns: - A memoryview of the remaining plaintext bytes. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. Raises: TypeError: If tag length is invalid. ValueError: If authentication fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - out = into if into is not None else bytearray(TAILBYTES_MAX) - out = memoryview(out) - written = ffi.new("size_t *") rc = _lib.aegis256_state_decrypt_detached_final( - self._st, - ffi.from_buffer(out), - out.nbytes, - written, - _ptr(mac), - maclen, + self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") - w = int(written[0]) - self._bytes_out += w - return out[:w] def new_state(): diff --git a/pyaegis/aegis256x2.py b/pyaegis/aegis256x2.py index 3659f41..97b09e3 100644 --- a/pyaegis/aegis256x2.py +++ b/pyaegis/aegis256x2.py @@ -10,11 +10,10 @@ Error return codes from the C library raise ValueError. # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! import errno -from collections.abc import Buffer from ._loader import ffi from ._loader import lib as _lib -from .util import new_aligned_struct +from .util import Buffer, new_aligned_struct # Constants exposed as functions in C; mirror them as integers at module import time KEYBYTES = _lib.aegis256x2_keybytes() @@ -39,7 +38,7 @@ def encrypt_detached( maclen: int = ABYTES_MIN, ct_into: Buffer | None = None, mac_into: Buffer | None = None, -) -> tuple[memoryview, memoryview]: +) -> tuple[bytearray | memoryview, bytearray | memoryview]: """Encrypt message with associated data, returning ciphertext and MAC separately. Args: @@ -58,35 +57,34 @@ def encrypt_detached( TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - ct_into = memoryview(ct_into) if ct_into is not None else None - mac_into = memoryview(mac_into) if mac_into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) - mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) - if c.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") - if mac.nbytes != maclen: - raise TypeError("mac_into length must equal maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -94,7 +92,10 @@ def encrypt_detached( 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, mac + return ( + c if ct_into is None else memoryview(c)[: len(message)], + mac if mac_into is None else memoryview(mac)[:maclen], + ) def decrypt_detached( @@ -105,7 +106,7 @@ def decrypt_detached( ad: Buffer | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with detached MAC and associated data. Args: @@ -117,44 +118,40 @@ def decrypt_detached( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - mac = memoryview(mac) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if m_out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), _ptr(mac), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return memoryview(m_out) + return out if into is None else memoryview(out)[: len(ct)] # type: ignore def encrypt( @@ -165,7 +162,7 @@ def encrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message with associated data, returning ciphertext with appended MAC. Args: @@ -177,35 +174,32 @@ def encrypt( into: Buffer to write ciphertext+MAC into (default: bytearray created). Returns: - Ciphertext with appended MAC as bytearray if into not provided. + Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) - if out.nbytes != message.nbytes + maclen: - raise TypeError("into length must be len(message)+maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -213,7 +207,7 @@ def encrypt( err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"encrypt failed: {err_name}") - return out + return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore def decrypt( @@ -224,7 +218,7 @@ def decrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with appended MAC and associated data. Args: @@ -236,43 +230,43 @@ def decrypt( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if ct.nbytes < maclen: + if len(ct) < maclen: raise TypeError("ciphertext too short for tag") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) - if m_out.nbytes != ct.nbytes - maclen: - raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return m_out + return out if into is None else memoryview(out)[:expected_out] # type: ignore def stream( @@ -281,7 +275,7 @@ def stream( length: int | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | Buffer: """Generate a stream of pseudorandom bytes. Args: @@ -291,29 +285,30 @@ def stream( into: Buffer to write stream into (default: bytearray created). Returns: - Pseudorandom bytes as bytearray if into not provided. + Pseudorandom bytes as bytearray, or into returned directly. Raises: TypeError: If lengths are invalid or neither length nor into provided. """ - nonce = memoryview(nonce) if nonce is not None else None - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce is not None and nonce.nbytes != NPUBBYTES: + if nonce is not None and len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if into is None and length is None: - raise TypeError("provide either into or length") - out = into if into is not None else memoryview(bytearray(int(length or 0))) + 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), - out.nbytes, + len(out), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: length or len(out)] # type: ignore def encrypt_unauthenticated( @@ -322,7 +317,7 @@ def encrypt_unauthenticated( message: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). Args: @@ -332,31 +327,29 @@ def encrypt_unauthenticated( into: Buffer to write ciphertext into (default: bytearray created). Returns: - Ciphertext as bytearray if into not provided. + Ciphertext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - message = memoryview(message) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes)) - if out.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") + 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), - message.nbytes, + len(message), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(message)] # type: ignore def decrypt_unauthenticated( @@ -365,7 +358,7 @@ def decrypt_unauthenticated( ct: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). Args: @@ -375,31 +368,29 @@ def decrypt_unauthenticated( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - ct = memoryview(ct) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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), - ct.nbytes, + len(ct), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(ct)] # type: ignore # This is missing from C API but convenient to have here @@ -408,7 +399,8 @@ def mac( nonce: Buffer, data: Buffer, maclen: int = ABYTES_MIN, -) -> memoryview: + into: Buffer | None = None, +) -> bytearray | memoryview: """Compute a MAC for the given data in one shot. Args: @@ -416,13 +408,14 @@ def mac( nonce: Nonce (32 bytes) data: Data to MAC maclen: MAC length (16 or 32, default 16) + into: Buffer to write MAC into (default: bytearray created) Returns: - MAC bytes + 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) + return mac_state.final(maclen, into) class Mac: @@ -460,11 +453,9 @@ class Mac: _lib.aegis256x2_mac_state_clone(self._st, _other._st) return # Normal init path - nonce = memoryview(nonce) - key = memoryview(key) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") _lib.aegis256x2_mac_init(self._st, _ptr(key), _ptr(nonce)) @@ -487,8 +478,7 @@ class Mac: Raises: RuntimeError: If the underlying C function reports an error. """ - data = memoryview(data) - rc = _lib.aegis256x2_mac_update(self._st, _ptr(data), data.nbytes) + 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}") @@ -498,7 +488,7 @@ class Mac: self, maclen: int = ABYTES_MIN, into: Buffer | None = None, - ) -> memoryview: + ) -> bytearray | memoryview: """Finalize and return the MAC tag. Args: @@ -506,7 +496,7 @@ class Mac: into: Optional buffer to write the tag into (default: bytearray created). Returns: - The tag as a memoryview; if ``into`` is provided, it views that buffer. + The tag as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. @@ -514,16 +504,19 @@ class Mac: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - out = into if into is not None else bytearray(maclen) - out = memoryview(out) - if out.nbytes != maclen: - raise TypeError("into length must equal maclen") - rc = _lib.aegis256x2_mac_final(self._st, ffi.from_buffer(out), maclen) + 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 + return out if into is None else memoryview(out)[:maclen] # type: ignore def verify(self, mac: Buffer) -> bool: """Verify a tag for the current MAC state. @@ -538,8 +531,7 @@ class Mac: TypeError: If tag length is invalid. ValueError: If verification fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + 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) @@ -569,23 +561,20 @@ class Encryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total plaintext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -602,7 +591,9 @@ class Encryptor: """ return self._bytes_out - def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + def update( + self, message: Buffer, into: Buffer | None = None + ) -> bytearray | memoryview: """Encrypt a chunk of the message. Args: @@ -610,28 +601,27 @@ class Encryptor: into: Optional destination buffer; must be >= len(message). Returns: - The ciphertext for this chunk as a memoryview; when ``into`` is - provided, a view of that buffer up to the number of bytes written. + 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. """ - message = memoryview(message) - expected_out = message.nbytes - out = memoryview(into if into is not None else bytearray(expected_out)) - if out.nbytes < expected_out: + 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(message), - message.nbytes, + len(message), ) if rc != 0: err_num = ffi.errno @@ -641,11 +631,13 @@ class Encryptor: ) w = int(written[0]) assert w == expected_out - self._bytes_in += message.nbytes + self._bytes_in += len(message) self._bytes_out += w - return 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) -> memoryview: + def final( + self, into: Buffer | None = None, maclen: int = ABYTES_MIN + ) -> bytearray | memoryview: """Finalize encryption, writing any remaining bytes and the tag. Args: @@ -653,9 +645,7 @@ class Encryptor: maclen: Tag length (16 or 32). Defaults to 16. Returns: - A memoryview of the produced bytes (tail + tag). When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. Raises: TypeError: If maclen is invalid. @@ -663,14 +653,13 @@ class Encryptor: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag - out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) - out = memoryview(out) + # 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), - out.nbytes, + len(out), written, maclen, ) @@ -679,15 +668,18 @@ class Encryptor: 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[: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[, into]) -> returns any remaining plaintext bytes + - final(mac) -> verifies the MAC tag """ __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") @@ -703,23 +695,20 @@ class Decryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total ciphertext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -730,13 +719,10 @@ class Decryptor: @property def bytes_out(self) -> int: - """Total plaintext bytes produced so far. - - Includes bytes written by update() and by final(). - """ + """Total plaintext bytes produced so far.""" return self._bytes_out - def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview: """Process a chunk of ciphertext. Args: @@ -744,75 +730,54 @@ class Decryptor: into: Optional destination buffer; must be >= len(ciphertext). Returns: - A memoryview of the decrypted bytes for this chunk. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + 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. """ - ct = memoryview(ct) - expected_out = ct.nbytes + expected_out = len(ct) out = into if into is not None else bytearray(expected_out) - out = memoryview(out) - if out.nbytes < 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(ct), - ct.nbytes, + 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 - self._bytes_in += ct.nbytes + 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[:w] + return out if into is None else memoryview(out)[:w] # type: ignore - def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: - """Finalize decryption by verifying tag and flushing remaining bytes. + def final(self, mac: Buffer) -> None: + """Finalize decryption by verifying the MAC tag. Args: mac: Tag to verify (16 or 32 bytes). - into: Optional destination buffer for the remaining plaintext bytes. - - Returns: - A memoryview of the remaining plaintext bytes. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. Raises: TypeError: If tag length is invalid. ValueError: If authentication fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - out = into if into is not None else bytearray(TAILBYTES_MAX) - out = memoryview(out) - written = ffi.new("size_t *") rc = _lib.aegis256x2_state_decrypt_detached_final( - self._st, - ffi.from_buffer(out), - out.nbytes, - written, - _ptr(mac), - maclen, + self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") - w = int(written[0]) - self._bytes_out += w - return out[:w] def new_state(): diff --git a/pyaegis/aegis256x4.py b/pyaegis/aegis256x4.py index c0fba41..ac956e7 100644 --- a/pyaegis/aegis256x4.py +++ b/pyaegis/aegis256x4.py @@ -10,11 +10,10 @@ Error return codes from the C library raise ValueError. # DO NOT EDIT OTHER ALGORITHM FILES MANUALLY! import errno -from collections.abc import Buffer from ._loader import ffi from ._loader import lib as _lib -from .util import new_aligned_struct +from .util import Buffer, new_aligned_struct # Constants exposed as functions in C; mirror them as integers at module import time KEYBYTES = _lib.aegis256x4_keybytes() @@ -39,7 +38,7 @@ def encrypt_detached( maclen: int = ABYTES_MIN, ct_into: Buffer | None = None, mac_into: Buffer | None = None, -) -> tuple[memoryview, memoryview]: +) -> tuple[bytearray | memoryview, bytearray | memoryview]: """Encrypt message with associated data, returning ciphertext and MAC separately. Args: @@ -58,35 +57,34 @@ def encrypt_detached( TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - ct_into = memoryview(ct_into) if ct_into is not None else None - mac_into = memoryview(mac_into) if mac_into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - c = ct_into if ct_into is not None else memoryview(bytearray(message.nbytes)) - mac = mac_into if mac_into is not None else memoryview(bytearray(maclen)) - if c.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") - if mac.nbytes != maclen: - raise TypeError("mac_into length must equal maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -94,7 +92,10 @@ def encrypt_detached( 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, mac + return ( + c if ct_into is None else memoryview(c)[: len(message)], + mac if mac_into is None else memoryview(mac)[:maclen], + ) def decrypt_detached( @@ -105,7 +106,7 @@ def decrypt_detached( ad: Buffer | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with detached MAC and associated data. Args: @@ -117,44 +118,40 @@ def decrypt_detached( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - mac = memoryview(mac) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if m_out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), _ptr(mac), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return memoryview(m_out) + return out if into is None else memoryview(out)[: len(ct)] # type: ignore def encrypt( @@ -165,7 +162,7 @@ def encrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message with associated data, returning ciphertext with appended MAC. Args: @@ -177,35 +174,32 @@ def encrypt( into: Buffer to write ciphertext+MAC into (default: bytearray created). Returns: - Ciphertext with appended MAC as bytearray if into not provided. + Ciphertext with appended MAC as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. RuntimeError: If encryption fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - message = memoryview(message) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes + maclen)) - if out.nbytes != message.nbytes + maclen: - raise TypeError("into length must be len(message)+maclen") + 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), - message.nbytes, + len(message), _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) @@ -213,7 +207,7 @@ def encrypt( err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"encrypt failed: {err_name}") - return out + return out if into is None else memoryview(out)[: len(message) + maclen] # type: ignore def decrypt( @@ -224,7 +218,7 @@ def decrypt( *, maclen: int = ABYTES_MIN, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext with appended MAC and associated data. Args: @@ -236,43 +230,43 @@ def decrypt( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. ValueError: If authentication fails. """ - nonce = memoryview(nonce) - key = memoryview(key) - ct = memoryview(ct) - ad = memoryview(ad) if ad is not None else None - into = memoryview(into) if into is not None else None - if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if ct.nbytes < maclen: + if len(ct) < maclen: raise TypeError("ciphertext too short for tag") - m_out = into if into is not None else memoryview(bytearray(ct.nbytes - maclen)) - if m_out.nbytes != ct.nbytes - maclen: - raise TypeError("into length must be len(ciphertext_with_tag)-maclen") + 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(m_out), + ffi.from_buffer(out), _ptr(ct), - ct.nbytes, + len(ct), maclen, _ptr(ad), - 0 if ad is None else ad.nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) if rc != 0: raise ValueError("authentication failed") - return m_out + return out if into is None else memoryview(out)[:expected_out] # type: ignore def stream( @@ -281,7 +275,7 @@ def stream( length: int | None = None, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | Buffer: """Generate a stream of pseudorandom bytes. Args: @@ -291,29 +285,30 @@ def stream( into: Buffer to write stream into (default: bytearray created). Returns: - Pseudorandom bytes as bytearray if into not provided. + Pseudorandom bytes as bytearray, or into returned directly. Raises: TypeError: If lengths are invalid or neither length nor into provided. """ - nonce = memoryview(nonce) if nonce is not None else None - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce is not None and nonce.nbytes != NPUBBYTES: + if nonce is not None and len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - if into is None and length is None: - raise TypeError("provide either into or length") - out = into if into is not None else memoryview(bytearray(int(length or 0))) + 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), - out.nbytes, + len(out), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: length or len(out)] # type: ignore def encrypt_unauthenticated( @@ -322,7 +317,7 @@ def encrypt_unauthenticated( message: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). Args: @@ -332,31 +327,29 @@ def encrypt_unauthenticated( into: Buffer to write ciphertext into (default: bytearray created). Returns: - Ciphertext as bytearray if into not provided. + Ciphertext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - message = memoryview(message) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(message.nbytes)) - if out.nbytes != message.nbytes: - raise TypeError("into length must equal len(message)") + 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), - message.nbytes, + len(message), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(message)] # type: ignore def decrypt_unauthenticated( @@ -365,7 +358,7 @@ def decrypt_unauthenticated( ct: Buffer, *, into: Buffer | None = None, -) -> memoryview: +) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). Args: @@ -375,31 +368,29 @@ def decrypt_unauthenticated( into: Buffer to write plaintext into (default: bytearray created). Returns: - Plaintext as bytearray if into not provided. + Plaintext as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. """ - ct = memoryview(ct) - nonce = memoryview(nonce) - key = memoryview(key) - into = memoryview(into) if into is not None else None - - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") - out = into if into is not None else memoryview(bytearray(ct.nbytes)) - if out.nbytes != ct.nbytes: - raise TypeError("into length must equal len(ciphertext)") + 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), - ct.nbytes, + len(ct), _ptr(nonce), _ptr(key), ) - return out + return out if into is None else memoryview(out)[: len(ct)] # type: ignore # This is missing from C API but convenient to have here @@ -408,7 +399,8 @@ def mac( nonce: Buffer, data: Buffer, maclen: int = ABYTES_MIN, -) -> memoryview: + into: Buffer | None = None, +) -> bytearray | memoryview: """Compute a MAC for the given data in one shot. Args: @@ -416,13 +408,14 @@ def mac( nonce: Nonce (32 bytes) data: Data to MAC maclen: MAC length (16 or 32, default 16) + into: Buffer to write MAC into (default: bytearray created) Returns: - MAC bytes + 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) + return mac_state.final(maclen, into) class Mac: @@ -460,11 +453,9 @@ class Mac: _lib.aegis256x4_mac_state_clone(self._st, _other._st) return # Normal init path - nonce = memoryview(nonce) - key = memoryview(key) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + if len(nonce) != NPUBBYTES: raise TypeError(f"nonce length must be {NPUBBYTES}") _lib.aegis256x4_mac_init(self._st, _ptr(key), _ptr(nonce)) @@ -487,8 +478,7 @@ class Mac: Raises: RuntimeError: If the underlying C function reports an error. """ - data = memoryview(data) - rc = _lib.aegis256x4_mac_update(self._st, _ptr(data), data.nbytes) + 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}") @@ -498,7 +488,7 @@ class Mac: self, maclen: int = ABYTES_MIN, into: Buffer | None = None, - ) -> memoryview: + ) -> bytearray | memoryview: """Finalize and return the MAC tag. Args: @@ -506,7 +496,7 @@ class Mac: into: Optional buffer to write the tag into (default: bytearray created). Returns: - The tag as a memoryview; if ``into`` is provided, it views that buffer. + The tag as bytearray if into not provided, memoryview of into otherwise. Raises: TypeError: If lengths are invalid. @@ -514,16 +504,19 @@ class Mac: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - out = into if into is not None else bytearray(maclen) - out = memoryview(out) - if out.nbytes != maclen: - raise TypeError("into length must equal maclen") - rc = _lib.aegis256x4_mac_final(self._st, ffi.from_buffer(out), maclen) + 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 + return out if into is None else memoryview(out)[:maclen] # type: ignore def verify(self, mac: Buffer) -> bool: """Verify a tag for the current MAC state. @@ -538,8 +531,7 @@ class Mac: TypeError: If tag length is invalid. ValueError: If verification fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + 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) @@ -569,23 +561,20 @@ class Encryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total plaintext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -602,7 +591,9 @@ class Encryptor: """ return self._bytes_out - def update(self, message: Buffer, into: Buffer | None = None) -> memoryview: + def update( + self, message: Buffer, into: Buffer | None = None + ) -> bytearray | memoryview: """Encrypt a chunk of the message. Args: @@ -610,28 +601,27 @@ class Encryptor: into: Optional destination buffer; must be >= len(message). Returns: - The ciphertext for this chunk as a memoryview; when ``into`` is - provided, a view of that buffer up to the number of bytes written. + 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. """ - message = memoryview(message) - expected_out = message.nbytes - out = memoryview(into if into is not None else bytearray(expected_out)) - if out.nbytes < expected_out: + 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(message), - message.nbytes, + len(message), ) if rc != 0: err_num = ffi.errno @@ -641,11 +631,13 @@ class Encryptor: ) w = int(written[0]) assert w == expected_out - self._bytes_in += message.nbytes + self._bytes_in += len(message) self._bytes_out += w - return 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) -> memoryview: + def final( + self, into: Buffer | None = None, maclen: int = ABYTES_MIN + ) -> bytearray | memoryview: """Finalize encryption, writing any remaining bytes and the tag. Args: @@ -653,9 +645,7 @@ class Encryptor: maclen: Tag length (16 or 32). Defaults to 16. Returns: - A memoryview of the produced bytes (tail + tag). When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. Raises: TypeError: If maclen is invalid. @@ -663,14 +653,13 @@ class Encryptor: """ if maclen not in (16, 32): raise TypeError("maclen must be 16 or 32") - # Worst-case final length is leftover tail (<= TAILBYTES_MAX) plus tag - out = into if into is not None else bytearray(TAILBYTES_MAX + maclen) - out = memoryview(out) + # 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), - out.nbytes, + len(out), written, maclen, ) @@ -679,15 +668,18 @@ class Encryptor: 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[: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[, into]) -> returns any remaining plaintext bytes + - final(mac) -> verifies the MAC tag """ __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") @@ -703,23 +695,20 @@ class Decryptor: Raises: TypeError: If key or nonce lengths are invalid. """ - key = memoryview(key) - nonce = memoryview(nonce) - if key.nbytes != KEYBYTES: + if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") - if nonce.nbytes != NPUBBYTES: + 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 memoryview(ad).nbytes, + 0 if ad is None else len(ad), _ptr(nonce), _ptr(key), ) self._st = st self._owner = owner - # Track total ciphertext bytes passed through update() so far self._bytes_in = 0 self._bytes_out = 0 @@ -730,13 +719,10 @@ class Decryptor: @property def bytes_out(self) -> int: - """Total plaintext bytes produced so far. - - Includes bytes written by update() and by final(). - """ + """Total plaintext bytes produced so far.""" return self._bytes_out - def update(self, ct: Buffer, into: Buffer | None = None) -> memoryview: + def update(self, ct: Buffer, into: Buffer | None = None) -> bytearray | memoryview: """Process a chunk of ciphertext. Args: @@ -744,75 +730,54 @@ class Decryptor: into: Optional destination buffer; must be >= len(ciphertext). Returns: - A memoryview of the decrypted bytes for this chunk. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. + 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. """ - ct = memoryview(ct) - expected_out = ct.nbytes + expected_out = len(ct) out = into if into is not None else bytearray(expected_out) - out = memoryview(out) - if out.nbytes < 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), - out.nbytes, + ffi.from_buffer(out_mv), + len(out_mv), written, _ptr(ct), - ct.nbytes, + 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 - self._bytes_in += ct.nbytes + 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[:w] + return out if into is None else memoryview(out)[:w] # type: ignore - def final(self, mac: Buffer, into: Buffer | None = None) -> memoryview: - """Finalize decryption by verifying tag and flushing remaining bytes. + def final(self, mac: Buffer) -> None: + """Finalize decryption by verifying the MAC tag. Args: mac: Tag to verify (16 or 32 bytes). - into: Optional destination buffer for the remaining plaintext bytes. - - Returns: - A memoryview of the remaining plaintext bytes. When ``into`` is - provided, the returned view references that buffer up to the number - of bytes written. Raises: TypeError: If tag length is invalid. ValueError: If authentication fails. """ - mac = memoryview(mac) - maclen = mac.nbytes + maclen = len(mac) if maclen not in (16, 32): raise TypeError("mac length must be 16 or 32") - out = into if into is not None else bytearray(TAILBYTES_MAX) - out = memoryview(out) - written = ffi.new("size_t *") rc = _lib.aegis256x4_state_decrypt_detached_final( - self._st, - ffi.from_buffer(out), - out.nbytes, - written, - _ptr(mac), - maclen, + self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") - w = int(written[0]) - self._bytes_out += w - return out[:w] def new_state(): diff --git a/pyaegis/util.py b/pyaegis/util.py index ee61fcd..41ad4f0 100644 --- a/pyaegis/util.py +++ b/pyaegis/util.py @@ -6,9 +6,22 @@ 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"] +__all__ = ["new_aligned_struct", "aligned_address", "Buffer"] + +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: diff --git a/tests/test_encrypt.py b/tests/test_encrypt.py index a60aab7..333afc9 100644 --- a/tests/test_encrypt.py +++ b/tests/test_encrypt.py @@ -174,8 +174,7 @@ def test_encrypt_decrypt_incremental(vector): pt_chunks = [] for chunk in random_split_bytes(computed_ct): pt_chunks.append(bytes(decryptor.update(chunk))) - final_pt = decryptor.final(expected_tag128) - pt_chunks.append(bytes(final_pt)) + decryptor.final(expected_tag128) # Combine plaintext chunks computed_pt = b"".join(pt_chunks) @@ -215,8 +214,7 @@ def test_encrypt_decrypt_incremental(vector): pt_chunks = [] for chunk in random_split_bytes(computed_ct): pt_chunks.append(bytes(decryptor.update(chunk))) - final_pt = decryptor.final(expected_tag256) - pt_chunks.append(bytes(final_pt)) + decryptor.final(expected_tag256) # Combine plaintext chunks computed_pt = b"".join(pt_chunks)