"""Tests for i2p_crypto.chacha20 — ChaCha20 stream cipher. Covers: - RFC 7539 Section 2.4.2 test vector - Round-trip encrypt/decrypt - 16-byte nonce (counter prefix) variant - Invalid key/nonce rejection - Empty plaintext """ import os import pytest from i2p_crypto.chacha20 import ChaCha20 # ---- RFC 7539 Section 2.4.2 test vector ---- RFC7539_KEY = bytes(range(32)) # 00 01 02 ... 1f RFC7539_NONCE = bytes([ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, ]) RFC7539_PLAINTEXT = ( b"Ladies and Gentlemen of the class of '99: " b"If I could offer you only one tip for the future, " b"sunscreen would be it." ) RFC7539_CIPHERTEXT_HEX = ( "6e2e359a2568f98041ba0728dd0d6981" "e97e7aec1d4360c20a27afccfd9fae0b" "f91b65c5524733ab8f593dabcd62b357" "1639d624e65152ab8f530c359f0861d8" "07ca0dbf500d6a6156a38e088a22b65e" "52bc514d16ccf806818ce91ab7793736" "5af90bbf74a35be6b40b8eedf2785e42" "874d" ) RFC7539_CIPHERTEXT = bytes.fromhex(RFC7539_CIPHERTEXT_HEX) class TestChaCha20RFC7539: """RFC 7539 Section 2.4.2 test vectors.""" def test_encrypt_matches_rfc_vector(self) -> None: ct = ChaCha20.encrypt(RFC7539_KEY, RFC7539_NONCE, RFC7539_PLAINTEXT) assert ct == RFC7539_CIPHERTEXT def test_decrypt_matches_rfc_vector(self) -> None: pt = ChaCha20.decrypt(RFC7539_KEY, RFC7539_NONCE, RFC7539_CIPHERTEXT) assert pt == RFC7539_PLAINTEXT class TestChaCha20RoundTrip: """Encrypt then decrypt produces original plaintext.""" def test_round_trip_short(self) -> None: key = os.urandom(32) nonce = os.urandom(12) plaintext = b"hello world" ct = ChaCha20.encrypt(key, nonce, plaintext) assert ct != plaintext pt = ChaCha20.decrypt(key, nonce, ct) assert pt == plaintext def test_round_trip_1024_bytes(self) -> None: key = os.urandom(32) nonce = os.urandom(12) plaintext = os.urandom(1024) ct = ChaCha20.encrypt(key, nonce, plaintext) pt = ChaCha20.decrypt(key, nonce, ct) assert pt == plaintext def test_round_trip_empty(self) -> None: key = os.urandom(32) nonce = os.urandom(12) ct = ChaCha20.encrypt(key, nonce, b"") assert ct == b"" pt = ChaCha20.decrypt(key, nonce, ct) assert pt == b"" def test_round_trip_16_byte_nonce(self) -> None: """16-byte nonce: 4-byte LE counter prefix + 12-byte nonce.""" key = os.urandom(32) # counter=1 little-endian + 12-byte nonce counter_prefix = (1).to_bytes(4, "little") nonce_12 = os.urandom(12) nonce_16 = counter_prefix + nonce_12 plaintext = b"sixteen-byte nonce test" ct = ChaCha20.encrypt(key, nonce_16, plaintext) pt = ChaCha20.decrypt(key, nonce_16, ct) assert pt == plaintext def test_12_and_16_byte_nonce_equivalence(self) -> None: """12-byte nonce and 16-byte nonce with counter=1 produce the same output.""" key = os.urandom(32) nonce_12 = os.urandom(12) nonce_16 = (1).to_bytes(4, "little") + nonce_12 plaintext = b"equivalence check" ct_12 = ChaCha20.encrypt(key, nonce_12, plaintext) ct_16 = ChaCha20.encrypt(key, nonce_16, plaintext) assert ct_12 == ct_16 class TestChaCha20Validation: """Input validation.""" def test_short_key_rejected(self) -> None: with pytest.raises(ValueError, match="at least 32 bytes"): ChaCha20.encrypt(b"\x00" * 16, b"\x00" * 12, b"data") def test_bad_nonce_length_rejected(self) -> None: with pytest.raises(ValueError, match="12 or 16 bytes"): ChaCha20.encrypt(b"\x00" * 32, b"\x00" * 8, b"data") def test_no_instantiation(self) -> None: with pytest.raises(TypeError): ChaCha20() def test_different_keys_produce_different_ciphertext(self) -> None: nonce = os.urandom(12) plaintext = b"key sensitivity test" ct1 = ChaCha20.encrypt(os.urandom(32), nonce, plaintext) ct2 = ChaCha20.encrypt(os.urandom(32), nonce, plaintext) assert ct1 != ct2 def test_different_nonces_produce_different_ciphertext(self) -> None: key = os.urandom(32) plaintext = b"nonce sensitivity test" ct1 = ChaCha20.encrypt(key, os.urandom(12), plaintext) ct2 = ChaCha20.encrypt(key, os.urandom(12), plaintext) assert ct1 != ct2