1#![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#[cfg(feature = "steam")]
84const STEAM_CHARS: &str = "23456789BCDFGHJKMNPQRTVWXY";
85
86#[derive(Debug, Copy, Clone, Eq, PartialEq)]
88#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
89pub enum Algorithm {
90 SHA1,
93 SHA256,
96 SHA512,
99 #[cfg(feature = "steam")]
100 #[cfg_attr(docsrs, doc(cfg(feature = "steam")))]
101 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#[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 #[cfg_attr(feature = "zeroize", zeroize(skip))]
155 pub algorithm: Algorithm,
156 pub digits: usize,
158 pub skew: u8,
160 pub step: u64,
162 pub secret: Vec<u8>,
166 #[cfg(feature = "otpauth")]
167 #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
168 pub issuer: Option<String>,
172 #[cfg(feature = "otpauth")]
173 #[cfg_attr(docsrs, doc(cfg(feature = "otpauth")))]
174 pub account_name: String,
177}
178
179impl PartialEq for TOTP {
180 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#[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 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 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 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 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 pub fn from_rfc6238(rfc: Rfc6238) -> Result<TOTP, TotpUrlError> {
407 TOTP::try_from(rfc)
408 }
409
410 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 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 pub fn next_step(&self, time: u64) -> u64 {
449 let step = time / self.step;
450
451 (step + 1) * self.step
452 }
453
454 pub fn next_step_current(&self) -> Result<u64, SystemTimeError> {
457 let t = system_time()?;
458 Ok(self.next_step(t))
459 }
460
461 pub fn ttl(&self) -> Result<u64, SystemTimeError> {
463 let t = system_time()?;
464 Ok(self.step - (t % self.step))
465 }
466
467 pub fn generate_current(&self) -> Result<String, SystemTimeError> {
469 let t = system_time()?;
470 Ok(self.generate(t))
471 }
472
473 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 pub fn check_current(&self, token: &str) -> Result<bool, SystemTimeError> {
488 let t = system_time()?;
489 Ok(self.check(token, t))
490 }
491
492 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 #[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 #[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 #[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 }
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 #[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 pub fn get_qr_base64(&self) -> Result<String, String> {
699 let url = self.get_url();
700 qrcodegen_image::draw_base64(&url)
701 }
702
703 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 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}