Skip to main content

authnz_totp/
lib.rs

1//! This library permits the creation of 2FA authentification tokens per TOTP, the verification of said tokens, with configurable time skew, validity time of each token, algorithm and number of digits! Default features are kept as low-dependency as possible to ensure small binaries and short compilation time
2//!
3//! Be aware that some authenticator apps will accept the `SHA256`
4//! and `SHA512` algorithms but silently fallback to `SHA1` which will
5//! make the `check()` function fail due to mismatched algorithms.
6//!
7//! Use the `SHA1` algorithm to avoid this problem.
8//!
9//! # Examples
10//!
11//! ```rust
12//! # #[cfg(feature = "otpauth")] {
13//! use authnz_totp::{Algorithm, TOTP, Secret};
14//! use std::time::SystemTime;
15//!
16//! let totp = TOTP::new(
17//!     Algorithm::SHA1,
18//!     6,
19//!     1,
20//!     30,
21//!     Secret::Raw("TestSecretSuperSecret".as_bytes().to_vec()).to_bytes().unwrap(),
22//!     Some("Github".to_string()),
23//!     "constantoine@github.com".to_string(),
24//! ).unwrap();
25//! let token = totp.generate_current().unwrap();
26//! println!("{}", token);
27//! # }
28//! ```
29//!
30//! ```rust
31//! # #[cfg(feature = "qr")] {
32//! use authnz_totp::{Algorithm, TOTP};
33//!
34//! let totp = TOTP::new(
35//!     Algorithm::SHA1,
36//!     6,
37//!     1,
38//!     30,
39//!     "supersecret_topsecret".as_bytes().to_vec(),
40//!     Some("Github".to_string()),
41//!     "constantoine@github.com".to_string(),
42//! ).unwrap();
43//! let url = totp.get_url();
44//! println!("{}", url);
45//! let code = totp.get_qr_base64().unwrap();
46//! println!("{}", code);
47//! # }
48//! ```
49
50// enable `doc_cfg` feature for `docs.rs`.
51#![cfg_attr(docsrs, feature(doc_cfg))]
52
53mod custom_providers;
54mod rfc;
55mod secret;
56mod url_error;
57
58#[cfg(feature = "qr")]
59pub use qrcodegen_image;
60
61pub use rfc::{Rfc6238, Rfc6238Error};
62pub use secret::{Secret, SecretParseError};
63pub use url_error::TotpUrlError;
64
65use constant_time_eq::constant_time_eq;
66
67#[cfg(feature = "serde_support")]
68use serde::{Deserialize, Serialize};
69
70use core::fmt;
71
72#[cfg(feature = "otpauth")]
73use url::{Host, Url};
74
75use hmac::Mac;
76use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};
77
78type HmacSha1 = hmac::Hmac<sha1::Sha1>;
79type HmacSha256 = hmac::Hmac<sha2::Sha256>;
80type HmacSha512 = hmac::Hmac<sha2::Sha512>;
81
82/// Alphabet for Steam tokens.
83#[cfg(feature = "steam")]
84const STEAM_CHARS: &str = "23456789BCDFGHJKMNPQRTVWXY";
85
86/// Algorithm enum holds the three standards algorithms for TOTP as per the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A)
87#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
88#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
89pub enum Algorithm {
90  #[default]
91  SHA1,
92  SHA256,
93  SHA512,
94  #[cfg(feature = "steam")]
95  #[cfg_attr(docsrs, doc(cfg(feature = "steam")))]
96  /// Steam TOTP token algorithm
97  Steam,
98}
99
100impl fmt::Display for Algorithm {
101  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102    match self {
103      Algorithm::SHA1 => f.write_str("SHA1"),
104      Algorithm::SHA256 => f.write_str("SHA256"),
105      Algorithm::SHA512 => f.write_str("SHA512"),
106      #[cfg(feature = "steam")]
107      Algorithm::Steam => f.write_str("SHA1"),
108    }
109  }
110}
111
112impl Algorithm {
113  fn hash<D>(mut digest: D, data: &[u8]) -> Vec<u8>
114  where
115    D: Mac,
116  {
117    digest.update(data);
118    digest.finalize().into_bytes().to_vec()
119  }
120
121  fn sign(&self, key: &[u8], data: &[u8]) -> Vec<u8> {
122    match self {
123      Algorithm::SHA1 => Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data),
124      Algorithm::SHA256 => Algorithm::hash(HmacSha256::new_from_slice(key).unwrap(), data),
125      Algorithm::SHA512 => Algorithm::hash(HmacSha512::new_from_slice(key).unwrap(), data),
126      #[cfg(feature = "steam")]
127      Algorithm::Steam => Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data),
128    }
129  }
130}
131
132fn system_time() -> Result<u64, SystemTimeError> {
133  let t = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
134  Ok(t)
135}
136
137/// TOTP holds informations as to how to generate an auth code and validate it. Its [secret](struct.TOTP.html#structfield.secret) field is sensitive data, treat it accordingly
138#[derive(Debug, Clone)]
139#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
140#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
141pub struct TOTP {
142  /// SHA-1 is the most widespread algorithm used, and for totp pursposes, SHA-1 hash collisions are [not a problem](https://tools.ietf.org/html/rfc4226#appendix-B.2) as HMAC-SHA-1 is not impacted. It's also the main one cited in [rfc-6238](https://tools.ietf.org/html/rfc6238#section-3) even though the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A) permits the use of SHA-1, SHA-256 and SHA-512. Not all clients support other algorithms then SHA-1
143  #[cfg_attr(feature = "zeroize", zeroize(skip))]
144  pub algorithm: Algorithm,
145  /// The number of digits composing the auth code. Per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-5.3), this can oscilate between 6 and 8 digits
146  pub digits: usize,
147  /// Number of steps allowed as network delay. 1 would mean one step before current step and one step after are valids. The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 1. Anything more is sketchy, and anyone recommending more is, by definition, ugly and stupid
148  pub skew: u8,
149  /// Duration in seconds of a step. The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 30 seconds
150  pub step: u64,
151  /// As per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-4) the secret should come from a strong source, most likely a CSPRNG. It should be at least 128 bits, but 160 are recommended
152  ///
153  /// non-encoded value
154  pub secret: Vec<u8>,
155  #[cfg(feature = "otpauth")]
156  #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
157  /// The "Github" part of "Github:constantoine@github.com". Must not contain a colon `:`
158  /// For example, the name of your service/website.
159  /// Not mandatory, but strongly recommended!
160  pub issuer: Option<String>,
161  #[cfg(feature = "otpauth")]
162  #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
163  /// The "constantoine@github.com" part of "Github:constantoine@github.com". Must not contain a colon `:`
164  /// For example, the name of your user's account.
165  pub account_name: String,
166}
167
168impl PartialEq for TOTP {
169  /// Will not check for issuer and account_name equality
170  /// As they aren't taken in account for token generation/token checking
171  fn eq(&self, other: &Self) -> bool {
172    if self.algorithm != other.algorithm {
173      return false;
174    }
175    if self.digits != other.digits {
176      return false;
177    }
178    if self.skew != other.skew {
179      return false;
180    }
181    if self.step != other.step {
182      return false;
183    }
184    constant_time_eq(self.secret.as_ref(), other.secret.as_ref())
185  }
186}
187
188#[cfg(feature = "otpauth")]
189impl core::fmt::Display for TOTP {
190  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191    write!(
192      f,
193      "digits: {}; step: {}; alg: {}; issuer: <{}>({})",
194      self.digits,
195      self.step,
196      self.algorithm,
197      self.issuer.clone().unwrap_or_else(|| "None".to_string()),
198      self.account_name
199    )
200  }
201}
202
203#[cfg(not(feature = "otpauth"))]
204impl core::fmt::Display for TOTP {
205  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206    write!(f, "digits: {}; step: {}; alg: {}", self.digits, self.step, self.algorithm,)
207  }
208}
209
210#[cfg(all(feature = "gen_secret", not(feature = "otpauth")))]
211// because `Default` is implemented regardless of `otpauth` feature we don't specify it here
212#[cfg_attr(docsrs, doc(cfg(feature = "gen_secret")))]
213impl Default for TOTP {
214  fn default() -> Self {
215    return TOTP::new(Algorithm::SHA1, 6, 1, 30, Secret::generate_secret().to_bytes().unwrap()).unwrap();
216  }
217}
218
219#[cfg(all(feature = "gen_secret", feature = "otpauth"))]
220#[cfg_attr(docsrs, doc(cfg(feature = "gen_secret")))]
221impl Default for TOTP {
222  fn default() -> Self {
223    TOTP::new(
224      Algorithm::SHA1,
225      6,
226      1,
227      30,
228      Secret::generate_secret().to_bytes().unwrap(),
229      None,
230      "".to_string(),
231    )
232    .unwrap()
233  }
234}
235
236impl TOTP {
237  #[cfg(feature = "otpauth")]
238  /// Will create a new instance of TOTP with given parameters. See [the doc](struct.TOTP.html#fields) for reference as to how to choose those values
239  ///
240  /// # Description
241  /// * `secret`: expect a non-encoded value, to pass in base32 string use `Secret::Encoded(String)`
242  /// * `digits`: MUST be between 6 & 8
243  /// * `secret`: Must have bitsize of at least 128
244  /// * `account_name`: Must not contain `:`
245  /// * `issuer`: Must not contain `:`
246  ///
247  /// # Example
248  ///
249  /// ```rust
250  /// use authnz_totp::{Secret, TOTP, Algorithm};
251  /// let secret = Secret::Encoded("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG".to_string());
252  /// let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap(), None, "".to_string()).unwrap();
253  /// ```
254  ///
255  /// # Errors
256  ///
257  /// Will return an error if the `digit` or `secret` size is invalid or if `issuer` or `label` contain the character ':'
258  pub fn new(
259    algorithm: Algorithm,
260    digits: usize,
261    skew: u8,
262    step: u64,
263    secret: Vec<u8>,
264    issuer: Option<String>,
265    account_name: String,
266  ) -> Result<TOTP, TotpUrlError> {
267    crate::rfc::assert_digits(&digits)?;
268    crate::rfc::assert_secret_length(secret.as_ref())?;
269    if issuer.is_some() && issuer.as_ref().unwrap().contains(':') {
270      return Err(TotpUrlError::Issuer(issuer.as_ref().unwrap().to_string()));
271    }
272    if account_name.contains(':') {
273      return Err(TotpUrlError::AccountName(account_name));
274    }
275    Ok(Self::new_unchecked(algorithm, digits, skew, step, secret, issuer, account_name))
276  }
277
278  #[cfg(feature = "otpauth")]
279  /// Will create a new instance of TOTP with given parameters. See [the doc](struct.TOTP.html#fields) for reference as to how to choose those values. This is unchecked and does not check the `digits` and `secret` size
280  ///
281  /// # Description
282  /// * `secret`: expect a non-encoded value, to pass in base32 string use `Secret::Encoded(String)`
283  ///
284  /// # Example
285  ///
286  /// ```rust
287  /// use authnz_totp::{Secret, TOTP, Algorithm};
288  /// let secret = Secret::Encoded("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG".to_string());
289  /// let totp = TOTP::new_unchecked(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap(), None, "".to_string());
290  /// ```
291  pub fn new_unchecked(
292    algorithm: Algorithm,
293    digits: usize,
294    skew: u8,
295    step: u64,
296    secret: Vec<u8>,
297    issuer: Option<String>,
298    account_name: String,
299  ) -> TOTP {
300    TOTP {
301      algorithm,
302      digits,
303      skew,
304      step,
305      secret,
306      issuer,
307      account_name,
308    }
309  }
310
311  #[cfg(not(feature = "otpauth"))]
312  /// Will create a new instance of TOTP with given parameters. See [the doc](struct.TOTP.html#fields) for reference as to how to choose those values
313  ///
314  /// # Description
315  /// * `secret`: expect a non-encoded value, to pass in base32 string use `Secret::Encoded(String)`
316  /// * `digits`: MUST be between 6 & 8
317  /// * `secret`: Must have bitsize of at least 128
318  ///
319  /// # Example
320  ///
321  /// ```rust
322  /// use authnz_totp::{Secret, TOTP, Algorithm};
323  /// let secret = Secret::Encoded("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG".to_string());
324  /// let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap()).unwrap();
325  /// ```
326  ///
327  /// # Errors
328  ///
329  /// Will return an error if the `digit` or `secret` size is invalid
330  pub fn new(algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: Vec<u8>) -> Result<TOTP, TotpUrlError> {
331    crate::rfc::assert_digits(&digits)?;
332    crate::rfc::assert_secret_length(secret.as_ref())?;
333    Ok(Self::new_unchecked(algorithm, digits, skew, step, secret))
334  }
335
336  #[cfg(not(feature = "otpauth"))]
337  /// Will create a new instance of TOTP with given parameters. See [the doc](struct.TOTP.html#fields) for reference as to how to choose those values. This is unchecked and does not check the `digits` and `secret` size
338  ///
339  /// # Description
340  /// * `secret`: expect a non-encoded value, to pass in base32 string use `Secret::Encoded(String)`
341  ///
342  /// # Example
343  ///
344  /// ```rust
345  /// use authnz_totp::{Secret, TOTP, Algorithm};
346  /// let secret = Secret::Encoded("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG".to_string());
347  /// let totp = TOTP::new_unchecked(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap());
348  /// ```
349  pub fn new_unchecked(algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: Vec<u8>) -> TOTP {
350    TOTP {
351      algorithm,
352      digits,
353      skew,
354      step,
355      secret,
356    }
357  }
358
359  /// Will create a new instance of TOTP from the given [Rfc6238](struct.Rfc6238.html) struct
360  ///
361  /// # Errors
362  ///
363  /// Will return an error in case issuer or label contain the character ':'
364  pub fn from_rfc6238(rfc: Rfc6238) -> Result<TOTP, TotpUrlError> {
365    TOTP::try_from(rfc)
366  }
367
368  /// Will sign the given timestamp
369  pub fn sign(&self, time: u64) -> Vec<u8> {
370    self.algorithm.sign(self.secret.as_ref(), (time / self.step).to_be_bytes().as_ref())
371  }
372
373  /// Will generate a token given the provided timestamp in seconds
374  pub fn generate(&self, time: u64) -> String {
375    let result: &[u8] = &self.sign(time);
376    let offset = (result.last().unwrap() & 15) as usize;
377    #[allow(unused_mut)]
378    let mut result = u32::from_be_bytes(result[offset..offset + 4].try_into().unwrap()) & 0x7fff_ffff;
379
380    match self.algorithm {
381      Algorithm::SHA1 | Algorithm::SHA256 | Algorithm::SHA512 => {
382        format!("{1:00$}", self.digits, result % 10_u32.pow(self.digits as u32))
383      }
384      #[cfg(feature = "steam")]
385      Algorithm::Steam => (0..self.digits)
386        .map(|_| {
387          let c = STEAM_CHARS.chars().nth(result as usize % STEAM_CHARS.len()).unwrap();
388          result /= STEAM_CHARS.len() as u32;
389          c
390        })
391        .collect(),
392    }
393  }
394
395  /// Returns the timestamp of the first second for the next step
396  /// given the provided timestamp in seconds
397  pub fn next_step(&self, time: u64) -> u64 {
398    let step = time / self.step;
399
400    (step + 1) * self.step
401  }
402
403  /// Returns the timestamp of the first second of the next step
404  /// According to system time
405  pub fn next_step_current(&self) -> Result<u64, SystemTimeError> {
406    let t = system_time()?;
407    Ok(self.next_step(t))
408  }
409
410  /// Give the ttl (in seconds) of the current token
411  pub fn ttl(&self) -> Result<u64, SystemTimeError> {
412    let t = system_time()?;
413    Ok(self.step - (t % self.step))
414  }
415
416  /// Generate a token from the current system time
417  pub fn generate_current(&self) -> Result<String, SystemTimeError> {
418    let t = system_time()?;
419    Ok(self.generate(t))
420  }
421
422  /// Will check if token is valid given the provided timestamp in seconds, accounting [skew](struct.TOTP.html#structfield.skew)
423  pub fn check(&self, token: &str, time: u64) -> bool {
424    let basestep = time / self.step - (self.skew as u64);
425    for i in 0..(self.skew as u16) * 2 + 1 {
426      let step_time = (basestep + (i as u64)) * self.step;
427
428      if constant_time_eq(self.generate(step_time).as_bytes(), token.as_bytes()) {
429        return true;
430      }
431    }
432    false
433  }
434
435  /// Will check if token is valid by current system time, accounting [skew](struct.TOTP.html#structfield.skew)
436  pub fn check_current(&self, token: &str) -> Result<bool, SystemTimeError> {
437    let t = system_time()?;
438    Ok(self.check(token, t))
439  }
440
441  /// Will return the base32 representation of the secret, which might be useful when users want to manually add the secret to their authenticator
442  pub fn get_secret_base32(&self) -> String {
443    base32::encode(base32::Alphabet::RFC4648 { padding: false }, self.secret.as_ref())
444  }
445
446  /// Generate a TOTP from the standard otpauth URL
447  #[cfg(feature = "otpauth")]
448  #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
449  pub fn from_url<S: AsRef<str>>(url: S) -> Result<TOTP, TotpUrlError> {
450    let (algorithm, digits, skew, step, secret, issuer, account_name) = Self::parts_from_url(url)?;
451    TOTP::new(algorithm, digits, skew, step, secret, issuer, account_name)
452  }
453
454  /// Generate a TOTP from the standard otpauth URL, using `TOTP::new_unchecked` internally
455  #[cfg(feature = "otpauth")]
456  #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
457  pub fn from_url_unchecked<S: AsRef<str>>(url: S) -> Result<TOTP, TotpUrlError> {
458    let (algorithm, digits, skew, step, secret, issuer, account_name) = Self::parts_from_url(url)?;
459    Ok(TOTP::new_unchecked(algorithm, digits, skew, step, secret, issuer, account_name))
460  }
461
462  /// Parse the TOTP parts from the standard otpauth URL
463  #[cfg(feature = "otpauth")]
464  fn parts_from_url<S: AsRef<str>>(url: S) -> Result<(Algorithm, usize, u8, u64, Vec<u8>, Option<String>, String), TotpUrlError> {
465    let mut algorithm = Algorithm::SHA1;
466    let mut digits = 6;
467    let mut step = 30;
468    let mut secret = Vec::new();
469    let mut issuer: Option<String> = None;
470    let mut account_name: String;
471
472    let url = Url::parse(url.as_ref()).map_err(TotpUrlError::Url)?;
473    if url.scheme() != "otpauth" {
474      return Err(TotpUrlError::Scheme(url.scheme().to_string()));
475    }
476    match url.host() {
477      Some(Host::Domain("totp")) => {}
478      #[cfg(feature = "steam")]
479      Some(Host::Domain("steam")) => {
480        algorithm = Algorithm::Steam;
481      }
482      _ => {
483        return Err(TotpUrlError::Host(url.host().unwrap().to_string()));
484      }
485    }
486
487    let path = url.path().trim_start_matches('/');
488    let path = urlencoding::decode(path)
489      .map_err(|_| TotpUrlError::AccountNameDecoding(path.to_string()))?
490      .to_string();
491    if path.contains(':') {
492      let parts = path.split_once(':').unwrap();
493      issuer = Some(parts.0.to_owned());
494      account_name = parts.1.to_owned();
495    } else {
496      account_name = path;
497    }
498
499    account_name = urlencoding::decode(account_name.as_str())
500      .map_err(|_| TotpUrlError::AccountName(account_name.to_string()))?
501      .to_string();
502
503    for (key, value) in url.query_pairs() {
504      match key.as_ref() {
505        #[cfg(feature = "steam")]
506        "algorithm" if algorithm == Algorithm::Steam => {
507          // Do not change used algorithm if this is Steam
508        }
509        "algorithm" => {
510          algorithm = match value.as_ref() {
511            "SHA1" => Algorithm::SHA1,
512            "SHA256" => Algorithm::SHA256,
513            "SHA512" => Algorithm::SHA512,
514            _ => return Err(TotpUrlError::Algorithm(value.to_string())),
515          }
516        }
517        "digits" => {
518          digits = value.parse::<usize>().map_err(|_| TotpUrlError::Digits(value.to_string()))?;
519        }
520        "period" => {
521          step = value.parse::<u64>().map_err(|_| TotpUrlError::Step(value.to_string()))?;
522        }
523        "secret" => {
524          secret = base32::decode(base32::Alphabet::RFC4648 { padding: false }, value.as_ref())
525            .ok_or_else(|| TotpUrlError::Secret(value.to_string()))?;
526        }
527        #[cfg(feature = "steam")]
528        "issuer" if value.to_lowercase() == "steam" => {
529          algorithm = Algorithm::Steam;
530          digits = 5;
531          issuer = Some(value.into());
532        }
533        "issuer" => {
534          let param_issuer: String = value.into();
535          if issuer.is_some() && param_issuer.as_str() != issuer.as_ref().unwrap() {
536            return Err(TotpUrlError::IssuerMistmatch(issuer.as_ref().unwrap().to_string(), param_issuer));
537          }
538          issuer = Some(param_issuer);
539          #[cfg(feature = "steam")]
540          if issuer == Some("Steam".into()) {
541            algorithm = Algorithm::Steam;
542          }
543        }
544        _ => {}
545      }
546    }
547
548    #[cfg(feature = "steam")]
549    if algorithm == Algorithm::Steam {
550      digits = 5;
551      step = 30;
552      issuer = Some("Steam".into());
553    }
554
555    if secret.is_empty() {
556      return Err(TotpUrlError::Secret("".to_string()));
557    }
558
559    Ok((algorithm, digits, 1, step, secret, issuer, account_name))
560  }
561
562  /// Will generate a standard URL used to automatically add TOTP auths. Usually used with qr codes
563  ///
564  /// Label and issuer will be URL-encoded if needed be
565  /// Secret will be base 32'd without padding, as per RFC.
566  #[cfg(feature = "otpauth")]
567  #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
568  pub fn get_url(&self) -> String {
569    #[allow(unused_mut)]
570    let mut host = "totp";
571    #[cfg(feature = "steam")]
572    if self.algorithm == Algorithm::Steam {
573      host = "steam";
574    }
575    let account_name = urlencoding::encode(self.account_name.as_str()).to_string();
576    let mut params = vec![format!("secret={}", self.get_secret_base32())];
577    if self.digits != 6 {
578      params.push(format!("digits={}", self.digits));
579    }
580    if self.algorithm != Algorithm::SHA1 {
581      params.push(format!("algorithm={}", self.algorithm));
582    }
583    let label = if let Some(issuer) = &self.issuer {
584      let issuer = urlencoding::encode(issuer);
585      params.push(format!("issuer={}", issuer));
586      format!("{}:{}", issuer, account_name)
587    } else {
588      account_name
589    };
590    if self.step != 30 {
591      params.push(format!("period={}", self.step));
592    }
593
594    format!("otpauth://{}/{}?{}", host, label, params.join("&"))
595  }
596}
597
598#[cfg(feature = "qr")]
599#[cfg_attr(docsrs, doc(cfg(feature = "qr")))]
600impl TOTP {
601  #[deprecated(
602    since = "5.3.0",
603    note = "get_qr was forcing the use of png as a base64. Use get_qr_base64 or get_qr_png instead. Will disappear in 6.0."
604  )]
605  pub fn get_qr(&self) -> Result<String, String> {
606    let url = self.get_url();
607    qrcodegen_image::draw_base64(&url)
608  }
609
610  /// Will return a qrcode to automatically add a TOTP as a base64 string. Needs feature `qr` to be enabled!
611  /// Result will be in the form of a string containing a base64-encoded png, which you can embed in HTML without needing
612  /// To store the png as a file.
613  ///
614  /// # Errors
615  ///
616  /// This will return an error in case the URL gets too long to encode into a QR code.
617  /// This would require the get_url method to generate an url bigger than 2000 characters,
618  /// Which would be too long for some browsers anyway.
619  ///
620  /// It will also return an error in case it can't encode the qr into a png.
621  /// This shouldn't happen unless either the qrcode library returns malformed data, or the image library doesn't encode the data correctly
622  pub fn get_qr_base64(&self) -> Result<String, String> {
623    let url = self.get_url();
624    qrcodegen_image::draw_base64(&url)
625  }
626
627  /// Will return a qrcode to automatically add a TOTP as a byte array. Needs feature `qr` to be enabled!
628  /// Result will be in the form of a png file as bytes.
629  ///
630  /// # Errors
631  ///
632  /// This will return an error in case the URL gets too long to encode into a QR code.
633  /// This would require the get_url method to generate an url bigger than 2000 characters,
634  /// Which would be too long for some browsers anyway.
635  ///
636  /// It will also return an error in case it can't encode the qr into a png.
637  /// This shouldn't happen unless either the qrcode library returns malformed data, or the image library doesn't encode the data correctly
638  pub fn get_qr_png(&self) -> Result<Vec<u8>, String> {
639    let url = self.get_url();
640    qrcodegen_image::draw_png(&url)
641  }
642}
643
644#[cfg(test)]
645mod tests {
646  use super::*;
647
648  #[test]
649  #[cfg(feature = "gen_secret")]
650  fn default_values() {
651    let totp = TOTP::default();
652    assert_eq!(totp.algorithm, Algorithm::SHA1);
653    assert_eq!(totp.digits, 6);
654    assert_eq!(totp.skew, 1);
655    assert_eq!(totp.step, 30)
656  }
657
658  #[test]
659  #[cfg(feature = "otpauth")]
660  fn new_wrong_issuer() {
661    let totp = TOTP::new(
662      Algorithm::SHA1,
663      6,
664      1,
665      1,
666      "TestSecretSuperSecret".as_bytes().to_vec(),
667      Some("Github:".to_string()),
668      "constantoine@github.com".to_string(),
669    );
670    assert!(totp.is_err());
671    assert!(matches!(totp.unwrap_err(), TotpUrlError::Issuer(_)));
672  }
673
674  #[test]
675  #[cfg(feature = "otpauth")]
676  fn new_wrong_account_name() {
677    let totp = TOTP::new(
678      Algorithm::SHA1,
679      6,
680      1,
681      1,
682      "TestSecretSuperSecret".as_bytes().to_vec(),
683      Some("Github".to_string()),
684      "constantoine:github.com".to_string(),
685    );
686    assert!(totp.is_err());
687    assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_)));
688  }
689
690  #[test]
691  #[cfg(feature = "otpauth")]
692  fn new_wrong_account_name_no_issuer() {
693    let totp = TOTP::new(
694      Algorithm::SHA1,
695      6,
696      1,
697      1,
698      "TestSecretSuperSecret".as_bytes().to_vec(),
699      None,
700      "constantoine:github.com".to_string(),
701    );
702    assert!(totp.is_err());
703    assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_)));
704  }
705
706  #[test]
707  #[cfg(feature = "otpauth")]
708  fn comparison_ok() {
709    let reference = TOTP::new(
710      Algorithm::SHA1,
711      6,
712      1,
713      1,
714      "TestSecretSuperSecret".as_bytes().to_vec(),
715      Some("Github".to_string()),
716      "constantoine@github.com".to_string(),
717    )
718    .unwrap();
719    let test = TOTP::new(
720      Algorithm::SHA1,
721      6,
722      1,
723      1,
724      "TestSecretSuperSecret".as_bytes().to_vec(),
725      Some("Github".to_string()),
726      "constantoine@github.com".to_string(),
727    )
728    .unwrap();
729    assert_eq!(reference, test);
730  }
731
732  #[test]
733  #[cfg(not(feature = "otpauth"))]
734  fn comparison_different_algo() {
735    let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
736    let test = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
737    assert_ne!(reference, test);
738  }
739
740  #[test]
741  #[cfg(not(feature = "otpauth"))]
742  fn comparison_different_digits() {
743    let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
744    let test = TOTP::new(Algorithm::SHA1, 8, 1, 1, "TestSecretSuperSecret".into()).unwrap();
745    assert_ne!(reference, test);
746  }
747
748  #[test]
749  #[cfg(not(feature = "otpauth"))]
750  fn comparison_different_skew() {
751    let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
752    let test = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret".into()).unwrap();
753    assert_ne!(reference, test);
754  }
755
756  #[test]
757  #[cfg(not(feature = "otpauth"))]
758  fn comparison_different_step() {
759    let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
760    let test = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".into()).unwrap();
761    assert_ne!(reference, test);
762  }
763
764  #[test]
765  #[cfg(not(feature = "otpauth"))]
766  fn comparison_different_secret() {
767    let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
768    let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretDifferentSecret".into()).unwrap();
769    assert_ne!(reference, test);
770  }
771
772  #[test]
773  #[cfg(feature = "otpauth")]
774  fn url_for_secret_matches_sha1_without_issuer() {
775    let totp = TOTP::new(
776      Algorithm::SHA1,
777      6,
778      1,
779      30,
780      "TestSecretSuperSecret".as_bytes().to_vec(),
781      None,
782      "constantoine@github.com".to_string(),
783    )
784    .unwrap();
785    let url = totp.get_url();
786    assert_eq!(
787      url.as_str(),
788      "otpauth://totp/constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ"
789    );
790  }
791
792  #[test]
793  #[cfg(feature = "otpauth")]
794  fn url_for_secret_matches_sha1() {
795    let totp = TOTP::new(
796      Algorithm::SHA1,
797      6,
798      1,
799      30,
800      "TestSecretSuperSecret".as_bytes().to_vec(),
801      Some("Github".to_string()),
802      "constantoine@github.com".to_string(),
803    )
804    .unwrap();
805    let url = totp.get_url();
806    assert_eq!(
807      url.as_str(),
808      "otpauth://totp/Github:constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&issuer=Github"
809    );
810  }
811
812  #[test]
813  #[cfg(feature = "otpauth")]
814  fn url_for_secret_matches_sha256() {
815    let totp = TOTP::new(
816      Algorithm::SHA256,
817      6,
818      1,
819      30,
820      "TestSecretSuperSecret".as_bytes().to_vec(),
821      Some("Github".to_string()),
822      "constantoine@github.com".to_string(),
823    )
824    .unwrap();
825    let url = totp.get_url();
826    assert_eq!(
827      url.as_str(),
828      "otpauth://totp/Github:constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&algorithm=SHA256&issuer=Github"
829    );
830  }
831
832  #[test]
833  #[cfg(feature = "otpauth")]
834  fn url_for_secret_matches_sha512() {
835    let totp = TOTP::new(
836      Algorithm::SHA512,
837      6,
838      1,
839      30,
840      "TestSecretSuperSecret".as_bytes().to_vec(),
841      Some("Github".to_string()),
842      "constantoine@github.com".to_string(),
843    )
844    .unwrap();
845    let url = totp.get_url();
846    assert_eq!(
847      url.as_str(),
848      "otpauth://totp/Github:constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&algorithm=SHA512&issuer=Github"
849    );
850  }
851
852  #[test]
853  #[cfg(all(feature = "otpauth", feature = "gen_secret"))]
854  fn ttl() {
855    let secret = Secret::default();
856    let totp_rfc = Rfc6238::with_defaults(secret.to_bytes().unwrap()).unwrap();
857    let totp = TOTP::from_rfc6238(totp_rfc);
858    assert!(totp.is_ok());
859  }
860
861  #[test]
862  #[cfg(feature = "otpauth")]
863  fn ttl_ok() {
864    let totp = TOTP::new(
865      Algorithm::SHA512,
866      6,
867      1,
868      1,
869      "TestSecretSuperSecret".as_bytes().to_vec(),
870      Some("Github".to_string()),
871      "constantoine@github.com".to_string(),
872    )
873    .unwrap();
874    assert!(totp.ttl().is_ok());
875  }
876
877  #[test]
878  #[cfg(not(feature = "otpauth"))]
879  fn returns_base32() {
880    let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
881    assert_eq!(totp.get_secret_base32().as_str(), "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ");
882  }
883
884  #[test]
885  #[cfg(not(feature = "otpauth"))]
886  fn generate_token() {
887    let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
888    assert_eq!(totp.generate(1000).as_str(), "659761");
889  }
890
891  #[test]
892  #[cfg(not(feature = "otpauth"))]
893  fn generate_token_current() {
894    let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
895    let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
896    assert_eq!(totp.generate(time).as_str(), totp.generate_current().unwrap());
897  }
898
899  #[test]
900  #[cfg(not(feature = "otpauth"))]
901  fn generates_token_sha256() {
902    let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
903    assert_eq!(totp.generate(1000).as_str(), "076417");
904  }
905
906  #[test]
907  #[cfg(not(feature = "otpauth"))]
908  fn generates_token_sha512() {
909    let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
910    assert_eq!(totp.generate(1000).as_str(), "473536");
911  }
912
913  #[test]
914  #[cfg(not(feature = "otpauth"))]
915  fn checks_token() {
916    let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret".into()).unwrap();
917    assert!(totp.check("659761", 1000));
918  }
919
920  #[test]
921  #[cfg(not(feature = "otpauth"))]
922  fn checks_token_big_skew() {
923    let totp = TOTP::new(Algorithm::SHA1, 6, 255, 1, "TestSecretSuperSecret".into()).unwrap();
924    assert!(totp.check("659761", 1000));
925  }
926
927  #[test]
928  #[cfg(not(feature = "otpauth"))]
929  fn checks_token_current() {
930    let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret".into()).unwrap();
931    assert!(totp.check_current(&totp.generate_current().unwrap()).unwrap());
932    assert!(!totp.check_current("bogus").unwrap());
933  }
934
935  #[test]
936  #[cfg(not(feature = "otpauth"))]
937  fn checks_token_with_skew() {
938    let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap();
939    assert!(totp.check("174269", 1000) && totp.check("659761", 1000) && totp.check("260393", 1000));
940  }
941
942  #[test]
943  #[cfg(not(feature = "otpauth"))]
944  fn next_step() {
945    let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".into()).unwrap();
946    assert!(totp.next_step(0) == 30);
947    assert!(totp.next_step(29) == 30);
948    assert!(totp.next_step(30) == 60);
949  }
950
951  #[test]
952  #[cfg(not(feature = "otpauth"))]
953  fn next_step_current() {
954    let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".into()).unwrap();
955    let t = system_time().unwrap();
956    assert!(totp.next_step_current().unwrap() == totp.next_step(t));
957  }
958
959  #[test]
960  #[cfg(feature = "otpauth")]
961  fn from_url_err() {
962    assert!(TOTP::from_url("otpauth://hotp/123").is_err());
963    assert!(TOTP::from_url("otpauth://totp/GitHub:test").is_err());
964    assert!(TOTP::from_url("otpauth://totp/GitHub:test:?secret=ABC&digits=8&period=60&algorithm=SHA256").is_err());
965    assert!(
966      TOTP::from_url(
967        "otpauth://totp/Github:constantoine%40github.com?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1"
968      )
969      .is_err()
970    )
971  }
972
973  #[test]
974  #[cfg(feature = "otpauth")]
975  fn from_url_default() {
976    let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap();
977    assert_eq!(
978      totp.secret,
979      base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap()
980    );
981    assert_eq!(totp.algorithm, Algorithm::SHA1);
982    assert_eq!(totp.digits, 6);
983    assert_eq!(totp.skew, 1);
984    assert_eq!(totp.step, 30);
985  }
986
987  #[test]
988  #[cfg(feature = "otpauth")]
989  fn from_url_query() {
990    let totp =
991      TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap();
992    assert_eq!(
993      totp.secret,
994      base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap()
995    );
996    assert_eq!(totp.algorithm, Algorithm::SHA256);
997    assert_eq!(totp.digits, 8);
998    assert_eq!(totp.skew, 1);
999    assert_eq!(totp.step, 60);
1000  }
1001
1002  #[test]
1003  #[cfg(feature = "otpauth")]
1004  fn from_url_query_sha512() {
1005    let totp =
1006      TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA512").unwrap();
1007    assert_eq!(
1008      totp.secret,
1009      base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap()
1010    );
1011    assert_eq!(totp.algorithm, Algorithm::SHA512);
1012    assert_eq!(totp.digits, 8);
1013    assert_eq!(totp.skew, 1);
1014    assert_eq!(totp.step, 60);
1015  }
1016
1017  #[test]
1018  #[cfg(feature = "otpauth")]
1019  fn from_url_to_url() {
1020    let totp = TOTP::from_url(
1021      "otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1",
1022    )
1023    .unwrap();
1024    let totp_bis = TOTP::new(
1025      Algorithm::SHA1,
1026      6,
1027      1,
1028      30,
1029      "TestSecretSuperSecret".as_bytes().to_vec(),
1030      Some("Github".to_string()),
1031      "constantoine@github.com".to_string(),
1032    )
1033    .unwrap();
1034    assert_eq!(totp.get_url(), totp_bis.get_url());
1035  }
1036
1037  #[test]
1038  #[cfg(feature = "otpauth")]
1039  fn from_url_unknown_param() {
1040    let totp =
1041      TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256&foo=bar")
1042        .unwrap();
1043    assert_eq!(
1044      totp.secret,
1045      base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap()
1046    );
1047    assert_eq!(totp.algorithm, Algorithm::SHA256);
1048    assert_eq!(totp.digits, 8);
1049    assert_eq!(totp.skew, 1);
1050    assert_eq!(totp.step, 60);
1051  }
1052
1053  #[test]
1054  #[cfg(feature = "otpauth")]
1055  fn from_url_issuer_special() {
1056    let totp = TOTP::from_url("otpauth://totp/Github%40:constantoine%40github.com?issuer=Github%40&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap();
1057    let totp_bis = TOTP::new(
1058      Algorithm::SHA1,
1059      6,
1060      1,
1061      30,
1062      "TestSecretSuperSecret".as_bytes().to_vec(),
1063      Some("Github@".to_string()),
1064      "constantoine@github.com".to_string(),
1065    )
1066    .unwrap();
1067    assert_eq!(totp.get_url(), totp_bis.get_url());
1068    assert_eq!(totp.issuer.as_ref().unwrap(), "Github@");
1069  }
1070
1071  #[test]
1072  #[cfg(feature = "otpauth")]
1073  fn from_url_account_name_issuer() {
1074    let totp =
1075      TOTP::from_url("otpauth://totp/Github:constantoine?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1")
1076        .unwrap();
1077    let totp_bis = TOTP::new(
1078      Algorithm::SHA1,
1079      6,
1080      1,
1081      30,
1082      "TestSecretSuperSecret".as_bytes().to_vec(),
1083      Some("Github".to_string()),
1084      "constantoine".to_string(),
1085    )
1086    .unwrap();
1087    assert_eq!(totp.get_url(), totp_bis.get_url());
1088    assert_eq!(totp.account_name, "constantoine");
1089    assert_eq!(totp.issuer.as_ref().unwrap(), "Github");
1090  }
1091
1092  #[test]
1093  #[cfg(feature = "otpauth")]
1094  fn from_url_account_name_issuer_encoded() {
1095    let totp = TOTP::from_url(
1096      "otpauth://totp/Github%3Aconstantoine?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1",
1097    )
1098    .unwrap();
1099    let totp_bis = TOTP::new(
1100      Algorithm::SHA1,
1101      6,
1102      1,
1103      30,
1104      "TestSecretSuperSecret".as_bytes().to_vec(),
1105      Some("Github".to_string()),
1106      "constantoine".to_string(),
1107    )
1108    .unwrap();
1109    assert_eq!(totp.get_url(), totp_bis.get_url());
1110    assert_eq!(totp.account_name, "constantoine");
1111    assert_eq!(totp.issuer.as_ref().unwrap(), "Github");
1112  }
1113
1114  #[test]
1115  #[cfg(feature = "otpauth")]
1116  fn from_url_query_issuer() {
1117    let totp = TOTP::from_url(
1118      "otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256",
1119    )
1120    .unwrap();
1121    assert_eq!(
1122      totp.secret,
1123      base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap()
1124    );
1125    assert_eq!(totp.algorithm, Algorithm::SHA256);
1126    assert_eq!(totp.digits, 8);
1127    assert_eq!(totp.skew, 1);
1128    assert_eq!(totp.step, 60);
1129    assert_eq!(totp.issuer.as_ref().unwrap(), "GitHub");
1130  }
1131
1132  #[test]
1133  #[cfg(feature = "otpauth")]
1134  fn from_url_wrong_scheme() {
1135    let totp =
1136      TOTP::from_url("http://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256");
1137    assert!(totp.is_err());
1138    let err = totp.unwrap_err();
1139    assert!(matches!(err, TotpUrlError::Scheme(_)));
1140  }
1141
1142  #[test]
1143  #[cfg(feature = "otpauth")]
1144  fn from_url_wrong_algo() {
1145    let totp =
1146      TOTP::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=MD5");
1147    assert!(totp.is_err());
1148    let err = totp.unwrap_err();
1149    assert!(matches!(err, TotpUrlError::Algorithm(_)));
1150  }
1151
1152  #[test]
1153  #[cfg(feature = "otpauth")]
1154  fn from_url_query_different_issuers() {
1155    let totp = TOTP::from_url(
1156      "otpauth://totp/GitHub:test?issuer=Gitlab&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256",
1157    );
1158    assert!(totp.is_err());
1159    assert!(matches!(totp.unwrap_err(), TotpUrlError::IssuerMistmatch(_, _)));
1160  }
1161
1162  #[test]
1163  #[cfg(feature = "qr")]
1164  fn generates_qr() {
1165    use qrcodegen_image::qrcodegen;
1166    use sha2::{Digest, Sha512};
1167
1168    let totp = TOTP::new(
1169      Algorithm::SHA1,
1170      6,
1171      1,
1172      30,
1173      "TestSecretSuperSecret".as_bytes().to_vec(),
1174      Some("Github".to_string()),
1175      "constantoine@github.com".to_string(),
1176    )
1177    .unwrap();
1178    let url = totp.get_url();
1179    let qr = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Medium).expect("could not generate qr");
1180    let data = qrcodegen_image::draw_canvas(qr).into_raw();
1181
1182    // Create hash from image
1183    let hash_digest = Sha512::digest(data);
1184    assert_eq!(
1185      format!("{:x}", hash_digest).as_str(),
1186      "fbb0804f1e4f4c689d22292c52b95f0783b01b4319973c0c50dd28af23dbbbe663dce4eb05a7959086d9092341cb9f103ec5a9af4a973867944e34c063145328"
1187    );
1188  }
1189
1190  #[test]
1191  #[cfg(feature = "qr")]
1192  fn generates_qr_base64_ok() {
1193    let totp = TOTP::new(
1194      Algorithm::SHA1,
1195      6,
1196      1,
1197      1,
1198      "TestSecretSuperSecret".as_bytes().to_vec(),
1199      Some("Github".to_string()),
1200      "constantoine@github.com".to_string(),
1201    )
1202    .unwrap();
1203    let qr = totp.get_qr_base64();
1204    assert!(qr.is_ok());
1205  }
1206
1207  #[test]
1208  #[cfg(feature = "qr")]
1209  fn generates_qr_png_ok() {
1210    let totp = TOTP::new(
1211      Algorithm::SHA1,
1212      6,
1213      1,
1214      1,
1215      "TestSecretSuperSecret".as_bytes().to_vec(),
1216      Some("Github".to_string()),
1217      "constantoine@github.com".to_string(),
1218    )
1219    .unwrap();
1220    let qr = totp.get_qr_png();
1221    assert!(qr.is_ok());
1222  }
1223}