A Python port of the Invisible Internet Project (I2P)
at main 214 lines 7.7 kB view raw
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