Skip to main content

modo/auth/session/
token.rs

1use sha2::{Digest, Sha256};
2use std::fmt;
3
4/// A cryptographically random 32-byte session token.
5///
6/// The raw bytes are never transmitted; only the hex-encoded form is written
7/// to the signed cookie, and the SHA-256 hash is stored in the database so
8/// that a stolen database cannot be used to forge cookies.
9///
10/// `Debug` and `Display` both redact the value as `"****"` to prevent
11/// accidental logging.
12#[derive(Clone, PartialEq, Eq, Hash)]
13pub struct SessionToken([u8; 32]);
14
15impl SessionToken {
16    /// Generate a new random session token.
17    pub fn generate() -> Self {
18        let mut bytes = [0u8; 32];
19        rand::fill(&mut bytes);
20        Self(bytes)
21    }
22
23    /// Decode a session token from a 64-character lowercase hex string.
24    ///
25    /// # Errors
26    ///
27    /// Returns `Err` if the string is not exactly 64 characters or contains
28    /// non-hexadecimal characters.
29    pub fn from_hex(s: &str) -> Result<Self, &'static str> {
30        if s.len() != 64 {
31            return Err("token must be 64 hex characters");
32        }
33        let mut bytes = [0u8; 32];
34        for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
35            let hi = hex_digit(chunk[0]).ok_or("invalid hex character")?;
36            let lo = hex_digit(chunk[1]).ok_or("invalid hex character")?;
37            bytes[i] = (hi << 4) | lo;
38        }
39        Ok(Self(bytes))
40    }
41
42    /// Encode the token as a 64-character lowercase hex string.
43    ///
44    /// This is the value written into the session cookie.
45    pub fn as_hex(&self) -> String {
46        crate::encoding::hex::encode(&self.0)
47    }
48
49    /// Compute the SHA-256 hash of the token and return it as a 64-character
50    /// lowercase hex string.
51    ///
52    /// This hash is what is stored in `sessions.token_hash`. Storing only
53    /// the hash ensures that a read of the database cannot be used to impersonate
54    /// users.
55    pub fn hash(&self) -> String {
56        let digest = Sha256::digest(self.0);
57        crate::encoding::hex::encode(&digest)
58    }
59}
60
61impl fmt::Debug for SessionToken {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        write!(f, "SessionToken(****)")
64    }
65}
66
67impl fmt::Display for SessionToken {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        f.write_str("****")
70    }
71}
72
73fn hex_digit(b: u8) -> Option<u8> {
74    match b {
75        b'0'..=b'9' => Some(b - b'0'),
76        b'a'..=b'f' => Some(b - b'a' + 10),
77        b'A'..=b'F' => Some(b - b'A' + 10),
78        _ => None,
79    }
80}