A Python port of the Invisible Internet Project (I2P)
at main 256 lines 9.3 kB view raw
1"""Tests for i2p_crypto.aes_cbc — AES-256-CBC for NTCP2 handshake obfuscation. 2 3Covers: 4 - Round-trip encrypt/decrypt for 16, 32, 48 bytes 5 - 32-byte block specifically (the NTCP2 use case) 6 - Known NIST AES-256-CBC test vectors (SP 800-38A F.2.5/F.2.6) 7 - Ciphertext differs from plaintext 8 - Different IVs produce different ciphertext 9 - Wrong key/IV fails to decrypt correctly 10 - IV chaining: encrypting 32 bytes, bytes 16-31 of ciphertext become new IV 11 - Input validation (key size, IV size, block alignment) 12""" 13 14from __future__ import annotations 15 16import hashlib 17import os 18 19import pytest 20 21from i2p_crypto.aes_cbc import aes_cbc_decrypt, aes_cbc_encrypt 22 23 24# --------------------------------------------------------------------------- 25# Helpers 26# --------------------------------------------------------------------------- 27 28def _random_key() -> bytes: 29 """Return a random 32-byte AES-256 key.""" 30 return os.urandom(32) 31 32 33def _random_iv() -> bytes: 34 """Return a random 16-byte IV.""" 35 return os.urandom(16) 36 37 38# --------------------------------------------------------------------------- 39# Round-trip tests 40# --------------------------------------------------------------------------- 41 42class TestRoundTrip: 43 """encrypt then decrypt must return the original plaintext.""" 44 45 def test_16_bytes(self): 46 key, iv = _random_key(), _random_iv() 47 plaintext = os.urandom(16) 48 ciphertext = aes_cbc_encrypt(key, iv, plaintext) 49 assert len(ciphertext) == 16 50 assert aes_cbc_decrypt(key, iv, ciphertext) == plaintext 51 52 def test_32_bytes(self): 53 key, iv = _random_key(), _random_iv() 54 plaintext = os.urandom(32) 55 ciphertext = aes_cbc_encrypt(key, iv, plaintext) 56 assert len(ciphertext) == 32 57 assert aes_cbc_decrypt(key, iv, ciphertext) == plaintext 58 59 def test_48_bytes(self): 60 key, iv = _random_key(), _random_iv() 61 plaintext = os.urandom(48) 62 ciphertext = aes_cbc_encrypt(key, iv, plaintext) 63 assert len(ciphertext) == 48 64 assert aes_cbc_decrypt(key, iv, ciphertext) == plaintext 65 66 def test_deterministic(self): 67 """Same key+IV+plaintext must produce identical ciphertext.""" 68 key, iv = _random_key(), _random_iv() 69 plaintext = os.urandom(32) 70 c1 = aes_cbc_encrypt(key, iv, plaintext) 71 c2 = aes_cbc_encrypt(key, iv, plaintext) 72 assert c1 == c2 73 74 75# --------------------------------------------------------------------------- 76# NTCP2 32-byte use case 77# --------------------------------------------------------------------------- 78 79class TestNTCP2UseCase: 80 """NTCP2 handshake message 1: 32-byte ephemeral key X encrypted.""" 81 82 def test_ntcp2_encrypt_decrypt_32_byte_key(self): 83 """Simulate NTCP2 message 1 obfuscation of ephemeral key X. 84 85 Key = SHA-256(responder RouterInfo hash) 86 IV = responder 'i' parameter (16 bytes) 87 Plaintext = 32 bytes (ephemeral key X, exactly 2 AES blocks) 88 """ 89 router_info_hash = os.urandom(32) 90 aes_key = hashlib.sha256(router_info_hash).digest() 91 iv = os.urandom(16) # responder's "i" parameter 92 ephemeral_key_x = os.urandom(32) 93 94 ciphertext = aes_cbc_encrypt(aes_key, iv, ephemeral_key_x) 95 assert len(ciphertext) == 32 96 recovered = aes_cbc_decrypt(aes_key, iv, ciphertext) 97 assert recovered == ephemeral_key_x 98 99 def test_iv_chaining_for_message_2(self): 100 """After encrypting 32 bytes, bytes 16-31 of ciphertext become 101 the IV for message 2 encryption. 102 103 This tests that the IV chaining property of CBC holds: the last 104 ciphertext block is usable as an IV for subsequent encryption. 105 """ 106 aes_key = _random_key() 107 iv1 = _random_iv() 108 plaintext_msg1 = os.urandom(32) 109 110 ciphertext_msg1 = aes_cbc_encrypt(aes_key, iv1, plaintext_msg1) 111 112 # In NTCP2, bytes 16-31 of the first ciphertext become IV for msg2 113 iv2 = ciphertext_msg1[16:32] 114 115 plaintext_msg2 = os.urandom(32) 116 ciphertext_msg2 = aes_cbc_encrypt(aes_key, iv2, plaintext_msg2) 117 recovered_msg2 = aes_cbc_decrypt(aes_key, iv2, ciphertext_msg2) 118 assert recovered_msg2 == plaintext_msg2 119 120 # The two ciphertexts should differ (different IVs, different plaintexts) 121 assert ciphertext_msg1 != ciphertext_msg2 122 123 124# --------------------------------------------------------------------------- 125# Known test vectors — NIST SP 800-38A AES-256-CBC (F.2.5 / F.2.6) 126# --------------------------------------------------------------------------- 127 128class TestKnownVectors: 129 """NIST SP 800-38A AES-256-CBC test vectors.""" 130 131 NIST_KEY = bytes.fromhex( 132 "603deb1015ca71be2b73aef0857d7781" 133 "1f352c073b6108d72d9810a30914dff4" 134 ) 135 NIST_IV = bytes.fromhex("000102030405060708090a0b0c0d0e0f") 136 137 NIST_PLAINTEXT = bytes.fromhex( 138 "6bc1bee22e409f96e93d7e117393172a" 139 "ae2d8a571e03ac9c9eb76fac45af8e51" 140 "30c81c46a35ce411e5fbc1191a0a52ef" 141 "f69f2445df4f9b17ad2b417be66c3710" 142 ) 143 144 NIST_CIPHERTEXT = bytes.fromhex( 145 "f58c4c04d6e5f1ba779eabfb5f7bfbd6" 146 "9cfc4e967edb808d679f777bc6702c7d" 147 "39f23369a9d9bacfa530e26304231461" 148 "b2eb05e2c39be9fcda6c19078c6a9d1b" 149 ) 150 151 def test_encrypt(self): 152 ct = aes_cbc_encrypt(self.NIST_KEY, self.NIST_IV, self.NIST_PLAINTEXT) 153 assert ct == self.NIST_CIPHERTEXT 154 155 def test_decrypt(self): 156 pt = aes_cbc_decrypt(self.NIST_KEY, self.NIST_IV, self.NIST_CIPHERTEXT) 157 assert pt == self.NIST_PLAINTEXT 158 159 def test_round_trip(self): 160 ct = aes_cbc_encrypt(self.NIST_KEY, self.NIST_IV, self.NIST_PLAINTEXT) 161 pt = aes_cbc_decrypt(self.NIST_KEY, self.NIST_IV, ct) 162 assert pt == self.NIST_PLAINTEXT 163 164 165# --------------------------------------------------------------------------- 166# Ciphertext differs from plaintext 167# --------------------------------------------------------------------------- 168 169class TestCiphertextDiffers: 170 """Ciphertext must not equal plaintext.""" 171 172 def test_ciphertext_not_plaintext_16(self): 173 plaintext = os.urandom(16) 174 ct = aes_cbc_encrypt(_random_key(), _random_iv(), plaintext) 175 assert ct != plaintext 176 177 def test_ciphertext_not_plaintext_32(self): 178 plaintext = os.urandom(32) 179 ct = aes_cbc_encrypt(_random_key(), _random_iv(), plaintext) 180 assert ct != plaintext 181 182 183# --------------------------------------------------------------------------- 184# Different IVs produce different ciphertext 185# --------------------------------------------------------------------------- 186 187class TestDifferentIVs: 188 """Same key + plaintext but different IVs must yield different ciphertext.""" 189 190 def test_different_ivs_different_ciphertext(self): 191 key = _random_key() 192 plaintext = os.urandom(32) 193 iv1 = _random_iv() 194 iv2 = _random_iv() 195 # Ensure IVs are actually different (astronomically unlikely to collide) 196 assert iv1 != iv2 197 198 ct1 = aes_cbc_encrypt(key, iv1, plaintext) 199 ct2 = aes_cbc_encrypt(key, iv2, plaintext) 200 assert ct1 != ct2 201 202 203# --------------------------------------------------------------------------- 204# Wrong key / wrong IV fails to decrypt correctly 205# --------------------------------------------------------------------------- 206 207class TestWrongKeyIV: 208 """Decrypting with wrong key or IV must not return original plaintext.""" 209 210 def test_wrong_key(self): 211 key1, key2 = _random_key(), _random_key() 212 iv = _random_iv() 213 plaintext = os.urandom(32) 214 ciphertext = aes_cbc_encrypt(key1, iv, plaintext) 215 decrypted = aes_cbc_decrypt(key2, iv, ciphertext) 216 assert decrypted != plaintext 217 218 def test_wrong_iv(self): 219 key = _random_key() 220 iv1, iv2 = _random_iv(), _random_iv() 221 plaintext = os.urandom(32) 222 ciphertext = aes_cbc_encrypt(key, iv1, plaintext) 223 decrypted = aes_cbc_decrypt(key, iv2, ciphertext) 224 assert decrypted != plaintext 225 226 227# --------------------------------------------------------------------------- 228# Input validation 229# --------------------------------------------------------------------------- 230 231class TestValidation: 232 """Key, IV, and plaintext sizes must be enforced.""" 233 234 def test_key_not_32_bytes(self): 235 with pytest.raises(ValueError, match="32 bytes"): 236 aes_cbc_encrypt(os.urandom(16), _random_iv(), os.urandom(16)) 237 238 def test_iv_not_16_bytes(self): 239 with pytest.raises(ValueError, match="16 bytes"): 240 aes_cbc_encrypt(_random_key(), os.urandom(12), os.urandom(16)) 241 242 def test_plaintext_not_aligned(self): 243 with pytest.raises(ValueError, match="multiple of 16"): 244 aes_cbc_encrypt(_random_key(), _random_iv(), os.urandom(15)) 245 246 def test_plaintext_empty(self): 247 with pytest.raises(ValueError, match="empty"): 248 aes_cbc_encrypt(_random_key(), _random_iv(), b"") 249 250 def test_ciphertext_not_aligned(self): 251 with pytest.raises(ValueError, match="multiple of 16"): 252 aes_cbc_decrypt(_random_key(), _random_iv(), os.urandom(17)) 253 254 def test_ciphertext_empty(self): 255 with pytest.raises(ValueError, match="empty"): 256 aes_cbc_decrypt(_random_key(), _random_iv(), b"")