bonsaidb_core/admin/
authentication_token.rs1use 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 {}