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