1mod hash;
2mod store;
3
4pub use hash::*;
5pub use store::*;
6
7use sha2::Digest;
8
9#[derive(Clone)]
16pub struct Token([u8; 32]);
17
18impl Token {
19 #[must_use]
22 pub fn new(bytes: [u8; 32]) -> Self {
23 Self(bytes)
24 }
25
26 #[must_use]
33 pub fn random() -> Self {
34 let mut bytes = [0u8; 32];
35 getrandom::fill(&mut bytes).expect("the session token randomness source failed");
36 Self::new(bytes)
37 }
38
39 pub fn decode(s: &str) -> Result<Self, DecodeError> {
46 use base64::{DecodeSliceError, Engine as _, engine::general_purpose::URL_SAFE};
47 let mut bytes = [0u8; 32];
48 let num_bytes = URL_SAFE
49 .decode_slice(s, &mut bytes)
50 .map_err(|error| match error {
51 DecodeSliceError::OutputSliceTooSmall => DecodeError::Length,
52 DecodeSliceError::DecodeError(error) => error.into(),
53 })?;
54 if num_bytes != bytes.len() {
55 return Err(DecodeError::Length);
56 }
57 Ok(Self::new(bytes))
58 }
59
60 #[must_use]
63 pub fn encode(&self) -> String {
64 use base64::{Engine as _, engine::general_purpose::URL_SAFE};
65 URL_SAFE.encode(self.0)
66 }
67
68 #[must_use]
71 pub fn hash(&self) -> TokenHash {
72 let mut hasher = sha2::Sha256::new();
73 hasher.update(self.0);
74 TokenHash::new(hasher.finalize().0)
75 }
76
77 #[must_use]
82 pub fn dangerous_as_array(&self) -> &[u8; 32] {
83 &self.0
84 }
85}
86
87impl std::fmt::Debug for Token {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct(stringify!(Token)).finish()
90 }
91}
92
93#[derive(Debug, thiserror::Error)]
95pub enum DecodeError {
96 #[error("base64 decoding failed")]
97 Base64(#[from] base64::DecodeError),
98 #[error("invalid number of bytes in token")]
99 Length,
100}
101
102#[cfg(test)]
103mod tests {
104 use base64::{Engine as _, engine::general_purpose::URL_SAFE};
105
106 use super::*;
107
108 #[test]
109 fn encode_then_decode_round_trips() {
110 let token = Token::random();
111 let decoded = Token::decode(&token.encode()).expect("an encoded token decodes back");
112 assert_eq!(decoded.dangerous_as_array(), token.dangerous_as_array());
113 }
114
115 #[test]
116 fn decode_accept_correct_length() {
117 let encoded = URL_SAFE.encode([0u8; 32]);
118 assert_eq!(
119 Token::decode(&encoded).unwrap().dangerous_as_array(),
120 &[0u8; 32]
121 );
122 }
123
124 #[test]
125 fn decode_rejects_length_too_short() {
126 let encoded = URL_SAFE.encode([0u8; 31]);
127 assert!(matches!(Token::decode(&encoded), Err(DecodeError::Length)));
128 }
129
130 #[test]
131 fn decode_rejects_length_too_long() {
132 let encoded = URL_SAFE.encode([0u8; 33]);
133 assert!(matches!(Token::decode(&encoded), Err(DecodeError::Length)));
134 }
135
136 #[test]
137 fn decode_rejects_invalid_base64() {
138 assert!(matches!(
139 Token::decode("not*valid*base64"),
140 Err(DecodeError::Base64(_))
141 ));
142 }
143}