use ring::digest::{Context, SHA256};
pub fn hash_vtoken(plain: &str) -> String {
let mut ctx = Context::new(&SHA256);
ctx.update(plain.as_bytes());
let digest = ctx.finish();
hex_lower(digest.as_ref())
}
pub fn is_vtoken_hash(s: &str) -> bool {
s.len() == 64
&& s.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
}
fn hex_lower(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_is_64_lowercase_hex() {
let h = hash_vtoken("vhub_abc");
assert_eq!(h.len(), 64);
assert!(h
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
}
#[test]
fn hash_is_deterministic() {
assert_eq!(hash_vtoken("vhub_abc"), hash_vtoken("vhub_abc"));
}
#[test]
fn hash_differs_per_input() {
assert_ne!(hash_vtoken("vhub_abc"), hash_vtoken("vhub_abd"));
}
#[test]
fn hash_matches_known_sha256_vector() {
let h = hash_vtoken("vhub_abc");
assert_eq!(h.len(), 64);
assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn hash_empty_string_is_well_defined() {
let h = hash_vtoken("");
assert_eq!(
h,
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn is_vtoken_hash_accepts_canonical_forms() {
assert!(is_vtoken_hash(&hash_vtoken("anything")));
assert!(is_vtoken_hash(
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
));
}
#[test]
fn is_vtoken_hash_rejects_non_canonical() {
assert!(!is_vtoken_hash(""));
assert!(!is_vtoken_hash("vhub_abc"));
assert!(!is_vtoken_hash(&hash_vtoken("x").to_uppercase())); assert!(!is_vtoken_hash(&"a".repeat(63))); assert!(!is_vtoken_hash(&"a".repeat(65))); let mut bad = "a".repeat(64);
bad.replace_range(0..1, "g");
assert!(!is_vtoken_hash(&bad));
}
}