A Python port of the Invisible Internet Project (I2P)
1"""Tests for i2p_crypto.aes — AES-256-CBC engine.
2
3Covers:
4 - Round-trip encrypt/decrypt (CBC)
5 - Round-trip encrypt_block/decrypt_block (single block ECB)
6 - Known NIST test vectors (AES-256-CBC)
7 - Wrong-key detection
8 - Block-alignment enforcement (payload not a multiple of 16)
9 - IV length enforcement
10 - Empty payload rejection
11"""
12
13from __future__ import annotations
14
15import os
16
17import pytest
18
19from i2p_crypto.aes import AESEngine
20
21
22# ---------------------------------------------------------------------------
23# Helpers
24# ---------------------------------------------------------------------------
25
26def _random_key() -> bytes:
27 """Return a random 32-byte AES-256 key."""
28 return os.urandom(32)
29
30
31def _random_iv() -> bytes:
32 """Return a random 16-byte IV."""
33 return os.urandom(16)
34
35
36# ---------------------------------------------------------------------------
37# Round-trip tests
38# ---------------------------------------------------------------------------
39
40class TestRoundTrip:
41 """encrypt then decrypt must return the original plaintext."""
42
43 def test_single_block(self):
44 key, iv = _random_key(), _random_iv()
45 plaintext = os.urandom(16)
46 ciphertext = AESEngine.encrypt(plaintext, key, iv)
47 assert AESEngine.decrypt(ciphertext, key, iv) == plaintext
48
49 def test_multi_block(self):
50 key, iv = _random_key(), _random_iv()
51 plaintext = os.urandom(16 * 8) # 128 bytes
52 ciphertext = AESEngine.encrypt(plaintext, key, iv)
53 assert len(ciphertext) == len(plaintext)
54 assert AESEngine.decrypt(ciphertext, key, iv) == plaintext
55
56 def test_large_payload(self):
57 key, iv = _random_key(), _random_iv()
58 plaintext = os.urandom(16 * 64) # 1024 bytes
59 ciphertext = AESEngine.encrypt(plaintext, key, iv)
60 assert AESEngine.decrypt(ciphertext, key, iv) == plaintext
61
62 def test_in_place_round_trip(self):
63 """Mirrors Java's testED2: encrypt, then decrypt, verify equality."""
64 key, iv = _random_key(), _random_iv()
65 orig = os.urandom(128)
66 data = AESEngine.encrypt(orig, key, iv)
67 data = AESEngine.decrypt(data, key, iv)
68 assert data == orig
69
70 def test_deterministic(self):
71 """Same key+IV+plaintext must produce same ciphertext."""
72 key, iv = _random_key(), _random_iv()
73 plaintext = os.urandom(48)
74 c1 = AESEngine.encrypt(plaintext, key, iv)
75 c2 = AESEngine.encrypt(plaintext, key, iv)
76 assert c1 == c2
77
78
79# ---------------------------------------------------------------------------
80# Single-block (ECB) tests
81# ---------------------------------------------------------------------------
82
83class TestBlockOperations:
84 """encrypt_block / decrypt_block round-trip and validation."""
85
86 def test_round_trip(self):
87 key = _random_key()
88 block = os.urandom(16)
89 enc = AESEngine.encrypt_block(block, key)
90 assert AESEngine.decrypt_block(enc, key) == block
91
92 def test_wrong_size_encrypt(self):
93 key = _random_key()
94 with pytest.raises(ValueError, match="exactly 16 bytes"):
95 AESEngine.encrypt_block(os.urandom(15), key)
96
97 def test_wrong_size_decrypt(self):
98 key = _random_key()
99 with pytest.raises(ValueError, match="exactly 16 bytes"):
100 AESEngine.decrypt_block(os.urandom(17), key)
101
102
103# ---------------------------------------------------------------------------
104# Known test vectors — NIST AES-256-CBC (F.2.5 / F.2.6)
105# ---------------------------------------------------------------------------
106
107class TestKnownVectors:
108 """NIST SP 800-38A AES-256-CBC test vectors."""
109
110 # Key, IV, plaintext blocks, expected ciphertext blocks from NIST SP 800-38A
111 NIST_KEY = bytes.fromhex(
112 "603deb1015ca71be2b73aef0857d7781"
113 "1f352c073b6108d72d9810a30914dff4"
114 )
115 NIST_IV = bytes.fromhex("000102030405060708090a0b0c0d0e0f")
116
117 NIST_PLAINTEXT = bytes.fromhex(
118 "6bc1bee22e409f96e93d7e117393172a"
119 "ae2d8a571e03ac9c9eb76fac45af8e51"
120 "30c81c46a35ce411e5fbc1191a0a52ef"
121 "f69f2445df4f9b17ad2b417be66c3710"
122 )
123
124 NIST_CIPHERTEXT = bytes.fromhex(
125 "f58c4c04d6e5f1ba779eabfb5f7bfbd6"
126 "9cfc4e967edb808d679f777bc6702c7d"
127 "39f23369a9d9bacfa530e26304231461"
128 "b2eb05e2c39be9fcda6c19078c6a9d1b"
129 )
130
131 def test_encrypt(self):
132 ct = AESEngine.encrypt(self.NIST_PLAINTEXT, self.NIST_KEY, self.NIST_IV)
133 assert ct == self.NIST_CIPHERTEXT
134
135 def test_decrypt(self):
136 pt = AESEngine.decrypt(self.NIST_CIPHERTEXT, self.NIST_KEY, self.NIST_IV)
137 assert pt == self.NIST_PLAINTEXT
138
139
140# ---------------------------------------------------------------------------
141# Wrong-key detection
142# ---------------------------------------------------------------------------
143
144class TestWrongKey:
145 """Decrypting with a different key must not return the original plaintext."""
146
147 def test_wrong_key_cbc(self):
148 key1, key2 = _random_key(), _random_key()
149 iv = _random_iv()
150 plaintext = os.urandom(64)
151 ciphertext = AESEngine.encrypt(plaintext, key1, iv)
152 decrypted = AESEngine.decrypt(ciphertext, key2, iv)
153 assert decrypted != plaintext
154
155 def test_wrong_key_block(self):
156 key1, key2 = _random_key(), _random_key()
157 block = os.urandom(16)
158 enc = AESEngine.encrypt_block(block, key1)
159 dec = AESEngine.decrypt_block(enc, key2)
160 assert dec != block
161
162
163# ---------------------------------------------------------------------------
164# Alignment / validation enforcement
165# ---------------------------------------------------------------------------
166
167class TestValidation:
168 """Payload length and IV size must be enforced."""
169
170 def test_non_aligned_encrypt(self):
171 with pytest.raises(ValueError, match="multiple of 16"):
172 AESEngine.encrypt(os.urandom(15), _random_key(), _random_iv())
173
174 def test_non_aligned_decrypt(self):
175 with pytest.raises(ValueError, match="multiple of 16"):
176 AESEngine.decrypt(os.urandom(17), _random_key(), _random_iv())
177
178 def test_empty_payload_encrypt(self):
179 with pytest.raises(ValueError, match="empty"):
180 AESEngine.encrypt(b"", _random_key(), _random_iv())
181
182 def test_empty_payload_decrypt(self):
183 with pytest.raises(ValueError, match="empty"):
184 AESEngine.decrypt(b"", _random_key(), _random_iv())
185
186 def test_bad_iv_length(self):
187 with pytest.raises(ValueError, match="IV must be exactly 16"):
188 AESEngine.encrypt(os.urandom(16), _random_key(), os.urandom(12))
189
190 def test_odd_size_payload(self):
191 """Payload of 7 bytes — not aligned."""
192 with pytest.raises(ValueError, match="multiple of 16"):
193 AESEngine.encrypt(os.urandom(7), _random_key(), _random_iv())
194
195 def test_33_byte_payload(self):
196 """33 bytes — one byte over two blocks."""
197 with pytest.raises(ValueError, match="multiple of 16"):
198 AESEngine.encrypt(os.urandom(33), _random_key(), _random_iv())
199
200
201# ---------------------------------------------------------------------------
202# Key size variants
203# ---------------------------------------------------------------------------
204
205class TestKeySizes:
206 """AES supports 128, 192, and 256-bit keys."""
207
208 @pytest.mark.parametrize("key_len", [16, 24, 32])
209 def test_round_trip_key_sizes(self, key_len: int):
210 key = os.urandom(key_len)
211 iv = _random_iv()
212 plaintext = os.urandom(48)
213 ct = AESEngine.encrypt(plaintext, key, iv)
214 assert AESEngine.decrypt(ct, key, iv) == plaintext