use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
pub fn generate() -> (String, String) {
let mut bytes = [0u8; 32];
use rand::RngCore;
rand::rng().fill_bytes(&mut bytes);
let raw = data_encoding::BASE64URL_NOPAD.encode(&bytes);
let hash = sha256_hex(raw.as_bytes());
(raw, hash)
}
pub fn verify(raw: &str, hash: &str) -> bool {
if raw.is_empty() || hash.is_empty() {
return false;
}
let computed = sha256_hex(raw.as_bytes());
computed.as_bytes().ct_eq(hash.as_bytes()).into()
}
fn sha256_hex(input: &[u8]) -> String {
let mut h = Sha256::new();
h.update(input);
let digest = h.finalize();
let mut out = String::with_capacity(digest.len() * 2);
for byte in digest {
out.push_str(&format!("{byte:02x}"));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_returns_distinct_pairs() {
let (raw1, hash1) = generate();
let (raw2, hash2) = generate();
assert_ne!(raw1, raw2);
assert_ne!(hash1, hash2);
}
#[test]
fn verify_matches_generated_pair() {
let (raw, hash) = generate();
assert!(verify(&raw, &hash));
}
#[test]
fn verify_rejects_mismatch() {
let (_raw, hash) = generate();
assert!(!verify("not_the_token", &hash));
}
#[test]
fn verify_rejects_empty() {
assert!(!verify("", "deadbeef"));
assert!(!verify("xx", ""));
}
#[test]
fn sha256_hex_known_vector() {
assert_eq!(
sha256_hex(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
}