1use std::fmt;
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5pub type Result<T> = std::result::Result<T, Error>;
6
7#[derive(Debug, Clone)]
9pub struct Error(pub String);
10
11impl fmt::Display for Error {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 f.write_str(&self.0)
14 }
15}
16impl std::error::Error for Error {}
17impl From<std::io::Error> for Error {
18 fn from(e: std::io::Error) -> Self {
19 Error(e.to_string())
20 }
21}
22impl From<serde_json::Error> for Error {
23 fn from(e: serde_json::Error) -> Self {
24 Error(format!("json: {e}"))
25 }
26}
27impl From<toml::de::Error> for Error {
28 fn from(e: toml::de::Error) -> Self {
29 Error(format!("toml: {e}"))
30 }
31}
32
33pub fn err<T>(msg: impl Into<String>) -> Result<T> {
34 Err(Error(msg.into()))
35}
36
37pub fn now_iso() -> String {
40 let since_epoch = SystemTime::now()
41 .duration_since(UNIX_EPOCH)
42 .unwrap_or_default();
43 format_rfc3339_utc(since_epoch.as_secs(), since_epoch.subsec_nanos())
44}
45
46fn format_rfc3339_utc(unix_secs: u64, nanos: u32) -> String {
47 let days = (unix_secs / 86_400) as i64;
48 let rem = unix_secs % 86_400;
49 let z = days + 719_468;
51 let era = z.div_euclid(146_097);
52 let doe = z.rem_euclid(146_097);
53 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
54 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
55 let mp = (5 * doy + 2) / 153;
56 let day = doy - (153 * mp + 2) / 5 + 1;
57 let month = if mp < 10 { mp + 3 } else { mp - 9 };
58 let year = yoe + era * 400 + i64::from(month <= 2);
59 let mut out = format!(
60 "{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}",
61 rem / 3_600,
62 rem % 3_600 / 60,
63 rem % 60
64 );
65 if nanos != 0 {
66 let frac = format!("{nanos:09}");
67 out.push('.');
68 out.push_str(frac.trim_end_matches('0'));
69 }
70 out.push('Z');
71 out
72}
73
74pub fn now_ms() -> u128 {
75 SystemTime::now()
76 .duration_since(UNIX_EPOCH)
77 .map(|d| d.as_millis())
78 .unwrap_or(0)
79}
80
81pub fn sha256_hex(bytes: &[u8]) -> String {
82 rightkit_fs::digest::sha256_bytes(bytes)
83}
84
85pub fn new_id() -> String {
87 let mut b = [0u8; 16];
88 getrandom::fill(&mut b).expect("operating system random source unavailable");
89 b[6] = (b[6] & 0x0f) | 0x40;
90 b[8] = (b[8] & 0x3f) | 0x80;
91 let h = |r: std::ops::Range<usize>| b[r].iter().map(|x| format!("{x:02x}")).collect::<String>();
92 format!(
93 "{}-{}-{}-{}-{}",
94 h(0..4),
95 h(4..6),
96 h(6..8),
97 h(8..10),
98 h(10..16)
99 )
100}
101
102pub fn sleep_ms(ms: u64) {
103 std::thread::sleep(Duration::from_millis(ms));
104}
105
106pub fn tail(s: &str, max: usize) -> String {
108 if s.len() <= max {
109 return s.to_string();
110 }
111 let mut start = s.len() - max;
112 while !s.is_char_boundary(start) {
113 start += 1;
114 }
115 s[start..].to_string()
116}
117
118pub fn require_text(value: &str, name: &str) -> Result<String> {
119 let v = value.trim();
120 if v.is_empty() {
121 return err(format!("{name} is required"));
122 }
123 Ok(v.to_string())
124}
125
126pub fn platform() -> Result<&'static str> {
128 if cfg!(target_os = "macos") {
129 Ok("darwin")
130 } else if cfg!(target_os = "windows") {
131 Ok("windows")
132 } else {
133 err(format!(
134 "rightkit-qa supports macOS and Windows, received {}",
135 std::env::consts::OS
136 ))
137 }
138}
139
140const PLAIN_PATH_LIMIT: usize = 200;
143
144fn is_reserved_device_name(component: &str) -> bool {
145 let stem = component
147 .split('.')
148 .next()
149 .unwrap_or("")
150 .to_ascii_uppercase();
151 matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL")
152 || ((stem.starts_with("COM") || stem.starts_with("LPT"))
153 && stem.len() == 4
154 && matches!(stem.as_bytes()[3], b'1'..=b'9'))
155}
156
157pub fn plain_path_text(text: &str) -> String {
164 let (plain, rest) = if let Some(unc) = text.strip_prefix(r"\\?\UNC\") {
165 (format!(r"\\{unc}"), unc)
166 } else if let Some(rest) = text.strip_prefix(r"\\?\") {
167 let b = rest.as_bytes();
168 let drive = b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && b[2] == b'\\';
169 if !drive {
170 return text.to_owned();
171 }
172 (rest.to_owned(), &rest[3..])
173 } else {
174 return text.to_owned();
175 };
176 if plain.chars().count() > PLAIN_PATH_LIMIT {
177 return text.to_owned();
178 }
179 let mut components = rest.split('\\').peekable();
180 while let Some(c) = components.next() {
181 if c.is_empty() {
182 if components.peek().is_none() && rest.len() <= 3 {
184 continue;
185 }
186 return text.to_owned();
187 }
188 if c.ends_with('.') && c != "."
189 || c.ends_with(' ')
190 || c == "."
191 || c == ".."
192 || is_reserved_device_name(c)
193 || c.chars()
194 .any(|ch| ch.is_control() || "<>:\"|?*/".contains(ch))
195 {
196 return text.to_owned();
197 }
198 }
199 plain
200}
201
202pub fn plain_path(path: &std::path::Path) -> std::path::PathBuf {
206 if !cfg!(windows) {
207 return path.to_path_buf();
208 }
209 match path.to_str() {
210 Some(text) if text.starts_with(r"\\?\") => std::path::PathBuf::from(plain_path_text(text)),
211 _ => path.to_path_buf(),
212 }
213}
214
215pub fn canonical_plain(path: &std::path::Path) -> std::io::Result<std::path::PathBuf> {
217 std::fs::canonicalize(path).map(|p| plain_path(&p))
218}
219
220#[cfg(test)]
221mod plain_path_tests {
222 use super::*;
223
224 #[test]
225 fn drive_and_unc_paths_lose_the_verbatim_prefix() {
226 assert_eq!(
227 plain_path_text(r"\\?\C:\Users\a\run-1"),
228 r"C:\Users\a\run-1"
229 );
230 assert_eq!(plain_path_text(r"\\?\C:\"), r"C:\");
231 assert_eq!(plain_path_text(r"\\?\UNC\srv\share\x"), r"\\srv\share\x");
232 assert_eq!(plain_path_text(r"C:\already\plain"), r"C:\already\plain");
233 assert_eq!(plain_path_text("/usr/local"), "/usr/local");
234 }
235
236 #[test]
237 fn unsafe_paths_stay_verbatim() {
238 for p in [
239 r"\\?\C:\work\nul",
240 r"\\?\C:\work\NUL.txt",
241 r"\\?\C:\work\com1\x",
242 r"\\?\C:\work\LPT9.log",
243 r"\\?\C:\work\trailing.",
244 r"\\?\C:\work\trailing \x",
245 r"\\?\C:\work\a:b",
246 r"\\?\C:\work\a?b",
247 r"\\?\C:\work\\double",
248 r"\\?\C:\work\..\up",
249 r"\\?\Volume{1234}\x",
250 r"\\?\GLOBALROOT\Device\x",
251 ] {
252 assert_eq!(plain_path_text(p), p, "{p}");
253 }
254 assert_eq!(plain_path_text(r"\\?\C:\w\com0"), r"C:\w\com0");
256 assert_eq!(plain_path_text(r"\\?\C:\w\console"), r"C:\w\console");
257 }
258
259 #[test]
260 fn a_path_over_the_ordinary_limit_stays_verbatim() {
261 let long = format!(r"\\?\C:\{}", "d".repeat(PLAIN_PATH_LIMIT));
262 assert_eq!(plain_path_text(&long), long);
263 let fits = format!(r"\\?\C:\{}", "d".repeat(PLAIN_PATH_LIMIT - 10));
264 assert_eq!(plain_path_text(&fits), fits[4..].to_string());
265 }
266
267 #[cfg(windows)]
268 #[test]
269 fn canonical_plain_hands_out_a_drive_path() {
270 let dir = std::env::temp_dir();
271 let verbatim = std::fs::canonicalize(&dir).unwrap();
272 assert!(verbatim.to_string_lossy().starts_with(r"\\?\"));
273 let plain = canonical_plain(&dir).unwrap();
274 let text = plain.to_string_lossy();
275 assert!(!text.starts_with(r"\\?\"), "{text}");
276 assert!(plain.is_dir());
277 assert_eq!(std::fs::canonicalize(&plain).unwrap(), verbatim);
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn rfc3339_known_instants() {
288 assert_eq!(format_rfc3339_utc(0, 0), "1970-01-01T00:00:00Z");
289 assert_eq!(format_rfc3339_utc(951_782_400, 0), "2000-02-29T00:00:00Z");
290 assert_eq!(
291 format_rfc3339_utc(1_791_635_696, 789_000_000),
292 "2026-10-10T12:34:56.789Z"
293 );
294 assert_eq!(
295 format_rfc3339_utc(86_399, 123_456_789),
296 "1970-01-01T23:59:59.123456789Z"
297 );
298 let now = now_iso();
299 assert!(now.ends_with('Z') && now.len() >= 20, "{now}");
300 }
301
302 #[test]
303 fn new_id_is_a_version_4_uuid() {
304 let id = new_id();
305 assert_eq!(id.len(), 36);
306 let parts: Vec<&str> = id.split('-').collect();
307 assert_eq!(
308 parts.iter().map(|p| p.len()).collect::<Vec<_>>(),
309 vec![8, 4, 4, 4, 12]
310 );
311 assert!(parts[2].starts_with('4'));
312 assert!(matches!(parts[3].as_bytes()[0], b'8' | b'9' | b'a' | b'b'));
313 assert!(id.bytes().all(|c| c == b'-' || c.is_ascii_hexdigit()));
314 assert_ne!(id, new_id());
315 }
316}
317
318pub fn base64_decode(s: &str) -> Option<Vec<u8>> {
320 let mut out = Vec::with_capacity(s.len() * 3 / 4);
321 let (mut acc, mut bits) = (0u32, 0u32);
322 for c in s.bytes() {
323 let v = match c {
324 b'A'..=b'Z' => c - b'A',
325 b'a'..=b'z' => c - b'a' + 26,
326 b'0'..=b'9' => c - b'0' + 52,
327 b'+' => 62,
328 b'/' => 63,
329 b'=' | b'\n' | b'\r' | b' ' => continue,
330 _ => return None,
331 } as u32;
332 acc = (acc << 6) | v;
333 bits += 6;
334 if bits >= 8 {
335 bits -= 8;
336 out.push((acc >> bits) as u8);
337 acc &= (1 << bits) - 1;
338 }
339 }
340 Some(out)
341}
342
343#[cfg(test)]
344mod base64_tests {
345 use super::base64_decode;
346 #[test]
347 fn decodes_rfc4648_vectors_and_rejects_junk() {
348 for (i, o) in [
349 ("", ""),
350 ("Zg==", "f"),
351 ("Zm8=", "fo"),
352 ("Zm9v", "foo"),
353 ("Zm9vYg==", "foob"),
354 ("Zm9vYmFy", "foobar"),
355 ] {
356 assert_eq!(base64_decode(i).unwrap(), o.as_bytes());
357 }
358 assert!(base64_decode("Zm9v!").is_none());
359 assert_eq!(base64_decode("Zm8").unwrap(), b"fo");
361 assert_eq!(base64_decode("Zm9v\nYmFy ").unwrap(), b"foobar");
362 }
363}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct SecretForm {
370 pub name: &'static str,
371 pub text: String,
372}
373
374pub fn base64_encode(bytes: &[u8]) -> String {
376 b64(bytes, false, true)
377}
378
379pub fn base64url_encode(bytes: &[u8]) -> String {
381 b64(bytes, true, false)
382}
383
384fn b64_padded(bytes: &[u8], url: bool) -> String {
386 const STD: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
387 const URL: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
388 let alpha = if url { URL } else { STD };
389 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
390 for chunk in bytes.chunks(3) {
391 let b0 = u32::from(chunk[0]);
392 let b1 = u32::from(chunk.get(1).copied().unwrap_or(0));
393 let b2 = u32::from(chunk.get(2).copied().unwrap_or(0));
394 let n = (b0 << 16) | (b1 << 8) | b2;
395 out.push(alpha[((n >> 18) & 63) as usize] as char);
396 out.push(alpha[((n >> 12) & 63) as usize] as char);
397 out.push(if chunk.len() > 1 {
398 alpha[((n >> 6) & 63) as usize] as char
399 } else {
400 '='
401 });
402 out.push(if chunk.len() > 2 {
403 alpha[(n & 63) as usize] as char
404 } else {
405 '='
406 });
407 }
408 out
409}
410
411fn b64(bytes: &[u8], url: bool, pad: bool) -> String {
413 let mut out = b64_padded(bytes, url);
414 if !pad {
415 out.truncate(out.trim_end_matches('=').len());
416 }
417 out
418}
419
420const STREAM_NAMES: [(&str, &str); 3] = [
421 ("base64_at0", "base64url_at0"),
422 ("base64_at1", "base64url_at1"),
423 ("base64_at2", "base64url_at2"),
424];
425
426fn b64_stream(bytes: &[u8], k: usize, url: bool) -> Option<String> {
430 let mut buf = vec![0u8; k];
431 buf.extend_from_slice(bytes);
432 let full = b64(&buf, url, false);
433 let lead = [0, 2, 3][k];
434 let trail = usize::from(!buf.len().is_multiple_of(3));
435 let end = full.len().checked_sub(trail)?;
436 let text = full.get(lead..end)?;
437 (text.len() >= 4).then(|| text.to_string())
438}
439
440pub fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
443 const BLOCK: usize = 64;
444 let mut k = [0u8; BLOCK];
445 if key.len() > BLOCK {
446 let mut h = rightkit_fs::digest::Sha256Hasher::new();
447 h.update(key);
448 k[..32].copy_from_slice(&h.finalize_bytes());
449 } else {
450 k[..key.len()].copy_from_slice(key);
451 }
452 let mut inner = rightkit_fs::digest::Sha256Hasher::new();
453 let ipad: Vec<u8> = k.iter().map(|b| b ^ 0x36).collect();
454 inner.update(&ipad);
455 inner.update(message);
456 let inner_digest = inner.finalize_bytes();
457 let mut outer = rightkit_fs::digest::Sha256Hasher::new();
458 let opad: Vec<u8> = k.iter().map(|b| b ^ 0x5c).collect();
459 outer.update(&opad);
460 outer.update(&inner_digest);
461 outer.finalize_bytes()
462}
463
464pub fn hmac_sha256_hex(key: &[u8], message: &[u8]) -> String {
466 rightkit_fs::digest::hex(&hmac_sha256(key, message))
467}
468
469fn pct(text: &str, upper: bool, form: bool) -> String {
472 let mut out = String::new();
473 for &b in text.as_bytes() {
474 if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~') {
475 out.push(b as char);
476 } else if form && b == b' ' {
477 out.push('+');
478 } else if upper {
479 out.push_str(&format!("%{b:02X}"));
480 } else {
481 out.push_str(&format!("%{b:02x}"));
482 }
483 }
484 out
485}
486
487pub fn secret_forms(secret: &str) -> Vec<SecretForm> {
498 let mut out: Vec<SecretForm> = Vec::new();
499 if secret.is_empty() {
500 return out;
501 }
502 let bytes = secret.as_bytes();
503 let hex = rightkit_fs::digest::hex(bytes);
504 let mut add = |name: &'static str, text: String| {
505 if !out.iter().any(|f| f.text == text) {
506 out.push(SecretForm { name, text });
507 }
508 };
509 add("raw", secret.to_string());
510 add("hex_lower", hex.to_ascii_lowercase());
511 add("hex_upper", hex.to_ascii_uppercase());
512 add("base64", b64(bytes, false, true));
513 add("base64", b64(bytes, false, false));
514 add("base64url", b64(bytes, true, true));
515 add("base64url", b64(bytes, true, false));
516 for (k, names) in STREAM_NAMES.iter().enumerate() {
519 for (url, name) in [(false, names.0), (true, names.1)] {
520 if let Some(t) = b64_stream(bytes, k, url) {
521 add(name, t);
522 }
523 }
524 }
525 add("percent_lower", pct(secret, false, false));
526 add("percent_upper", pct(secret, true, false));
527 add("form_lower", pct(secret, false, true));
528 add("form_upper", pct(secret, true, true));
529 out
530}
531
532pub fn secret_leaks(haystack: &[u8], secret: &str) -> Vec<&'static str> {
535 secret_forms(secret)
536 .into_iter()
537 .filter(|f| {
538 haystack
539 .windows(f.text.len())
540 .any(|w| w == f.text.as_bytes())
541 })
542 .map(|f| f.name)
543 .collect()
544}
545
546#[cfg(test)]
547mod base64_encode_tests {
548 use super::*;
549
550 #[test]
551 fn public_encoders_match_rfc4648_and_round_trip() {
552 assert_eq!(base64_encode(b"fo"), "Zm8=");
553 assert_eq!(base64url_encode(b"fo"), "Zm8");
554 assert_eq!(base64url_encode(&[0xfb, 0xff, 0xfe]), "-__-");
555 assert_eq!(base64_encode(&[0xfb, 0xff, 0xfe]), "+//+");
556 assert_eq!(base64url_encode(b""), "");
557 let data: Vec<u8> = (0..=255).collect();
558 assert_eq!(base64_decode(&base64_encode(&data)).unwrap(), data);
559 }
560}
561
562#[cfg(test)]
563mod secret_form_tests {
564 use super::*;
565
566 fn text(forms: &[SecretForm], name: &str) -> Vec<String> {
567 forms
568 .iter()
569 .filter(|f| f.name == name)
570 .map(|f| f.text.clone())
571 .collect()
572 }
573
574 #[test]
575 fn plain_secret_yields_raw_hex_and_base64_only() {
576 let f = secret_forms("foobar");
577 assert_eq!(text(&f, "raw"), vec!["foobar"]);
578 assert_eq!(text(&f, "hex_lower"), vec!["666f6f626172"]);
579 assert_eq!(text(&f, "hex_upper"), vec!["666F6F626172"]);
580 assert_eq!(text(&f, "base64"), vec!["Zm9vYmFy"]);
581 assert!(text(&f, "percent_lower").is_empty() && text(&f, "form_upper").is_empty());
582 }
583
584 #[test]
585 fn base64_matches_rfc4648_padding_and_url_alphabet() {
586 let f = secret_forms("fo");
587 assert_eq!(text(&f, "base64"), vec!["Zm8=", "Zm8"]);
588 assert!(text(&f, "base64url").is_empty());
589 let f = secret_forms("a?>b~");
590 assert_eq!(base64_decode(&text(&f, "base64")[0]).unwrap(), b"a?>b~");
591 assert!(text(&f, "base64")[0].contains(['+', '/']));
592 assert!(text(&f, "base64url")[0].contains(['-', '_']));
593 assert!(!text(&f, "base64url")[0].contains(['+', '/']));
594 assert_eq!(text(&f, "base64url").len(), 2);
595 }
596
597 #[test]
598 fn percent_and_form_cases_and_space() {
599 let f = secret_forms("a b/c+é");
600 assert_eq!(text(&f, "percent_lower"), vec!["a%20b%2fc%2b%c3%a9"]);
601 assert_eq!(text(&f, "percent_upper"), vec!["a%20b%2Fc%2B%C3%A9"]);
602 assert_eq!(text(&f, "form_lower"), vec!["a+b%2fc%2b%c3%a9"]);
603 assert_eq!(text(&f, "form_upper"), vec!["a+b%2Fc%2B%C3%A9"]);
604 let f = secret_forms("a/b");
605 assert_eq!(text(&f, "percent_upper"), vec!["a%2Fb"]);
606 assert!(text(&f, "form_upper").is_empty());
607 }
608
609 #[test]
610 fn empty_secret_yields_nothing_and_leaks_are_named() {
611 assert!(secret_forms("").is_empty());
612 let secret = "tok en/1";
613 let hay = format!("x={} y", pct(secret, true, true));
614 assert_eq!(secret_leaks(hay.as_bytes(), secret), vec!["form_upper"]);
615 assert!(secret_leaks(b"clean", secret).is_empty());
616 assert_eq!(secret_leaks(b"has tok en/1 inside", secret), vec!["raw"]);
617 }
618
619 #[test]
620 fn base64_stream_forms_cover_all_three_alignments() {
621 let secret = "sk_live_abc~def>123?";
624 let forms = secret_forms(secret);
625 for (pre, post) in [
626 ("", ""),
627 ("A", "Z"),
628 ("AB", "YZ"),
629 ("ABC", "XYZ"),
630 ("x", ""),
631 ("xy", "!"),
632 ] {
633 let mut stream = pre.as_bytes().to_vec();
634 stream.extend_from_slice(secret.as_bytes());
635 stream.extend_from_slice(post.as_bytes());
636 let enc = b64(&stream, false, true);
637 assert!(
638 forms
639 .iter()
640 .any(|f| enc.contains(&f.text) && f.name.starts_with("base64")),
641 "no form found the secret at offset {} in {enc}",
642 pre.len()
643 );
644 let url = b64(&stream, true, false);
645 assert!(forms
646 .iter()
647 .any(|f| f.name.starts_with("base64url") && url.contains(&f.text)));
648 assert!(
649 !secret_leaks(enc.as_bytes(), secret).is_empty(),
650 "offset {}",
651 pre.len()
652 );
653 }
654 }
655
656 #[test]
657 fn base64_stream_forms_trim_the_edges() {
658 assert_eq!(b64_stream(b"foobar", 0, false).unwrap(), "Zm9vYmFy");
661 assert_eq!(
662 b64_stream(b"foobar", 1, false).unwrap(),
663 b64(b"\0foobar", false, false)[2..9]
664 );
665 assert_eq!(
666 b64_stream(b"foobar", 2, false).unwrap(),
667 b64(b"\0\0foobar", false, false)[3..10]
668 );
669 assert!(b64_stream(b"fo", 1, false).is_none());
670 let f = secret_forms("foobar-secret");
672 assert!(
673 f.iter().any(|x| x.name == "base64_at1") && f.iter().any(|x| x.name == "base64_at2")
674 );
675 }
676
677 #[test]
678 fn hmac_sha256_matches_rfc4231() {
679 let long = [0xaau8; 131];
680 assert_eq!(
681 hmac_sha256_hex(&[0x0b; 20], b"Hi There"),
682 "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
683 );
684 assert_eq!(
685 hmac_sha256_hex(b"Jefe", b"what do ya want for nothing?"),
686 "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
687 );
688 assert_eq!(
689 hmac_sha256_hex(&[0xaa; 20], &[0xdd; 50]),
690 "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe"
691 );
692 assert_eq!(
693 hmac_sha256_hex(
694 &long,
695 b"Test Using Larger Than Block-Size Key - Hash Key First"
696 ),
697 "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"
698 );
699 assert_eq!(hmac_sha256(b"k", b"m").len(), 32);
700 }
701}