totp_sos/
lib.rs

1#![deny(missing_docs)]
2#![forbid(unsafe_code)]
3
4//! This library was a fork of `totp-rs` that is no longer maintained, instead all the features 
5//! we needed have been merged into `totp-rs` so use that crate instead.
6
7mod error;
8
9#[cfg(feature = "qr")]
10pub mod qr;
11
12pub use error::Error;
13
14/// Result type for the TOTP library.
15pub type Result<T> = std::result::Result<T, Error>;
16
17use constant_time_eq::constant_time_eq;
18use hmac::Mac;
19use std::{
20    fmt,
21    time::{SystemTime, UNIX_EPOCH},
22};
23use url::{Host, Url};
24
25#[cfg(feature = "serde")]
26use serde::{Deserialize, Serialize};
27
28type HmacSha1 = hmac::Hmac<sha1::Sha1>;
29type HmacSha256 = hmac::Hmac<sha2::Sha256>;
30type HmacSha512 = hmac::Hmac<sha2::Sha512>;
31
32/// Algorithm enum holds the three standards algorithms for TOTP as per the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A)
33#[derive(Debug, Copy, Clone, Eq, PartialEq)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35pub enum Algorithm {
36    /// The SHA1 algorithm.
37    SHA1,
38    /// The SHA256 algorithm.
39    SHA256,
40    /// The SHA512 algorithm.
41    SHA512,
42}
43
44impl std::default::Default for Algorithm {
45    fn default() -> Self {
46        Algorithm::SHA1
47    }
48}
49
50impl fmt::Display for Algorithm {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Algorithm::SHA1 => f.write_str("SHA1"),
54            Algorithm::SHA256 => f.write_str("SHA256"),
55            Algorithm::SHA512 => f.write_str("SHA512"),
56        }
57    }
58}
59
60impl Algorithm {
61    fn hash<D>(mut digest: D, data: &[u8]) -> Vec<u8>
62    where
63        D: Mac,
64    {
65        digest.update(data);
66        digest.finalize().into_bytes().to_vec()
67    }
68
69    fn sign(&self, key: &[u8], data: &[u8]) -> Vec<u8> {
70        match self {
71            Algorithm::SHA1 => {
72                Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data)
73            }
74            Algorithm::SHA256 => Algorithm::hash(
75                HmacSha256::new_from_slice(key).unwrap(),
76                data,
77            ),
78            Algorithm::SHA512 => Algorithm::hash(
79                HmacSha512::new_from_slice(key).unwrap(),
80                data,
81            ),
82        }
83    }
84}
85
86fn system_time() -> Result<u64> {
87    let t = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
88    Ok(t)
89}
90
91/// 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
92#[derive(Debug, Clone)]
93#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
94#[cfg_attr(
95    feature = "zeroize",
96    derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop)
97)]
98pub struct TOTP {
99    /// SHA-1 is the most widespread algorithm used, 
100    /// and for totp pursposes, SHA-1 hash collisions 
101    /// are [not a problem](https://tools.ietf.org/html/rfc4226#appendix-B.2) 
102    /// as HMAC-SHA-1 is not impacted. It's also the main 
103    /// one cited in [rfc-6238](https://tools.ietf.org/html/rfc6238#section-3) 
104    /// even though the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A) 
105    /// permits the use of SHA-1, SHA-256 and SHA-512.
106    ///
107    /// Not all clients support other algorithms then SHA-1
108    #[cfg_attr(feature = "zeroize", zeroize(skip))]
109    pub algorithm: Algorithm,
110
111    /// The number of digits for the auth code.
112    ///
113    /// Per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-5.3), 
114    /// this can be in the range between 6 and 8 digits
115    pub digits: usize,
116
117    /// Number of steps allowed as network delay.
118    ///
119    /// One would mean one step before current step and 
120    /// one step after are valid.
121    ///
122    /// The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 1. Anything more is sketchy and should not be used.
123    pub skew: u8,
124
125    /// Duration in seconds of a step.
126    ///
127    /// The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 30 seconds
128    pub step: u64,
129
130    /// As per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-4) 
131    /// the secret should come from a strong source, most likely a CSPRNG.
132    ///
133    /// It should be at least 128 bits, but 160 are recommended.
134    pub secret: Vec<u8>,
135
136    /// The account name, typically either an email address or username.
137    ///
138    /// The "mock@example.com" part of "Github:mock@example.com".
139    ///
140    /// Must not contain a colon `:`.
141    pub account_name: String,
142
143    /// The name of your service/website.
144    ///
145    /// The "Github" part of "Github:mock@example.com".
146    ///
147    /// Must not contain a colon `:`.
148    pub issuer: Option<String>,
149}
150
151impl PartialEq for TOTP {
152    fn eq(&self, other: &Self) -> bool {
153        constant_time_eq(self.secret.as_ref(), other.secret.as_ref())
154    }
155}
156
157impl TOTP {
158    /// Create a new instance of TOTP with given parameters.
159    ///
160    /// See [the doc](struct.TOTP.html#fields) for reference as to how to choose those values.
161    ///
162    /// * `digits`: MUST be between 6 & 8
163    /// * `secret`: Must have bitsize of at least 128
164    /// * `account_name`: Must not contain `:`
165    /// * `issuer`: Must not contain `:`
166    ///
167    pub fn new(
168        algorithm: Algorithm,
169        digits: usize,
170        skew: u8,
171        step: u64,
172        secret: Vec<u8>,
173        account_name: String,
174        issuer: Option<String>,
175    ) -> Result<TOTP> {
176        if !(6..=8).contains(&digits) {
177            return Err(Error::InvalidDigits(digits));
178        }
179
180        if secret.len() < 16 {
181            return Err(Error::SecretTooSmall(secret.len() * 8));
182        }
183
184        if account_name.contains(':') {
185            return Err(Error::AccountName(account_name));
186        }
187
188        if let Some(issuer) = &issuer {
189            if issuer.contains(':') {
190                return Err(Error::Issuer(issuer.to_string()));
191            }
192        }
193
194        Ok(TOTP {
195            algorithm,
196            digits,
197            skew,
198            step,
199            secret,
200            account_name,
201            issuer,
202        })
203    }
204
205    /// Sign the given timestamp
206    pub fn sign(&self, time: u64) -> Vec<u8> {
207        self.algorithm.sign(
208            self.secret.as_ref(),
209            (time / self.step).to_be_bytes().as_ref(),
210        )
211    }
212
213    /// Generate a token given the provided timestamp in seconds
214    pub fn generate(&self, time: u64) -> String {
215        let result: &[u8] = &self.sign(time);
216        let offset = (result.last().unwrap() & 15) as usize;
217        let result = u32::from_be_bytes(
218            result[offset..offset + 4].try_into().unwrap(),
219        ) & 0x7fff_ffff;
220        format!(
221            "{1:00$}",
222            self.digits,
223            result % 10_u32.pow(self.digits as u32)
224        )
225    }
226
227    /// Returns the timestamp of the first second for the next step
228    /// given the provided timestamp in seconds
229    pub fn next_step(&self, time: u64) -> u64 {
230        let step = time / self.step;
231        (step + 1) * self.step
232    }
233
234    /// Returns the timestamp of the first second of the next step
235    /// According to system time
236    pub fn next_step_current(&self) -> Result<u64> {
237        let t = system_time()?;
238        Ok(self.next_step(t))
239    }
240
241    /// Give the ttl (in seconds) of the current token
242    pub fn ttl(&self) -> Result<u64> {
243        let t = system_time()?;
244        Ok(self.step - (t % self.step))
245    }
246
247    /// Generate a token from the current system time
248    pub fn generate_current(&self) -> Result<String> {
249        let t = system_time()?;
250        Ok(self.generate(t))
251    }
252
253    /// Check if token is valid given the provided timestamp 
254    /// in seconds, accounting [skew](struct.TOTP.html#structfield.skew)
255    pub fn check(&self, token: &str, time: u64) -> bool {
256        let basestep = time / self.step - (self.skew as u64);
257        for i in 0..self.skew * 2 + 1 {
258            let step_time = (basestep + (i as u64)) * (self.step as u64);
259
260            if constant_time_eq(
261                self.generate(step_time).as_bytes(),
262                token.as_bytes(),
263            ) {
264                return true;
265            }
266        }
267        false
268    }
269
270    /// Check if token is valid by current system time, 
271    /// accounting [skew](struct.TOTP.html#structfield.skew).
272    pub fn check_current(&self, token: &str) -> Result<bool> {
273        let t = system_time()?;
274        Ok(self.check(token, t))
275    }
276
277    /// Return the base32 representation of the secret, which 
278    /// might be useful when users want to manually add the 
279    /// secret to their authenticator.
280    pub fn to_secret_base32(&self) -> String {
281        base32::encode(
282            base32::Alphabet::RFC4648 { padding: false },
283            self.secret.as_ref(),
284        )
285    }
286
287    /// Convert a base32 secret into a TOTP.
288    ///
289    /// The account name is the empty string and the issuer is None; 
290    /// so you should set them explicitly after decoding the secret bytes.
291    pub fn from_secret_base32<S: AsRef<str>>(secret: S) -> Result<TOTP> {
292        let buffer = base32::decode(
293            base32::Alphabet::RFC4648 { padding: false },
294            secret.as_ref(),
295        )
296        .ok_or(Error::Secret(secret.as_ref().to_string()))?;
297
298        TOTP::new(Algorithm::SHA1, 6, 1, 30, buffer, String::new(), None)
299    }
300
301    /// Generate a TOTP from the standard otpauth URL
302    pub fn from_url<S: AsRef<str>>(url: S) -> Result<TOTP> {
303        let url = Url::parse(url.as_ref())?;
304
305        if url.scheme() != "otpauth" {
306            return Err(Error::Scheme(url.scheme().to_string()));
307        }
308        if url.host() != Some(Host::Domain("totp")) {
309            return Err(Error::Host(url.host().unwrap().to_string()));
310        }
311
312        let mut algorithm = Algorithm::SHA1;
313        let mut digits = 6;
314        let mut step = 30;
315        let mut secret = Vec::new();
316        let mut account_name: String;
317        let mut issuer: Option<String> = None;
318
319        let path = url.path().trim_start_matches('/');
320        if path.contains(':') {
321            let parts = path.split_once(':').unwrap();
322            issuer = Some(
323                urlencoding::decode(parts.0.to_owned().as_str())
324                    .map_err(|_| Error::IssuerDecoding(parts.0.to_owned()))?
325                    .to_string(),
326            );
327            account_name = parts.1.trim_start_matches(':').to_owned();
328        } else {
329            account_name = path.to_owned();
330        }
331
332        account_name = urlencoding::decode(account_name.as_str())
333            .map_err(|_| Error::AccountName(account_name.to_string()))?
334            .to_string();
335
336        for (key, value) in url.query_pairs() {
337            match key.as_ref() {
338                "algorithm" => {
339                    algorithm = match value.as_ref() {
340                        "SHA1" => Algorithm::SHA1,
341                        "SHA256" => Algorithm::SHA256,
342                        "SHA512" => Algorithm::SHA512,
343                        _ => return Err(Error::Algorithm(value.to_string())),
344                    }
345                }
346                "digits" => {
347                    digits = value
348                        .parse::<usize>()
349                        .map_err(|_| Error::Digits(value.to_string()))?;
350                }
351                "period" => {
352                    step = value
353                        .parse::<u64>()
354                        .map_err(|_| Error::Step(value.to_string()))?;
355                }
356                "secret" => {
357                    secret = base32::decode(
358                        base32::Alphabet::RFC4648 { padding: false },
359                        value.as_ref(),
360                    )
361                    .ok_or_else(|| Error::Secret(value.to_string()))?;
362                }
363                "issuer" => {
364                    let param_issuer = value
365                        .parse::<String>()
366                        .map_err(|_| Error::Issuer(value.to_string()))?;
367                    if issuer.is_some()
368                        && param_issuer.as_str() != issuer.as_ref().unwrap()
369                    {
370                        return Err(Error::IssuerMismatch(
371                            issuer.as_ref().unwrap().to_string(),
372                            param_issuer,
373                        ));
374                    }
375                    issuer = Some(param_issuer);
376                }
377                _ => {}
378            }
379        }
380
381        if secret.is_empty() {
382            return Err(Error::Secret("".to_string()));
383        }
384
385        TOTP::new(algorithm, digits, 1, step, secret, account_name, issuer)
386    }
387
388    /// Generate a standard URL used to automatically add TOTP auths.
389    ///
390    /// Usually used with a QR code.
391    ///
392    /// Label and issuer will be URL-encoded; the secret will be 
393    /// converted to base32 without padding, as per the RFC.
394    pub fn get_url(&self) -> String {
395        let account_name: String =
396            urlencoding::encode(self.account_name.as_str()).to_string();
397        let mut label: String = format!("{}?", account_name);
398        if self.issuer.is_some() {
399            let issuer: String =
400                urlencoding::encode(self.issuer.as_ref().unwrap().as_str())
401                    .to_string();
402            label = format!("{0}:{1}?issuer={0}&", issuer, account_name);
403        }
404
405        format!(
406            "otpauth://totp/{}secret={}&digits={}&algorithm={}",
407            label,
408            self.to_secret_base32(),
409            self.digits,
410            self.algorithm,
411        )
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn new_wrong_issuer() {
421        let totp = TOTP::new(
422            Algorithm::SHA1,
423            6,
424            1,
425            1,
426            "TestSecretSuperSecret".as_bytes().to_vec(),
427            "mock@example.com".to_string(),
428            Some("Github:".to_string()),
429        );
430        assert!(totp.is_err());
431        assert!(matches!(totp.unwrap_err(), Error::Issuer(_)));
432    }
433
434    #[test]
435    fn new_wrong_account_name() {
436        let totp = TOTP::new(
437            Algorithm::SHA1,
438            6,
439            1,
440            1,
441            "TestSecretSuperSecret".as_bytes().to_vec(),
442            "mock:example.com".to_string(),
443            Some("Github".to_string()),
444        );
445        assert!(totp.is_err());
446        assert!(matches!(totp.unwrap_err(), Error::AccountName(_)));
447    }
448
449    #[test]
450    fn new_wrong_account_name_no_issuer() {
451        let totp = TOTP::new(
452            Algorithm::SHA1,
453            6,
454            1,
455            1,
456            "TestSecretSuperSecret".as_bytes().to_vec(),
457            "mock:example.com".to_string(),
458            None,
459        );
460        assert!(totp.is_err());
461        assert!(matches!(totp.unwrap_err(), Error::AccountName(_)));
462    }
463
464    #[test]
465    fn comparison_ok() {
466        let reference = TOTP::new(
467            Algorithm::SHA1,
468            6,
469            1,
470            1,
471            "TestSecretSuperSecret".as_bytes().to_vec(),
472            "mock@example.com".to_string(),
473            Some("Github".to_string()),
474        )
475        .unwrap();
476        let test = TOTP::new(
477            Algorithm::SHA1,
478            6,
479            1,
480            1,
481            "TestSecretSuperSecret".as_bytes().to_vec(),
482            "mock@example.com".to_string(),
483            Some("Github".to_string()),
484        )
485        .unwrap();
486        assert_eq!(reference, test);
487    }
488
489    #[test]
490    fn url_for_secret_matches_sha1_without_issuer() {
491        let totp = TOTP::new(
492            Algorithm::SHA1,
493            6,
494            1,
495            1,
496            "TestSecretSuperSecret".as_bytes().to_vec(),
497            "mock@example.com".to_string(),
498            None,
499        )
500        .unwrap();
501        let url = totp.get_url();
502        assert_eq!(url.as_str(), "otpauth://totp/mock%40example.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1");
503    }
504
505    #[test]
506    fn url_for_secret_matches_sha1() {
507        let totp = TOTP::new(
508            Algorithm::SHA1,
509            6,
510            1,
511            1,
512            "TestSecretSuperSecret".as_bytes().to_vec(),
513            "mock@example.com".to_string(),
514            Some("Github".to_string()),
515        )
516        .unwrap();
517        let url = totp.get_url();
518        assert_eq!(url.as_str(), "otpauth://totp/Github:mock%40example.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1");
519    }
520
521    #[test]
522    fn url_for_secret_matches_sha256() {
523        let totp = TOTP::new(
524            Algorithm::SHA256,
525            6,
526            1,
527            1,
528            "TestSecretSuperSecret".as_bytes().to_vec(),
529            "mock@example.com".to_string(),
530            Some("Github".to_string()),
531        )
532        .unwrap();
533        let url = totp.get_url();
534        assert_eq!(url.as_str(), "otpauth://totp/Github:mock%40example.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA256");
535    }
536
537    #[test]
538    fn url_for_secret_matches_sha512() {
539        let totp = TOTP::new(
540            Algorithm::SHA512,
541            6,
542            1,
543            1,
544            "TestSecretSuperSecret".as_bytes().to_vec(),
545            "mock@example.com".to_string(),
546            Some("Github".to_string()),
547        )
548        .unwrap();
549        let url = totp.get_url();
550        assert_eq!(url.as_str(), "otpauth://totp/Github:mock%40example.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA512");
551    }
552
553    #[test]
554    fn ttl_ok() {
555        let totp = TOTP::new(
556            Algorithm::SHA512,
557            6,
558            1,
559            1,
560            "TestSecretSuperSecret".as_bytes().to_vec(),
561            "mock@example.com".to_string(),
562            Some("Github".to_string()),
563        )
564        .unwrap();
565        assert!(totp.ttl().is_ok());
566    }
567
568    #[test]
569    fn returns_base32() {
570        let totp = TOTP::new(
571            Algorithm::SHA1,
572            6,
573            1,
574            1,
575            "TestSecretSuperSecret".as_bytes().to_vec(),
576            "mock@example.com".to_string(),
577            None,
578        )
579        .unwrap();
580        assert_eq!(
581            totp.to_secret_base32().as_str(),
582            "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ"
583        );
584    }
585
586    #[test]
587    fn generate_token() {
588        let totp = TOTP::new(
589            Algorithm::SHA1,
590            6,
591            1,
592            1,
593            "TestSecretSuperSecret".as_bytes().to_vec(),
594            "mock@example.com".to_string(),
595            None,
596        )
597        .unwrap();
598        assert_eq!(totp.generate(1000).as_str(), "659761");
599    }
600
601    #[test]
602    fn generate_token_current() {
603        let totp = TOTP::new(
604            Algorithm::SHA1,
605            6,
606            1,
607            1,
608            "TestSecretSuperSecret".as_bytes().to_vec(),
609            "mock@example.com".to_string(),
610            None,
611        )
612        .unwrap();
613        let time = SystemTime::now()
614            .duration_since(SystemTime::UNIX_EPOCH)
615            .unwrap()
616            .as_secs();
617        assert_eq!(
618            totp.generate(time).as_str(),
619            totp.generate_current().unwrap()
620        );
621    }
622
623    #[test]
624    fn generates_token_sha256() {
625        let totp = TOTP::new(
626            Algorithm::SHA256,
627            6,
628            1,
629            1,
630            "TestSecretSuperSecret".as_bytes().to_vec(),
631            "mock@example.com".to_string(),
632            None,
633        )
634        .unwrap();
635        assert_eq!(totp.generate(1000).as_str(), "076417");
636    }
637
638    #[test]
639    fn generates_token_sha512() {
640        let totp = TOTP::new(
641            Algorithm::SHA512,
642            6,
643            1,
644            1,
645            "TestSecretSuperSecret".as_bytes().to_vec(),
646            "mock@example.com".to_string(),
647            None,
648        )
649        .unwrap();
650        assert_eq!(totp.generate(1000).as_str(), "473536");
651    }
652
653    #[test]
654    fn checks_token() {
655        let totp = TOTP::new(
656            Algorithm::SHA1,
657            6,
658            0,
659            1,
660            "TestSecretSuperSecret".as_bytes().to_vec(),
661            "mock@example.com".to_string(),
662            None,
663        )
664        .unwrap();
665        assert!(totp.check("659761", 1000));
666    }
667
668    #[test]
669    fn checks_token_current() {
670        let totp = TOTP::new(
671            Algorithm::SHA1,
672            6,
673            0,
674            1,
675            "TestSecretSuperSecret".as_bytes().to_vec(),
676            "mock@example.com".to_string(),
677            None,
678        )
679        .unwrap();
680        assert!(totp
681            .check_current(&totp.generate_current().unwrap())
682            .unwrap());
683        assert!(!totp.check_current("bogus").unwrap());
684    }
685
686    #[test]
687    fn checks_token_with_skew() {
688        let totp = TOTP::new(
689            Algorithm::SHA1,
690            6,
691            1,
692            1,
693            "TestSecretSuperSecret".as_bytes().to_vec(),
694            "mock@example.com".to_string(),
695            None,
696        )
697        .unwrap();
698        assert!(
699            totp.check("174269", 1000)
700                && totp.check("659761", 1000)
701                && totp.check("260393", 1000)
702        );
703    }
704
705    #[test]
706    fn next_step() {
707        let totp = TOTP::new(
708            Algorithm::SHA1,
709            6,
710            1,
711            30,
712            "TestSecretSuperSecret".as_bytes().to_vec(),
713            "mock@example.com".to_string(),
714            Some("Mock Service".to_string()),
715        )
716        .unwrap();
717        assert!(totp.next_step(0) == 30);
718        assert!(totp.next_step(29) == 30);
719        assert!(totp.next_step(30) == 60);
720    }
721
722    #[test]
723    fn from_url_err() {
724        assert!(TOTP::from_url("otpauth://hotp/123").is_err());
725        assert!(TOTP::from_url("otpauth://totp/GitHub:test").is_err());
726        assert!(TOTP::from_url(
727            "otpauth://totp/GitHub:test:?secret=ABC&digits=8&period=60&algorithm=SHA256"
728        )
729        .is_err());
730        assert!(TOTP::from_url("otpauth://totp/Github:mock%40example.com?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").is_err())
731    }
732
733    #[test]
734    fn from_url_default() {
735        let totp = TOTP::from_url(
736            "otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ",
737        )
738        .unwrap();
739        assert_eq!(
740            totp.secret,
741            base32::decode(
742                base32::Alphabet::RFC4648 { padding: false },
743                "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ"
744            )
745            .unwrap()
746        );
747        assert_eq!(totp.algorithm, Algorithm::SHA1);
748        assert_eq!(totp.digits, 6);
749        assert_eq!(totp.skew, 1);
750        assert_eq!(totp.step, 30);
751    }
752
753    #[test]
754    fn from_url_query() {
755        let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap();
756        assert_eq!(
757            totp.secret,
758            base32::decode(
759                base32::Alphabet::RFC4648 { padding: false },
760                "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ"
761            )
762            .unwrap()
763        );
764        assert_eq!(totp.algorithm, Algorithm::SHA256);
765        assert_eq!(totp.digits, 8);
766        assert_eq!(totp.skew, 1);
767        assert_eq!(totp.step, 60);
768    }
769
770    #[test]
771    fn from_url_query_sha512() {
772        let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA512").unwrap();
773        assert_eq!(
774            totp.secret,
775            base32::decode(
776                base32::Alphabet::RFC4648 { padding: false },
777                "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ"
778            )
779            .unwrap()
780        );
781        assert_eq!(totp.algorithm, Algorithm::SHA512);
782        assert_eq!(totp.digits, 8);
783        assert_eq!(totp.skew, 1);
784        assert_eq!(totp.step, 60);
785    }
786
787    #[test]
788    fn from_url_to_url() {
789        let totp = TOTP::from_url("otpauth://totp/Github:mock%40example.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap();
790        let totp_bis = TOTP::new(
791            Algorithm::SHA1,
792            6,
793            1,
794            1,
795            "TestSecretSuperSecret".as_bytes().to_vec(),
796            "mock@example.com".to_string(),
797            Some("Github".to_string()),
798        )
799        .unwrap();
800        assert_eq!(totp.get_url(), totp_bis.get_url());
801    }
802
803    #[test]
804    fn from_url_unknown_param() {
805        let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256&foo=bar").unwrap();
806        assert_eq!(
807            totp.secret,
808            base32::decode(
809                base32::Alphabet::RFC4648 { padding: false },
810                "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ"
811            )
812            .unwrap()
813        );
814        assert_eq!(totp.algorithm, Algorithm::SHA256);
815        assert_eq!(totp.digits, 8);
816        assert_eq!(totp.skew, 1);
817        assert_eq!(totp.step, 60);
818    }
819
820    #[test]
821    fn from_url_issuer_special() {
822        let totp = TOTP::from_url("otpauth://totp/Github%40:mock%40example.com?issuer=Github%40&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap();
823        let totp_bis = TOTP::new(
824            Algorithm::SHA1,
825            6,
826            1,
827            1,
828            "TestSecretSuperSecret".as_bytes().to_vec(),
829            "mock@example.com".to_string(),
830            Some("Github@".to_string()),
831        )
832        .unwrap();
833        assert_eq!(totp.get_url(), totp_bis.get_url());
834        assert_eq!(totp.issuer.as_ref().unwrap(), "Github@");
835    }
836
837    #[test]
838    fn from_url_query_issuer() {
839        let totp = TOTP::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap();
840        assert_eq!(
841            totp.secret,
842            base32::decode(
843                base32::Alphabet::RFC4648 { padding: false },
844                "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ"
845            )
846            .unwrap()
847        );
848        assert_eq!(totp.algorithm, Algorithm::SHA256);
849        assert_eq!(totp.digits, 8);
850        assert_eq!(totp.skew, 1);
851        assert_eq!(totp.step, 60);
852        assert_eq!(totp.issuer.as_ref().unwrap(), "GitHub");
853    }
854
855    #[test]
856    fn from_url_wrong_scheme() {
857        let totp = TOTP::from_url("http://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256");
858        assert!(totp.is_err());
859        let err = totp.unwrap_err();
860        assert!(matches!(err, Error::Scheme(_)));
861    }
862
863    #[test]
864    fn from_url_wrong_algo() {
865        let totp = TOTP::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=MD5");
866        assert!(totp.is_err());
867        let err = totp.unwrap_err();
868        assert!(matches!(err, Error::Algorithm(_)));
869    }
870
871    #[test]
872    fn from_url_query_different_issuers() {
873        let totp = TOTP::from_url("otpauth://totp/GitHub:test?issuer=Gitlab&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256");
874        assert!(totp.is_err());
875        assert!(matches!(totp.unwrap_err(), Error::IssuerMismatch(_, _)));
876    }
877}