we (web engine): Experimental web browser project to understand the limits of Claude
1//! HKDF: HMAC-based Extract-and-Expand Key Derivation Function (RFC 5869).
2
3use crate::hmac::{HashFunction, Hmac};
4
5/// HKDF-Extract: derive a pseudorandom key (PRK) from input keying material.
6///
7/// `salt` is an optional non-secret random value; if empty, a string of
8/// `H::OUTPUT_SIZE` zeros is used (per RFC 5869 §2.2).
9pub fn hkdf_extract<H: HashFunction>(salt: &[u8], ikm: &[u8]) -> Vec<u8> {
10 let effective_salt: Vec<u8>;
11 let salt = if salt.is_empty() {
12 effective_salt = vec![0u8; H::OUTPUT_SIZE];
13 &effective_salt
14 } else {
15 salt
16 };
17 let mut hmac = Hmac::<H>::new(salt);
18 hmac.update(ikm);
19 hmac.finalize()
20}
21
22/// HKDF-Expand: expand a PRK into output keying material of length `len`.
23///
24/// `len` must be <= 255 * `H::OUTPUT_SIZE`.
25/// Returns `None` if `len` exceeds the maximum.
26pub fn hkdf_expand<H: HashFunction>(prk: &[u8], info: &[u8], len: usize) -> Option<Vec<u8>> {
27 let hash_len = H::OUTPUT_SIZE;
28 if len > 255 * hash_len {
29 return None;
30 }
31
32 let n = len.div_ceil(hash_len);
33 let mut okm = Vec::with_capacity(n * hash_len);
34 let mut t_prev: Vec<u8> = Vec::new();
35
36 for i in 1..=n {
37 let mut hmac = Hmac::<H>::new(prk);
38 hmac.update(&t_prev);
39 hmac.update(info);
40 hmac.update(&[i as u8]);
41 t_prev = hmac.finalize();
42 okm.extend_from_slice(&t_prev);
43 }
44
45 okm.truncate(len);
46 Some(okm)
47}
48
49/// Combined HKDF: extract then expand in one call.
50///
51/// Returns `None` if `len` exceeds 255 * `H::OUTPUT_SIZE`.
52pub fn hkdf<H: HashFunction>(salt: &[u8], ikm: &[u8], info: &[u8], len: usize) -> Option<Vec<u8>> {
53 let prk = hkdf_extract::<H>(salt, ikm);
54 hkdf_expand::<H>(&prk, info, len)
55}
56
57// ---------------------------------------------------------------------------
58// Tests — RFC 5869 test vectors
59// ---------------------------------------------------------------------------
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use crate::sha2::Sha256;
65
66 fn hex(bytes: &[u8]) -> String {
67 bytes.iter().map(|b| format!("{b:02x}")).collect()
68 }
69
70 fn from_hex(s: &str) -> Vec<u8> {
71 (0..s.len())
72 .step_by(2)
73 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
74 .collect()
75 }
76
77 // -----------------------------------------------------------------------
78 // Test Case 1: Basic test case with SHA-256
79 // -----------------------------------------------------------------------
80
81 #[test]
82 fn rfc5869_case1_extract() {
83 let ikm = from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
84 let salt = from_hex("000102030405060708090a0b0c");
85 let prk = hkdf_extract::<Sha256>(&salt, &ikm);
86 assert_eq!(
87 hex(&prk),
88 "077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5"
89 );
90 }
91
92 #[test]
93 fn rfc5869_case1_expand() {
94 let prk = from_hex("077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5");
95 let info = from_hex("f0f1f2f3f4f5f6f7f8f9");
96 let okm = hkdf_expand::<Sha256>(&prk, &info, 42).unwrap();
97 assert_eq!(
98 hex(&okm),
99 "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
100 );
101 }
102
103 #[test]
104 fn rfc5869_case1_combined() {
105 let ikm = from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
106 let salt = from_hex("000102030405060708090a0b0c");
107 let info = from_hex("f0f1f2f3f4f5f6f7f8f9");
108 let okm = hkdf::<Sha256>(&salt, &ikm, &info, 42).unwrap();
109 assert_eq!(
110 hex(&okm),
111 "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
112 );
113 }
114
115 // -----------------------------------------------------------------------
116 // Test Case 2: Longer inputs/outputs with SHA-256
117 // -----------------------------------------------------------------------
118
119 #[test]
120 fn rfc5869_case2_extract() {
121 let ikm = from_hex(
122 "000102030405060708090a0b0c0d0e0f\
123 101112131415161718191a1b1c1d1e1f\
124 202122232425262728292a2b2c2d2e2f\
125 303132333435363738393a3b3c3d3e3f\
126 404142434445464748494a4b4c4d4e4f",
127 );
128 let salt = from_hex(
129 "606162636465666768696a6b6c6d6e6f\
130 707172737475767778797a7b7c7d7e7f\
131 808182838485868788898a8b8c8d8e8f\
132 909192939495969798999a9b9c9d9e9f\
133 a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
134 );
135 let prk = hkdf_extract::<Sha256>(&salt, &ikm);
136 assert_eq!(
137 hex(&prk),
138 "06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244"
139 );
140 }
141
142 #[test]
143 fn rfc5869_case2_expand() {
144 let prk = from_hex("06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244");
145 let info = from_hex(
146 "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf\
147 c0c1c2c3c4c5c6c7c8c9cacbcccdcecf\
148 d0d1d2d3d4d5d6d7d8d9dadbdcdddedf\
149 e0e1e2e3e4e5e6e7e8e9eaebecedeeef\
150 f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
151 );
152 let okm = hkdf_expand::<Sha256>(&prk, &info, 82).unwrap();
153 assert_eq!(
154 hex(&okm),
155 "b11e398dc80327a1c8e7f78c596a4934\
156 4f012eda2d4efad8a050cc4c19afa97c\
157 59045a99cac7827271cb41c65e590e09\
158 da3275600c2f09b8367793a9aca3db71\
159 cc30c58179ec3e87c14c01d5c1f3434f\
160 1d87"
161 );
162 }
163
164 #[test]
165 fn rfc5869_case2_combined() {
166 let ikm = from_hex(
167 "000102030405060708090a0b0c0d0e0f\
168 101112131415161718191a1b1c1d1e1f\
169 202122232425262728292a2b2c2d2e2f\
170 303132333435363738393a3b3c3d3e3f\
171 404142434445464748494a4b4c4d4e4f",
172 );
173 let salt = from_hex(
174 "606162636465666768696a6b6c6d6e6f\
175 707172737475767778797a7b7c7d7e7f\
176 808182838485868788898a8b8c8d8e8f\
177 909192939495969798999a9b9c9d9e9f\
178 a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
179 );
180 let info = from_hex(
181 "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf\
182 c0c1c2c3c4c5c6c7c8c9cacbcccdcecf\
183 d0d1d2d3d4d5d6d7d8d9dadbdcdddedf\
184 e0e1e2e3e4e5e6e7e8e9eaebecedeeef\
185 f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
186 );
187 let okm = hkdf::<Sha256>(&salt, &ikm, &info, 82).unwrap();
188 assert_eq!(
189 hex(&okm),
190 "b11e398dc80327a1c8e7f78c596a4934\
191 4f012eda2d4efad8a050cc4c19afa97c\
192 59045a99cac7827271cb41c65e590e09\
193 da3275600c2f09b8367793a9aca3db71\
194 cc30c58179ec3e87c14c01d5c1f3434f\
195 1d87"
196 );
197 }
198
199 // -----------------------------------------------------------------------
200 // Test Case 3: SHA-256, zero-length salt and info
201 // -----------------------------------------------------------------------
202
203 #[test]
204 fn rfc5869_case3_extract() {
205 let ikm = from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
206 let prk = hkdf_extract::<Sha256>(&[], &ikm);
207 assert_eq!(
208 hex(&prk),
209 "19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04"
210 );
211 }
212
213 #[test]
214 fn rfc5869_case3_expand() {
215 let prk = from_hex("19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04");
216 let okm = hkdf_expand::<Sha256>(&prk, &[], 42).unwrap();
217 assert_eq!(
218 hex(&okm),
219 "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"
220 );
221 }
222
223 #[test]
224 fn rfc5869_case3_combined() {
225 let ikm = from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
226 let okm = hkdf::<Sha256>(&[], &ikm, &[], 42).unwrap();
227 assert_eq!(
228 hex(&okm),
229 "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8"
230 );
231 }
232
233 // -----------------------------------------------------------------------
234 // Output length validation
235 // -----------------------------------------------------------------------
236
237 #[test]
238 fn expand_rejects_oversized_output() {
239 let prk = [0x42u8; 32];
240 // 255 * 32 = 8160 is the max for SHA-256
241 assert!(hkdf_expand::<Sha256>(&prk, &[], 8160).is_some());
242 assert!(hkdf_expand::<Sha256>(&prk, &[], 8161).is_none());
243 }
244
245 #[test]
246 fn hkdf_rejects_oversized_output() {
247 let ikm = [0x0bu8; 22];
248 assert!(hkdf::<Sha256>(&[], &ikm, &[], 8161).is_none());
249 }
250
251 // -----------------------------------------------------------------------
252 // Edge cases
253 // -----------------------------------------------------------------------
254
255 #[test]
256 fn expand_zero_length_output() {
257 let prk = [0x42u8; 32];
258 let okm = hkdf_expand::<Sha256>(&prk, &[], 0).unwrap();
259 assert!(okm.is_empty());
260 }
261
262 #[test]
263 fn expand_exact_hash_length() {
264 let prk = [0x42u8; 32];
265 let okm = hkdf_expand::<Sha256>(&prk, b"info", 32).unwrap();
266 assert_eq!(okm.len(), 32);
267 }
268
269 #[test]
270 fn extract_expand_with_sha512() {
271 use crate::sha2::Sha512;
272
273 // Use Test Case 1 inputs but with SHA-512 — verify it produces
274 // the correct output length and doesn't panic.
275 let ikm = from_hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
276 let salt = from_hex("000102030405060708090a0b0c");
277 let prk = hkdf_extract::<Sha512>(&salt, &ikm);
278 assert_eq!(prk.len(), 64);
279
280 let okm = hkdf_expand::<Sha512>(&prk, b"test info", 100).unwrap();
281 assert_eq!(okm.len(), 100);
282 }
283
284 #[test]
285 fn extract_expand_with_sha384() {
286 use crate::sha2::Sha384;
287
288 let ikm = [0xaau8; 80];
289 let prk = hkdf_extract::<Sha384>(&[], &ikm);
290 assert_eq!(prk.len(), 48);
291
292 let okm = hkdf_expand::<Sha384>(&prk, b"context", 60).unwrap();
293 assert_eq!(okm.len(), 60);
294 }
295}