otp_std/
base.rs

1//! Base One-Time Password (OTP) functionality.
2
3use std::array;
4
5use bon::Builder;
6use constant_time_eq::constant_time_eq;
7
8#[cfg(feature = "auth")]
9use miette::Diagnostic;
10
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13
14#[cfg(feature = "auth")]
15use thiserror::Error;
16
17use crate::{algorithm::Algorithm, digits::Digits, secret::core::Secret};
18
19#[cfg(feature = "auth")]
20use crate::{
21    algorithm,
22    auth::{query::Query, url::Url},
23    digits, secret,
24};
25
26/// Represents OTP base configuration.
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Builder)]
28#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
29pub struct Base<'b> {
30    /// The secret to use as the key.
31    pub secret: Secret<'b>,
32    /// The algorithm to use.
33    #[builder(default)]
34    #[cfg_attr(feature = "serde", serde(default))]
35    pub algorithm: Algorithm,
36    /// The number of digits to return.
37    #[builder(default)]
38    #[cfg_attr(feature = "serde", serde(default))]
39    pub digits: Digits,
40}
41
42/// The mask used to extract relevant bits.
43pub const MASK: u32 = 0x7FFF_FFFF;
44
45/// The half byte to extract the offset.
46pub const HALF_BYTE: u8 = 0xF;
47
48impl Base<'_> {
49    /// Generates codes based on the given input.
50    ///
51    /// # Panics
52    ///
53    /// Even though [`unwrap`] and indexing are used, the code will never panic,
54    /// provided the HMAC implementation is correct.
55    ///
56    /// [`unwrap`]: Option::unwrap
57    pub fn generate(&self, input: u64) -> u32 {
58        let hmac = self
59            .algorithm
60            .hmac(self.secret.as_ref(), input.to_be_bytes());
61
62        let offset = (hmac.last().unwrap() & HALF_BYTE) as usize;
63        let bytes = array::from_fn(|index| hmac[offset + index]);
64
65        let value = u32::from_be_bytes(bytes) & MASK;
66
67        value % self.digits.power()
68    }
69
70    /// Calls [`generate`] and returns the string representation of the resulting code.
71    ///
72    /// The resulting string is padded with zeros if needed (see [`string`]).
73    ///
74    /// [`generate`]: Self::generate
75    /// [`string`]: Digits::string
76    pub fn generate_string(&self, input: u64) -> String {
77        self.digits.string(self.generate(input))
78    }
79
80    /// Verifies that the given code matches the given input.
81    pub fn verify(&self, input: u64, code: u32) -> bool {
82        self.generate(input) == code
83    }
84
85    /// Verifies that the given string code matches the given input in constant time.
86    ///
87    /// This method exists to simplify verification.
88    pub fn verify_string<S: AsRef<str>>(&self, input: u64, code: S) -> bool {
89        constant_time_eq(
90            self.generate_string(input).as_bytes(),
91            code.as_ref().as_bytes(),
92        )
93    }
94}
95
96/// The `secret` literal.
97#[cfg(feature = "auth")]
98pub const SECRET: &str = "secret";
99
100/// The `algorithm` literal.
101#[cfg(feature = "auth")]
102pub const ALGORITHM: &str = "algorithm";
103
104/// The `digits` literal.
105#[cfg(feature = "auth")]
106pub const DIGITS: &str = "digits";
107
108/// Represents errors returned when the secret is not found in the OTP URL.
109#[cfg(feature = "auth")]
110#[derive(Debug, Error, Diagnostic)]
111#[error("failed to find secret")]
112#[diagnostic(code(otp_std::base::secret), help("make sure the secret is present"))]
113pub struct SecretNotFoundError;
114
115/// Represents sources of errors that can occur when extracting base configurations
116/// from OTP URLs.
117#[cfg(feature = "auth")]
118#[derive(Debug, Error, Diagnostic)]
119#[error(transparent)]
120#[diagnostic(transparent)]
121pub enum ErrorSource {
122    /// The secret was not found in the OTP URL.
123    SecretNotFound(#[from] SecretNotFoundError),
124    /// The secret was found, but could not be parsed.
125    Secret(#[from] secret::core::Error),
126    /// The algorithm could not be parsed.
127    Algorithm(#[from] algorithm::Error),
128    /// The number of digits could not be parsed.
129    Digits(#[from] digits::ParseError),
130}
131
132/// Represents errors that can occur when extracting the base from OTP URLs.
133#[cfg(feature = "auth")]
134#[derive(Debug, Error, Diagnostic)]
135#[error("failed to extract base from OTP URL")]
136#[diagnostic(
137    code(otp_std::base::extract),
138    help("see the report for more information")
139)]
140pub struct Error {
141    /// The source of this error.
142    #[source]
143    #[diagnostic_source]
144    pub source: ErrorSource,
145}
146
147#[cfg(feature = "auth")]
148impl Error {
149    /// Constructs [`Self`].
150    pub const fn new(source: ErrorSource) -> Self {
151        Self { source }
152    }
153
154    /// Constructs [`Self`] from [`SecretNotFoundError`].
155    pub fn secret_not_found(error: SecretNotFoundError) -> Self {
156        Self::new(error.into())
157    }
158
159    /// Creates [`SecretNotFoundError`] and constructs [`Self`] from it.
160    pub fn new_secret_not_found() -> Self {
161        Self::secret_not_found(SecretNotFoundError)
162    }
163
164    /// Constructs [`Self`] from [`secret::core::Error`].
165    pub fn secret(error: secret::core::Error) -> Self {
166        Self::new(error.into())
167    }
168
169    /// Constructs [`Self`] from [`algorithm::Error`].
170    pub fn algorithm(error: algorithm::Error) -> Self {
171        Self::new(error.into())
172    }
173
174    /// Constructs [`Self`] from [`digits::ParseError`].
175    pub fn digits(error: digits::ParseError) -> Self {
176        Self::new(error.into())
177    }
178}
179
180#[cfg(feature = "auth")]
181impl Base<'_> {
182    /// Applies the base configuration to the given URL.
183    pub fn query_for(&self, url: &mut Url) {
184        let secret = self.secret.encode();
185
186        let algorithm = self.algorithm.static_str();
187
188        let digits = self.digits.to_string();
189
190        url.query_pairs_mut()
191            .append_pair(SECRET, secret.as_str())
192            .append_pair(ALGORITHM, algorithm)
193            .append_pair(DIGITS, digits.as_str());
194    }
195
196    /// Extracts the base configuration from the given query.
197    ///
198    /// # Errors
199    ///
200    /// Returns [`struct@Error`] if the base configuration can not be extracted.
201    pub fn extract_from(query: &mut Query<'_>) -> Result<Self, Error> {
202        let secret = query
203            .remove(SECRET)
204            .ok_or_else(Error::new_secret_not_found)?
205            .parse()
206            .map_err(Error::secret)?;
207
208        let maybe_algorithm = query
209            .remove(ALGORITHM)
210            .map(|string| string.parse())
211            .transpose()
212            .map_err(Error::algorithm)?;
213
214        let maybe_digits = query
215            .remove(DIGITS)
216            .map(|string| string.parse())
217            .transpose()
218            .map_err(Error::digits)?;
219
220        let base = Self::builder()
221            .secret(secret)
222            .maybe_algorithm(maybe_algorithm)
223            .maybe_digits(maybe_digits)
224            .build();
225
226        Ok(base)
227    }
228}
229
230/// Represents owned [`Base`].
231pub type Owned = Base<'static>;
232
233impl Base<'_> {
234    /// Converts [`Self`] into [`Owned`].
235    pub fn into_owned(self) -> Owned {
236        Owned::builder()
237            .secret(self.secret.into_owned())
238            .algorithm(self.algorithm)
239            .digits(self.digits)
240            .build()
241    }
242}