A Python port of the Invisible Internet Project (I2P)
1"""Tests for i2p_crypto — hash, HMAC, HKDF with byte-identical parity checks."""
2
3import hashlib
4import hmac as stdlib_hmac
5
6import pytest
7
8
9# === Hash data structures ===
10
11class TestHash:
12 def test_create_32_bytes(self):
13 from i2p_crypto.hash_data import Hash
14 data = bytes(range(32))
15 h = Hash(data)
16 assert h.data == data
17
18 def test_reject_wrong_length(self):
19 from i2p_crypto.hash_data import Hash
20 with pytest.raises(ValueError):
21 Hash(b"too short")
22
23 def test_equality(self):
24 from i2p_crypto.hash_data import Hash
25 a = Hash(b"\x00" * 32)
26 b = Hash(b"\x00" * 32)
27 assert a == b
28
29 def test_inequality(self):
30 from i2p_crypto.hash_data import Hash
31 a = Hash(b"\x00" * 32)
32 b = Hash(b"\x01" + b"\x00" * 31)
33 assert a != b
34
35 def test_hash_code_uses_first_4_bytes(self):
36 from i2p_crypto.hash_data import Hash
37 data = b"\xde\xad\xbe\xef" + b"\x00" * 28
38 h = Hash(data)
39 assert hash(h) == int.from_bytes(b"\xde\xad\xbe\xef", "big")
40
41 def test_create_from_offset(self):
42 from i2p_crypto.hash_data import Hash
43 full = b"\xff" * 10 + bytes(range(32)) + b"\xff" * 10
44 h = Hash.create(full, 10)
45 assert h.data == bytes(range(32))
46
47 def test_fake_hash(self):
48 from i2p_crypto.hash_data import Hash
49 assert Hash.FAKE_HASH.data == b"\x00" * 32
50
51 def test_bytes_conversion(self):
52 from i2p_crypto.hash_data import Hash
53 data = bytes(range(32))
54 h = Hash(data)
55 assert bytes(h) == data
56
57 def test_none_creates_zero_hash(self):
58 from i2p_crypto.hash_data import Hash
59 h = Hash()
60 assert h.data == b"\x00" * 32
61
62 def test_immutable_data(self):
63 from i2p_crypto.hash_data import Hash
64 data = bytearray(32)
65 h = Hash(bytes(data))
66 data[0] = 0xFF
67 assert h.data[0] == 0 # Should not be affected
68
69
70class TestSHA1Hash:
71 def test_create_20_bytes(self):
72 from i2p_crypto.hash_data import SHA1Hash
73 data = bytes(range(20))
74 h = SHA1Hash(data)
75 assert h.data == data
76 assert SHA1Hash.HASH_LENGTH == 20
77
78 def test_reject_wrong_length(self):
79 from i2p_crypto.hash_data import SHA1Hash
80 with pytest.raises(ValueError):
81 SHA1Hash(b"too short")
82
83 def test_equality(self):
84 from i2p_crypto.hash_data import SHA1Hash
85 a = SHA1Hash(b"\x00" * 20)
86 b = SHA1Hash(b"\x00" * 20)
87 assert a == b
88
89
90class TestHash384:
91 def test_length(self):
92 from i2p_crypto.hash_data import Hash384
93 assert Hash384.HASH_LENGTH == 48
94 h = Hash384(b"\x00" * 48)
95 assert len(h.data) == 48
96
97 def test_reject_wrong_length(self):
98 from i2p_crypto.hash_data import Hash384
99 with pytest.raises(ValueError):
100 Hash384(b"\x00" * 32)
101
102
103class TestHash512:
104 def test_length(self):
105 from i2p_crypto.hash_data import Hash512
106 assert Hash512.HASH_LENGTH == 64
107 h = Hash512(b"\x00" * 64)
108 assert len(h.data) == 64
109
110 def test_reject_wrong_length(self):
111 from i2p_crypto.hash_data import Hash512
112 with pytest.raises(ValueError):
113 Hash512(b"\x00" * 32)
114
115
116# === SHA256Generator — byte-identical with hashlib ===
117
118class TestSHA256Generator:
119 def test_known_vector_empty(self):
120 """SHA-256 of empty string — NIST test vector."""
121 from i2p_crypto.sha256_generator import SHA256Generator
122 gen = SHA256Generator.get_instance()
123 h = gen.calculate_hash(b"")
124 expected = bytes.fromhex(
125 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
126 )
127 assert h.data == expected
128
129 def test_known_vector_abc(self):
130 """SHA-256 of 'abc' — NIST test vector."""
131 from i2p_crypto.sha256_generator import SHA256Generator
132 gen = SHA256Generator.get_instance()
133 h = gen.calculate_hash(b"abc")
134 expected = bytes.fromhex(
135 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
136 )
137 assert h.data == expected
138
139 def test_known_vector_long(self):
140 """SHA-256 of 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'."""
141 from i2p_crypto.sha256_generator import SHA256Generator
142 gen = SHA256Generator.get_instance()
143 msg = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
144 h = gen.calculate_hash(msg)
145 expected = bytes.fromhex(
146 "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
147 )
148 assert h.data == expected
149
150 def test_matches_hashlib(self):
151 """Byte-identical with Python's hashlib.sha256."""
152 from i2p_crypto.sha256_generator import SHA256Generator
153 gen = SHA256Generator.get_instance()
154 for data in [b"", b"hello", b"x" * 1000, bytes(range(256))]:
155 assert gen.calculate_hash(data).data == hashlib.sha256(data).digest()
156
157 def test_offset_and_length(self):
158 from i2p_crypto.sha256_generator import SHA256Generator
159 gen = SHA256Generator.get_instance()
160 data = b"XXXhelloXXX"
161 h = gen.calculate_hash(data, 3, 5)
162 assert h.data == hashlib.sha256(b"hello").digest()
163
164 def test_calculate_hash_into(self):
165 from i2p_crypto.sha256_generator import SHA256Generator
166 gen = SHA256Generator.get_instance()
167 out = bytearray(48)
168 gen.calculate_hash_into(b"test", 0, 4, out, 8)
169 expected = hashlib.sha256(b"test").digest()
170 assert out[8:40] == expected
171 assert out[:8] == b"\x00" * 8
172 assert out[40:] == b"\x00" * 8
173
174 def test_digest_convenience(self):
175 from i2p_crypto.sha256_generator import SHA256Generator
176 assert SHA256Generator.digest(b"hello") == hashlib.sha256(b"hello").digest()
177
178 def test_singleton(self):
179 from i2p_crypto.sha256_generator import SHA256Generator
180 a = SHA256Generator.get_instance()
181 b = SHA256Generator.get_instance()
182 assert a is b
183
184 def test_returns_hash_object(self):
185 from i2p_crypto.sha256_generator import SHA256Generator
186 from i2p_crypto.hash_data import Hash
187 gen = SHA256Generator.get_instance()
188 h = gen.calculate_hash(b"data")
189 assert isinstance(h, Hash)
190 assert len(h.data) == 32
191
192
193# === HMAC256Generator — byte-identical with hmac module ===
194
195class TestHMAC256Generator:
196 def test_rfc4231_vector_1(self):
197 """RFC 4231 Test Case 1 for HMAC-SHA256."""
198 from i2p_crypto.hmac_generator import HMAC256Generator
199 gen = HMAC256Generator.get_instance()
200 key = b"\x0b" * 20 + b"\x00" * 12 # pad to 32
201 data = b"Hi There"
202 out = bytearray(32)
203 gen.calculate(key, data, 0, len(data), out, 0)
204 # HMAC-SHA256 with key=0b*20 (first 32 bytes, padded with zeros)
205 expected = stdlib_hmac.new(key[:32], data, hashlib.sha256).digest()
206 assert bytes(out) == expected
207
208 def test_matches_stdlib_hmac(self):
209 """Byte-identical with Python's hmac.new(key, data, sha256)."""
210 from i2p_crypto.hmac_generator import HMAC256Generator
211 gen = HMAC256Generator.get_instance()
212 for key, data in [
213 (b"\x00" * 32, b""),
214 (bytes(range(32)), b"hello world"),
215 (b"\xff" * 32, b"x" * 1000),
216 (bytes(range(32)), bytes(range(256))),
217 ]:
218 result = gen.calculate(key, data)
219 expected = stdlib_hmac.new(key[:32], data, hashlib.sha256).digest()
220 assert result == expected
221
222 def test_calculate_with_offset(self):
223 from i2p_crypto.hmac_generator import HMAC256Generator
224 gen = HMAC256Generator.get_instance()
225 key = bytes(range(32))
226 data = b"XXXhelloXXX"
227 result = gen.calculate(key, data, 3, 5)
228 expected = stdlib_hmac.new(key, b"hello", hashlib.sha256).digest()
229 assert result == expected
230
231 def test_calculate_into_buffer(self):
232 from i2p_crypto.hmac_generator import HMAC256Generator
233 gen = HMAC256Generator.get_instance()
234 key = bytes(range(32))
235 data = b"test"
236 target = bytearray(48)
237 gen.calculate(key, data, 0, 4, target, 8)
238 expected = stdlib_hmac.new(key, data, hashlib.sha256).digest()
239 assert target[8:40] == expected
240 assert target[:8] == b"\x00" * 8
241
242 def test_verify_correct(self):
243 from i2p_crypto.hmac_generator import HMAC256Generator
244 gen = HMAC256Generator.get_instance()
245 key = bytes(range(32))
246 data = b"verify me"
247 mac = gen.calculate(key, data)
248 assert gen.verify(key, data, 0, len(data), mac)
249
250 def test_verify_wrong_mac(self):
251 from i2p_crypto.hmac_generator import HMAC256Generator
252 gen = HMAC256Generator.get_instance()
253 key = bytes(range(32))
254 data = b"verify me"
255 mac = bytearray(gen.calculate(key, data))
256 mac[0] ^= 0xFF # corrupt
257 assert not gen.verify(key, data, 0, len(data), bytes(mac))
258
259 def test_verify_partial_length(self):
260 from i2p_crypto.hmac_generator import HMAC256Generator
261 gen = HMAC256Generator.get_instance()
262 key = bytes(range(32))
263 data = b"partial"
264 mac = gen.calculate(key, data)
265 # Verify only first 16 bytes
266 assert gen.verify(key, data, 0, len(data), mac, 0, 16)
267
268 def test_singleton(self):
269 from i2p_crypto.hmac_generator import HMAC256Generator
270 a = HMAC256Generator.get_instance()
271 b = HMAC256Generator.get_instance()
272 assert a is b
273
274
275# === HKDF — RFC 5869 test vectors ===
276
277class TestHKDF:
278 def test_rfc5869_case1(self):
279 """RFC 5869 Test Case 1."""
280 from i2p_crypto.hkdf import HKDF
281 hkdf = HKDF()
282 ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
283 salt = bytes.fromhex("000102030405060708090a0b0c")
284 info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9")
285 expected_okm = bytes.fromhex(
286 "3cb25f25faacd57a90434f64d0362f2a"
287 "2d2d0a90cf1a5a4c5db02d56ecc4c5bf"
288 "34007208d5b887185865"
289 )
290 okm = hkdf.extract_and_expand(salt, ikm, info, 42)
291 assert okm == expected_okm
292
293 def test_rfc5869_case2(self):
294 """RFC 5869 Test Case 2."""
295 from i2p_crypto.hkdf import HKDF
296 hkdf = HKDF()
297 ikm = bytes.fromhex(
298 "000102030405060708090a0b0c0d0e0f"
299 "101112131415161718191a1b1c1d1e1f"
300 "202122232425262728292a2b2c2d2e2f"
301 "303132333435363738393a3b3c3d3e3f"
302 "404142434445464748494a4b4c4d4e4f"
303 )
304 salt = bytes.fromhex(
305 "606162636465666768696a6b6c6d6e6f"
306 "707172737475767778797a7b7c7d7e7f"
307 "808182838485868788898a8b8c8d8e8f"
308 "909192939495969798999a9b9c9d9e9f"
309 "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"
310 )
311 info = bytes.fromhex(
312 "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf"
313 "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf"
314 "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf"
315 "e0e1e2e3e4e5e6e7e8e9eaebecedeeef"
316 "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"
317 )
318 expected_okm = bytes.fromhex(
319 "b11e398dc80327a1c8e7f78c596a4934"
320 "4f012eda2d4efad8a050cc4c19afa97c"
321 "59045a99cac7827271cb41c65e590e09"
322 "da3275600c2f09b8367793a9aca3db71"
323 "cc30c58179ec3e87c14c01d5c1f3434f"
324 "1d87"
325 )
326 okm = hkdf.extract_and_expand(salt, ikm, info, 82)
327 assert okm == expected_okm
328
329 def test_rfc5869_case3(self):
330 """RFC 5869 Test Case 3 — zero-length salt and info."""
331 from i2p_crypto.hkdf import HKDF
332 hkdf = HKDF()
333 ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
334 salt = b""
335 info = b""
336 expected_okm = bytes.fromhex(
337 "8da4e775a563c18f715f802a063c5a31"
338 "b8a11f5c5ee1879ec3454e5f3c738d2d"
339 "9d201395faa4b61a96c8"
340 )
341 okm = hkdf.extract_and_expand(salt, ikm, info, 42)
342 assert okm == expected_okm
343
344 def test_i2p_style_one_output(self):
345 """Test I2P-style calculate() with one 32-byte output."""
346 from i2p_crypto.hkdf import HKDF
347 hkdf = HKDF()
348 key = bytes(range(32))
349 data = b"test input keying material"
350 out = bytearray(32)
351 result = hkdf.calculate(key, data, out=out)
352 assert len(result) == 32
353 assert bytes(out) == result
354 # Verify against manual HMAC computation
355 prk = stdlib_hmac.new(key, data, hashlib.sha256).digest()
356 t1 = stdlib_hmac.new(prk, b"\x01", hashlib.sha256).digest()
357 assert result == t1
358
359 def test_i2p_style_one_output_with_info(self):
360 """Test I2P-style calculate() with info string."""
361 from i2p_crypto.hkdf import HKDF
362 hkdf = HKDF()
363 key = bytes(range(32))
364 data = b"ikm"
365 info = "some info"
366 out = bytearray(32)
367 result = hkdf.calculate(key, data, info=info, out=out)
368 # Verify against manual computation
369 prk = stdlib_hmac.new(key, data, hashlib.sha256).digest()
370 t1 = stdlib_hmac.new(prk, info.encode("ascii") + b"\x01", hashlib.sha256).digest()
371 assert result == t1
372
373 def test_i2p_style_two_outputs(self):
374 """Test I2P-style calculate() with two 32-byte outputs."""
375 from i2p_crypto.hkdf import HKDF
376 hkdf = HKDF()
377 key = bytes(range(32))
378 data = b"key material"
379 out = bytearray(32)
380 out2 = bytearray(48)
381 result = hkdf.calculate(key, data, out=out, out2=out2, off2=8)
382 # out should have T1
383 assert bytes(out) == result
384 # out2[8:40] should have T2
385 prk = stdlib_hmac.new(key, data, hashlib.sha256).digest()
386 t1 = stdlib_hmac.new(prk, b"\x01", hashlib.sha256).digest()
387 t2 = stdlib_hmac.new(prk, t1 + b"\x02", hashlib.sha256).digest()
388 assert result == t1
389 assert out2[8:40] == t2
390 assert out2[:8] == b"\x00" * 8
391
392 def test_i2p_style_two_outputs_with_info(self):
393 """Test I2P-style calculate() with two outputs and info."""
394 from i2p_crypto.hkdf import HKDF
395 hkdf = HKDF()
396 key = bytes(range(32))
397 data = b"ikm"
398 info = "ctx"
399 out = bytearray(32)
400 out2 = bytearray(32)
401 hkdf.calculate(key, data, info=info, out=out, out2=out2)
402 # Verify
403 prk = stdlib_hmac.new(key, data, hashlib.sha256).digest()
404 info_bytes = info.encode("ascii")
405 t1 = stdlib_hmac.new(prk, info_bytes + b"\x01", hashlib.sha256).digest()
406 t2 = stdlib_hmac.new(prk, t1 + info_bytes + b"\x02", hashlib.sha256).digest()
407 assert bytes(out) == t1
408 assert bytes(out2) == t2
409
410 def test_deterministic(self):
411 from i2p_crypto.hkdf import HKDF
412 hkdf = HKDF()
413 key = b"\xaa" * 32
414 data = b"same input"
415 r1 = hkdf.calculate(key, data)
416 r2 = hkdf.calculate(key, data)
417 assert r1 == r2