bore_cli/
auth.rs

1//! Auth implementation for bore client and server.
2
3use anyhow::{bail, ensure, Result};
4use hmac::{Hmac, Mac};
5use sha2::{Digest, Sha256};
6use tokio::io::{AsyncRead, AsyncWrite};
7use uuid::Uuid;
8
9use crate::shared::{ClientMessage, Delimited, ServerMessage};
10
11/// Wrapper around a MAC used for authenticating clients that have a secret.
12pub struct Authenticator(Hmac<Sha256>);
13
14impl Authenticator {
15    /// Generate an authenticator from a secret.
16    pub fn new(secret: &str) -> Self {
17        let hashed_secret = Sha256::new().chain_update(secret).finalize();
18        Self(Hmac::new_from_slice(&hashed_secret).expect("HMAC can take key of any size"))
19    }
20
21    /// Generate a reply message for a challenge.
22    pub fn answer(&self, challenge: &Uuid) -> String {
23        let mut hmac = self.0.clone();
24        hmac.update(challenge.as_bytes());
25        hex::encode(hmac.finalize().into_bytes())
26    }
27
28    /// Validate a reply to a challenge.
29    ///
30    /// ```
31    /// use bore_cli::auth::Authenticator;
32    /// use uuid::Uuid;
33    ///
34    /// let auth = Authenticator::new("secret");
35    /// let challenge = Uuid::new_v4();
36    ///
37    /// assert!(auth.validate(&challenge, &auth.answer(&challenge)));
38    /// assert!(!auth.validate(&challenge, "wrong answer"));
39    /// ```
40    pub fn validate(&self, challenge: &Uuid, tag: &str) -> bool {
41        if let Ok(tag) = hex::decode(tag) {
42            let mut hmac = self.0.clone();
43            hmac.update(challenge.as_bytes());
44            hmac.verify_slice(&tag).is_ok()
45        } else {
46            false
47        }
48    }
49
50    /// As the server, send a challenge to the client and validate their response.
51    pub async fn server_handshake<T: AsyncRead + AsyncWrite + Unpin>(
52        &self,
53        stream: &mut Delimited<T>,
54    ) -> Result<()> {
55        let challenge = Uuid::new_v4();
56        stream.send(ServerMessage::Challenge(challenge)).await?;
57        match stream.recv_timeout().await? {
58            Some(ClientMessage::Authenticate(tag)) => {
59                ensure!(self.validate(&challenge, &tag), "invalid secret");
60                Ok(())
61            }
62            _ => bail!("server requires secret, but no secret was provided"),
63        }
64    }
65
66    /// As the client, answer a challenge to attempt to authenticate with the server.
67    pub async fn client_handshake<T: AsyncRead + AsyncWrite + Unpin>(
68        &self,
69        stream: &mut Delimited<T>,
70    ) -> Result<()> {
71        let challenge = match stream.recv_timeout().await? {
72            Some(ServerMessage::Challenge(challenge)) => challenge,
73            _ => bail!("expected authentication challenge, but no secret was required"),
74        };
75        let tag = self.answer(&challenge);
76        stream.send(ClientMessage::Authenticate(tag)).await?;
77        Ok(())
78    }
79}