pacenotes_server/auth/
token_secret.rs1pub const TOKEN_PREFIX: &str = "pcn_";
3
4const TOKEN_BODY_LEN: usize = 43;
6
7pub fn mint_token() -> String {
10 let mut bytes = [0u8; 32];
11 <rand::rngs::OsRng as rand::RngCore>::fill_bytes(&mut rand::rngs::OsRng, &mut bytes);
12 format!("{TOKEN_PREFIX}{}", base64url(&bytes))
13}
14
15pub fn hash_token(raw: &str) -> String {
17 crate::store::sha256_bytes(raw.as_bytes())
18}
19
20pub fn well_formed(raw: &str) -> bool {
23 raw.strip_prefix(TOKEN_PREFIX).is_some_and(|body| {
24 body.len() == TOKEN_BODY_LEN
25 && body
26 .bytes()
27 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
28 })
29}
30
31fn base64url(bytes: &[u8]) -> String {
32 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
33 let mut encoded = String::with_capacity(bytes.len().div_ceil(3) * 4);
34 for chunk in bytes.chunks(3) {
35 let padded = [
36 chunk[0],
37 chunk.get(1).copied().unwrap_or(0),
38 chunk.get(2).copied().unwrap_or(0),
39 ];
40 let group =
41 (u32::from(padded[0]) << 16) | (u32::from(padded[1]) << 8) | u32::from(padded[2]);
42 let sextets = [
43 group >> 18 & 63,
44 group >> 12 & 63,
45 group >> 6 & 63,
46 group & 63,
47 ];
48 let unpadded_length = chunk.len() + 1;
49 for sextet in sextets.iter().take(unpadded_length) {
50 encoded.push(ALPHABET[*sextet as usize] as char);
51 }
52 }
53 encoded
54}
55
56#[cfg(test)]
57mod tests {
58 #[test]
59 fn hashing_is_stable_and_hides_the_token() {
60 assert_eq!(
61 super::hash_token("abc"),
62 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
63 );
64 let token = super::mint_token();
65 assert!(!super::hash_token(&token).contains(&token));
66 }
67
68 #[test]
69 fn minted_tokens_are_prefixed_unique_and_url_safe() {
70 let first = super::mint_token();
71 let second = super::mint_token();
72 assert_ne!(first, second);
73 assert!(first.starts_with(super::TOKEN_PREFIX), "{first}");
74 let encoded_32_bytes = 43;
75 assert_eq!(
76 first.len(),
77 super::TOKEN_PREFIX.len() + encoded_32_bytes,
78 "{first}"
79 );
80 assert!(
81 first[super::TOKEN_PREFIX.len()..]
82 .bytes()
83 .all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_'),
84 "{first}"
85 );
86 }
87
88 #[test]
89 fn only_minted_shapes_are_well_formed() {
90 let body = "A".repeat(43);
91 for good in [
92 super::mint_token(),
93 format!("pcn_{body}"),
94 format!("pcn_{}abc", "-_9z".repeat(10)),
95 ] {
96 assert!(super::well_formed(&good), "{good}");
97 }
98 for bad in [
99 String::new(),
100 "not-a-token".to_string(),
101 body.clone(),
102 format!("pcn_{}", "A".repeat(42)),
103 format!("pcn_{}", "A".repeat(44)),
104 format!("pcn_{}=", "A".repeat(42)),
105 format!("pcn_{}+", "A".repeat(42)),
106 format!("PCN_{body}"),
107 ] {
108 assert!(!super::well_formed(&bad), "{bad}");
109 }
110 }
111
112 #[test]
113 fn base64url_matches_known_vectors() {
114 let vectors: [(&[u8], &str); 6] = [
115 (b"", ""),
116 (b"f", "Zg"),
117 (b"fo", "Zm8"),
118 (b"foo", "Zm9v"),
119 (b"foobar", "Zm9vYmFy"),
120 (&[0xfb, 0xff], "-_8"),
121 ];
122 for (input, expected) in vectors {
123 assert_eq!(super::base64url(input), expected, "{input:?}");
124 }
125 }
126}