A Python port of the Invisible Internet Project (I2P)
1"""HKDF — HKDF-SHA256 key derivation.
2
3Ported from net.i2p.crypto.HKDF.
4Implements RFC 5869 with HMAC-SHA256.
5One or two 32-byte outputs, with or without info.
6"""
7
8import hmac as _hmac
9import hashlib
10
11
12class HKDF:
13 """HKDF-SHA256 key derivation. Thread-safe, no state."""
14
15 def __init__(self) -> None:
16 pass
17
18 def calculate(self, key: bytes, data: bytes,
19 info: str = "",
20 out: bytearray | None = None,
21 out2: bytearray | None = None,
22 off2: int = 0) -> bytes:
23 """HKDF-SHA256 extract-then-expand.
24
25 Args:
26 key: first 32 bytes used as the salt for extract
27 data: input keying material
28 info: optional context string (ASCII)
29 out: if provided, 32-byte output buffer (output 1)
30 out2: if provided, 32-byte output buffer (output 2)
31 off2: offset into out2
32
33 Returns:
34 First 32-byte output (T1). If out2 is provided, also writes T2.
35 """
36 k = key[:32]
37
38 # Extract: PRK = HMAC-SHA256(salt=key, data)
39 prk = _hmac.new(k, data, hashlib.sha256).digest()
40
41 # Expand T1: HMAC-SHA256(PRK, info || 0x01)
42 info_bytes = info.encode("ascii") if info else b""
43 t1 = _hmac.new(prk, info_bytes + b"\x01", hashlib.sha256).digest()
44
45 if out is not None:
46 out[0:32] = t1
47
48 if out2 is not None:
49 # Expand T2: HMAC-SHA256(PRK, T1 || info || 0x02)
50 t2 = _hmac.new(prk, t1 + info_bytes + b"\x02", hashlib.sha256).digest()
51 out2[off2 : off2 + 32] = t2
52
53 return t1
54
55 def extract_and_expand(self, salt: bytes, ikm: bytes, info: bytes, length: int) -> bytes:
56 """General-purpose HKDF-SHA256 per RFC 5869.
57
58 Args:
59 salt: salt bytes
60 ikm: input keying material
61 info: context/application-specific info
62 length: output length in bytes (max 255 * 32 = 8160)
63
64 Returns:
65 Derived key material of requested length.
66 """
67 # Extract
68 prk = _hmac.new(salt, ikm, hashlib.sha256).digest()
69
70 # Expand
71 n = (length + 31) // 32
72 okm = b""
73 t = b""
74 for i in range(1, n + 1):
75 t = _hmac.new(prk, t + info + bytes([i]), hashlib.sha256).digest()
76 okm += t
77
78 return okm[:length]