Skip to main content

klick_domain/authentication/
nonce.rs

1use std::{fmt, str::FromStr, string::FromUtf8Error};
2
3use thiserror::Error;
4use time::OffsetDateTime;
5use uuid::Uuid;
6
7use crate::authentication::{EmailAddress, EmailAddressParseError};
8
9#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub struct Nonce(Uuid);
11
12impl Nonce {
13    pub const STR_LEN: usize = 32;
14
15    #[must_use]
16    pub fn new() -> Self {
17        Self(Uuid::new_v4())
18    }
19}
20
21#[derive(Debug, Error)]
22#[error("invalid nonce")]
23pub struct NonceParseError;
24
25impl FromStr for Nonce {
26    type Err = NonceParseError;
27
28    fn from_str(nonce_str: &str) -> Result<Self, Self::Err> {
29        nonce_str
30            .parse::<Uuid>()
31            .map(Nonce)
32            .map_err(|_| NonceParseError)
33    }
34}
35
36impl fmt::Display for Nonce {
37    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), std::fmt::Error> {
38        write!(f, "{}", self.0.as_simple())
39    }
40}
41
42#[derive(Debug, Clone, Eq, PartialEq)]
43pub struct EmailNonce {
44    pub email: EmailAddress,
45    pub nonce: Nonce,
46}
47
48pub type ActualTokenLen = usize;
49
50#[derive(Debug, Error)]
51pub enum EmailNonceDecodingError {
52    #[error(transparent)]
53    Bs58(#[from] bs58::decode::Error),
54    #[error(transparent)]
55    Utf8(#[from] FromUtf8Error),
56    #[error("nonce is too short: {0} instead of {}", Nonce::STR_LEN)]
57    TooShort(ActualTokenLen),
58    #[error(transparent)]
59    Parse(#[from] NonceParseError),
60    #[error(transparent)]
61    EmailAddress(#[from] EmailAddressParseError),
62}
63
64impl EmailNonce {
65    #[must_use]
66    pub fn encode_to_string(&self) -> String {
67        let nonce = self.nonce.to_string();
68        debug_assert_eq!(Nonce::STR_LEN, nonce.len());
69        let mut concat = String::with_capacity(self.email.as_str().len() + nonce.len());
70        concat += self.email.as_str();
71        concat += &nonce;
72        bs58::encode(concat).into_string()
73    }
74
75    pub fn decode_from_str(encoded: &str) -> Result<Self, EmailNonceDecodingError> {
76        let decoded = bs58::decode(encoded).into_vec()?;
77        let mut concat = String::from_utf8(decoded)?;
78        if concat.len() < Nonce::STR_LEN {
79            return Err(EmailNonceDecodingError::TooShort(concat.len()));
80        }
81        let email_len = concat.len() - Nonce::STR_LEN;
82        let nonce_slice: &str = &concat[email_len..];
83        let nonce = nonce_slice.parse::<Nonce>()?;
84        concat.truncate(email_len);
85        let email = concat.parse()?;
86        Ok(Self { email, nonce })
87    }
88}
89
90#[derive(Debug, Clone, Eq, PartialEq)]
91pub struct AccountToken {
92    pub email_nonce: EmailNonce,
93    pub expires_at: OffsetDateTime,
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn encode_decode_email_nonce() {
102        let example = EmailNonce {
103            email: "test@example.com".parse().unwrap(),
104            nonce: Nonce::new(),
105        };
106        let encoded = example.encode_to_string();
107        let decoded = EmailNonce::decode_from_str(&encoded).unwrap();
108        assert_eq!(example, decoded);
109    }
110
111    #[test]
112    fn decode_empty_email_nonce() {
113        assert!(EmailNonce::decode_from_str("").is_err());
114    }
115
116    #[test]
117    fn should_generate_unique_instances() {
118        let n1 = Nonce::new();
119        let n2 = Nonce::new();
120        assert_ne!(n1, n2);
121    }
122
123    #[test]
124    fn should_convert_from_to_string() {
125        let n1 = Nonce::new();
126        let s1 = n1.to_string();
127        assert_eq!(Nonce::STR_LEN, s1.len());
128        let n2 = s1.parse::<Nonce>().unwrap();
129        assert_eq!(n1, n2);
130        assert_eq!(s1, n2.to_string());
131    }
132}