Skip to main content

topcoat_session/
token.rs

1mod hash;
2mod store;
3
4pub use hash::*;
5pub use store::*;
6
7use sha2::Digest;
8
9/// A session token: 32 bytes of cryptographically secure randomness, held by
10/// the client as its proof of a session.
11///
12/// The raw token only ever travels between the client and the [`TokenStore`].
13/// Applications persist its [`hash`](Self::hash) instead, so a leaked session
14/// database never contains a credential a client could present.
15#[derive(Clone)]
16pub struct Token([u8; 32]);
17
18impl Token {
19    /// Creates a token from raw bytes, typically inside a [`TokenStore`] that
20    /// has deserialized a client-presented token.
21    #[must_use]
22    pub fn new(bytes: [u8; 32]) -> Self {
23        Self(bytes)
24    }
25
26    /// Generates a fresh random token.
27    #[must_use]
28    pub fn random() -> Self {
29        Self::new(rand::random())
30    }
31
32    /// Parses a token from its URL-safe base64 [`encode`](Self::encode)d form.
33    ///
34    /// # Errors
35    ///
36    /// Returns a [`DecodeError`] when `s` is not valid base64 or does not
37    /// decode to exactly 32 bytes.
38    pub fn decode(s: &str) -> Result<Self, DecodeError> {
39        use base64::{DecodeSliceError, Engine as _, engine::general_purpose::URL_SAFE};
40        let mut bytes = [0u8; 32];
41        let num_bytes = URL_SAFE
42            .decode_slice(s, &mut bytes)
43            .map_err(|error| match error {
44                DecodeSliceError::OutputSliceTooSmall => DecodeError::Length,
45                DecodeSliceError::DecodeError(error) => error.into(),
46            })?;
47        if num_bytes != bytes.len() {
48            return Err(DecodeError::Length);
49        }
50        Ok(Self::new(bytes))
51    }
52
53    /// Encodes the token as URL-safe base64, for a [`TokenStore`] to send to
54    /// the client.
55    #[must_use]
56    pub fn encode(&self) -> String {
57        use base64::{Engine as _, engine::general_purpose::URL_SAFE};
58        URL_SAFE.encode(self.0)
59    }
60
61    /// Returns the [`TokenHash`] identifying this token's session, safe for
62    /// the application to persist.
63    #[must_use]
64    pub fn hash(&self) -> TokenHash {
65        let mut hasher = sha2::Sha256::new();
66        hasher.update(self.0);
67        TokenHash::new(hasher.finalize().0)
68    }
69
70    /// Exposes the raw token bytes.
71    ///
72    /// Only a [`TokenStore`] serializing the token for the client should need
73    /// this; never persist the raw bytes server-side.
74    #[must_use]
75    pub fn dangerous_as_array(&self) -> &[u8; 32] {
76        &self.0
77    }
78}
79
80impl std::fmt::Debug for Token {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct(stringify!(Token)).finish()
83    }
84}
85
86/// The reason a [`Token::decode`] call rejected its input.
87#[derive(Debug, thiserror::Error)]
88pub enum DecodeError {
89    #[error("base64 decoding failed")]
90    Base64(#[from] base64::DecodeError),
91    #[error("invalid number of bytes in token")]
92    Length,
93}