Skip to main content

totp_rfc/
code.rs

1use core::fmt;
2
3use subtle::{Choice, ConstantTimeEq};
4
5use crate::{CodeError, Error};
6
7/// The number of decimal digits in an HOTP or TOTP code.
8///
9/// RFC 4226 requires implementations to support at least six digits and
10/// defines six-, seven-, and eight-digit output.
11#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
12pub struct Digits(u8);
13
14impl Digits {
15    /// A six-digit code, the RFC 4226 minimum and HOTP default.
16    pub const SIX: Self = Self(6);
17    /// A seven-digit code.
18    pub const SEVEN: Self = Self(7);
19    /// An eight-digit code, used by the RFC 6238 test vectors.
20    pub const EIGHT: Self = Self(8);
21
22    /// Validates an OTP width.
23    ///
24    /// # Errors
25    ///
26    /// Returns [`Error::InvalidDigits`] unless `value` is 6, 7, or 8.
27    pub const fn new(value: u8) -> Result<Self, Error> {
28        if value >= 6 && value <= 8 {
29            Ok(Self(value))
30        } else {
31            Err(Error::InvalidDigits { actual: value })
32        }
33    }
34
35    /// Returns the decimal width.
36    #[must_use]
37    pub const fn get(self) -> u8 {
38        self.0
39    }
40
41    pub(crate) const fn modulus(self) -> u32 {
42        match self.0 {
43            6 => 1_000_000,
44            7 => 10_000_000,
45            8 => 100_000_000,
46            _ => unreachable!(),
47        }
48    }
49}
50
51impl TryFrom<u8> for Digits {
52    type Error = Error;
53
54    fn try_from(value: u8) -> Result<Self, Self::Error> {
55        Self::new(value)
56    }
57}
58
59impl From<Digits> for u8 {
60    fn from(value: Digits) -> Self {
61        value.get()
62    }
63}
64
65/// A generated or parsed decimal one-time password.
66///
67/// Formatting always preserves leading zeroes to the configured width.
68#[derive(Clone, Copy, Debug)]
69pub struct Code {
70    value: u32,
71    digits: Digits,
72}
73
74impl Code {
75    pub(crate) const fn generated(value: u32, digits: Digits) -> Self {
76        Self { value, digits }
77    }
78
79    /// Parses exactly the configured number of ASCII decimal digits.
80    ///
81    /// Whitespace, signs, non-ASCII numerals, and missing leading zeroes are
82    /// rejected rather than normalized.
83    ///
84    /// # Errors
85    ///
86    /// Returns [`CodeError::InvalidLength`] for a different byte length or
87    /// [`CodeError::NonDecimal`] for any non-ASCII-decimal byte.
88    pub fn parse(input: &str, digits: Digits) -> Result<Self, CodeError> {
89        let bytes = input.as_bytes();
90        if bytes.len() != usize::from(digits.get()) {
91            return Err(CodeError::InvalidLength {
92                actual: bytes.len(),
93                expected: digits.get(),
94            });
95        }
96
97        let mut value = 0_u32;
98        for (index, byte) in bytes.iter().copied().enumerate() {
99            if !byte.is_ascii_digit() {
100                return Err(CodeError::NonDecimal { index });
101            }
102            value = value * 10 + u32::from(byte - b'0');
103        }
104
105        Ok(Self { value, digits })
106    }
107
108    /// Returns the numeric value. Use [`Display`](fmt::Display) to retain
109    /// leading zeroes.
110    #[must_use]
111    pub const fn value(self) -> u32 {
112        self.value
113    }
114
115    /// Returns the configured decimal width.
116    #[must_use]
117    pub const fn digits(self) -> Digits {
118        self.digits
119    }
120
121    pub(crate) fn ct_eq_choice(self, other: Self) -> Choice {
122        let values_equal = self.value.to_be_bytes().ct_eq(&other.value.to_be_bytes());
123        let widths_equal = self.digits.get().ct_eq(&other.digits.get());
124        values_equal & widths_equal
125    }
126
127    pub(crate) fn ct_eq(self, other: Self) -> bool {
128        bool::from(self.ct_eq_choice(other))
129    }
130}
131
132impl PartialEq for Code {
133    fn eq(&self, other: &Self) -> bool {
134        (*self).ct_eq(*other)
135    }
136}
137
138impl Eq for Code {}
139
140impl fmt::Display for Code {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(
143            f,
144            "{:0width$}",
145            self.value,
146            width = usize::from(self.digits.get())
147        )
148    }
149}