1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Builder)]
28#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
29pub struct Base<'b> {
30 pub secret: Secret<'b>,
32 #[builder(default)]
34 #[cfg_attr(feature = "serde", serde(default))]
35 pub algorithm: Algorithm,
36 #[builder(default)]
38 #[cfg_attr(feature = "serde", serde(default))]
39 pub digits: Digits,
40}
41
42pub const MASK: u32 = 0x7FFF_FFFF;
44
45pub const HALF_BYTE: u8 = 0xF;
47
48impl Base<'_> {
49 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 pub fn generate_string(&self, input: u64) -> String {
77 self.digits.string(self.generate(input))
78 }
79
80 pub fn verify(&self, input: u64, code: u32) -> bool {
82 self.generate(input) == code
83 }
84
85 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#[cfg(feature = "auth")]
98pub const SECRET: &str = "secret";
99
100#[cfg(feature = "auth")]
102pub const ALGORITHM: &str = "algorithm";
103
104#[cfg(feature = "auth")]
106pub const DIGITS: &str = "digits";
107
108#[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#[cfg(feature = "auth")]
118#[derive(Debug, Error, Diagnostic)]
119#[error(transparent)]
120#[diagnostic(transparent)]
121pub enum ErrorSource {
122 SecretNotFound(#[from] SecretNotFoundError),
124 Secret(#[from] secret::core::Error),
126 Algorithm(#[from] algorithm::Error),
128 Digits(#[from] digits::ParseError),
130}
131
132#[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 #[source]
143 #[diagnostic_source]
144 pub source: ErrorSource,
145}
146
147#[cfg(feature = "auth")]
148impl Error {
149 pub const fn new(source: ErrorSource) -> Self {
151 Self { source }
152 }
153
154 pub fn secret_not_found(error: SecretNotFoundError) -> Self {
156 Self::new(error.into())
157 }
158
159 pub fn new_secret_not_found() -> Self {
161 Self::secret_not_found(SecretNotFoundError)
162 }
163
164 pub fn secret(error: secret::core::Error) -> Self {
166 Self::new(error.into())
167 }
168
169 pub fn algorithm(error: algorithm::Error) -> Self {
171 Self::new(error.into())
172 }
173
174 pub fn digits(error: digits::ParseError) -> Self {
176 Self::new(error.into())
177 }
178}
179
180#[cfg(feature = "auth")]
181impl Base<'_> {
182 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 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
230pub type Owned = Base<'static>;
232
233impl Base<'_> {
234 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}