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