use crate::crypto::zeroize::Zeroizing;
use crate::crypto::{RandomError, Sha256, fill_random};
use crate::encoding::{hex_decode, hex_encode};
#[must_use = "this returns a Result that may report a CSPRNG failure"]
pub fn generate_token(byte_len: usize) -> Result<Zeroizing<String>, RandomError> {
let mut buf = vec![0u8; byte_len];
fill_random(&mut buf)?;
let token = hex_encode(&buf);
crate::crypto::zeroize::zeroize(&mut buf);
Ok(Zeroizing::new(token))
}
#[must_use = "this returns a Result that may report a CSPRNG failure"]
pub fn generate_token_with_hash(
byte_len: usize,
) -> Result<(Zeroizing<String>, [u8; 32]), RandomError> {
let mut buf = vec![0u8; byte_len];
fill_random(&mut buf)?;
let token = hex_encode(&buf);
let hash = Sha256::digest(&buf);
crate::crypto::zeroize::zeroize(&mut buf);
Ok((Zeroizing::new(token), hash))
}
#[must_use]
pub fn hash_token_for_lookup(token: &str) -> [u8; 32] {
match hex_decode(token) {
Ok(raw) => Sha256::digest(&raw),
Err(_) => Sha256::digest(token.as_bytes()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_token_correct_length() {
for byte_len in [0, 1, 8, 16, 32, 64] {
let token = generate_token(byte_len).unwrap();
assert_eq!(
token.len(),
byte_len * 2,
"hex token length should be byte_len * 2 for byte_len={byte_len}",
);
}
}
#[test]
fn generate_token_different_each_call() {
let a = generate_token(32).unwrap();
let b = generate_token(32).unwrap();
assert_ne!(&*a, &*b, "two generated tokens should differ");
}
#[test]
fn generate_token_hex_characters_only() {
let token = generate_token(32).unwrap();
assert!(
token.chars().all(|c| c.is_ascii_hexdigit()),
"token should contain only hex characters: {}",
&*token,
);
}
#[test]
fn generate_token_with_hash_produces_verifiable_hash() {
let (token, hash) = generate_token_with_hash(32).unwrap();
let raw_bytes = crate::encoding::hex_decode(&token).unwrap();
let expected_hash = Sha256::digest(&raw_bytes);
assert_eq!(
hash, expected_hash,
"hash should match SHA-256 of raw bytes"
);
}
#[test]
fn generate_token_with_hash_different_each_call() {
let (token_a, hash_a) = generate_token_with_hash(32).unwrap();
let (token_b, hash_b) = generate_token_with_hash(32).unwrap();
assert_ne!(&*token_a, &*token_b, "two generated tokens should differ");
assert_ne!(hash_a, hash_b, "two generated hashes should differ");
}
#[test]
fn generate_token_with_hash_correct_lengths() {
let (token, hash) = generate_token_with_hash(16).unwrap();
assert_eq!(token.len(), 32, "16-byte token should produce 32 hex chars");
assert_eq!(hash.len(), 32, "SHA-256 hash should be 32 bytes");
}
#[test]
fn hash_token_for_lookup_matches_mint_hash() {
let (token, mint_hash) = generate_token_with_hash(32).unwrap();
assert_eq!(
hash_token_for_lookup(&token),
mint_hash,
"lookup hash must match the stored mint hash for the same token",
);
}
#[test]
fn hash_token_for_lookup_non_hex_is_deterministic_and_distinct() {
assert_eq!(
hash_token_for_lookup("not-a-hex-token"),
hash_token_for_lookup("not-a-hex-token"),
);
assert_ne!(
hash_token_for_lookup("not-a-hex-token"),
hash_token_for_lookup("00"),
);
}
}