use crate::encoding::hex;
use subtle::ConstantTimeEq;
const BASE62: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const ULID_LEN: usize = 26;
pub(crate) struct ParsedToken<'a> {
pub id: &'a str,
pub secret: &'a str,
}
const BIAS_LIMIT: u8 = 248;
pub(crate) fn generate_secret(len: usize) -> String {
let mut result = String::with_capacity(len);
let mut buf = [0u8; 1];
while result.len() < len {
rand::fill(&mut buf[..]);
let b = buf[0];
if b < BIAS_LIMIT {
result.push(BASE62[(b as usize) % 62] as char);
}
}
result
}
pub(crate) fn format_token(prefix: &str, ulid: &str, secret: &str) -> String {
format!("{prefix}_{ulid}{secret}")
}
pub(crate) fn parse_token<'a>(raw: &'a str, expected_prefix: &str) -> Option<ParsedToken<'a>> {
let (prefix, body) = raw.split_once('_')?;
if prefix != expected_prefix {
return None;
}
if body.len() <= ULID_LEN {
return None;
}
let (id, secret) = body.split_at(ULID_LEN);
Some(ParsedToken { id, secret })
}
pub(crate) fn hash_secret(secret: &str) -> String {
hex::sha256(secret.as_bytes())
}
pub(crate) fn verify_hash(secret: &str, stored_hash: &str) -> bool {
let computed = hash_secret(secret);
computed.as_bytes().ct_eq(stored_hash.as_bytes()).into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_secret_correct_length() {
let secret = generate_secret(32);
assert_eq!(secret.len(), 32);
}
#[test]
fn generate_secret_is_base62() {
let secret = generate_secret(32);
assert!(
secret.chars().all(|c| c.is_ascii_alphanumeric()),
"secret contains non-base62 chars: {secret}"
);
}
#[test]
fn generate_secret_unique() {
let a = generate_secret(32);
let b = generate_secret(32);
assert_ne!(a, b);
}
#[test]
fn format_token_structure() {
let token = format_token("modo", "01JQXK5M3N8R4T6V2W9Y0ZABCD", "secret123");
assert_eq!(token, "modo_01JQXK5M3N8R4T6V2W9Y0ZABCDsecret123");
}
#[test]
fn parse_token_roundtrip() {
let token = format_token("modo", "01JQXK5M3N8R4T6V2W9Y0ZABCD", "abcdefghij");
let parsed = parse_token(&token, "modo").unwrap();
assert_eq!(parsed.id, "01JQXK5M3N8R4T6V2W9Y0ZABCD");
assert_eq!(parsed.secret, "abcdefghij");
}
#[test]
fn parse_token_wrong_prefix() {
let token = "sk_01JQXK5M3N8R4T6V2W9Y0ZABCDsecret";
assert!(parse_token(token, "modo").is_none());
}
#[test]
fn parse_token_no_underscore() {
assert!(parse_token("nounderscore", "modo").is_none());
}
#[test]
fn parse_token_body_too_short() {
let token = "modo_SHORT";
assert!(parse_token(token, "modo").is_none());
}
#[test]
fn hash_secret_produces_64_char_hex() {
let hash = hash_secret("testsecret");
assert_eq!(hash.len(), 64);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn hash_secret_deterministic() {
let a = hash_secret("same");
let b = hash_secret("same");
assert_eq!(a, b);
}
#[test]
fn hash_secret_different_inputs_differ() {
let a = hash_secret("one");
let b = hash_secret("two");
assert_ne!(a, b);
}
#[test]
fn verify_hash_correct_secret() {
let hash = hash_secret("mysecret");
assert!(verify_hash("mysecret", &hash));
}
#[test]
fn verify_hash_wrong_secret() {
let hash = hash_secret("mysecret");
assert!(!verify_hash("wrong", &hash));
}
}