1use crate::Algorithm;
2use crate::TotpUrlError;
3use crate::TOTP;
4
5#[cfg(feature = "serde_support")]
6use serde::{Deserialize, Serialize};
7
8#[cfg(feature = "zeroize")]
9use zeroize;
10
11#[derive(Debug, Eq, PartialEq)]
13pub enum Rfc6238Error {
14 InvalidDigits(usize),
16 SecretTooSmall(usize),
18}
19
20impl std::error::Error for Rfc6238Error {}
21
22impl std::fmt::Display for Rfc6238Error {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 Rfc6238Error::InvalidDigits(digits) => write!(
26 f,
27 "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. {} digits is not allowed",
28 digits,
29 ),
30 Rfc6238Error::SecretTooSmall(bits) => write!(
31 f,
32 "The length of the shared secret MUST be at least 128 bits. {} bits is not enough",
33 bits,
34 ),
35 }
36 }
37}
38
39pub fn assert_digits(digits: &usize) -> Result<(), Rfc6238Error> {
42 if !(&6..=&8).contains(&digits) {
43 Err(Rfc6238Error::InvalidDigits(*digits))
44 } else {
45 Ok(())
46 }
47}
48
49pub fn assert_secret_length(secret: &[u8]) -> Result<(), Rfc6238Error> {
52 if secret.as_ref().len() < 16 {
53 Err(Rfc6238Error::SecretTooSmall(secret.as_ref().len() * 8))
54 } else {
55 Ok(())
56 }
57}
58
59#[derive(Debug, Clone)]
75#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
76#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
77pub struct Rfc6238 {
78 #[cfg_attr(feature = "zeroize", zeroize(skip))]
80 algorithm: Algorithm,
81 digits: usize,
83 skew: u8,
85 step: u64,
87 secret: Vec<u8>,
89 #[cfg(feature = "otpauth")]
90 #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
91 issuer: Option<String>,
95 #[cfg(feature = "otpauth")]
96 #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
97 account_name: String,
100}
101
102impl Rfc6238 {
103 #[cfg(feature = "otpauth")]
111 #[allow(unused_mut)]
112 pub fn new(
113 digits: usize,
114 mut secret: Vec<u8>,
115 issuer: Option<String>,
116 account_name: String,
117 ) -> Result<Rfc6238, Rfc6238Error> {
118 let validate = || {
119 assert_digits(&digits)?;
120 assert_secret_length(secret.as_ref())?;
121
122 Ok(())
126 };
127
128 if let Err(e) = validate() {
129 #[cfg(feature = "zeroize")]
130 zeroize::Zeroize::zeroize(&mut secret);
131
132 return Err(e);
133 }
134
135 Ok(Rfc6238 {
136 algorithm: Algorithm::SHA1,
137 digits,
138 skew: 1,
139 step: 30,
140 secret,
141 issuer,
142 account_name,
143 })
144 }
145 #[cfg(not(feature = "otpauth"))]
146 #[allow(unused_mut)]
147 pub fn new(digits: usize, mut secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
148 let validate = || {
149 crate::rfc::assert_digits(&digits)?;
150 crate::rfc::assert_secret_length(secret.as_ref())?;
151 Ok(())
152 };
153
154 if let Err(e) = validate() {
155 #[cfg(feature = "zeroize")]
156 zeroize::Zeroize::zeroize(&mut secret);
157
158 return Err(e);
159 }
160
161 Ok(Rfc6238 {
162 algorithm: Algorithm::SHA1,
163 digits,
164 skew: 1,
165 step: 30,
166 secret,
167 })
168 }
169
170 #[cfg(feature = "otpauth")]
179 pub fn with_defaults(secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
180 Rfc6238::new(6, secret, Some("".to_string()), "".to_string())
181 }
182
183 #[cfg(not(feature = "otpauth"))]
184 pub fn with_defaults(secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
185 Rfc6238::new(6, secret)
186 }
187
188 pub fn digits(&mut self, value: usize) -> Result<(), Rfc6238Error> {
190 assert_digits(&value)?;
191 self.digits = value;
192 Ok(())
193 }
194
195 #[cfg(feature = "otpauth")]
196 #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
197 pub fn issuer(&mut self, value: String) {
199 self.issuer = Some(value);
200 }
201
202 #[cfg(feature = "otpauth")]
203 #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
204 pub fn account_name(&mut self, value: String) {
206 self.account_name = value;
207 }
208}
209
210#[cfg(not(feature = "otpauth"))]
211impl TryFrom<Rfc6238> for TOTP {
212 type Error = TotpUrlError;
213
214 fn try_from(mut rfc: Rfc6238) -> Result<Self, Self::Error> {
216 TOTP::new(
217 rfc.algorithm,
218 rfc.digits,
219 rfc.skew,
220 rfc.step,
221 std::mem::take(&mut rfc.secret),
222 )
223 }
224}
225
226#[cfg(feature = "otpauth")]
227impl TryFrom<Rfc6238> for TOTP {
228 type Error = TotpUrlError;
229
230 fn try_from(mut rfc: Rfc6238) -> Result<Self, Self::Error> {
232 TOTP::new(
233 rfc.algorithm,
234 rfc.digits,
235 rfc.skew,
236 rfc.step,
237 std::mem::take(&mut rfc.secret),
238 std::mem::take(&mut rfc.issuer),
239 std::mem::take(&mut rfc.account_name),
240 )
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 #[cfg(feature = "otpauth")]
247 use crate::TotpUrlError;
248
249 use super::{Rfc6238, TOTP};
250
251 #[cfg(not(feature = "otpauth"))]
252 use super::Rfc6238Error;
253
254 #[cfg(not(feature = "otpauth"))]
255 use crate::Secret;
256
257 const GOOD_SECRET: &str = "01234567890123456789";
258 #[cfg(feature = "otpauth")]
259 const ISSUER: Option<&str> = None;
260 #[cfg(feature = "otpauth")]
261 const ACCOUNT: &str = "valid-account";
262 #[cfg(feature = "otpauth")]
263 const INVALID_ACCOUNT: &str = ":invalid-account";
264
265 #[test]
266 #[cfg(not(feature = "otpauth"))]
267 fn new_rfc_digits() {
268 for x in 0..=20 {
269 let rfc = Rfc6238::new(x, GOOD_SECRET.into());
270 if !(6..=8).contains(&x) {
271 assert!(rfc.is_err());
272 assert!(matches!(rfc.unwrap_err(), Rfc6238Error::InvalidDigits(_)));
273 } else {
274 assert!(rfc.is_ok());
275 }
276 }
277 }
278
279 #[test]
280 #[cfg(not(feature = "otpauth"))]
281 fn new_rfc_secret() {
282 let mut secret = String::from("");
283 for _ in 0..=20 {
284 secret = format!("{}{}", secret, "0");
285 let rfc = Rfc6238::new(6, secret.as_bytes().to_vec());
286 let rfc_default = Rfc6238::with_defaults(secret.as_bytes().to_vec());
287 if secret.len() < 16 {
288 assert!(rfc.is_err());
289 assert!(matches!(rfc.unwrap_err(), Rfc6238Error::SecretTooSmall(_)));
290 assert!(rfc_default.is_err());
291 assert!(matches!(
292 rfc_default.unwrap_err(),
293 Rfc6238Error::SecretTooSmall(_)
294 ));
295 } else {
296 assert!(rfc.is_ok());
297 assert!(rfc_default.is_ok());
298 }
299 }
300 }
301
302 #[test]
303 #[cfg(not(feature = "otpauth"))]
304 fn rfc_to_totp_ok() {
305 let rfc = Rfc6238::new(8, GOOD_SECRET.into()).unwrap();
306 let totp = TOTP::try_from(rfc);
307 assert!(totp.is_ok());
308 let otp = totp.unwrap();
309 assert_eq!(&otp.secret, GOOD_SECRET.as_bytes());
310 assert_eq!(otp.algorithm, crate::Algorithm::SHA1);
311 assert_eq!(otp.digits, 8);
312 assert_eq!(otp.skew, 1);
313 assert_eq!(otp.step, 30)
314 }
315
316 #[test]
317 #[cfg(not(feature = "otpauth"))]
318 fn rfc_to_totp_ok_2() {
319 let rfc = Rfc6238::with_defaults(
320 Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string())
321 .to_bytes()
322 .unwrap(),
323 )
324 .unwrap();
325 let totp = TOTP::try_from(rfc);
326 assert!(totp.is_ok());
327 let otp = totp.unwrap();
328 assert_eq!(otp.algorithm, crate::Algorithm::SHA1);
329 assert_eq!(otp.digits, 6);
330 assert_eq!(otp.skew, 1);
331 assert_eq!(otp.step, 30)
332 }
333
334 #[test]
335 #[cfg(feature = "otpauth")]
336 fn rfc_to_totp_fail() {
337 let rfc = Rfc6238::new(
338 8,
339 GOOD_SECRET.as_bytes().to_vec(),
340 ISSUER.map(str::to_string),
341 INVALID_ACCOUNT.to_string(),
342 )
343 .unwrap();
344 let totp = TOTP::try_from(rfc);
345 assert!(totp.is_err());
346 assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_)))
347 }
348
349 #[test]
350 #[cfg(feature = "otpauth")]
351 fn rfc_to_totp_ok() {
352 let rfc = Rfc6238::new(
353 8,
354 GOOD_SECRET.as_bytes().to_vec(),
355 ISSUER.map(str::to_string),
356 ACCOUNT.to_string(),
357 )
358 .unwrap();
359 let totp = TOTP::try_from(rfc);
360 assert!(totp.is_ok());
361 }
362
363 #[test]
364 #[cfg(feature = "otpauth")]
365 fn rfc_with_default_set_values() {
366 let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.as_bytes().to_vec()).unwrap();
367 let ok = rfc.digits(8);
368 assert!(ok.is_ok());
369 assert_eq!(rfc.account_name, "");
370 assert_eq!(rfc.issuer, Some("".to_string()));
371 rfc.issuer("Github".to_string());
372 rfc.account_name("constantoine".to_string());
373 assert_eq!(rfc.account_name, "constantoine");
374 assert_eq!(rfc.issuer, Some("Github".to_string()));
375 assert_eq!(rfc.digits, 8)
376 }
377
378 #[test]
379 #[cfg(not(feature = "otpauth"))]
380 fn rfc_with_default_set_values() {
381 let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.as_bytes().to_vec()).unwrap();
382 let fail = rfc.digits(4);
383 assert!(fail.is_err());
384 assert!(matches!(fail.unwrap_err(), Rfc6238Error::InvalidDigits(_)));
385 assert_eq!(rfc.digits, 6);
386 let ok = rfc.digits(8);
387 assert!(ok.is_ok());
388 assert_eq!(rfc.digits, 8)
389 }
390
391 #[test]
392 #[cfg(not(feature = "otpauth"))]
393 fn digits_error() {
394 let error = crate::Rfc6238Error::InvalidDigits(9);
395 assert_eq!(
396 error.to_string(),
397 "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. 9 digits is not allowed".to_string()
398 )
399 }
400
401 #[test]
402 #[cfg(not(feature = "otpauth"))]
403 fn secret_length_error() {
404 let error = Rfc6238Error::SecretTooSmall(120);
405 assert_eq!(
406 error.to_string(),
407 "The length of the shared secret MUST be at least 128 bits. 120 bits is not enough"
408 .to_string()
409 )
410 }
411}