bonsaidb_core/admin/
authentication_token.rs

1use serde::{Deserialize, Serialize};
2
3use crate::connection::{IdentityId, SensitiveString};
4use crate::key::time::TimestampAsNanoseconds;
5use crate::schema::Collection;
6
7#[derive(Collection, Clone, Serialize, Deserialize, Debug)]
8#[collection(name = "authentication-tokens", authority = "bonsaidb", core = crate)]
9pub struct AuthenticationToken {
10    pub identity: IdentityId,
11    pub token: SensitiveString,
12    pub created_at: TimestampAsNanoseconds,
13}
14
15#[cfg(feature = "token-authentication")]
16mod implementation {
17    use rand::seq::SliceRandom;
18    use rand::{thread_rng, Rng};
19    use zeroize::Zeroize;
20
21    use super::AuthenticationToken;
22    use crate::connection::{
23        AsyncConnection, Connection, IdentityId, IdentityReference, SensitiveString,
24        TokenChallengeAlgorithm,
25    };
26    use crate::document::CollectionDocument;
27    use crate::key::time::TimestampAsNanoseconds;
28    use crate::schema::SerializedCollection;
29
30    impl AuthenticationToken {
31        fn random(identity: IdentityId) -> (u64, Self) {
32            const ALPHABET: &[u8] =
33                b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.+/#";
34            let mut rng = thread_rng();
35            let id = rng.gen();
36            let token = SensitiveString(
37                std::iter::repeat_with(|| ALPHABET.choose(&mut rng))
38                    .take(32)
39                    .map(|c| *c.unwrap() as char)
40                    .collect(),
41            );
42            (
43                id,
44                Self {
45                    identity,
46                    token,
47                    created_at: TimestampAsNanoseconds::now(),
48                },
49            )
50        }
51
52        pub fn create<C: Connection>(
53            identity: &IdentityReference<'_>,
54            database: &C,
55        ) -> Result<CollectionDocument<Self>, crate::Error> {
56            let identity_id = identity
57                .resolve(database)?
58                .ok_or(crate::Error::InvalidCredentials)?;
59            loop {
60                let (id, token) = Self::random(identity_id);
61                match token.insert_into(&id, database) {
62                    Err(err) if err.error.conflicting_document::<Self>().is_some() => continue,
63                    other => break other.map_err(|err| err.error),
64                }
65            }
66        }
67
68        pub async fn create_async<C: AsyncConnection>(
69            identity: IdentityReference<'_>,
70            database: &C,
71        ) -> Result<CollectionDocument<Self>, crate::Error> {
72            let identity_id = identity
73                .resolve_async(database)
74                .await?
75                .ok_or(crate::Error::InvalidCredentials)?;
76            loop {
77                let (id, token) = Self::random(identity_id);
78                match token.insert_into_async(&id, database).await {
79                    Err(err) if err.error.conflicting_document::<Self>().is_some() => continue,
80                    other => break other.map_err(|err| err.error),
81                }
82            }
83        }
84
85        pub fn validate_challenge(
86            &self,
87            algorithm: TokenChallengeAlgorithm,
88            server_timestamp: TimestampAsNanoseconds,
89            nonce: &[u8],
90            hash: &[u8],
91        ) -> Result<(), crate::Error> {
92            let TokenChallengeAlgorithm::Blake3 = algorithm;
93            let computed_hash =
94                Self::compute_challenge_response_blake3(&self.token, nonce, server_timestamp);
95            let hash: [u8; blake3::OUT_LEN] = hash
96                .try_into()
97                .map_err(|_| crate::Error::InvalidCredentials)?;
98
99            if computed_hash == hash {
100                Ok(())
101            } else {
102                Err(crate::Error::InvalidCredentials)
103            }
104        }
105
106        #[must_use]
107        pub fn compute_challenge_response_blake3(
108            token: &SensitiveString,
109            nonce: &[u8],
110            timestamp: TimestampAsNanoseconds,
111        ) -> blake3::Hash {
112            let context = format!("bonsaidb {timestamp} token-challenge");
113            let mut key = blake3::derive_key(&context, token.0.as_bytes());
114            let hash = blake3::keyed_hash(&key, nonce);
115            key.zeroize();
116            hash
117        }
118
119        pub fn check_request_time(
120            request_time: TimestampAsNanoseconds,
121            request_time_check: &[u8],
122            algorithm: TokenChallengeAlgorithm,
123            token: &SensitiveString,
124        ) -> Result<(), crate::Error> {
125            match algorithm {
126                TokenChallengeAlgorithm::Blake3 => {
127                    let request_time_check: [u8; blake3::OUT_LEN] =
128                        request_time_check
129                            .try_into()
130                            .map_err(|_| crate::Error::InvalidCredentials)?;
131                    if Self::compute_request_time_hash_blake3(request_time, token)
132                        == request_time_check
133                    {
134                        Ok(())
135                    } else {
136                        Err(crate::Error::InvalidCredentials)
137                    }
138                }
139            }
140        }
141
142        pub(crate) fn compute_request_time_hash_blake3(
143            request_time: TimestampAsNanoseconds,
144            private_token: &SensitiveString,
145        ) -> blake3::Hash {
146            let context = format!("bonsaidb {request_time} token-authentication");
147            let mut key = blake3::derive_key(&context, private_token.0.as_bytes());
148            let hash = blake3::keyed_hash(&key, &request_time.representation().to_be_bytes());
149            key.zeroize();
150            hash
151        }
152    }
153}
154
155impl AuthenticationToken {}