Files

339 lines
12 KiB
Python

"""Tests for Encryptor and Decryptor finalization behavior.
This module verifies that Encryptor and Decryptor objects become unusable
after calling final(), preventing accidental misuse.
"""
import pytest
from aeg import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
# All AEGIS algorithm modules
ALL_ALGORITHMS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4]
class TestMacFinalization:
"""Test that Mac becomes unusable after final()."""
@pytest.mark.parametrize(
"alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1]
)
def test_update_after_final_raises(self, alg):
"""Test that calling update() after final() raises RuntimeError."""
key = alg.random_key()
nonce = alg.random_nonce()
mac = alg.Mac(key, nonce)
mac.update(b"Hello, world!")
mac.final()
# Attempting to update after final should raise RuntimeError
with pytest.raises(RuntimeError, match="Cannot update after final\\(\\)"):
mac.update(b"More data")
@pytest.mark.parametrize(
"alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1]
)
def test_final_after_final_raises(self, alg):
"""Test that calling final() after final() raises RuntimeError."""
key = alg.random_key()
nonce = alg.random_nonce()
mac = alg.Mac(key, nonce)
mac.update(b"Hello, world!")
mac.final()
# Attempting to call final again should raise RuntimeError
with pytest.raises(RuntimeError, match="The MAC can only be calculated once"):
mac.final()
@pytest.mark.parametrize(
"alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1]
)
def test_digest_after_final_raises(self, alg):
"""Test that digest() and hexdigest() raise after final()."""
key = alg.random_key()
nonce = alg.random_nonce()
mac = alg.Mac(key, nonce)
mac.update(b"Hello, world!")
mac.final()
# digest() should raise after final()
with pytest.raises(RuntimeError, match="The MAC can only be calculated once"):
mac.digest()
# hexdigest() should also raise after final()
with pytest.raises(RuntimeError, match="The MAC can only be calculated once"):
mac.hexdigest()
@pytest.mark.parametrize(
"alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1]
)
def test_update_after_digest_raises(self, alg):
"""Test that calling update() after digest() raises RuntimeError."""
key = alg.random_key()
nonce = alg.random_nonce()
mac = alg.Mac(key, nonce)
mac.update(b"Hello, world!")
mac.digest()
# Attempting to update after digest should raise RuntimeError
with pytest.raises(RuntimeError, match="Cannot update after final\\(\\)"):
mac.update(b"More data")
@pytest.mark.parametrize(
"alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1]
)
def test_final_after_digest_raises(self, alg):
"""Test that calling final() after digest() raises RuntimeError."""
key = alg.random_key()
nonce = alg.random_nonce()
mac = alg.Mac(key, nonce)
mac.update(b"Hello, world!")
mac.digest()
# Attempting to call final after digest should raise RuntimeError
with pytest.raises(RuntimeError, match="The MAC can only be calculated once"):
mac.final()
class TestEncryptorFinalization:
"""Test that Encryptor becomes unusable after final()."""
def test_update_after_final_raises(self):
"""Test that calling update() after final() raises RuntimeError."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
encryptor = aegis256x4.Encryptor(key, nonce)
# Encrypt some data and finalize
encryptor.update(b"Hello, world!")
encryptor.final()
# Attempting to update after final should raise RuntimeError
with pytest.raises(
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
):
encryptor.update(b"More data")
def test_final_after_final_raises(self):
"""Test that calling final() after final() raises RuntimeError."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
encryptor = aegis256x4.Encryptor(key, nonce)
# Encrypt some data and finalize
encryptor.update(b"Hello, world!")
encryptor.final()
# Attempting to call final again should raise RuntimeError
with pytest.raises(
RuntimeError, match="Cannot call final\\(\\) after final\\(\\)"
):
encryptor.final()
def test_update_then_final_after_final_raises(self):
"""Test that both update() and final() fail after final()."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
encryptor = aegis256x4.Encryptor(key, nonce)
# Encrypt and finalize
encryptor.update(b"Test data")
encryptor.final()
# Both operations should fail
with pytest.raises(
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
):
encryptor.update(b"More data")
with pytest.raises(
RuntimeError, match="Cannot call final\\(\\) after final\\(\\)"
):
encryptor.final()
def test_empty_encryption_finalization(self):
"""Test that finalization works correctly with no update() calls."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
encryptor = aegis256x4.Encryptor(key, nonce)
# Finalize without any updates
tag = encryptor.final()
assert len(tag) == aegis256x4.MACBYTES
# Should still be unusable after
with pytest.raises(
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
):
encryptor.update(b"Data")
class TestDecryptorFinalization:
"""Test that Decryptor becomes unusable after final()."""
def test_update_after_final_raises(self):
"""Test that calling update() after final() raises RuntimeError."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
message = b"Hello, world!"
# Encrypt first to get valid ciphertext and tag
ct, tag = aegis256x4.encrypt_detached(key, nonce, message)
# Now test decryption
decryptor = aegis256x4.Decryptor(key, nonce)
decryptor.update(ct)
decryptor.final(tag)
# Attempting to update after final should raise RuntimeError
with pytest.raises(
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
):
decryptor.update(b"More ciphertext")
def test_final_after_final_raises(self):
"""Test that calling final() after final() raises RuntimeError."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
message = b"Hello, world!"
# Encrypt first to get valid ciphertext and tag
ct, tag = aegis256x4.encrypt_detached(key, nonce, message)
# Now test decryption
decryptor = aegis256x4.Decryptor(key, nonce)
decryptor.update(ct)
decryptor.final(tag)
# Attempting to call final again should raise RuntimeError
with pytest.raises(
RuntimeError, match="Cannot call final\\(\\) after final\\(\\)"
):
decryptor.final(tag)
def test_update_then_final_after_final_raises(self):
"""Test that both update() and final() fail after final()."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
message = b"Test data"
# Encrypt first
ct, tag = aegis256x4.encrypt_detached(key, nonce, message)
# Decrypt and finalize
decryptor = aegis256x4.Decryptor(key, nonce)
decryptor.update(ct)
decryptor.final(tag)
# Both operations should fail
with pytest.raises(
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
):
decryptor.update(b"More ciphertext")
with pytest.raises(
RuntimeError, match="Cannot call final\\(\\) after final\\(\\)"
):
decryptor.final(tag)
def test_empty_decryption_finalization(self):
"""Test that finalization works correctly with no update() calls."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
# Encrypt empty message
ct, tag = aegis256x4.encrypt_detached(key, nonce, b"")
# Decrypt without any updates
decryptor = aegis256x4.Decryptor(key, nonce)
decryptor.final(tag) # Should work with empty ciphertext
# Should still be unusable after
with pytest.raises(
RuntimeError, match="Cannot call update\\(\\) after final\\(\\)"
):
decryptor.update(b"Data")
def test_failed_verification_still_finalizes(self):
"""Test that even if verification fails, the object becomes unusable."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
message = b"Hello, world!"
# Encrypt first
ct, tag = aegis256x4.encrypt_detached(key, nonce, message)
# Decrypt but use wrong tag
decryptor = aegis256x4.Decryptor(key, nonce)
decryptor.update(ct)
# Try to finalize with invalid tag - should raise ValueError
bad_tag = bytes(len(tag)) # All zeros
with pytest.raises(ValueError, match="authentication failed"):
decryptor.final(bad_tag)
# Object should NOT be finalized on failure - should still be usable
# This is a design decision: failed verification shouldn't lock the object
# Let's verify current behavior
try:
decryptor.update(b"test")
# If this doesn't raise, the object is still usable after failed verification
# This might be the desired behavior
except RuntimeError:
# If this raises, failed verification also finalizes the object
pass
class TestMultipleChunksBeforeFinalization:
"""Test that multiple update() calls work before final()."""
def test_encryptor_multiple_updates(self):
"""Test that Encryptor can handle multiple update() calls before final()."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
encryptor = aegis256x4.Encryptor(key, nonce)
# Multiple updates
encryptor.update(b"Hello, ")
encryptor.update(b"world!")
encryptor.update(b" More data.")
# Should still work
tag = encryptor.final()
assert len(tag) == aegis256x4.MACBYTES
# Now unusable
with pytest.raises(RuntimeError):
encryptor.update(b"More")
def test_decryptor_multiple_updates(self):
"""Test that Decryptor can handle multiple update() calls before final()."""
key = aegis256x4.random_key()
nonce = aegis256x4.random_nonce()
# Encrypt in chunks
encryptor = aegis256x4.Encryptor(key, nonce)
ct1 = encryptor.update(b"Hello, ")
ct2 = encryptor.update(b"world!")
tag = encryptor.final()
# Decrypt in chunks
decryptor = aegis256x4.Decryptor(key, nonce)
decryptor.update(ct1)
decryptor.update(ct2)
decryptor.final(tag)
# Now unusable
with pytest.raises(RuntimeError):
decryptor.update(b"More")