From 1eb0b6e8c0bcaf25d25b025645fbff53f9a488a5 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sun, 4 Oct 2026 15:57:28 +0000 Subject: [PATCH] Add stream_xor(); deprecate encrypt/decrypt_unauthenticated Following upstream libaegis 0.10.5 (874f884): the absorbing-mode unauthenticated pair is deprecated in favor of the pure keystream XOR. - stream_xor(key, nonce, data, into=None): same function encrypts and decrypts; output equals data XOR stream(key, nonce); supports in-place use via into (same buffer semantics as elsewhere). - encrypt_unauthenticated/decrypt_unauthenticated kept for the old format but now emit DeprecationWarning; removed from README (stream_xor documented under Keystream generation instead). --- README.md | 7 ++-- src/aeg/_typing.py | 12 +++++-- src/aeg/aegis128l.py | 71 ++++++++++++++++++++++++++++++++++++++++ src/aeg/aegis128x2.py | 71 ++++++++++++++++++++++++++++++++++++++++ src/aeg/aegis128x4.py | 71 ++++++++++++++++++++++++++++++++++++++++ src/aeg/aegis256.py | 71 ++++++++++++++++++++++++++++++++++++++++ src/aeg/aegis256x2.py | 71 ++++++++++++++++++++++++++++++++++++++++ src/aeg/aegis256x4.py | 71 ++++++++++++++++++++++++++++++++++++++++ tests/test_stream_xor.py | 54 ++++++++++++++++++++++++++++++ 9 files changed, 492 insertions(+), 7 deletions(-) create mode 100644 tests/test_stream_xor.py diff --git a/README.md b/README.md index a6fa919..a416544 100644 --- a/README.md +++ b/README.md @@ -79,10 +79,6 @@ The MAC tag is handled separately of ciphertext: - encrypt_detached(key, nonce, message, ad=None, maclen=16, ct_into=None, mac_into=None) -> (ct, mac) - decrypt_detached(key, nonce, ct, mac, ad=None, into=None) -> plaintext -No MAC tag, vulnerable to alterations: -- encrypt_unauthenticated(key, nonce, message, into=None) -> ciphertext (testing only) -- decrypt_unauthenticated(key, nonce, ct, into=None) -> plaintext (testing only) - ### Incremental AEAD Stateful classes that can be used for processing the data in separate chunks: @@ -116,6 +112,7 @@ The `Mac` class follows the Python hashlib API for compatibility with code expec Useful for creating pseudo random bytes as rapidly as possible. Reuse of the same (key, nonce) creates identical output. - stream(key, nonce=None, length=None, into=None) -> randombytes +- stream_xor(key, nonce, data, into=None) -> data XOR keystream (unauthenticated; encrypts and decrypts) ### Miscellaneous @@ -311,7 +308,7 @@ pt = ciph.decrypt(key, nonce, ct, into=buf) print(bytes(pt)) ``` -Detached and unauthenticated modes can use same size input and output (no MAC added to ciphertext). Detached encryption instead of `into` takes `ct_into` and `mac_into` separately and returns memoryviews to both. +Detached mode can use same size input and output (no MAC added to ciphertext). Detached encryption instead of `into` takes `ct_into` and `mac_into` separately and returns memoryviews to both. ## Performance diff --git a/src/aeg/_typing.py b/src/aeg/_typing.py index f493f09..61af8bc 100644 --- a/src/aeg/_typing.py +++ b/src/aeg/_typing.py @@ -100,7 +100,15 @@ class Cipher(Protocol): into: "Buffer | None" = None, ) -> "bytearray | Buffer": ... @staticmethod - def encrypt_unauthenticated( + def stream_xor( + key: "Buffer", + nonce: "Buffer", + data: "Buffer", + *, + into: "Buffer | None" = None, + ) -> bytearray | memoryview: ... + @staticmethod + def encrypt_unauthenticated( # deprecated: use stream_xor key: "Buffer", nonce: "Buffer", message: "Buffer", @@ -108,7 +116,7 @@ class Cipher(Protocol): into: "Buffer | None" = None, ) -> bytearray | memoryview: ... @staticmethod - def decrypt_unauthenticated( + def decrypt_unauthenticated( # deprecated: use stream_xor key: "Buffer", nonce: "Buffer", ct: "Buffer", diff --git a/src/aeg/aegis128l.py b/src/aeg/aegis128l.py index e280cdc..3813b69 100644 --- a/src/aeg/aegis128l.py +++ b/src/aeg/aegis128l.py @@ -5,6 +5,7 @@ import errno import sys import threading +import warnings from typing import Literal from ._loader import ffi @@ -378,6 +379,55 @@ def stream( return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore +def stream_xor( + key: Buffer, + nonce: Buffer, + data: Buffer, + *, + into: Buffer | None = None, +) -> bytearray | memoryview: + """XOR data with the keystream from stream(); encrypts and decrypts. + + Unauthenticated: only use when the data is authenticated some other + way. Never reuse a nonce with the same key. + + Args: + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). + data: Input bytes to XOR. + into: Buffer to write the output into (default: bytearray created). + + Returns: + Output as bytearray if into not provided, memoryview of into otherwise. + + Raises: + TypeError: If lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + data = memoryview(data) + if into is not None: + into = memoryview(into) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NONCEBYTES: + raise TypeError(f"nonce length must be {NONCEBYTES}") + if into is None: + out = bytearray(data.nbytes) + else: + if into.nbytes < data.nbytes: + raise TypeError("into length must be at least data.nbytes") + out = into + _lib.aegis128l_stream_xor( + ffi.from_buffer(out), + _ptr(data), + data.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out if into is None else memoryview(out)[: data.nbytes] # type: ignore + + def encrypt_unauthenticated( key: Buffer, nonce: Buffer, @@ -387,6 +437,10 @@ def encrypt_unauthenticated( ) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (generate with random_key()). nonce: Public nonce (generate with random_nonce()). @@ -399,6 +453,12 @@ def encrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "encrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) message = memoryview(message) @@ -433,6 +493,10 @@ def decrypt_unauthenticated( ) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (same key used during encryption). nonce: Public nonce (same nonce used during encryption). @@ -445,6 +509,12 @@ def decrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "decrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) ct = memoryview(ct) @@ -1122,6 +1192,7 @@ __all__ = [ "encrypt", "decrypt", "stream", + "stream_xor", "encrypt_unauthenticated", "decrypt_unauthenticated", "mac", diff --git a/src/aeg/aegis128x2.py b/src/aeg/aegis128x2.py index 6397b3c..171b8e3 100644 --- a/src/aeg/aegis128x2.py +++ b/src/aeg/aegis128x2.py @@ -5,6 +5,7 @@ import errno import sys import threading +import warnings from typing import Literal from ._loader import ffi @@ -378,6 +379,55 @@ def stream( return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore +def stream_xor( + key: Buffer, + nonce: Buffer, + data: Buffer, + *, + into: Buffer | None = None, +) -> bytearray | memoryview: + """XOR data with the keystream from stream(); encrypts and decrypts. + + Unauthenticated: only use when the data is authenticated some other + way. Never reuse a nonce with the same key. + + Args: + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). + data: Input bytes to XOR. + into: Buffer to write the output into (default: bytearray created). + + Returns: + Output as bytearray if into not provided, memoryview of into otherwise. + + Raises: + TypeError: If lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + data = memoryview(data) + if into is not None: + into = memoryview(into) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NONCEBYTES: + raise TypeError(f"nonce length must be {NONCEBYTES}") + if into is None: + out = bytearray(data.nbytes) + else: + if into.nbytes < data.nbytes: + raise TypeError("into length must be at least data.nbytes") + out = into + _lib.aegis128x2_stream_xor( + ffi.from_buffer(out), + _ptr(data), + data.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out if into is None else memoryview(out)[: data.nbytes] # type: ignore + + def encrypt_unauthenticated( key: Buffer, nonce: Buffer, @@ -387,6 +437,10 @@ def encrypt_unauthenticated( ) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (generate with random_key()). nonce: Public nonce (generate with random_nonce()). @@ -399,6 +453,12 @@ def encrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "encrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) message = memoryview(message) @@ -433,6 +493,10 @@ def decrypt_unauthenticated( ) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (same key used during encryption). nonce: Public nonce (same nonce used during encryption). @@ -445,6 +509,12 @@ def decrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "decrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) ct = memoryview(ct) @@ -1122,6 +1192,7 @@ __all__ = [ "encrypt", "decrypt", "stream", + "stream_xor", "encrypt_unauthenticated", "decrypt_unauthenticated", "mac", diff --git a/src/aeg/aegis128x4.py b/src/aeg/aegis128x4.py index d8006ab..86c5aa3 100644 --- a/src/aeg/aegis128x4.py +++ b/src/aeg/aegis128x4.py @@ -5,6 +5,7 @@ import errno import sys import threading +import warnings from typing import Literal from ._loader import ffi @@ -378,6 +379,55 @@ def stream( return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore +def stream_xor( + key: Buffer, + nonce: Buffer, + data: Buffer, + *, + into: Buffer | None = None, +) -> bytearray | memoryview: + """XOR data with the keystream from stream(); encrypts and decrypts. + + Unauthenticated: only use when the data is authenticated some other + way. Never reuse a nonce with the same key. + + Args: + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). + data: Input bytes to XOR. + into: Buffer to write the output into (default: bytearray created). + + Returns: + Output as bytearray if into not provided, memoryview of into otherwise. + + Raises: + TypeError: If lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + data = memoryview(data) + if into is not None: + into = memoryview(into) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NONCEBYTES: + raise TypeError(f"nonce length must be {NONCEBYTES}") + if into is None: + out = bytearray(data.nbytes) + else: + if into.nbytes < data.nbytes: + raise TypeError("into length must be at least data.nbytes") + out = into + _lib.aegis128x4_stream_xor( + ffi.from_buffer(out), + _ptr(data), + data.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out if into is None else memoryview(out)[: data.nbytes] # type: ignore + + def encrypt_unauthenticated( key: Buffer, nonce: Buffer, @@ -387,6 +437,10 @@ def encrypt_unauthenticated( ) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (generate with random_key()). nonce: Public nonce (generate with random_nonce()). @@ -399,6 +453,12 @@ def encrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "encrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) message = memoryview(message) @@ -433,6 +493,10 @@ def decrypt_unauthenticated( ) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (same key used during encryption). nonce: Public nonce (same nonce used during encryption). @@ -445,6 +509,12 @@ def decrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "decrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) ct = memoryview(ct) @@ -1122,6 +1192,7 @@ __all__ = [ "encrypt", "decrypt", "stream", + "stream_xor", "encrypt_unauthenticated", "decrypt_unauthenticated", "mac", diff --git a/src/aeg/aegis256.py b/src/aeg/aegis256.py index 8dd19d1..c1d7e96 100644 --- a/src/aeg/aegis256.py +++ b/src/aeg/aegis256.py @@ -5,6 +5,7 @@ import errno import sys import threading +import warnings from typing import Literal from ._loader import ffi @@ -378,6 +379,55 @@ def stream( return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore +def stream_xor( + key: Buffer, + nonce: Buffer, + data: Buffer, + *, + into: Buffer | None = None, +) -> bytearray | memoryview: + """XOR data with the keystream from stream(); encrypts and decrypts. + + Unauthenticated: only use when the data is authenticated some other + way. Never reuse a nonce with the same key. + + Args: + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). + data: Input bytes to XOR. + into: Buffer to write the output into (default: bytearray created). + + Returns: + Output as bytearray if into not provided, memoryview of into otherwise. + + Raises: + TypeError: If lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + data = memoryview(data) + if into is not None: + into = memoryview(into) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NONCEBYTES: + raise TypeError(f"nonce length must be {NONCEBYTES}") + if into is None: + out = bytearray(data.nbytes) + else: + if into.nbytes < data.nbytes: + raise TypeError("into length must be at least data.nbytes") + out = into + _lib.aegis256_stream_xor( + ffi.from_buffer(out), + _ptr(data), + data.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out if into is None else memoryview(out)[: data.nbytes] # type: ignore + + def encrypt_unauthenticated( key: Buffer, nonce: Buffer, @@ -387,6 +437,10 @@ def encrypt_unauthenticated( ) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (generate with random_key()). nonce: Public nonce (generate with random_nonce()). @@ -399,6 +453,12 @@ def encrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "encrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) message = memoryview(message) @@ -433,6 +493,10 @@ def decrypt_unauthenticated( ) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (same key used during encryption). nonce: Public nonce (same nonce used during encryption). @@ -445,6 +509,12 @@ def decrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "decrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) ct = memoryview(ct) @@ -1122,6 +1192,7 @@ __all__ = [ "encrypt", "decrypt", "stream", + "stream_xor", "encrypt_unauthenticated", "decrypt_unauthenticated", "mac", diff --git a/src/aeg/aegis256x2.py b/src/aeg/aegis256x2.py index c38dfd9..5012c67 100644 --- a/src/aeg/aegis256x2.py +++ b/src/aeg/aegis256x2.py @@ -5,6 +5,7 @@ import errno import sys import threading +import warnings from typing import Literal from ._loader import ffi @@ -378,6 +379,55 @@ def stream( return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore +def stream_xor( + key: Buffer, + nonce: Buffer, + data: Buffer, + *, + into: Buffer | None = None, +) -> bytearray | memoryview: + """XOR data with the keystream from stream(); encrypts and decrypts. + + Unauthenticated: only use when the data is authenticated some other + way. Never reuse a nonce with the same key. + + Args: + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). + data: Input bytes to XOR. + into: Buffer to write the output into (default: bytearray created). + + Returns: + Output as bytearray if into not provided, memoryview of into otherwise. + + Raises: + TypeError: If lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + data = memoryview(data) + if into is not None: + into = memoryview(into) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NONCEBYTES: + raise TypeError(f"nonce length must be {NONCEBYTES}") + if into is None: + out = bytearray(data.nbytes) + else: + if into.nbytes < data.nbytes: + raise TypeError("into length must be at least data.nbytes") + out = into + _lib.aegis256x2_stream_xor( + ffi.from_buffer(out), + _ptr(data), + data.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out if into is None else memoryview(out)[: data.nbytes] # type: ignore + + def encrypt_unauthenticated( key: Buffer, nonce: Buffer, @@ -387,6 +437,10 @@ def encrypt_unauthenticated( ) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (generate with random_key()). nonce: Public nonce (generate with random_nonce()). @@ -399,6 +453,12 @@ def encrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "encrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) message = memoryview(message) @@ -433,6 +493,10 @@ def decrypt_unauthenticated( ) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (same key used during encryption). nonce: Public nonce (same nonce used during encryption). @@ -445,6 +509,12 @@ def decrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "decrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) ct = memoryview(ct) @@ -1122,6 +1192,7 @@ __all__ = [ "encrypt", "decrypt", "stream", + "stream_xor", "encrypt_unauthenticated", "decrypt_unauthenticated", "mac", diff --git a/src/aeg/aegis256x4.py b/src/aeg/aegis256x4.py index 540663e..5491d6f 100644 --- a/src/aeg/aegis256x4.py +++ b/src/aeg/aegis256x4.py @@ -5,6 +5,7 @@ import errno import sys import threading +import warnings from typing import Literal from ._loader import ffi @@ -378,6 +379,55 @@ def stream( return out if into is None else memoryview(out)[: length or memoryview(out).nbytes] # type: ignore +def stream_xor( + key: Buffer, + nonce: Buffer, + data: Buffer, + *, + into: Buffer | None = None, +) -> bytearray | memoryview: + """XOR data with the keystream from stream(); encrypts and decrypts. + + Unauthenticated: only use when the data is authenticated some other + way. Never reuse a nonce with the same key. + + Args: + key: Secret key (generate with random_key()). + nonce: Public nonce (generate with random_nonce()). + data: Input bytes to XOR. + into: Buffer to write the output into (default: bytearray created). + + Returns: + Output as bytearray if into not provided, memoryview of into otherwise. + + Raises: + TypeError: If lengths are invalid. + """ + key = memoryview(key) + nonce = memoryview(nonce) + data = memoryview(data) + if into is not None: + into = memoryview(into) + if key.nbytes != KEYBYTES: + raise TypeError(f"key length must be {KEYBYTES}") + if nonce.nbytes != NONCEBYTES: + raise TypeError(f"nonce length must be {NONCEBYTES}") + if into is None: + out = bytearray(data.nbytes) + else: + if into.nbytes < data.nbytes: + raise TypeError("into length must be at least data.nbytes") + out = into + _lib.aegis256x4_stream_xor( + ffi.from_buffer(out), + _ptr(data), + data.nbytes, + _ptr(nonce), + _ptr(key), + ) + return out if into is None else memoryview(out)[: data.nbytes] # type: ignore + + def encrypt_unauthenticated( key: Buffer, nonce: Buffer, @@ -387,6 +437,10 @@ def encrypt_unauthenticated( ) -> bytearray | memoryview: """Encrypt message without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (generate with random_key()). nonce: Public nonce (generate with random_nonce()). @@ -399,6 +453,12 @@ def encrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "encrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) message = memoryview(message) @@ -433,6 +493,10 @@ def decrypt_unauthenticated( ) -> bytearray | memoryview: """Decrypt ciphertext without authentication (for testing/debugging). + .. deprecated:: + Use stream_xor() in new code. Its output is different, so keep + this only for data in the old format. + Args: key: Secret key (same key used during encryption). nonce: Public nonce (same nonce used during encryption). @@ -445,6 +509,12 @@ def decrypt_unauthenticated( Raises: TypeError: If lengths are invalid. """ + warnings.warn( + "decrypt_unauthenticated is deprecated; use stream_xor " + "(its output differs, so keep this only for existing data)", + DeprecationWarning, + stacklevel=2, + ) key = memoryview(key) nonce = memoryview(nonce) ct = memoryview(ct) @@ -1122,6 +1192,7 @@ __all__ = [ "encrypt", "decrypt", "stream", + "stream_xor", "encrypt_unauthenticated", "decrypt_unauthenticated", "mac", diff --git a/tests/test_stream_xor.py b/tests/test_stream_xor.py new file mode 100644 index 0000000..54f5218 --- /dev/null +++ b/tests/test_stream_xor.py @@ -0,0 +1,54 @@ +import pytest + +from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4 + +CIPHERS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4] + + +def cipher_id(c): + return c.NAME + + +@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id) +def test_stream_xor_roundtrip(c): + key, nonce = c.random_key(), c.random_nonce() + msg = b"stream cipher test" * 100 # spans multiple blocks, unaligned tail + ct = c.stream_xor(key, nonce, msg) + assert isinstance(ct, bytearray) + assert len(ct) == len(msg) + assert ct != msg + # the same function decrypts + assert c.stream_xor(key, nonce, ct) == msg + + +@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id) +def test_stream_xor_is_stream_xor(c): + """Output equals data XOR stream(key, nonce) keystream.""" + key, nonce = c.random_key(), c.random_nonce() + msg = b"\x5a" * 500 + ks = c.stream(key, nonce, len(msg)) + ct = c.stream_xor(key, nonce, msg) + assert bytes(ct) == bytes(m ^ k for m, k in zip(msg, ks)) + + +@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id) +def test_stream_xor_into(c): + key, nonce = c.random_key(), c.random_nonce() + msg = b"into target" * 10 + buf = bytearray(len(msg) + 10) + out = c.stream_xor(key, nonce, msg, into=buf) + assert len(out) == len(msg) + assert bytes(out) == bytes(c.stream_xor(key, nonce, msg)) + + +@pytest.mark.parametrize("c", CIPHERS, ids=cipher_id) +def test_unauthenticated_deprecated(c): + key, nonce = c.random_key(), c.random_nonce() + msg = b"legacy format" * 20 # longer than one block: absorbing mode diverges + with pytest.warns(DeprecationWarning): + ct = c.encrypt_unauthenticated(key, nonce, msg) + with pytest.warns(DeprecationWarning): + pt = c.decrypt_unauthenticated(key, nonce, ct) + assert pt == msg + # deprecated pair is NOT compatible with stream_xor beyond the first block + assert bytes(ct) != bytes(c.stream_xor(key, nonce, msg))