1use argon2::{
12 Argon2,
13 password_hash::{PasswordHasher, PasswordVerifier, phc::PasswordHash},
14};
15use sha2::{Digest, Sha256};
16
17pub fn hash_token(token: &str) -> Result<String, argon2::password_hash::Error> {
21 let hash = Argon2::default().hash_password(token.as_bytes())?;
23 Ok(hash.to_string())
24}
25
26pub fn verify_token(hash: &str, token: &str) -> bool {
31 match PasswordHash::new(hash) {
32 Ok(parsed) => Argon2::default()
33 .verify_password(token.as_bytes(), &parsed)
34 .is_ok(),
35 Err(_) => false,
36 }
37}
38
39pub fn generate_token() -> String {
43 let mut bytes = [0u8; 32];
44 getrandom::fill(&mut bytes).expect("OS random number generator failed");
47 to_hex(&bytes)
48}
49
50pub fn content_hash(bytes: &[u8]) -> String {
53 let digest = Sha256::digest(bytes);
54 to_hex(&digest)
55}
56
57fn to_hex(bytes: &[u8]) -> String {
58 use std::fmt::Write;
59 bytes
60 .iter()
61 .fold(String::with_capacity(bytes.len() * 2), |mut acc, b| {
62 let _ = write!(acc, "{b:02x}");
63 acc
64 })
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn hash_and_verify_roundtrip() {
73 let hash = hash_token("correct horse battery staple").unwrap();
74 assert!(verify_token(&hash, "correct horse battery staple"));
75 }
76
77 #[test]
78 fn verify_rejects_wrong_token() {
79 let hash = hash_token("s3cret").unwrap();
80 assert!(!verify_token(&hash, "not-the-token"));
81 }
82
83 #[test]
88 fn verify_accepts_existing_reference_hash() {
89 let hash = "$argon2id$v=19$m=19456,t=2,p=1$bWJyc2FsdG1icnNhbHQ$qZe0wJsZ+QggSV7lpWfHIqHwB1yscboSULscxsCHiCQ";
90 assert!(verify_token(hash, "correct horse battery staple"));
91 assert!(!verify_token(hash, "correct horse battery stapler"));
92 }
93
94 #[test]
95 fn hash_token_uses_default_argon2id_params() {
96 let hash = hash_token("s3cret").unwrap();
97 assert!(
98 hash.starts_with("$argon2id$v=19$m=19456,t=2,p=1$"),
99 "unexpected PHC prefix: {hash}"
100 );
101 }
102
103 #[test]
104 fn verify_rejects_malformed_hash() {
105 assert!(!verify_token("not-a-phc-string", "whatever"));
106 assert!(!verify_token("", "whatever"));
107 }
108
109 #[test]
110 fn generated_tokens_are_hex_and_unique() {
111 let a = generate_token();
112 let b = generate_token();
113 assert_eq!(a.len(), 64);
114 assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
115 assert_ne!(a, b);
116 }
117
118 #[test]
119 fn content_hash_is_stable_and_sensitive() {
120 assert_eq!(content_hash(b"hello"), content_hash(b"hello"));
121 assert_ne!(content_hash(b"hello"), content_hash(b"hello!"));
122 assert_eq!(
124 content_hash(b""),
125 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
126 );
127 }
128}