use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use sha2::{Digest, Sha256};
pub fn verify_s256(code_verifier: &str, code_challenge: &str) -> bool {
let mut hasher = Sha256::new();
hasher.update(code_verifier.as_bytes());
let computed = URL_SAFE_NO_PAD.encode(hasher.finalize());
constant_time_eq(computed.as_bytes(), code_challenge.as_bytes())
}
pub fn compute_s256_challenge(code_verifier: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(code_verifier.as_bytes());
URL_SAFE_NO_PAD.encode(hasher.finalize())
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_s256_roundtrip() {
let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
let challenge = compute_s256_challenge(verifier);
assert!(verify_s256(verifier, &challenge));
}
#[test]
fn test_s256_rfc7636_example() {
let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
let expected_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
assert!(verify_s256(verifier, expected_challenge));
}
#[test]
fn test_s256_wrong_verifier() {
let challenge = compute_s256_challenge("correct-verifier");
assert!(!verify_s256("wrong-verifier", &challenge));
}
#[test]
fn test_s256_empty_strings() {
let challenge = compute_s256_challenge("");
assert!(verify_s256("", &challenge));
assert!(!verify_s256("notempty", &challenge));
}
}