From eed36e2463b45e043bb10a1868e6a96c65e51256 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sun, 9 Nov 2025 06:53:27 +0000 Subject: [PATCH] API updates: - Mac class follows hashlib API: digest functions added and finalization no longer modifies state. - Encryptor and Decryptor now raise RuntimeError if still used after final. Documentation updated with the changes and further examples. Tests updated with the changes, new test module for error cases (test_raises). Docstrings improved. --- README.md | 59 ++++++--- pyaegis/aegis128l.py | 255 +++++++++++++++++++++----------------- pyaegis/aegis128x2.py | 255 +++++++++++++++++++++----------------- pyaegis/aegis128x4.py | 255 +++++++++++++++++++++----------------- pyaegis/aegis256.py | 255 +++++++++++++++++++++----------------- pyaegis/aegis256x2.py | 255 +++++++++++++++++++++----------------- pyaegis/aegis256x4.py | 255 +++++++++++++++++++++----------------- pyaegis/benchmark.py | 4 +- tests/test_encrypt.py | 20 ++- tests/test_mac.py | 9 +- tests/test_raises.py | 280 ++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 1191 insertions(+), 711 deletions(-) create mode 100644 tests/test_raises.py diff --git a/README.md b/README.md index fa1756a..5d4be93 100644 --- a/README.md +++ b/README.md @@ -70,22 +70,26 @@ No MAC tag, vulnerable to alterations: ### Incremental AEAD Stateful classes that can be used for processing the data in separate chunks: -- Encryptor(key, nonce, ad=None) +- Encryptor(key, nonce, ad=None, maclen=16) - update(message[, into]) -> ciphertext_chunk - - final([into], maclen=16) -> mac_tag -- Decryptor(key, nonce, ad=None) + - final([into]) -> mac_tag +- Decryptor(key, nonce, ad=None, maclen=16) - update(ct_chunk[, into]) -> plaintext_chunk - - final(mac) -> None (raises ValueError on failure) + - final(mac) -> raises ValueError on failure ### Message Authentication Code No encryption, but prevents changes to the data without the correct key. - mac(key, nonce, data, maclen=16, into=None) -> mac -- Mac(key, nonce) +- Mac(key, nonce, maclen=16) - update(data) - - final(maclen=16[, into]) -> mac - - verify(mac) -> bool (True on success; raises ValueError on failure) + - final([into]) -> mac + - verify(mac) -> raises ValueError on failure + - digest() -> bytes + - hexdigest() -> str + +The `Mac` class follows the Python hashlib API for compatibility with code expecting hash objects. Finalizing does not alter the state, so further updates appending to the already input data can be issued even after calling the other methods that calculate the MAC. ### Keystream generation @@ -119,12 +123,18 @@ from pyaegis import aegis256x4 as ciph key, nonce = ciph.random_key(), bytes(ciph.NONCEBYTES) mac = ciph.mac(key, nonce, b"message", maclen=32) -print(mac) +print(mac.hex()) -st = ciph.Mac(key, nonce) -st.update(b"message") -st.update(b"Mallory Says Hello!") -st.verify(mac) # Raises ValueError +# Alternative class-based API +a = ciph.Mac(key, nonce, maclen=32) +a.update(b"message") +print(a.hexdigest()) + +# Verification +b = ciph.Mac(key, nonce, maclen=32) +b.update(b"message") +b.update(b"Mallory Says Hello!") +b.verify(mac) # Raises ValueError ``` ### Detached mode encryption and decryption @@ -151,12 +161,12 @@ Class-based interface for incremental updates is an alternative to the one-shot from pyaegis import aegis256x4 as ciph key, nonce = ciph.random_key(), ciph.random_nonce() -enc = ciph.Encryptor(key, nonce, ad=b"header") +enc = ciph.Encryptor(key, nonce, ad=b"header", maclen=16) c1 = enc.update(b"chunk1") c2 = enc.update(b"chunk2") -mac = enc.final(maclen=16) +mac = enc.final() -dec = ciph.Decryptor(key, nonce, ad=b"header") +dec = ciph.Decryptor(key, nonce, ad=b"header", maclen=16) p1 = dec.update(c1) p2 = dec.update(c2) dec.final(mac) # raises ValueError on failure @@ -204,6 +214,25 @@ with open("encrypted.bin", "rb") as f: print(pt) ``` +### Random generator + +The stream generator is much faster than any traditional random number generator, cryptographically secure and seekable. Use `random_key()` for unpredictable output. + +```python +from pyaegis import aegis128x4 as ciph + +key = b"SeedForReplay001" # A non-random deterministic seed (16 bytes) +nonce = bytearray(ciph.NONCEBYTES) # All-zeroes nonce + +# Generate multiple blocks of pseudorandom data +for i in range(5): + rand = ciph.stream(key, nonce, 10) + print(f"Block {int.from_bytes(nonce, "little")}: {rand.hex()}") + ciph.nonce_increment(nonce) +``` + +Note: this is seekable by converting the block number to nonce with `idx.to_bytes(ciph.NONCEBYTES, "little")`, given some fixed block size (e.g. 1 MiB). + ### Preallocated output buffers (into=) For advanced use cases, the output buffer can be supplied with `into` kwarg. Any type of writable buffer with len() >= space required can be used. This includes bytearrays, memoryviews, mmap files, numpy.getbuffer etc. diff --git a/pyaegis/aegis128l.py b/pyaegis/aegis128l.py index 892006c..8862a0c 100644 --- a/pyaegis/aegis128l.py +++ b/pyaegis/aegis128l.py @@ -9,7 +9,7 @@ from ._loader import ffi from ._loader import lib as _lib from .util import Buffer, new_aligned_struct, nonce_increment, wipe -NAME = "AEGIS-128L" #: Algorithm name +NAME = "AEGIS-128L" #: Algorithm display name KEYBYTES = 16 #: Key size in bytes (varies by algorithm) NONCEBYTES = 16 #: Nonce size in bytes (varies by algorithm) MACBYTES = 16 #: Normal MAC size (always 16) @@ -20,15 +20,22 @@ RATE = 64 #: Byte chunk size in internal processing def random_key() -> bytearray: """ - Generate a random key using cryptographically secure random bytes. + Generate a secret key using cryptographically secure random bytes. - It is recommended to wipe() the key after no longer needed to keep it secret. + It is recommended to wipe() the key after no longer needed. """ return bytearray(secrets.token_bytes(KEYBYTES)) def random_nonce() -> bytearray: - """Generate a random nonce using cryptographically secure random bytes.""" + """ + Generate a public nonce using cryptographically secure random bytes. + + Nonces (a number used once) are public data that may be sent together + with the ciphertext, but they need to be unique for each use. + + See also: nonce_increment() can be used to derive sequential nonces. + """ return bytearray(secrets.token_bytes(NONCEBYTES)) @@ -46,11 +53,11 @@ def encrypt_detached( ct_into: Buffer | None = None, mac_into: Buffer | None = None, ) -> tuple[bytearray | memoryview, bytearray | memoryview]: - f"""Encrypt message with associated data, returning ciphertext and MAC separately. + """Encrypt message with associated data, returning ciphertext and MAC separately. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -114,11 +121,11 @@ def decrypt_detached( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with detached MAC and associated data. + """Decrypt ciphertext with detached MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. mac: The MAC to verify. ad: Associated data (optional). @@ -170,11 +177,11 @@ def encrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message with associated data, returning ciphertext with appended MAC. + """Encrypt message with associated data, returning ciphertext with appended MAC. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -226,11 +233,11 @@ def decrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with appended MAC and associated data. + """Decrypt ciphertext with appended MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext with MAC to decrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -283,11 +290,11 @@ def stream( *, into: Buffer | None = None, ) -> bytearray | Buffer: - f"""Generate a stream of pseudorandom bytes. + """Generate a stream of pseudorandom bytes. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}, uses zeroes for nonce if None). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None). length: Number of bytes to generate (required if into is None). into: Buffer to write stream into (default: bytearray created). @@ -325,11 +332,11 @@ def encrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message without authentication (for testing/debugging). + """Encrypt message without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. into: Buffer to write ciphertext into (default: bytearray created). @@ -366,11 +373,11 @@ def decrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext without authentication (for testing/debugging). + """Decrypt ciphertext without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. into: Buffer to write plaintext into (default: bytearray created). @@ -408,11 +415,11 @@ def mac( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Compute a MAC for the given data in one shot. + """Compute a MAC for the given data in one shot. Args: - key: Key ({KEYBYTES=}) - nonce: Nonce ({NONCEBYTES=}) + key: Secret key (generate with random_key()) + nonce: Public nonce (generate with random_nonce()) data: Data to MAC maclen: MAC length (16 or 32, default 16) into: Buffer to write MAC into (default: bytearray created) @@ -420,70 +427,59 @@ def mac( Returns: MAC bytes as bytearray if into not provided, memoryview of into otherwise """ - mac_state = Mac(key, nonce) + mac_state = Mac(key, nonce, maclen) mac_state.update(data) - return mac_state.final(maclen, into) + return mac_state.final(into) class Mac: """AEGIS-128L MAC state wrapper. - Usage: - mac = Mac(key, nonce) - mac.update(data) - tag = mac.final() # defaults to 16-byte MAC - # or verify: - mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag) + Example: + a = Mac(key, nonce) + a.update(data) + mac = a.final() """ - __slots__ = ("_st", "_owner") + __slots__ = ("_st", "_owner", "_maclen") - def __init__( - self, - key: Buffer, - nonce: Buffer, - _other=None, - ) -> None: - f"""Initialize a MAC state with a nonce and key. - - Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None: + """Create a MAC with the given key, nonce, and tag length. Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ - st, owner = new_aligned_struct("aegis128l_mac_state", ALIGNMENT) - self._st = st - self._owner = owner - if _other is not None: # clone path - _lib.aegis128l_mac_state_clone(self._st, _other._st) - return - # Normal init path + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: raise TypeError(f"nonce length must be {NONCEBYTES}") + + self._maclen = maclen + st, owner = new_aligned_struct("aegis128l_mac_state", ALIGNMENT) + self._st = st + self._owner = owner _lib.aegis128l_mac_init(self._st, _ptr(key), _ptr(nonce)) def __deepcopy__(self) -> "Mac": """Return a clone of current MAC state.""" - return Mac(b"", b"", _other=self) + clone = object.__new__(Mac) + clone._maclen = self._maclen + clone._st, clone._owner = new_aligned_struct("aegis128l_mac_state", ALIGNMENT) + _lib.aegis128l_mac_state_clone(clone._st, self._st) + return clone clone = __deepcopy__ def reset(self) -> None: - """Reset the MAC state so it can be reused with the same nonce and key.""" + """Reset back to the original state, prior to any updates.""" _lib.aegis128l_mac_reset(self._st) def update(self, data: Buffer) -> None: - """Absorb data into the MAC state. + """Update the MAC state with more data. - Args: - data: Bytes-like object to authenticate. - - Raises: - RuntimeError: If the underlying C function reports an error. + Repeated calls to update() are equivalent to a single call with the concatenated data. """ rc = _lib.aegis128l_mac_update(self._st, _ptr(data), len(data)) if rc != 0: @@ -491,15 +487,13 @@ class Mac: err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac update failed: {err_name}") - def final( - self, - maclen: int = MACBYTES, - into: Buffer | None = None, - ) -> bytearray | memoryview: - """Finalize and return the MAC tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Calculate and return the MAC tag for the currently input data. + + Unlike the C library, this method does not alter the current state, + allowing for multiple calls and further updates on the same object. Args: - maclen: Tag length in bytes (16 or 32). Defaults to 16. into: Optional buffer to write the tag into (default: bytearray created). Returns: @@ -509,30 +503,36 @@ class Mac: TypeError: If lengths are invalid. RuntimeError: If finalization fails in the C library. """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + maclen = self._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) + + rc = _lib.aegis128l_mac_final(self.clone()._st, ffi.from_buffer(out), maclen) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac final failed: {err_name}") return out if into is None else memoryview(out)[:maclen] # type: ignore + def digest(self) -> bytes: + """Calculate and return the MAC tag as bytes.""" + return bytes(self.final()) + + def hexdigest(self) -> str: + """Calculate and return the MAC tag as a hex string.""" + return self.digest().hex() + def verify(self, mac: Buffer): - """Verify a tag for the current MAC state. + """Verify that the data entered so far matches the given MAC tag. + + Unlike the C library, this method does not alter the current state. Args: - mac: The tag to verify (16 or 32 bytes). - - Returns: - Only if verification succeeds. + mac: The tag to verify against (16 or 32 bytes). Raises: TypeError: If tag length is invalid. @@ -541,7 +541,9 @@ class Mac: 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) + + cloned = self.clone() + rc = _lib.aegis128l_mac_verify(cloned._st, _ptr(mac), maclen) if rc != 0: raise ValueError("mac verification failed") @@ -550,23 +552,31 @@ class Encryptor: """Incremental encryptor. - update(message[, into]) -> returns produced ciphertext bytes - - final([into], maclen=16) -> returns tail+tag bytes - - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + - final([into]) -> returns MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental encryptor. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental encryptor. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). ad: Associated data to bind to the encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -583,6 +593,7 @@ class Encryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -611,8 +622,10 @@ class Encryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(message) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -641,24 +654,21 @@ class Encryptor: self._bytes_out += w return out if into is None else memoryview(out)[:w] # type: ignore - def final( - self, into: Buffer | None = None, maclen: int = MACBYTES - ) -> bytearray | memoryview: - """Finalize encryption, writing any remaining bytes and the tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Finalize encryption and return the authentication tag. Args: - into: Optional destination buffer for the tail and tag. - maclen: Tag length (16 or 32). Defaults to 16. + into: Optional destination buffer for the tag. Returns: - A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. + The authentication tag as bytearray if into not provided, memoryview of into otherwise. Raises: - TypeError: If maclen is invalid. - RuntimeError: If the C final call fails. + RuntimeError: If the C final call fails or if called after final(). """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen # 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 *") @@ -678,6 +688,8 @@ class Encryptor: # Only the tag bytes are returned when we allocate the buffer assert w == maclen self._bytes_out += w + self._st = None + self._owner = None return out if into is None else memoryview(out)[:w] # type: ignore @@ -688,19 +700,28 @@ class Decryptor: - final(mac) -> verifies the MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental decryptor for detached tags. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental decryptor for detached tags. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ad: Associated data used during encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -717,6 +738,7 @@ class Decryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -740,8 +762,10 @@ class Decryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(ct) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -770,20 +794,25 @@ class Decryptor: """Finalize decryption by verifying the MAC tag. Args: - mac: Tag to verify (16 or 32 bytes). + mac: Tag to verify. Raises: - TypeError: If tag length is invalid. + TypeError: If tag length doesn't match the expected maclen. ValueError: If authentication fails. + RuntimeError: If called after final(). """ - maclen = len(mac) - if maclen not in (16, 32): - raise TypeError("mac length must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen + if len(mac) != maclen: + raise TypeError(f"mac length must be {maclen}") rc = _lib.aegis128l_state_decrypt_detached_final( self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") + self._st = None + self._owner = None def new_state(): diff --git a/pyaegis/aegis128x2.py b/pyaegis/aegis128x2.py index 4d126af..3c1a04c 100644 --- a/pyaegis/aegis128x2.py +++ b/pyaegis/aegis128x2.py @@ -9,7 +9,7 @@ from ._loader import ffi from ._loader import lib as _lib from .util import Buffer, new_aligned_struct, nonce_increment, wipe -NAME = "AEGIS-128X2" #: Algorithm name +NAME = "AEGIS-128X2" #: Algorithm display name KEYBYTES = 16 #: Key size in bytes (varies by algorithm) NONCEBYTES = 16 #: Nonce size in bytes (varies by algorithm) MACBYTES = 16 #: Normal MAC size (always 16) @@ -20,15 +20,22 @@ RATE = 64 #: Byte chunk size in internal processing def random_key() -> bytearray: """ - Generate a random key using cryptographically secure random bytes. + Generate a secret key using cryptographically secure random bytes. - It is recommended to wipe() the key after no longer needed to keep it secret. + It is recommended to wipe() the key after no longer needed. """ return bytearray(secrets.token_bytes(KEYBYTES)) def random_nonce() -> bytearray: - """Generate a random nonce using cryptographically secure random bytes.""" + """ + Generate a public nonce using cryptographically secure random bytes. + + Nonces (a number used once) are public data that may be sent together + with the ciphertext, but they need to be unique for each use. + + See also: nonce_increment() can be used to derive sequential nonces. + """ return bytearray(secrets.token_bytes(NONCEBYTES)) @@ -46,11 +53,11 @@ def encrypt_detached( ct_into: Buffer | None = None, mac_into: Buffer | None = None, ) -> tuple[bytearray | memoryview, bytearray | memoryview]: - f"""Encrypt message with associated data, returning ciphertext and MAC separately. + """Encrypt message with associated data, returning ciphertext and MAC separately. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -114,11 +121,11 @@ def decrypt_detached( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with detached MAC and associated data. + """Decrypt ciphertext with detached MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. mac: The MAC to verify. ad: Associated data (optional). @@ -170,11 +177,11 @@ def encrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message with associated data, returning ciphertext with appended MAC. + """Encrypt message with associated data, returning ciphertext with appended MAC. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -226,11 +233,11 @@ def decrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with appended MAC and associated data. + """Decrypt ciphertext with appended MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext with MAC to decrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -283,11 +290,11 @@ def stream( *, into: Buffer | None = None, ) -> bytearray | Buffer: - f"""Generate a stream of pseudorandom bytes. + """Generate a stream of pseudorandom bytes. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}, uses zeroes for nonce if None). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None). length: Number of bytes to generate (required if into is None). into: Buffer to write stream into (default: bytearray created). @@ -325,11 +332,11 @@ def encrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message without authentication (for testing/debugging). + """Encrypt message without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. into: Buffer to write ciphertext into (default: bytearray created). @@ -366,11 +373,11 @@ def decrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext without authentication (for testing/debugging). + """Decrypt ciphertext without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. into: Buffer to write plaintext into (default: bytearray created). @@ -408,11 +415,11 @@ def mac( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Compute a MAC for the given data in one shot. + """Compute a MAC for the given data in one shot. Args: - key: Key ({KEYBYTES=}) - nonce: Nonce ({NONCEBYTES=}) + key: Secret key (generate with random_key()) + nonce: Public nonce (generate with random_nonce()) data: Data to MAC maclen: MAC length (16 or 32, default 16) into: Buffer to write MAC into (default: bytearray created) @@ -420,70 +427,59 @@ def mac( Returns: MAC bytes as bytearray if into not provided, memoryview of into otherwise """ - mac_state = Mac(key, nonce) + mac_state = Mac(key, nonce, maclen) mac_state.update(data) - return mac_state.final(maclen, into) + return mac_state.final(into) class Mac: """AEGIS-128X2 MAC state wrapper. - Usage: - mac = Mac(key, nonce) - mac.update(data) - tag = mac.final() # defaults to 16-byte MAC - # or verify: - mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag) + Example: + a = Mac(key, nonce) + a.update(data) + mac = a.final() """ - __slots__ = ("_st", "_owner") + __slots__ = ("_st", "_owner", "_maclen") - def __init__( - self, - key: Buffer, - nonce: Buffer, - _other=None, - ) -> None: - f"""Initialize a MAC state with a nonce and key. - - Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None: + """Create a MAC with the given key, nonce, and tag length. Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ - st, owner = new_aligned_struct("aegis128x2_mac_state", ALIGNMENT) - self._st = st - self._owner = owner - if _other is not None: # clone path - _lib.aegis128x2_mac_state_clone(self._st, _other._st) - return - # Normal init path + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: raise TypeError(f"nonce length must be {NONCEBYTES}") + + self._maclen = maclen + st, owner = new_aligned_struct("aegis128x2_mac_state", ALIGNMENT) + self._st = st + self._owner = owner _lib.aegis128x2_mac_init(self._st, _ptr(key), _ptr(nonce)) def __deepcopy__(self) -> "Mac": """Return a clone of current MAC state.""" - return Mac(b"", b"", _other=self) + clone = object.__new__(Mac) + clone._maclen = self._maclen + clone._st, clone._owner = new_aligned_struct("aegis128x2_mac_state", ALIGNMENT) + _lib.aegis128x2_mac_state_clone(clone._st, self._st) + return clone clone = __deepcopy__ def reset(self) -> None: - """Reset the MAC state so it can be reused with the same nonce and key.""" + """Reset back to the original state, prior to any updates.""" _lib.aegis128x2_mac_reset(self._st) def update(self, data: Buffer) -> None: - """Absorb data into the MAC state. + """Update the MAC state with more data. - Args: - data: Bytes-like object to authenticate. - - Raises: - RuntimeError: If the underlying C function reports an error. + Repeated calls to update() are equivalent to a single call with the concatenated data. """ rc = _lib.aegis128x2_mac_update(self._st, _ptr(data), len(data)) if rc != 0: @@ -491,15 +487,13 @@ class Mac: err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac update failed: {err_name}") - def final( - self, - maclen: int = MACBYTES, - into: Buffer | None = None, - ) -> bytearray | memoryview: - """Finalize and return the MAC tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Calculate and return the MAC tag for the currently input data. + + Unlike the C library, this method does not alter the current state, + allowing for multiple calls and further updates on the same object. Args: - maclen: Tag length in bytes (16 or 32). Defaults to 16. into: Optional buffer to write the tag into (default: bytearray created). Returns: @@ -509,30 +503,36 @@ class Mac: TypeError: If lengths are invalid. RuntimeError: If finalization fails in the C library. """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + maclen = self._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) + + rc = _lib.aegis128x2_mac_final(self.clone()._st, ffi.from_buffer(out), maclen) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac final failed: {err_name}") return out if into is None else memoryview(out)[:maclen] # type: ignore + def digest(self) -> bytes: + """Calculate and return the MAC tag as bytes.""" + return bytes(self.final()) + + def hexdigest(self) -> str: + """Calculate and return the MAC tag as a hex string.""" + return self.digest().hex() + def verify(self, mac: Buffer): - """Verify a tag for the current MAC state. + """Verify that the data entered so far matches the given MAC tag. + + Unlike the C library, this method does not alter the current state. Args: - mac: The tag to verify (16 or 32 bytes). - - Returns: - Only if verification succeeds. + mac: The tag to verify against (16 or 32 bytes). Raises: TypeError: If tag length is invalid. @@ -541,7 +541,9 @@ class Mac: 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) + + cloned = self.clone() + rc = _lib.aegis128x2_mac_verify(cloned._st, _ptr(mac), maclen) if rc != 0: raise ValueError("mac verification failed") @@ -550,23 +552,31 @@ class Encryptor: """Incremental encryptor. - update(message[, into]) -> returns produced ciphertext bytes - - final([into], maclen=16) -> returns tail+tag bytes - - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + - final([into]) -> returns MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental encryptor. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental encryptor. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). ad: Associated data to bind to the encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -583,6 +593,7 @@ class Encryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -611,8 +622,10 @@ class Encryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(message) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -641,24 +654,21 @@ class Encryptor: self._bytes_out += w return out if into is None else memoryview(out)[:w] # type: ignore - def final( - self, into: Buffer | None = None, maclen: int = MACBYTES - ) -> bytearray | memoryview: - """Finalize encryption, writing any remaining bytes and the tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Finalize encryption and return the authentication tag. Args: - into: Optional destination buffer for the tail and tag. - maclen: Tag length (16 or 32). Defaults to 16. + into: Optional destination buffer for the tag. Returns: - A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. + The authentication tag as bytearray if into not provided, memoryview of into otherwise. Raises: - TypeError: If maclen is invalid. - RuntimeError: If the C final call fails. + RuntimeError: If the C final call fails or if called after final(). """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen # 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 *") @@ -678,6 +688,8 @@ class Encryptor: # Only the tag bytes are returned when we allocate the buffer assert w == maclen self._bytes_out += w + self._st = None + self._owner = None return out if into is None else memoryview(out)[:w] # type: ignore @@ -688,19 +700,28 @@ class Decryptor: - final(mac) -> verifies the MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental decryptor for detached tags. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental decryptor for detached tags. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ad: Associated data used during encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -717,6 +738,7 @@ class Decryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -740,8 +762,10 @@ class Decryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(ct) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -770,20 +794,25 @@ class Decryptor: """Finalize decryption by verifying the MAC tag. Args: - mac: Tag to verify (16 or 32 bytes). + mac: Tag to verify. Raises: - TypeError: If tag length is invalid. + TypeError: If tag length doesn't match the expected maclen. ValueError: If authentication fails. + RuntimeError: If called after final(). """ - maclen = len(mac) - if maclen not in (16, 32): - raise TypeError("mac length must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen + if len(mac) != maclen: + raise TypeError(f"mac length must be {maclen}") rc = _lib.aegis128x2_state_decrypt_detached_final( self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") + self._st = None + self._owner = None def new_state(): diff --git a/pyaegis/aegis128x4.py b/pyaegis/aegis128x4.py index 93c9561..fc469b5 100644 --- a/pyaegis/aegis128x4.py +++ b/pyaegis/aegis128x4.py @@ -9,7 +9,7 @@ from ._loader import ffi from ._loader import lib as _lib from .util import Buffer, new_aligned_struct, nonce_increment, wipe -NAME = "AEGIS-128X4" #: Algorithm name +NAME = "AEGIS-128X4" #: Algorithm display name KEYBYTES = 16 #: Key size in bytes (varies by algorithm) NONCEBYTES = 16 #: Nonce size in bytes (varies by algorithm) MACBYTES = 16 #: Normal MAC size (always 16) @@ -20,15 +20,22 @@ RATE = 64 #: Byte chunk size in internal processing def random_key() -> bytearray: """ - Generate a random key using cryptographically secure random bytes. + Generate a secret key using cryptographically secure random bytes. - It is recommended to wipe() the key after no longer needed to keep it secret. + It is recommended to wipe() the key after no longer needed. """ return bytearray(secrets.token_bytes(KEYBYTES)) def random_nonce() -> bytearray: - """Generate a random nonce using cryptographically secure random bytes.""" + """ + Generate a public nonce using cryptographically secure random bytes. + + Nonces (a number used once) are public data that may be sent together + with the ciphertext, but they need to be unique for each use. + + See also: nonce_increment() can be used to derive sequential nonces. + """ return bytearray(secrets.token_bytes(NONCEBYTES)) @@ -46,11 +53,11 @@ def encrypt_detached( ct_into: Buffer | None = None, mac_into: Buffer | None = None, ) -> tuple[bytearray | memoryview, bytearray | memoryview]: - f"""Encrypt message with associated data, returning ciphertext and MAC separately. + """Encrypt message with associated data, returning ciphertext and MAC separately. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -114,11 +121,11 @@ def decrypt_detached( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with detached MAC and associated data. + """Decrypt ciphertext with detached MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. mac: The MAC to verify. ad: Associated data (optional). @@ -170,11 +177,11 @@ def encrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message with associated data, returning ciphertext with appended MAC. + """Encrypt message with associated data, returning ciphertext with appended MAC. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -226,11 +233,11 @@ def decrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with appended MAC and associated data. + """Decrypt ciphertext with appended MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext with MAC to decrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -283,11 +290,11 @@ def stream( *, into: Buffer | None = None, ) -> bytearray | Buffer: - f"""Generate a stream of pseudorandom bytes. + """Generate a stream of pseudorandom bytes. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}, uses zeroes for nonce if None). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None). length: Number of bytes to generate (required if into is None). into: Buffer to write stream into (default: bytearray created). @@ -325,11 +332,11 @@ def encrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message without authentication (for testing/debugging). + """Encrypt message without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. into: Buffer to write ciphertext into (default: bytearray created). @@ -366,11 +373,11 @@ def decrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext without authentication (for testing/debugging). + """Decrypt ciphertext without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. into: Buffer to write plaintext into (default: bytearray created). @@ -408,11 +415,11 @@ def mac( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Compute a MAC for the given data in one shot. + """Compute a MAC for the given data in one shot. Args: - key: Key ({KEYBYTES=}) - nonce: Nonce ({NONCEBYTES=}) + key: Secret key (generate with random_key()) + nonce: Public nonce (generate with random_nonce()) data: Data to MAC maclen: MAC length (16 or 32, default 16) into: Buffer to write MAC into (default: bytearray created) @@ -420,70 +427,59 @@ def mac( Returns: MAC bytes as bytearray if into not provided, memoryview of into otherwise """ - mac_state = Mac(key, nonce) + mac_state = Mac(key, nonce, maclen) mac_state.update(data) - return mac_state.final(maclen, into) + return mac_state.final(into) class Mac: """AEGIS-128X4 MAC state wrapper. - Usage: - mac = Mac(key, nonce) - mac.update(data) - tag = mac.final() # defaults to 16-byte MAC - # or verify: - mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag) + Example: + a = Mac(key, nonce) + a.update(data) + mac = a.final() """ - __slots__ = ("_st", "_owner") + __slots__ = ("_st", "_owner", "_maclen") - def __init__( - self, - key: Buffer, - nonce: Buffer, - _other=None, - ) -> None: - f"""Initialize a MAC state with a nonce and key. - - Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None: + """Create a MAC with the given key, nonce, and tag length. Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ - st, owner = new_aligned_struct("aegis128x4_mac_state", ALIGNMENT) - self._st = st - self._owner = owner - if _other is not None: # clone path - _lib.aegis128x4_mac_state_clone(self._st, _other._st) - return - # Normal init path + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: raise TypeError(f"nonce length must be {NONCEBYTES}") + + self._maclen = maclen + st, owner = new_aligned_struct("aegis128x4_mac_state", ALIGNMENT) + self._st = st + self._owner = owner _lib.aegis128x4_mac_init(self._st, _ptr(key), _ptr(nonce)) def __deepcopy__(self) -> "Mac": """Return a clone of current MAC state.""" - return Mac(b"", b"", _other=self) + clone = object.__new__(Mac) + clone._maclen = self._maclen + clone._st, clone._owner = new_aligned_struct("aegis128x4_mac_state", ALIGNMENT) + _lib.aegis128x4_mac_state_clone(clone._st, self._st) + return clone clone = __deepcopy__ def reset(self) -> None: - """Reset the MAC state so it can be reused with the same nonce and key.""" + """Reset back to the original state, prior to any updates.""" _lib.aegis128x4_mac_reset(self._st) def update(self, data: Buffer) -> None: - """Absorb data into the MAC state. + """Update the MAC state with more data. - Args: - data: Bytes-like object to authenticate. - - Raises: - RuntimeError: If the underlying C function reports an error. + Repeated calls to update() are equivalent to a single call with the concatenated data. """ rc = _lib.aegis128x4_mac_update(self._st, _ptr(data), len(data)) if rc != 0: @@ -491,15 +487,13 @@ class Mac: err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac update failed: {err_name}") - def final( - self, - maclen: int = MACBYTES, - into: Buffer | None = None, - ) -> bytearray | memoryview: - """Finalize and return the MAC tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Calculate and return the MAC tag for the currently input data. + + Unlike the C library, this method does not alter the current state, + allowing for multiple calls and further updates on the same object. Args: - maclen: Tag length in bytes (16 or 32). Defaults to 16. into: Optional buffer to write the tag into (default: bytearray created). Returns: @@ -509,30 +503,36 @@ class Mac: TypeError: If lengths are invalid. RuntimeError: If finalization fails in the C library. """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + maclen = self._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) + + rc = _lib.aegis128x4_mac_final(self.clone()._st, ffi.from_buffer(out), maclen) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac final failed: {err_name}") return out if into is None else memoryview(out)[:maclen] # type: ignore + def digest(self) -> bytes: + """Calculate and return the MAC tag as bytes.""" + return bytes(self.final()) + + def hexdigest(self) -> str: + """Calculate and return the MAC tag as a hex string.""" + return self.digest().hex() + def verify(self, mac: Buffer): - """Verify a tag for the current MAC state. + """Verify that the data entered so far matches the given MAC tag. + + Unlike the C library, this method does not alter the current state. Args: - mac: The tag to verify (16 or 32 bytes). - - Returns: - Only if verification succeeds. + mac: The tag to verify against (16 or 32 bytes). Raises: TypeError: If tag length is invalid. @@ -541,7 +541,9 @@ class Mac: 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) + + cloned = self.clone() + rc = _lib.aegis128x4_mac_verify(cloned._st, _ptr(mac), maclen) if rc != 0: raise ValueError("mac verification failed") @@ -550,23 +552,31 @@ class Encryptor: """Incremental encryptor. - update(message[, into]) -> returns produced ciphertext bytes - - final([into], maclen=16) -> returns tail+tag bytes - - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + - final([into]) -> returns MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental encryptor. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental encryptor. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). ad: Associated data to bind to the encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -583,6 +593,7 @@ class Encryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -611,8 +622,10 @@ class Encryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(message) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -641,24 +654,21 @@ class Encryptor: self._bytes_out += w return out if into is None else memoryview(out)[:w] # type: ignore - def final( - self, into: Buffer | None = None, maclen: int = MACBYTES - ) -> bytearray | memoryview: - """Finalize encryption, writing any remaining bytes and the tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Finalize encryption and return the authentication tag. Args: - into: Optional destination buffer for the tail and tag. - maclen: Tag length (16 or 32). Defaults to 16. + into: Optional destination buffer for the tag. Returns: - A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. + The authentication tag as bytearray if into not provided, memoryview of into otherwise. Raises: - TypeError: If maclen is invalid. - RuntimeError: If the C final call fails. + RuntimeError: If the C final call fails or if called after final(). """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen # 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 *") @@ -678,6 +688,8 @@ class Encryptor: # Only the tag bytes are returned when we allocate the buffer assert w == maclen self._bytes_out += w + self._st = None + self._owner = None return out if into is None else memoryview(out)[:w] # type: ignore @@ -688,19 +700,28 @@ class Decryptor: - final(mac) -> verifies the MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental decryptor for detached tags. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental decryptor for detached tags. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ad: Associated data used during encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -717,6 +738,7 @@ class Decryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -740,8 +762,10 @@ class Decryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(ct) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -770,20 +794,25 @@ class Decryptor: """Finalize decryption by verifying the MAC tag. Args: - mac: Tag to verify (16 or 32 bytes). + mac: Tag to verify. Raises: - TypeError: If tag length is invalid. + TypeError: If tag length doesn't match the expected maclen. ValueError: If authentication fails. + RuntimeError: If called after final(). """ - maclen = len(mac) - if maclen not in (16, 32): - raise TypeError("mac length must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen + if len(mac) != maclen: + raise TypeError(f"mac length must be {maclen}") rc = _lib.aegis128x4_state_decrypt_detached_final( self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") + self._st = None + self._owner = None def new_state(): diff --git a/pyaegis/aegis256.py b/pyaegis/aegis256.py index d2ddca9..aa16405 100644 --- a/pyaegis/aegis256.py +++ b/pyaegis/aegis256.py @@ -9,7 +9,7 @@ from ._loader import ffi from ._loader import lib as _lib from .util import Buffer, new_aligned_struct, nonce_increment, wipe -NAME = "AEGIS-256" #: Algorithm name +NAME = "AEGIS-256" #: Algorithm display name KEYBYTES = 32 #: Key size in bytes (varies by algorithm) NONCEBYTES = 32 #: Nonce size in bytes (varies by algorithm) MACBYTES = 16 #: Normal MAC size (always 16) @@ -20,15 +20,22 @@ RATE = 64 #: Byte chunk size in internal processing def random_key() -> bytearray: """ - Generate a random key using cryptographically secure random bytes. + Generate a secret key using cryptographically secure random bytes. - It is recommended to wipe() the key after no longer needed to keep it secret. + It is recommended to wipe() the key after no longer needed. """ return bytearray(secrets.token_bytes(KEYBYTES)) def random_nonce() -> bytearray: - """Generate a random nonce using cryptographically secure random bytes.""" + """ + Generate a public nonce using cryptographically secure random bytes. + + Nonces (a number used once) are public data that may be sent together + with the ciphertext, but they need to be unique for each use. + + See also: nonce_increment() can be used to derive sequential nonces. + """ return bytearray(secrets.token_bytes(NONCEBYTES)) @@ -46,11 +53,11 @@ def encrypt_detached( ct_into: Buffer | None = None, mac_into: Buffer | None = None, ) -> tuple[bytearray | memoryview, bytearray | memoryview]: - f"""Encrypt message with associated data, returning ciphertext and MAC separately. + """Encrypt message with associated data, returning ciphertext and MAC separately. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -114,11 +121,11 @@ def decrypt_detached( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with detached MAC and associated data. + """Decrypt ciphertext with detached MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. mac: The MAC to verify. ad: Associated data (optional). @@ -170,11 +177,11 @@ def encrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message with associated data, returning ciphertext with appended MAC. + """Encrypt message with associated data, returning ciphertext with appended MAC. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -226,11 +233,11 @@ def decrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with appended MAC and associated data. + """Decrypt ciphertext with appended MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext with MAC to decrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -283,11 +290,11 @@ def stream( *, into: Buffer | None = None, ) -> bytearray | Buffer: - f"""Generate a stream of pseudorandom bytes. + """Generate a stream of pseudorandom bytes. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}, uses zeroes for nonce if None). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None). length: Number of bytes to generate (required if into is None). into: Buffer to write stream into (default: bytearray created). @@ -325,11 +332,11 @@ def encrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message without authentication (for testing/debugging). + """Encrypt message without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. into: Buffer to write ciphertext into (default: bytearray created). @@ -366,11 +373,11 @@ def decrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext without authentication (for testing/debugging). + """Decrypt ciphertext without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. into: Buffer to write plaintext into (default: bytearray created). @@ -408,11 +415,11 @@ def mac( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Compute a MAC for the given data in one shot. + """Compute a MAC for the given data in one shot. Args: - key: Key ({KEYBYTES=}) - nonce: Nonce ({NONCEBYTES=}) + key: Secret key (generate with random_key()) + nonce: Public nonce (generate with random_nonce()) data: Data to MAC maclen: MAC length (16 or 32, default 16) into: Buffer to write MAC into (default: bytearray created) @@ -420,70 +427,59 @@ def mac( Returns: MAC bytes as bytearray if into not provided, memoryview of into otherwise """ - mac_state = Mac(key, nonce) + mac_state = Mac(key, nonce, maclen) mac_state.update(data) - return mac_state.final(maclen, into) + return mac_state.final(into) class Mac: """AEGIS-256 MAC state wrapper. - Usage: - mac = Mac(key, nonce) - mac.update(data) - tag = mac.final() # defaults to 16-byte MAC - # or verify: - mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag) + Example: + a = Mac(key, nonce) + a.update(data) + mac = a.final() """ - __slots__ = ("_st", "_owner") + __slots__ = ("_st", "_owner", "_maclen") - def __init__( - self, - key: Buffer, - nonce: Buffer, - _other=None, - ) -> None: - f"""Initialize a MAC state with a nonce and key. - - Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None: + """Create a MAC with the given key, nonce, and tag length. Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ - st, owner = new_aligned_struct("aegis256_mac_state", ALIGNMENT) - self._st = st - self._owner = owner - if _other is not None: # clone path - _lib.aegis256_mac_state_clone(self._st, _other._st) - return - # Normal init path + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: raise TypeError(f"nonce length must be {NONCEBYTES}") + + self._maclen = maclen + st, owner = new_aligned_struct("aegis256_mac_state", ALIGNMENT) + self._st = st + self._owner = owner _lib.aegis256_mac_init(self._st, _ptr(key), _ptr(nonce)) def __deepcopy__(self) -> "Mac": """Return a clone of current MAC state.""" - return Mac(b"", b"", _other=self) + clone = object.__new__(Mac) + clone._maclen = self._maclen + clone._st, clone._owner = new_aligned_struct("aegis256_mac_state", ALIGNMENT) + _lib.aegis256_mac_state_clone(clone._st, self._st) + return clone clone = __deepcopy__ def reset(self) -> None: - """Reset the MAC state so it can be reused with the same nonce and key.""" + """Reset back to the original state, prior to any updates.""" _lib.aegis256_mac_reset(self._st) def update(self, data: Buffer) -> None: - """Absorb data into the MAC state. + """Update the MAC state with more data. - Args: - data: Bytes-like object to authenticate. - - Raises: - RuntimeError: If the underlying C function reports an error. + Repeated calls to update() are equivalent to a single call with the concatenated data. """ rc = _lib.aegis256_mac_update(self._st, _ptr(data), len(data)) if rc != 0: @@ -491,15 +487,13 @@ class Mac: err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac update failed: {err_name}") - def final( - self, - maclen: int = MACBYTES, - into: Buffer | None = None, - ) -> bytearray | memoryview: - """Finalize and return the MAC tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Calculate and return the MAC tag for the currently input data. + + Unlike the C library, this method does not alter the current state, + allowing for multiple calls and further updates on the same object. Args: - maclen: Tag length in bytes (16 or 32). Defaults to 16. into: Optional buffer to write the tag into (default: bytearray created). Returns: @@ -509,30 +503,36 @@ class Mac: TypeError: If lengths are invalid. RuntimeError: If finalization fails in the C library. """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + maclen = self._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) + + rc = _lib.aegis256_mac_final(self.clone()._st, ffi.from_buffer(out), maclen) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac final failed: {err_name}") return out if into is None else memoryview(out)[:maclen] # type: ignore + def digest(self) -> bytes: + """Calculate and return the MAC tag as bytes.""" + return bytes(self.final()) + + def hexdigest(self) -> str: + """Calculate and return the MAC tag as a hex string.""" + return self.digest().hex() + def verify(self, mac: Buffer): - """Verify a tag for the current MAC state. + """Verify that the data entered so far matches the given MAC tag. + + Unlike the C library, this method does not alter the current state. Args: - mac: The tag to verify (16 or 32 bytes). - - Returns: - Only if verification succeeds. + mac: The tag to verify against (16 or 32 bytes). Raises: TypeError: If tag length is invalid. @@ -541,7 +541,9 @@ class Mac: 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) + + cloned = self.clone() + rc = _lib.aegis256_mac_verify(cloned._st, _ptr(mac), maclen) if rc != 0: raise ValueError("mac verification failed") @@ -550,23 +552,31 @@ class Encryptor: """Incremental encryptor. - update(message[, into]) -> returns produced ciphertext bytes - - final([into], maclen=16) -> returns tail+tag bytes - - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + - final([into]) -> returns MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental encryptor. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental encryptor. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). ad: Associated data to bind to the encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -583,6 +593,7 @@ class Encryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -611,8 +622,10 @@ class Encryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(message) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -641,24 +654,21 @@ class Encryptor: self._bytes_out += w return out if into is None else memoryview(out)[:w] # type: ignore - def final( - self, into: Buffer | None = None, maclen: int = MACBYTES - ) -> bytearray | memoryview: - """Finalize encryption, writing any remaining bytes and the tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Finalize encryption and return the authentication tag. Args: - into: Optional destination buffer for the tail and tag. - maclen: Tag length (16 or 32). Defaults to 16. + into: Optional destination buffer for the tag. Returns: - A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. + The authentication tag as bytearray if into not provided, memoryview of into otherwise. Raises: - TypeError: If maclen is invalid. - RuntimeError: If the C final call fails. + RuntimeError: If the C final call fails or if called after final(). """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen # 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 *") @@ -678,6 +688,8 @@ class Encryptor: # Only the tag bytes are returned when we allocate the buffer assert w == maclen self._bytes_out += w + self._st = None + self._owner = None return out if into is None else memoryview(out)[:w] # type: ignore @@ -688,19 +700,28 @@ class Decryptor: - final(mac) -> verifies the MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental decryptor for detached tags. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental decryptor for detached tags. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ad: Associated data used during encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -717,6 +738,7 @@ class Decryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -740,8 +762,10 @@ class Decryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(ct) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -770,20 +794,25 @@ class Decryptor: """Finalize decryption by verifying the MAC tag. Args: - mac: Tag to verify (16 or 32 bytes). + mac: Tag to verify. Raises: - TypeError: If tag length is invalid. + TypeError: If tag length doesn't match the expected maclen. ValueError: If authentication fails. + RuntimeError: If called after final(). """ - maclen = len(mac) - if maclen not in (16, 32): - raise TypeError("mac length must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen + if len(mac) != maclen: + raise TypeError(f"mac length must be {maclen}") rc = _lib.aegis256_state_decrypt_detached_final( self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") + self._st = None + self._owner = None def new_state(): diff --git a/pyaegis/aegis256x2.py b/pyaegis/aegis256x2.py index ca59070..c062523 100644 --- a/pyaegis/aegis256x2.py +++ b/pyaegis/aegis256x2.py @@ -9,7 +9,7 @@ from ._loader import ffi from ._loader import lib as _lib from .util import Buffer, new_aligned_struct, nonce_increment, wipe -NAME = "AEGIS-256X2" #: Algorithm name +NAME = "AEGIS-256X2" #: Algorithm display name KEYBYTES = 32 #: Key size in bytes (varies by algorithm) NONCEBYTES = 32 #: Nonce size in bytes (varies by algorithm) MACBYTES = 16 #: Normal MAC size (always 16) @@ -20,15 +20,22 @@ RATE = 64 #: Byte chunk size in internal processing def random_key() -> bytearray: """ - Generate a random key using cryptographically secure random bytes. + Generate a secret key using cryptographically secure random bytes. - It is recommended to wipe() the key after no longer needed to keep it secret. + It is recommended to wipe() the key after no longer needed. """ return bytearray(secrets.token_bytes(KEYBYTES)) def random_nonce() -> bytearray: - """Generate a random nonce using cryptographically secure random bytes.""" + """ + Generate a public nonce using cryptographically secure random bytes. + + Nonces (a number used once) are public data that may be sent together + with the ciphertext, but they need to be unique for each use. + + See also: nonce_increment() can be used to derive sequential nonces. + """ return bytearray(secrets.token_bytes(NONCEBYTES)) @@ -46,11 +53,11 @@ def encrypt_detached( ct_into: Buffer | None = None, mac_into: Buffer | None = None, ) -> tuple[bytearray | memoryview, bytearray | memoryview]: - f"""Encrypt message with associated data, returning ciphertext and MAC separately. + """Encrypt message with associated data, returning ciphertext and MAC separately. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -114,11 +121,11 @@ def decrypt_detached( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with detached MAC and associated data. + """Decrypt ciphertext with detached MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. mac: The MAC to verify. ad: Associated data (optional). @@ -170,11 +177,11 @@ def encrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message with associated data, returning ciphertext with appended MAC. + """Encrypt message with associated data, returning ciphertext with appended MAC. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -226,11 +233,11 @@ def decrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with appended MAC and associated data. + """Decrypt ciphertext with appended MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext with MAC to decrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -283,11 +290,11 @@ def stream( *, into: Buffer | None = None, ) -> bytearray | Buffer: - f"""Generate a stream of pseudorandom bytes. + """Generate a stream of pseudorandom bytes. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}, uses zeroes for nonce if None). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None). length: Number of bytes to generate (required if into is None). into: Buffer to write stream into (default: bytearray created). @@ -325,11 +332,11 @@ def encrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message without authentication (for testing/debugging). + """Encrypt message without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. into: Buffer to write ciphertext into (default: bytearray created). @@ -366,11 +373,11 @@ def decrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext without authentication (for testing/debugging). + """Decrypt ciphertext without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. into: Buffer to write plaintext into (default: bytearray created). @@ -408,11 +415,11 @@ def mac( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Compute a MAC for the given data in one shot. + """Compute a MAC for the given data in one shot. Args: - key: Key ({KEYBYTES=}) - nonce: Nonce ({NONCEBYTES=}) + key: Secret key (generate with random_key()) + nonce: Public nonce (generate with random_nonce()) data: Data to MAC maclen: MAC length (16 or 32, default 16) into: Buffer to write MAC into (default: bytearray created) @@ -420,70 +427,59 @@ def mac( Returns: MAC bytes as bytearray if into not provided, memoryview of into otherwise """ - mac_state = Mac(key, nonce) + mac_state = Mac(key, nonce, maclen) mac_state.update(data) - return mac_state.final(maclen, into) + return mac_state.final(into) class Mac: """AEGIS-256X2 MAC state wrapper. - Usage: - mac = Mac(key, nonce) - mac.update(data) - tag = mac.final() # defaults to 16-byte MAC - # or verify: - mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag) + Example: + a = Mac(key, nonce) + a.update(data) + mac = a.final() """ - __slots__ = ("_st", "_owner") + __slots__ = ("_st", "_owner", "_maclen") - def __init__( - self, - key: Buffer, - nonce: Buffer, - _other=None, - ) -> None: - f"""Initialize a MAC state with a nonce and key. - - Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None: + """Create a MAC with the given key, nonce, and tag length. Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ - st, owner = new_aligned_struct("aegis256x2_mac_state", ALIGNMENT) - self._st = st - self._owner = owner - if _other is not None: # clone path - _lib.aegis256x2_mac_state_clone(self._st, _other._st) - return - # Normal init path + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: raise TypeError(f"nonce length must be {NONCEBYTES}") + + self._maclen = maclen + st, owner = new_aligned_struct("aegis256x2_mac_state", ALIGNMENT) + self._st = st + self._owner = owner _lib.aegis256x2_mac_init(self._st, _ptr(key), _ptr(nonce)) def __deepcopy__(self) -> "Mac": """Return a clone of current MAC state.""" - return Mac(b"", b"", _other=self) + clone = object.__new__(Mac) + clone._maclen = self._maclen + clone._st, clone._owner = new_aligned_struct("aegis256x2_mac_state", ALIGNMENT) + _lib.aegis256x2_mac_state_clone(clone._st, self._st) + return clone clone = __deepcopy__ def reset(self) -> None: - """Reset the MAC state so it can be reused with the same nonce and key.""" + """Reset back to the original state, prior to any updates.""" _lib.aegis256x2_mac_reset(self._st) def update(self, data: Buffer) -> None: - """Absorb data into the MAC state. + """Update the MAC state with more data. - Args: - data: Bytes-like object to authenticate. - - Raises: - RuntimeError: If the underlying C function reports an error. + Repeated calls to update() are equivalent to a single call with the concatenated data. """ rc = _lib.aegis256x2_mac_update(self._st, _ptr(data), len(data)) if rc != 0: @@ -491,15 +487,13 @@ class Mac: err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac update failed: {err_name}") - def final( - self, - maclen: int = MACBYTES, - into: Buffer | None = None, - ) -> bytearray | memoryview: - """Finalize and return the MAC tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Calculate and return the MAC tag for the currently input data. + + Unlike the C library, this method does not alter the current state, + allowing for multiple calls and further updates on the same object. Args: - maclen: Tag length in bytes (16 or 32). Defaults to 16. into: Optional buffer to write the tag into (default: bytearray created). Returns: @@ -509,30 +503,36 @@ class Mac: TypeError: If lengths are invalid. RuntimeError: If finalization fails in the C library. """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + maclen = self._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) + + rc = _lib.aegis256x2_mac_final(self.clone()._st, ffi.from_buffer(out), maclen) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac final failed: {err_name}") return out if into is None else memoryview(out)[:maclen] # type: ignore + def digest(self) -> bytes: + """Calculate and return the MAC tag as bytes.""" + return bytes(self.final()) + + def hexdigest(self) -> str: + """Calculate and return the MAC tag as a hex string.""" + return self.digest().hex() + def verify(self, mac: Buffer): - """Verify a tag for the current MAC state. + """Verify that the data entered so far matches the given MAC tag. + + Unlike the C library, this method does not alter the current state. Args: - mac: The tag to verify (16 or 32 bytes). - - Returns: - Only if verification succeeds. + mac: The tag to verify against (16 or 32 bytes). Raises: TypeError: If tag length is invalid. @@ -541,7 +541,9 @@ class Mac: 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) + + cloned = self.clone() + rc = _lib.aegis256x2_mac_verify(cloned._st, _ptr(mac), maclen) if rc != 0: raise ValueError("mac verification failed") @@ -550,23 +552,31 @@ class Encryptor: """Incremental encryptor. - update(message[, into]) -> returns produced ciphertext bytes - - final([into], maclen=16) -> returns tail+tag bytes - - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + - final([into]) -> returns MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental encryptor. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental encryptor. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). ad: Associated data to bind to the encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -583,6 +593,7 @@ class Encryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -611,8 +622,10 @@ class Encryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(message) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -641,24 +654,21 @@ class Encryptor: self._bytes_out += w return out if into is None else memoryview(out)[:w] # type: ignore - def final( - self, into: Buffer | None = None, maclen: int = MACBYTES - ) -> bytearray | memoryview: - """Finalize encryption, writing any remaining bytes and the tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Finalize encryption and return the authentication tag. Args: - into: Optional destination buffer for the tail and tag. - maclen: Tag length (16 or 32). Defaults to 16. + into: Optional destination buffer for the tag. Returns: - A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. + The authentication tag as bytearray if into not provided, memoryview of into otherwise. Raises: - TypeError: If maclen is invalid. - RuntimeError: If the C final call fails. + RuntimeError: If the C final call fails or if called after final(). """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen # 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 *") @@ -678,6 +688,8 @@ class Encryptor: # Only the tag bytes are returned when we allocate the buffer assert w == maclen self._bytes_out += w + self._st = None + self._owner = None return out if into is None else memoryview(out)[:w] # type: ignore @@ -688,19 +700,28 @@ class Decryptor: - final(mac) -> verifies the MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental decryptor for detached tags. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental decryptor for detached tags. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ad: Associated data used during encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -717,6 +738,7 @@ class Decryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -740,8 +762,10 @@ class Decryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(ct) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -770,20 +794,25 @@ class Decryptor: """Finalize decryption by verifying the MAC tag. Args: - mac: Tag to verify (16 or 32 bytes). + mac: Tag to verify. Raises: - TypeError: If tag length is invalid. + TypeError: If tag length doesn't match the expected maclen. ValueError: If authentication fails. + RuntimeError: If called after final(). """ - maclen = len(mac) - if maclen not in (16, 32): - raise TypeError("mac length must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen + if len(mac) != maclen: + raise TypeError(f"mac length must be {maclen}") rc = _lib.aegis256x2_state_decrypt_detached_final( self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") + self._st = None + self._owner = None def new_state(): diff --git a/pyaegis/aegis256x4.py b/pyaegis/aegis256x4.py index 1d9fa93..9df97e0 100644 --- a/pyaegis/aegis256x4.py +++ b/pyaegis/aegis256x4.py @@ -9,7 +9,7 @@ from ._loader import ffi from ._loader import lib as _lib from .util import Buffer, new_aligned_struct, nonce_increment, wipe -NAME = "AEGIS-256X4" #: Algorithm name +NAME = "AEGIS-256X4" #: Algorithm display name KEYBYTES = 32 #: Key size in bytes (varies by algorithm) NONCEBYTES = 32 #: Nonce size in bytes (varies by algorithm) MACBYTES = 16 #: Normal MAC size (always 16) @@ -20,15 +20,22 @@ RATE = 64 #: Byte chunk size in internal processing def random_key() -> bytearray: """ - Generate a random key using cryptographically secure random bytes. + Generate a secret key using cryptographically secure random bytes. - It is recommended to wipe() the key after no longer needed to keep it secret. + It is recommended to wipe() the key after no longer needed. """ return bytearray(secrets.token_bytes(KEYBYTES)) def random_nonce() -> bytearray: - """Generate a random nonce using cryptographically secure random bytes.""" + """ + Generate a public nonce using cryptographically secure random bytes. + + Nonces (a number used once) are public data that may be sent together + with the ciphertext, but they need to be unique for each use. + + See also: nonce_increment() can be used to derive sequential nonces. + """ return bytearray(secrets.token_bytes(NONCEBYTES)) @@ -46,11 +53,11 @@ def encrypt_detached( ct_into: Buffer | None = None, mac_into: Buffer | None = None, ) -> tuple[bytearray | memoryview, bytearray | memoryview]: - f"""Encrypt message with associated data, returning ciphertext and MAC separately. + """Encrypt message with associated data, returning ciphertext and MAC separately. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -114,11 +121,11 @@ def decrypt_detached( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with detached MAC and associated data. + """Decrypt ciphertext with detached MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. mac: The MAC to verify. ad: Associated data (optional). @@ -170,11 +177,11 @@ def encrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message with associated data, returning ciphertext with appended MAC. + """Encrypt message with associated data, returning ciphertext with appended MAC. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -226,11 +233,11 @@ def decrypt( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext with appended MAC and associated data. + """Decrypt ciphertext with appended MAC and associated data. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext with MAC to decrypt. ad: Associated data (optional). maclen: MAC length (16 or 32, default 16). @@ -283,11 +290,11 @@ def stream( *, into: Buffer | None = None, ) -> bytearray | Buffer: - f"""Generate a stream of pseudorandom bytes. + """Generate a stream of pseudorandom bytes. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}, uses zeroes for nonce if None). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce(), uses zeroes for nonce if None). length: Number of bytes to generate (required if into is None). into: Buffer to write stream into (default: bytearray created). @@ -325,11 +332,11 @@ def encrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Encrypt message without authentication (for testing/debugging). + """Encrypt message without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). message: The plaintext message to encrypt. into: Buffer to write ciphertext into (default: bytearray created). @@ -366,11 +373,11 @@ def decrypt_unauthenticated( *, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Decrypt ciphertext without authentication (for testing/debugging). + """Decrypt ciphertext without authentication (for testing/debugging). Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ct: The ciphertext to decrypt. into: Buffer to write plaintext into (default: bytearray created). @@ -408,11 +415,11 @@ def mac( maclen: int = MACBYTES, into: Buffer | None = None, ) -> bytearray | memoryview: - f"""Compute a MAC for the given data in one shot. + """Compute a MAC for the given data in one shot. Args: - key: Key ({KEYBYTES=}) - nonce: Nonce ({NONCEBYTES=}) + key: Secret key (generate with random_key()) + nonce: Public nonce (generate with random_nonce()) data: Data to MAC maclen: MAC length (16 or 32, default 16) into: Buffer to write MAC into (default: bytearray created) @@ -420,70 +427,59 @@ def mac( Returns: MAC bytes as bytearray if into not provided, memoryview of into otherwise """ - mac_state = Mac(key, nonce) + mac_state = Mac(key, nonce, maclen) mac_state.update(data) - return mac_state.final(maclen, into) + return mac_state.final(into) class Mac: """AEGIS-256X4 MAC state wrapper. - Usage: - mac = Mac(key, nonce) - mac.update(data) - tag = mac.final() # defaults to 16-byte MAC - # or verify: - mac2 = Mac(key, nonce); mac2.update(data); mac2.verify(tag) + Example: + a = Mac(key, nonce) + a.update(data) + mac = a.final() """ - __slots__ = ("_st", "_owner") + __slots__ = ("_st", "_owner", "_maclen") - def __init__( - self, - key: Buffer, - nonce: Buffer, - _other=None, - ) -> None: - f"""Initialize a MAC state with a nonce and key. - - Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + def __init__(self, key: Buffer, nonce: Buffer, maclen: int = MACBYTES) -> None: + """Create a MAC with the given key, nonce, and tag length. Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ - st, owner = new_aligned_struct("aegis256x4_mac_state", ALIGNMENT) - self._st = st - self._owner = owner - if _other is not None: # clone path - _lib.aegis256x4_mac_state_clone(self._st, _other._st) - return - # Normal init path + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: raise TypeError(f"nonce length must be {NONCEBYTES}") + + self._maclen = maclen + st, owner = new_aligned_struct("aegis256x4_mac_state", ALIGNMENT) + self._st = st + self._owner = owner _lib.aegis256x4_mac_init(self._st, _ptr(key), _ptr(nonce)) def __deepcopy__(self) -> "Mac": """Return a clone of current MAC state.""" - return Mac(b"", b"", _other=self) + clone = object.__new__(Mac) + clone._maclen = self._maclen + clone._st, clone._owner = new_aligned_struct("aegis256x4_mac_state", ALIGNMENT) + _lib.aegis256x4_mac_state_clone(clone._st, self._st) + return clone clone = __deepcopy__ def reset(self) -> None: - """Reset the MAC state so it can be reused with the same nonce and key.""" + """Reset back to the original state, prior to any updates.""" _lib.aegis256x4_mac_reset(self._st) def update(self, data: Buffer) -> None: - """Absorb data into the MAC state. + """Update the MAC state with more data. - Args: - data: Bytes-like object to authenticate. - - Raises: - RuntimeError: If the underlying C function reports an error. + Repeated calls to update() are equivalent to a single call with the concatenated data. """ rc = _lib.aegis256x4_mac_update(self._st, _ptr(data), len(data)) if rc != 0: @@ -491,15 +487,13 @@ class Mac: err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac update failed: {err_name}") - def final( - self, - maclen: int = MACBYTES, - into: Buffer | None = None, - ) -> bytearray | memoryview: - """Finalize and return the MAC tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Calculate and return the MAC tag for the currently input data. + + Unlike the C library, this method does not alter the current state, + allowing for multiple calls and further updates on the same object. Args: - maclen: Tag length in bytes (16 or 32). Defaults to 16. into: Optional buffer to write the tag into (default: bytearray created). Returns: @@ -509,30 +503,36 @@ class Mac: TypeError: If lengths are invalid. RuntimeError: If finalization fails in the C library. """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + maclen = self._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) + + rc = _lib.aegis256x4_mac_final(self.clone()._st, ffi.from_buffer(out), maclen) if rc != 0: err_num = ffi.errno err_name = errno.errorcode.get(err_num, f"errno_{err_num}") raise RuntimeError(f"mac final failed: {err_name}") return out if into is None else memoryview(out)[:maclen] # type: ignore + def digest(self) -> bytes: + """Calculate and return the MAC tag as bytes.""" + return bytes(self.final()) + + def hexdigest(self) -> str: + """Calculate and return the MAC tag as a hex string.""" + return self.digest().hex() + def verify(self, mac: Buffer): - """Verify a tag for the current MAC state. + """Verify that the data entered so far matches the given MAC tag. + + Unlike the C library, this method does not alter the current state. Args: - mac: The tag to verify (16 or 32 bytes). - - Returns: - Only if verification succeeds. + mac: The tag to verify against (16 or 32 bytes). Raises: TypeError: If tag length is invalid. @@ -541,7 +541,9 @@ class Mac: 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) + + cloned = self.clone() + rc = _lib.aegis256x4_mac_verify(cloned._st, _ptr(mac), maclen) if rc != 0: raise ValueError("mac verification failed") @@ -550,23 +552,31 @@ class Encryptor: """Incremental encryptor. - update(message[, into]) -> returns produced ciphertext bytes - - final([into], maclen=16) -> returns tail+tag bytes - - final_detached([ct_into], [mac_into], maclen=16) -> returns (tail_bytes, mac) + - final([into]) -> returns MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental encryptor. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental encryptor. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). ad: Associated data to bind to the encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -583,6 +593,7 @@ class Encryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -611,8 +622,10 @@ class Encryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(message) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -641,24 +654,21 @@ class Encryptor: self._bytes_out += w return out if into is None else memoryview(out)[:w] # type: ignore - def final( - self, into: Buffer | None = None, maclen: int = MACBYTES - ) -> bytearray | memoryview: - """Finalize encryption, writing any remaining bytes and the tag. + def final(self, into: Buffer | None = None) -> bytearray | memoryview: + """Finalize encryption and return the authentication tag. Args: - into: Optional destination buffer for the tail and tag. - maclen: Tag length (16 or 32). Defaults to 16. + into: Optional destination buffer for the tag. Returns: - A memoryview of the produced bytes (tail + tag) if into provided, bytearray slice otherwise. + The authentication tag as bytearray if into not provided, memoryview of into otherwise. Raises: - TypeError: If maclen is invalid. - RuntimeError: If the C final call fails. + RuntimeError: If the C final call fails or if called after final(). """ - if maclen not in (16, 32): - raise TypeError("maclen must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen # 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 *") @@ -678,6 +688,8 @@ class Encryptor: # Only the tag bytes are returned when we allocate the buffer assert w == maclen self._bytes_out += w + self._st = None + self._owner = None return out if into is None else memoryview(out)[:w] # type: ignore @@ -688,19 +700,28 @@ class Decryptor: - final(mac) -> verifies the MAC tag """ - __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out") + __slots__ = ("_st", "_owner", "_bytes_in", "_bytes_out", "_maclen") - def __init__(self, key: Buffer, nonce: Buffer, ad: Buffer | None = None): - f"""Create an incremental decryptor for detached tags. + def __init__( + self, + key: Buffer, + nonce: Buffer, + ad: Buffer | None = None, + maclen: int = MACBYTES, + ): + """Create an incremental decryptor for detached tags. Args: - key: Key ({KEYBYTES=}). - nonce: Nonce ({NONCEBYTES=}). + key: Secret key (same key used during encryption). + nonce: Public nonce (same nonce used during encryption). ad: Associated data used during encryption (optional). + maclen: MAC length (16 or 32, default 16). Raises: - TypeError: If key or nonce lengths are invalid. + TypeError: If key, nonce, or maclen are invalid. """ + if maclen not in (16, 32): + raise TypeError("maclen must be 16 or 32") if len(key) != KEYBYTES: raise TypeError(f"key length must be {KEYBYTES}") if len(nonce) != NONCEBYTES: @@ -717,6 +738,7 @@ class Decryptor: self._owner = owner self._bytes_in = 0 self._bytes_out = 0 + self._maclen = maclen @property def bytes_in(self) -> int: @@ -740,8 +762,10 @@ class Decryptor: Raises: TypeError: If destination buffer is too small. - RuntimeError: If the C update call fails. + RuntimeError: If the C update call fails or if called after final(). """ + if self._st is None: + raise RuntimeError("Cannot call update() after final()") expected_out = len(ct) out = into if into is not None else bytearray(expected_out) out_mv = memoryview(out) @@ -770,20 +794,25 @@ class Decryptor: """Finalize decryption by verifying the MAC tag. Args: - mac: Tag to verify (16 or 32 bytes). + mac: Tag to verify. Raises: - TypeError: If tag length is invalid. + TypeError: If tag length doesn't match the expected maclen. ValueError: If authentication fails. + RuntimeError: If called after final(). """ - maclen = len(mac) - if maclen not in (16, 32): - raise TypeError("mac length must be 16 or 32") + if self._st is None: + raise RuntimeError("Cannot call final() after final()") + maclen = self._maclen + if len(mac) != maclen: + raise TypeError(f"mac length must be {maclen}") rc = _lib.aegis256x4_state_decrypt_detached_final( self._st, ffi.NULL, 0, ffi.NULL, _ptr(mac), maclen ) if rc != 0: raise ValueError("authentication failed") + self._st = None + self._owner = None def new_state(): diff --git a/pyaegis/benchmark.py b/pyaegis/benchmark.py index 394356d..a477c8a 100644 --- a/pyaegis/benchmark.py +++ b/pyaegis/benchmark.py @@ -57,14 +57,14 @@ def bench_mac(ciph) -> None: buf = bytearray(MSG_LEN) buf[:] = _random_bytes(len(buf)) - mac0 = ciph.Mac(key, nonce) + mac0 = ciph.Mac(key, nonce, maclen=ciph.MACBYTES_LONG) mac_out = bytearray(ciph.MACBYTES_LONG) t0 = time.perf_counter() for _ in range(ITERATIONS): mac = mac0.clone() mac.update(buf) - mac.final(maclen=ciph.MACBYTES_LONG, into=mac_out) + mac.final(into=mac_out) t1 = time.perf_counter() _ = mac_out[0] diff --git a/tests/test_encrypt.py b/tests/test_encrypt.py index 333afc9..2841004 100644 --- a/tests/test_encrypt.py +++ b/tests/test_encrypt.py @@ -147,14 +147,12 @@ def test_encrypt_decrypt_incremental(vector): expected_tag128 = bytes.fromhex(vector["tag128"]) # Incremental encryption with random chunking - encryptor = alg.Encryptor(key, nonce, ad) + encryptor = alg.Encryptor(key, nonce, ad, maclen=16) ct_chunks = [] for chunk in random_split_bytes(msg): ct_result = encryptor.update(chunk) ct_chunks.append(bytes(ct_result)) - final_output = encryptor.final(maclen=16) - ct_chunks.append(bytes(final_output[:-16])) # ciphertext part - computed_mac = bytes(final_output[-16:]) # MAC part + computed_mac = bytes(encryptor.final()) # Combine ciphertext chunks computed_ct = b"".join(ct_chunks) @@ -170,7 +168,7 @@ def test_encrypt_decrypt_incremental(vector): ) # Incremental decryption with different random chunking - decryptor = alg.Decryptor(key, nonce, ad) + decryptor = alg.Decryptor(key, nonce, ad, maclen=16) pt_chunks = [] for chunk in random_split_bytes(computed_ct): pt_chunks.append(bytes(decryptor.update(chunk))) @@ -187,14 +185,12 @@ def test_encrypt_decrypt_incremental(vector): expected_tag256 = bytes.fromhex(vector["tag256"]) # Incremental encryption with random chunking - encryptor = alg.Encryptor(key, nonce, ad) + encryptor = alg.Encryptor(key, nonce, ad, maclen=32) ct_chunks = [] for chunk in random_split_bytes(msg): ct_result = encryptor.update(chunk) ct_chunks.append(bytes(ct_result)) - final_output = encryptor.final(maclen=32) - ct_chunks.append(bytes(final_output[:-32])) # ciphertext part - computed_mac = bytes(final_output[-32:]) # MAC part + computed_mac = bytes(encryptor.final()) # Combine ciphertext chunks computed_ct = b"".join(ct_chunks) @@ -210,7 +206,7 @@ def test_encrypt_decrypt_incremental(vector): ) # Incremental decryption with different random chunking - decryptor = alg.Decryptor(key, nonce, ad) + decryptor = alg.Decryptor(key, nonce, ad, maclen=32) pt_chunks = [] for chunk in random_split_bytes(computed_ct): pt_chunks.append(bytes(decryptor.update(chunk))) @@ -229,14 +225,14 @@ def test_encrypt_decrypt_incremental(vector): # Test that incremental decryption fails with the provided (invalid) MACs if "tag128" in vector: invalid_mac = bytes.fromhex(vector["tag128"]) - decryptor = alg.Decryptor(key, nonce, ad) + decryptor = alg.Decryptor(key, nonce, ad, maclen=16) decryptor.update(ct) # This should succeed with pytest.raises(ValueError, match="authentication failed"): decryptor.final(invalid_mac) if "tag256" in vector: invalid_mac = bytes.fromhex(vector["tag256"]) - decryptor = alg.Decryptor(key, nonce, ad) + decryptor = alg.Decryptor(key, nonce, ad, maclen=32) decryptor.update(ct) # This should succeed with pytest.raises(ValueError, match="authentication failed"): decryptor.final(invalid_mac) diff --git a/tests/test_mac.py b/tests/test_mac.py index 03a8b81..71709a6 100644 --- a/tests/test_mac.py +++ b/tests/test_mac.py @@ -2,6 +2,7 @@ import json from pathlib import Path import pytest + from pyaegis import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4 from .util import random_split_bytes @@ -81,10 +82,10 @@ def test_mac_class(vector): # Test 128-bit MAC if present if "tag128" in vector: expected_tag128 = bytes.fromhex(vector["tag128"]) - mac_state = alg.Mac(key, nonce) + mac_state = alg.Mac(key, nonce, maclen=16) for chunk in random_split_bytes(data): mac_state.update(chunk) - computed_tag128 = mac_state.final(maclen=16) + computed_tag128 = mac_state.final() assert computed_tag128 == expected_tag128, ( f"128-bit MAC mismatch for {vector['name']}" ) @@ -92,10 +93,10 @@ def test_mac_class(vector): # Test 256-bit MAC if present if "tag256" in vector: expected_tag256 = bytes.fromhex(vector["tag256"]) - mac_state = alg.Mac(key, nonce) + mac_state = alg.Mac(key, nonce, maclen=32) for chunk in random_split_bytes(data): mac_state.update(chunk) - computed_tag256 = mac_state.final(maclen=32) + computed_tag256 = mac_state.final() assert computed_tag256 == expected_tag256, ( f"256-bit MAC mismatch for {vector['name']}" ) diff --git a/tests/test_raises.py b/tests/test_raises.py new file mode 100644 index 0000000..05a5879 --- /dev/null +++ b/tests/test_raises.py @@ -0,0 +1,280 @@ +"""Tests for Encryptor and Decryptor finalization behavior. + +This module verifies that Encryptor and Decryptor objects become unusable +after calling final(), preventing accidental misuse. +""" + +import pytest + +from pyaegis import aegis256x4 + + +class TestEncryptorFinalization: + """Test that Encryptor becomes unusable after final().""" + + def test_update_after_final_raises(self): + """Test that calling update() after final() raises RuntimeError.""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + + encryptor = aegis256x4.Encryptor(key, nonce) + + # Encrypt some data and finalize + encryptor.update(b"Hello, world!") + tag = encryptor.final() + + # Attempting to update after final should raise RuntimeError + with pytest.raises( + RuntimeError, match="Cannot call update\\(\\) after final\\(\\)" + ): + encryptor.update(b"More data") + + def test_final_after_final_raises(self): + """Test that calling final() after final() raises RuntimeError.""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + + encryptor = aegis256x4.Encryptor(key, nonce) + + # Encrypt some data and finalize + encryptor.update(b"Hello, world!") + tag = encryptor.final() + + # Attempting to call final again should raise RuntimeError + with pytest.raises( + RuntimeError, match="Cannot call final\\(\\) after final\\(\\)" + ): + encryptor.final() + + def test_update_then_final_after_final_raises(self): + """Test that both update() and final() fail after final().""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + + encryptor = aegis256x4.Encryptor(key, nonce) + + # Encrypt and finalize + encryptor.update(b"Test data") + tag = encryptor.final() + + # Both operations should fail + with pytest.raises( + RuntimeError, match="Cannot call update\\(\\) after final\\(\\)" + ): + encryptor.update(b"More data") + + with pytest.raises( + RuntimeError, match="Cannot call final\\(\\) after final\\(\\)" + ): + encryptor.final() + + def test_properties_accessible_after_final(self): + """Test that properties like bytes_in and bytes_out are still accessible after final().""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + + encryptor = aegis256x4.Encryptor(key, nonce) + + message = b"Hello, world!" + encryptor.update(message) + tag = encryptor.final() + + # Properties should still be accessible + assert encryptor.bytes_in == len(message) + assert encryptor.bytes_out == len(message) + len(tag) + + def test_empty_encryption_finalization(self): + """Test that finalization works correctly with no update() calls.""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + + encryptor = aegis256x4.Encryptor(key, nonce) + + # Finalize without any updates + tag = encryptor.final() + assert len(tag) == aegis256x4.MACBYTES + + # Should still be unusable after + with pytest.raises( + RuntimeError, match="Cannot call update\\(\\) after final\\(\\)" + ): + encryptor.update(b"Data") + + +class TestDecryptorFinalization: + """Test that Decryptor becomes unusable after final().""" + + def test_update_after_final_raises(self): + """Test that calling update() after final() raises RuntimeError.""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + message = b"Hello, world!" + + # Encrypt first to get valid ciphertext and tag + ct, tag = aegis256x4.encrypt_detached(key, nonce, message) + + # Now test decryption + decryptor = aegis256x4.Decryptor(key, nonce) + decryptor.update(ct) + decryptor.final(tag) + + # Attempting to update after final should raise RuntimeError + with pytest.raises( + RuntimeError, match="Cannot call update\\(\\) after final\\(\\)" + ): + decryptor.update(b"More ciphertext") + + def test_final_after_final_raises(self): + """Test that calling final() after final() raises RuntimeError.""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + message = b"Hello, world!" + + # Encrypt first to get valid ciphertext and tag + ct, tag = aegis256x4.encrypt_detached(key, nonce, message) + + # Now test decryption + decryptor = aegis256x4.Decryptor(key, nonce) + decryptor.update(ct) + decryptor.final(tag) + + # Attempting to call final again should raise RuntimeError + with pytest.raises( + RuntimeError, match="Cannot call final\\(\\) after final\\(\\)" + ): + decryptor.final(tag) + + def test_update_then_final_after_final_raises(self): + """Test that both update() and final() fail after final().""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + message = b"Test data" + + # Encrypt first + ct, tag = aegis256x4.encrypt_detached(key, nonce, message) + + # Decrypt and finalize + decryptor = aegis256x4.Decryptor(key, nonce) + decryptor.update(ct) + decryptor.final(tag) + + # Both operations should fail + with pytest.raises( + RuntimeError, match="Cannot call update\\(\\) after final\\(\\)" + ): + decryptor.update(b"More ciphertext") + + with pytest.raises( + RuntimeError, match="Cannot call final\\(\\) after final\\(\\)" + ): + decryptor.final(tag) + + def test_properties_accessible_after_final(self): + """Test that properties like bytes_in and bytes_out are still accessible after final().""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + message = b"Hello, world!" + + # Encrypt first + ct, tag = aegis256x4.encrypt_detached(key, nonce, message) + + # Decrypt + decryptor = aegis256x4.Decryptor(key, nonce) + decryptor.update(ct) + decryptor.final(tag) + + # Properties should still be accessible + assert decryptor.bytes_in == len(ct) + assert decryptor.bytes_out == len(message) + + def test_empty_decryption_finalization(self): + """Test that finalization works correctly with no update() calls.""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + + # Encrypt empty message + ct, tag = aegis256x4.encrypt_detached(key, nonce, b"") + + # Decrypt without any updates + decryptor = aegis256x4.Decryptor(key, nonce) + decryptor.final(tag) # Should work with empty ciphertext + + # Should still be unusable after + with pytest.raises( + RuntimeError, match="Cannot call update\\(\\) after final\\(\\)" + ): + decryptor.update(b"Data") + + def test_failed_verification_still_finalizes(self): + """Test that even if verification fails, the object becomes unusable.""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + message = b"Hello, world!" + + # Encrypt first + ct, tag = aegis256x4.encrypt_detached(key, nonce, message) + + # Decrypt but use wrong tag + decryptor = aegis256x4.Decryptor(key, nonce) + decryptor.update(ct) + + # Try to finalize with invalid tag - should raise ValueError + bad_tag = bytes(len(tag)) # All zeros + with pytest.raises(ValueError, match="authentication failed"): + decryptor.final(bad_tag) + + # Object should NOT be finalized on failure - should still be usable + # This is a design decision: failed verification shouldn't lock the object + # Let's verify current behavior + try: + decryptor.update(b"test") + # If this doesn't raise, the object is still usable after failed verification + # This might be the desired behavior + except RuntimeError: + # If this raises, failed verification also finalizes the object + pass + + +class TestMultipleChunksBeforeFinalization: + """Test that multiple update() calls work before final().""" + + def test_encryptor_multiple_updates(self): + """Test that Encryptor can handle multiple update() calls before final().""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + + encryptor = aegis256x4.Encryptor(key, nonce) + + # Multiple updates + encryptor.update(b"Hello, ") + encryptor.update(b"world!") + encryptor.update(b" More data.") + + # Should still work + tag = encryptor.final() + assert len(tag) == aegis256x4.MACBYTES + + # Now unusable + with pytest.raises(RuntimeError): + encryptor.update(b"More") + + def test_decryptor_multiple_updates(self): + """Test that Decryptor can handle multiple update() calls before final().""" + key = aegis256x4.random_key() + nonce = aegis256x4.random_nonce() + + # Encrypt in chunks + encryptor = aegis256x4.Encryptor(key, nonce) + ct1 = encryptor.update(b"Hello, ") + ct2 = encryptor.update(b"world!") + tag = encryptor.final() + + # Decrypt in chunks + decryptor = aegis256x4.Decryptor(key, nonce) + decryptor.update(ct1) + decryptor.update(ct2) + decryptor.final(tag) + + # Now unusable + with pytest.raises(RuntimeError): + decryptor.update(b"More")