A Python port of the Invisible Internet Project (I2P)
at main 138 lines 4.5 kB view raw
1"""Tests for i2p_crypto.chacha20 — ChaCha20 stream cipher. 2 3Covers: 4- RFC 7539 Section 2.4.2 test vector 5- Round-trip encrypt/decrypt 6- 16-byte nonce (counter prefix) variant 7- Invalid key/nonce rejection 8- Empty plaintext 9""" 10 11import os 12 13import pytest 14 15from i2p_crypto.chacha20 import ChaCha20 16 17 18# ---- RFC 7539 Section 2.4.2 test vector ---- 19 20RFC7539_KEY = bytes(range(32)) # 00 01 02 ... 1f 21 22RFC7539_NONCE = bytes([ 23 0x00, 0x00, 0x00, 0x00, 24 0x00, 0x00, 0x00, 0x4a, 25 0x00, 0x00, 0x00, 0x00, 26]) 27 28RFC7539_PLAINTEXT = ( 29 b"Ladies and Gentlemen of the class of '99: " 30 b"If I could offer you only one tip for the future, " 31 b"sunscreen would be it." 32) 33 34RFC7539_CIPHERTEXT_HEX = ( 35 "6e2e359a2568f98041ba0728dd0d6981" 36 "e97e7aec1d4360c20a27afccfd9fae0b" 37 "f91b65c5524733ab8f593dabcd62b357" 38 "1639d624e65152ab8f530c359f0861d8" 39 "07ca0dbf500d6a6156a38e088a22b65e" 40 "52bc514d16ccf806818ce91ab7793736" 41 "5af90bbf74a35be6b40b8eedf2785e42" 42 "874d" 43) 44 45RFC7539_CIPHERTEXT = bytes.fromhex(RFC7539_CIPHERTEXT_HEX) 46 47 48class TestChaCha20RFC7539: 49 """RFC 7539 Section 2.4.2 test vectors.""" 50 51 def test_encrypt_matches_rfc_vector(self) -> None: 52 ct = ChaCha20.encrypt(RFC7539_KEY, RFC7539_NONCE, RFC7539_PLAINTEXT) 53 assert ct == RFC7539_CIPHERTEXT 54 55 def test_decrypt_matches_rfc_vector(self) -> None: 56 pt = ChaCha20.decrypt(RFC7539_KEY, RFC7539_NONCE, RFC7539_CIPHERTEXT) 57 assert pt == RFC7539_PLAINTEXT 58 59 60class TestChaCha20RoundTrip: 61 """Encrypt then decrypt produces original plaintext.""" 62 63 def test_round_trip_short(self) -> None: 64 key = os.urandom(32) 65 nonce = os.urandom(12) 66 plaintext = b"hello world" 67 ct = ChaCha20.encrypt(key, nonce, plaintext) 68 assert ct != plaintext 69 pt = ChaCha20.decrypt(key, nonce, ct) 70 assert pt == plaintext 71 72 def test_round_trip_1024_bytes(self) -> None: 73 key = os.urandom(32) 74 nonce = os.urandom(12) 75 plaintext = os.urandom(1024) 76 ct = ChaCha20.encrypt(key, nonce, plaintext) 77 pt = ChaCha20.decrypt(key, nonce, ct) 78 assert pt == plaintext 79 80 def test_round_trip_empty(self) -> None: 81 key = os.urandom(32) 82 nonce = os.urandom(12) 83 ct = ChaCha20.encrypt(key, nonce, b"") 84 assert ct == b"" 85 pt = ChaCha20.decrypt(key, nonce, ct) 86 assert pt == b"" 87 88 def test_round_trip_16_byte_nonce(self) -> None: 89 """16-byte nonce: 4-byte LE counter prefix + 12-byte nonce.""" 90 key = os.urandom(32) 91 # counter=1 little-endian + 12-byte nonce 92 counter_prefix = (1).to_bytes(4, "little") 93 nonce_12 = os.urandom(12) 94 nonce_16 = counter_prefix + nonce_12 95 plaintext = b"sixteen-byte nonce test" 96 ct = ChaCha20.encrypt(key, nonce_16, plaintext) 97 pt = ChaCha20.decrypt(key, nonce_16, ct) 98 assert pt == plaintext 99 100 def test_12_and_16_byte_nonce_equivalence(self) -> None: 101 """12-byte nonce and 16-byte nonce with counter=1 produce the same output.""" 102 key = os.urandom(32) 103 nonce_12 = os.urandom(12) 104 nonce_16 = (1).to_bytes(4, "little") + nonce_12 105 plaintext = b"equivalence check" 106 ct_12 = ChaCha20.encrypt(key, nonce_12, plaintext) 107 ct_16 = ChaCha20.encrypt(key, nonce_16, plaintext) 108 assert ct_12 == ct_16 109 110 111class TestChaCha20Validation: 112 """Input validation.""" 113 114 def test_short_key_rejected(self) -> None: 115 with pytest.raises(ValueError, match="at least 32 bytes"): 116 ChaCha20.encrypt(b"\x00" * 16, b"\x00" * 12, b"data") 117 118 def test_bad_nonce_length_rejected(self) -> None: 119 with pytest.raises(ValueError, match="12 or 16 bytes"): 120 ChaCha20.encrypt(b"\x00" * 32, b"\x00" * 8, b"data") 121 122 def test_no_instantiation(self) -> None: 123 with pytest.raises(TypeError): 124 ChaCha20() 125 126 def test_different_keys_produce_different_ciphertext(self) -> None: 127 nonce = os.urandom(12) 128 plaintext = b"key sensitivity test" 129 ct1 = ChaCha20.encrypt(os.urandom(32), nonce, plaintext) 130 ct2 = ChaCha20.encrypt(os.urandom(32), nonce, plaintext) 131 assert ct1 != ct2 132 133 def test_different_nonces_produce_different_ciphertext(self) -> None: 134 key = os.urandom(32) 135 plaintext = b"nonce sensitivity test" 136 ct1 = ChaCha20.encrypt(key, os.urandom(12), plaintext) 137 ct2 = ChaCha20.encrypt(key, os.urandom(12), plaintext) 138 assert ct1 != ct2