bonsaidb-core 0.5.0

Core functionality and types for BonsaiDb.
Documentation
use serde::{Deserialize, Serialize};

use crate::connection::{IdentityId, SensitiveString};
use crate::key::time::TimestampAsNanoseconds;
use crate::schema::Collection;

#[derive(Collection, Clone, Serialize, Deserialize, Debug)]
#[collection(name = "authentication-tokens", authority = "bonsaidb", core = crate)]
pub struct AuthenticationToken {
    pub identity: IdentityId,
    pub token: SensitiveString,
    pub created_at: TimestampAsNanoseconds,
}

#[cfg(feature = "token-authentication")]
mod implementation {
    use rand::seq::SliceRandom;
    use rand::{thread_rng, Rng};
    use zeroize::Zeroize;

    use super::AuthenticationToken;
    use crate::connection::{
        AsyncConnection, Connection, IdentityId, IdentityReference, SensitiveString,
        TokenChallengeAlgorithm,
    };
    use crate::document::CollectionDocument;
    use crate::key::time::TimestampAsNanoseconds;
    use crate::schema::SerializedCollection;

    impl AuthenticationToken {
        fn random(identity: IdentityId) -> (u64, Self) {
            const ALPHABET: &[u8] =
                b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.+/#";
            let mut rng = thread_rng();
            let id = rng.gen();
            let token = SensitiveString(
                std::iter::repeat_with(|| ALPHABET.choose(&mut rng))
                    .take(32)
                    .map(|c| *c.unwrap() as char)
                    .collect(),
            );
            (
                id,
                Self {
                    identity,
                    token,
                    created_at: TimestampAsNanoseconds::now(),
                },
            )
        }

        pub fn create<C: Connection>(
            identity: &IdentityReference<'_>,
            database: &C,
        ) -> Result<CollectionDocument<Self>, crate::Error> {
            let identity_id = identity
                .resolve(database)?
                .ok_or(crate::Error::InvalidCredentials)?;
            loop {
                let (id, token) = Self::random(identity_id);
                match token.insert_into(&id, database) {
                    Err(err) if err.error.conflicting_document::<Self>().is_some() => continue,
                    other => break other.map_err(|err| err.error),
                }
            }
        }

        pub async fn create_async<C: AsyncConnection>(
            identity: IdentityReference<'_>,
            database: &C,
        ) -> Result<CollectionDocument<Self>, crate::Error> {
            let identity_id = identity
                .resolve_async(database)
                .await?
                .ok_or(crate::Error::InvalidCredentials)?;
            loop {
                let (id, token) = Self::random(identity_id);
                match token.insert_into_async(&id, database).await {
                    Err(err) if err.error.conflicting_document::<Self>().is_some() => continue,
                    other => break other.map_err(|err| err.error),
                }
            }
        }

        pub fn validate_challenge(
            &self,
            algorithm: TokenChallengeAlgorithm,
            server_timestamp: TimestampAsNanoseconds,
            nonce: &[u8],
            hash: &[u8],
        ) -> Result<(), crate::Error> {
            let TokenChallengeAlgorithm::Blake3 = algorithm;
            let computed_hash =
                Self::compute_challenge_response_blake3(&self.token, nonce, server_timestamp);
            let hash: [u8; blake3::OUT_LEN] = hash
                .try_into()
                .map_err(|_| crate::Error::InvalidCredentials)?;

            if computed_hash == hash {
                Ok(())
            } else {
                Err(crate::Error::InvalidCredentials)
            }
        }

        #[must_use]
        pub fn compute_challenge_response_blake3(
            token: &SensitiveString,
            nonce: &[u8],
            timestamp: TimestampAsNanoseconds,
        ) -> blake3::Hash {
            let context = format!("bonsaidb {timestamp} token-challenge");
            let mut key = blake3::derive_key(&context, token.0.as_bytes());
            let hash = blake3::keyed_hash(&key, nonce);
            key.zeroize();
            hash
        }

        pub fn check_request_time(
            request_time: TimestampAsNanoseconds,
            request_time_check: &[u8],
            algorithm: TokenChallengeAlgorithm,
            token: &SensitiveString,
        ) -> Result<(), crate::Error> {
            match algorithm {
                TokenChallengeAlgorithm::Blake3 => {
                    let request_time_check: [u8; blake3::OUT_LEN] =
                        request_time_check
                            .try_into()
                            .map_err(|_| crate::Error::InvalidCredentials)?;
                    if Self::compute_request_time_hash_blake3(request_time, token)
                        == request_time_check
                    {
                        Ok(())
                    } else {
                        Err(crate::Error::InvalidCredentials)
                    }
                }
            }
        }

        pub(crate) fn compute_request_time_hash_blake3(
            request_time: TimestampAsNanoseconds,
            private_token: &SensitiveString,
        ) -> blake3::Hash {
            let context = format!("bonsaidb {request_time} token-authentication");
            let mut key = blake3::derive_key(&context, private_token.0.as_bytes());
            let hash = blake3::keyed_hash(&key, &request_time.representation().to_be_bytes());
            key.zeroize();
            hash
        }
    }
}

impl AuthenticationToken {}