Skip to main content

tact_memory/server/
credential.rs

1//! Startup credentials and retained authentication principals.
2//!
3//! Raw bearer tokens exist only while credentials are assembled. Server construction hashes each
4//! token, consumes the credential, and retains only the digest, namespace, and role.
5
6use super::protocol::{self, RemoteRole};
7use sha2::{Digest, Sha256};
8use std::{fmt, mem};
9use thiserror::Error;
10use zeroize::{Zeroize, Zeroizing};
11
12const MAX_BEARER_TOKEN_BYTES: usize = 4096;
13
14struct BearerToken(Zeroizing<String>);
15
16impl BearerToken {
17    fn new(token: String) -> Result<Self, CredentialError> {
18        let token = Zeroizing::new(token);
19        if token.is_empty()
20            || token.len() > MAX_BEARER_TOKEN_BYTES
21            || !token.bytes().all(is_bearer_token_byte)
22        {
23            return Err(CredentialError::InvalidBearerToken);
24        }
25        Ok(Self(token))
26    }
27
28    fn expose(&self) -> &str {
29        self.0.as_str()
30    }
31}
32
33impl fmt::Debug for BearerToken {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        formatter.write_str("BearerToken([REDACTED])")
36    }
37}
38
39impl Zeroize for BearerToken {
40    fn zeroize(&mut self) {
41        self.0.zeroize();
42    }
43}
44
45impl Drop for BearerToken {
46    fn drop(&mut self) {
47        self.zeroize();
48    }
49}
50
51/// A startup credential consumed by [`MemoryServer::new`](super::MemoryServer::new).
52///
53/// The server hashes the token and drops this value before serving requests. This type
54/// deliberately implements neither `Clone`, `Display`, nor serialization.
55pub struct Credential {
56    namespace: String,
57    role: RemoteRole,
58    token: BearerToken,
59}
60
61impl Credential {
62    /// Validates a namespace and bearer token for the given remote role.
63    pub fn new(
64        namespace: String,
65        role: RemoteRole,
66        bearer_token: String,
67    ) -> Result<Self, CredentialError> {
68        let token = BearerToken::new(bearer_token)?;
69        if !protocol::is_valid_namespace(&namespace) {
70            return Err(CredentialError::InvalidNamespace);
71        }
72        Ok(Self {
73            namespace,
74            role,
75            token,
76        })
77    }
78
79    pub(crate) fn into_hashed_principal(mut self) -> ([u8; 32], Principal) {
80        let token_hash = hash_token(self.token.expose());
81        let principal = Principal {
82            namespace: mem::take(&mut self.namespace),
83            role: self.role,
84        };
85        (token_hash, principal)
86    }
87}
88
89impl fmt::Debug for Credential {
90    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91        formatter
92            .debug_struct("Credential")
93            .field("namespace", &self.namespace)
94            .field("role", &self.role)
95            .field("token", &"[REDACTED]")
96            .finish()
97    }
98}
99
100impl Zeroize for Credential {
101    fn zeroize(&mut self) {
102        self.token.zeroize();
103    }
104}
105
106impl Drop for Credential {
107    fn drop(&mut self) {
108        self.zeroize();
109    }
110}
111
112/// Failure to construct a startup credential.
113#[derive(Debug, Error)]
114pub enum CredentialError {
115    /// The namespace does not satisfy the protocol grammar.
116    #[error("memory namespace is invalid")]
117    InvalidNamespace,
118    /// The bearer token is empty, too large, or contains an unsupported byte.
119    #[error("bearer token is invalid")]
120    InvalidBearerToken,
121}
122
123#[derive(Clone)]
124pub(crate) struct Principal {
125    pub(crate) namespace: String,
126    pub(crate) role: RemoteRole,
127}
128
129pub(crate) fn is_bearer_token_byte(byte: u8) -> bool {
130    byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'+' | b'/' | b'=')
131}
132
133pub(crate) fn hash_token(token: &str) -> [u8; 32] {
134    Sha256::digest(token.as_bytes()).into()
135}