1
use serde::{Deserialize, Serialize};
2

            
3
use crate::connection::{IdentityId, SensitiveString};
4
use crate::key::time::TimestampAsNanoseconds;
5
use crate::schema::Collection;
6

            
7
234240
#[derive(Collection, Clone, Serialize, Deserialize, Debug)]
8
#[collection(name = "authentication-tokens", authority = "bonsaidb", core = crate)]
9
pub struct AuthenticationToken {
10
    pub identity: IdentityId,
11
    pub token: SensitiveString,
12
    pub created_at: TimestampAsNanoseconds,
13
}
14

            
15
#[cfg(feature = "token-authentication")]
16
mod 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
640
        fn random(identity: IdentityId) -> (u64, Self) {
32
640
            const ALPHABET: &[u8] =
33
640
                b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.+/#";
34
640
            let mut rng = thread_rng();
35
640
            let id = rng.gen();
36
640
            let token = SensitiveString(
37
20480
                std::iter::repeat_with(|| ALPHABET.choose(&mut rng))
38
640
                    .take(32)
39
20480
                    .map(|c| *c.unwrap() as char)
40
640
                    .collect(),
41
640
            );
42
640
            (
43
640
                id,
44
640
                Self {
45
640
                    identity,
46
640
                    token,
47
640
                    created_at: TimestampAsNanoseconds::now(),
48
640
                },
49
640
            )
50
640
        }
51

            
52
6
        pub fn create<C: Connection>(
53
6
            identity: &IdentityReference<'_>,
54
6
            database: &C,
55
6
        ) -> Result<CollectionDocument<Self>, crate::Error> {
56
6
            let identity_id = identity
57
6
                .resolve(database)?
58
6
                .ok_or(crate::Error::InvalidCredentials)?;
59
6
            loop {
60
6
                let (id, token) = Self::random(identity_id);
61
6
                match token.insert_into(&id, database) {
62
                    Err(err) if err.error.conflicting_document::<Self>().is_some() => continue,
63
6
                    other => break other.map_err(|err| err.error),
64
                }
65
            }
66
6
        }
67

            
68
10
        pub async fn create_async<C: AsyncConnection>(
69
10
            identity: IdentityReference<'_>,
70
10
            database: &C,
71
10
        ) -> Result<CollectionDocument<Self>, crate::Error> {
72
10
            let identity_id = identity
73
10
                .resolve_async(database)
74
5
                .await?
75
10
                .ok_or(crate::Error::InvalidCredentials)?;
76
10
            loop {
77
10
                let (id, token) = Self::random(identity_id);
78
10
                match token.insert_into_async(&id, database).await {
79
                    Err(err) if err.error.conflicting_document::<Self>().is_some() => continue,
80
10
                    other => break other.map_err(|err| err.error),
81
                }
82
            }
83
10
        }
84

            
85
640
        pub fn validate_challenge(
86
640
            &self,
87
640
            algorithm: TokenChallengeAlgorithm,
88
640
            server_timestamp: TimestampAsNanoseconds,
89
640
            nonce: &[u8],
90
640
            hash: &[u8],
91
640
        ) -> Result<(), crate::Error> {
92
640
            let TokenChallengeAlgorithm::Blake3 = algorithm;
93
640
            let computed_hash =
94
640
                Self::compute_challenge_response_blake3(&self.token, nonce, server_timestamp);
95
640
            let hash: [u8; blake3::OUT_LEN] = hash
96
640
                .try_into()
97
640
                .map_err(|_| crate::Error::InvalidCredentials)?;
98

            
99
640
            if computed_hash == hash {
100
640
                Ok(())
101
            } else {
102
                Err(crate::Error::InvalidCredentials)
103
            }
104
640
        }
105

            
106
        #[must_use]
107
1280
        pub fn compute_challenge_response_blake3(
108
1280
            token: &SensitiveString,
109
1280
            nonce: &[u8],
110
1280
            timestamp: TimestampAsNanoseconds,
111
1280
        ) -> blake3::Hash {
112
1280
            let context = format!("bonsaidb {timestamp} token-challenge");
113
1280
            let mut key = blake3::derive_key(&context, token.0.as_bytes());
114
1280
            let hash = blake3::keyed_hash(&key, nonce);
115
1280
            key.zeroize();
116
1280
            hash
117
1280
        }
118

            
119
640
        pub fn check_request_time(
120
640
            request_time: TimestampAsNanoseconds,
121
640
            request_time_check: &[u8],
122
640
            algorithm: TokenChallengeAlgorithm,
123
640
            token: &SensitiveString,
124
640
        ) -> Result<(), crate::Error> {
125
640
            match algorithm {
126
                TokenChallengeAlgorithm::Blake3 => {
127
640
                    let request_time_check: [u8; blake3::OUT_LEN] =
128
640
                        request_time_check
129
640
                            .try_into()
130
640
                            .map_err(|_| crate::Error::InvalidCredentials)?;
131
640
                    if Self::compute_request_time_hash_blake3(request_time, token)
132
640
                        == request_time_check
133
                    {
134
640
                        Ok(())
135
                    } else {
136
                        Err(crate::Error::InvalidCredentials)
137
                    }
138
                }
139
            }
140
640
        }
141

            
142
1280
        pub(crate) fn compute_request_time_hash_blake3(
143
1280
            request_time: TimestampAsNanoseconds,
144
1280
            private_token: &SensitiveString,
145
1280
        ) -> blake3::Hash {
146
1280
            let context = format!("bonsaidb {request_time} token-authentication");
147
1280
            let mut key = blake3::derive_key(&context, private_token.0.as_bytes());
148
1280
            let hash = blake3::keyed_hash(&key, &request_time.representation().to_be_bytes());
149
1280
            key.zeroize();
150
1280
            hash
151
1280
        }
152
    }
153
}
154

            
155
impl AuthenticationToken {}