use argon2::{
Argon2,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
};
use sha2::{Digest, Sha256};
pub fn hash_token(token: &str) -> Result<String, argon2::password_hash::Error> {
let salt = SaltString::generate(&mut OsRng);
let hash = Argon2::default().hash_password(token.as_bytes(), &salt)?;
Ok(hash.to_string())
}
pub fn verify_token(hash: &str, token: &str) -> bool {
match PasswordHash::new(hash) {
Ok(parsed) => Argon2::default()
.verify_password(token.as_bytes(), &parsed)
.is_ok(),
Err(_) => false,
}
}
pub fn generate_token() -> String {
use argon2::password_hash::rand_core::RngCore;
let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
to_hex(&bytes)
}
pub fn content_hash(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
to_hex(&digest)
}
fn to_hex(bytes: &[u8]) -> String {
use std::fmt::Write;
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut acc, b| {
let _ = write!(acc, "{b:02x}");
acc
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_and_verify_roundtrip() {
let hash = hash_token("correct horse battery staple").unwrap();
assert!(verify_token(&hash, "correct horse battery staple"));
}
#[test]
fn verify_rejects_wrong_token() {
let hash = hash_token("s3cret").unwrap();
assert!(!verify_token(&hash, "not-the-token"));
}
#[test]
fn verify_rejects_malformed_hash() {
assert!(!verify_token("not-a-phc-string", "whatever"));
assert!(!verify_token("", "whatever"));
}
#[test]
fn generated_tokens_are_hex_and_unique() {
let a = generate_token();
let b = generate_token();
assert_eq!(a.len(), 64);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
assert_ne!(a, b);
}
#[test]
fn content_hash_is_stable_and_sensitive() {
assert_eq!(content_hash(b"hello"), content_hash(b"hello"));
assert_ne!(content_hash(b"hello"), content_hash(b"hello!"));
assert_eq!(
content_hash(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
}