//! Access tokens: `forge_` + base64url(32 random bytes). Only the sha256 hash is stored. use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine as _}; use sha2::{Digest, Sha256}; use subtle::ConstantTimeEq; /// Fresh random 32 bytes from the thread RNG, a CSPRNG seeded from the OS. It is /// infallible, so callers need no error path. pub(crate) fn random_32() -> [u8; 32] { let mut buf = [0u8; 32]; rand::fill(&mut buf); buf } /// Alphabet for codes typed by people: lowercase letters and digits 2-9, without look-alikes /// such as `l`, `o`, `0` and `1`. It has exactly 32 symbols, so the low five bits of a random /// byte pick one uniformly. pub const READABLE_ALPHABET: &[u8; 32] = b"abcdefghijkmnpqrstuvwxyz23456789"; /// `len` random characters from [`READABLE_ALPHABET`]. pub fn random_readable(len: usize) -> String { let mut buf = vec![0u8; len]; rand::fill(buf.as_mut_slice()); buf.iter() .map(|b| char::from(READABLE_ALPHABET[usize::from(b & 31)])) .collect() } pub fn generate_token() -> String { format!("forge_{}", BASE64_URL_SAFE_NO_PAD.encode(random_32())) } /// Hex-encoded sha256 of the token. pub fn hash_token(token: &str) -> String { hex::encode(Sha256::digest(token.as_bytes())) } /// Constant-time byte comparison. pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { a.ct_eq(b).into() } #[cfg(test)] mod tests { use super::*; #[test] fn token_has_prefix_and_43_char_body() { let t = generate_token(); let body = t.strip_prefix("forge_").unwrap(); assert_eq!(body.len(), 43); assert!( body.bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') ); assert_ne!(t, generate_token()); } #[test] fn hash_is_hex_sha256() { // Known vector: sha256("abc"). assert_eq!( hash_token("abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" ); } #[test] fn constant_time_eq_behaves_like_eq() { assert!(constant_time_eq(b"same", b"same")); assert!(!constant_time_eq(b"same", b"diff")); assert!(!constant_time_eq(b"short", b"longer")); assert!(constant_time_eq(b"", b"")); } }