| 1 | //! The one constant-time comparison for bearer tokens, nonces and proofs. |
| 2 | //! |
| 3 | //! Every credential check in the engine (Runtime API token, web and mobile |
| 4 | //! session proofs, computer-display tokens, the app-server bearer) goes |
| 5 | //! through [`constant_time_eq`], so a timing fix lands once. |
| 6 | |
| 7 | /// Compares the full length of both inputs regardless of where they first |
| 8 | /// differ, so an auth failure leaks neither the matching prefix length nor, |
| 9 | /// beyond the loop bound, which input was shorter. |
| 10 | #[must_use] |
| 11 | pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { |
| 12 | let mut diff = a.len() ^ b.len(); |
| 13 | for i in 0..a.len().max(b.len()) { |
| 14 | let x = a.get(i).copied().unwrap_or(0); |
| 15 | let y = b.get(i).copied().unwrap_or(0); |
| 16 | diff |= usize::from(x ^ y); |
| 17 | } |
| 18 | std::hint::black_box(diff) == 0 |
| 19 | } |
| 20 | |
| 21 | #[cfg(test)] |
| 22 | mod tests { |
| 23 | use super::constant_time_eq; |
| 24 | |
| 25 | #[test] |
| 26 | fn equal_inputs_match() { |
| 27 | assert!(constant_time_eq(b"", b"")); |
| 28 | assert!(constant_time_eq(b"secret-token", b"secret-token")); |
| 29 | } |
| 30 | |
| 31 | #[test] |
| 32 | fn different_inputs_do_not_match() { |
| 33 | assert!(!constant_time_eq(b"secret-token", b"secret-tokem")); |
| 34 | assert!(!constant_time_eq(b"secret", b"secret-token")); |
| 35 | assert!(!constant_time_eq(b"secret-token", b"secret")); |
| 36 | assert!(!constant_time_eq(b"", b"x")); |
| 37 | // A zero-padded prefix must not collide with the shorter input. |
| 38 | assert!(!constant_time_eq(b"abc", b"abc\0")); |
| 39 | } |
| 40 | } |
| 41 |