diff --git a/src/lib.rs b/src/lib.rs index 2696ea5..7aee579 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ //! 6, //! 1, //! 30, -//! "supersecret", +//! "supersecret_topsecret", //! Some("Github".to_string()), //! "constantoine@github.com".to_string(), //! ).unwrap(); @@ -34,7 +34,7 @@ //! 6, //! 1, //! 30, -//! "supersecret", +//! "supersecret_topsecret", //! Some("Github".to_string()), //! "constantoine@github.com".to_string(), //! ).unwrap(); @@ -47,6 +47,9 @@ mod rfc; +mod url_error; +use url_error::TotpUrlError; + pub use rfc::{Rfc6238, Rfc6238Error}; use constant_time_eq::constant_time_eq; @@ -60,7 +63,7 @@ use core::fmt; use {base64, image::Luma, qrcodegen}; #[cfg(feature = "otpauth")] -use url::{Host, ParseError, Url}; +use url::{Host, Url}; #[cfg(feature = "otpauth")] use urlencoding; @@ -80,18 +83,18 @@ pub enum Algorithm { SHA512, } +impl std::default::Default for Algorithm { + fn default() -> Self { + Algorithm::SHA1 + } +} + impl fmt::Display for Algorithm { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - return match *self { - Algorithm::SHA1 => { - f.write_str("SHA1") - } - Algorithm::SHA256 => { - f.write_str("SHA256") - } - Algorithm::SHA512 => { - f.write_str("SHA512") - } + match self { + Algorithm::SHA1 => f.write_str("SHA1"), + Algorithm::SHA256 => f.write_str("SHA256"), + Algorithm::SHA512 => f.write_str("SHA512"), } } } @@ -106,7 +109,7 @@ impl Algorithm { } fn sign(&self, key: &[u8], data: &[u8]) -> Vec { - match *self { + match self { Algorithm::SHA1 => Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data), Algorithm::SHA256 => Algorithm::hash(HmacSha256::new_from_slice(key).unwrap(), data), Algorithm::SHA512 => Algorithm::hash(HmacSha512::new_from_slice(key).unwrap(), data), @@ -121,29 +124,6 @@ fn system_time() -> Result { Ok(t) } -#[derive(Debug, Eq, PartialEq)] -pub enum TotpUrlError { - #[cfg(feature = "otpauth")] - Url(ParseError), - Scheme, - Host, - Secret, - Algorithm, - Digits, - Step, - Issuer, - AccountName, -} - -impl From for TotpUrlError { - fn from(e: Rfc6238Error) -> Self { - match e { - Rfc6238Error::InvalidDigits => TotpUrlError::Digits, - Rfc6238Error::SecretTooSmall => TotpUrlError::Secret, - } - } -} - /// 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 #[derive(Debug, Clone)] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] @@ -192,17 +172,21 @@ impl> TOTP { /// /// # Description /// * `digits`: MUST be between 6 & 8 + /// * `secret`: Must have bitsize of at least 128 + /// * `account_name`: Must not contain `:` + /// * `issuer`: Must not contain `:` /// /// # Errors /// /// Will return an error in case issuer or label contain the character ':' pub fn new(algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: T, issuer: Option, account_name: String) -> Result, TotpUrlError> { crate::rfc::assert_digits(&digits)?; + crate::rfc::assert_secret_length(secret.as_ref())?; if issuer.is_some() && issuer.as_ref().unwrap().contains(':') { - return Err(TotpUrlError::Issuer); + return Err(TotpUrlError::Issuer(issuer.as_ref().unwrap().to_string())); } if account_name.contains(':') { - return Err(TotpUrlError::AccountName); + return Err(TotpUrlError::AccountName(account_name)); } Ok(TOTP { algorithm, @@ -240,7 +224,7 @@ impl> TOTP { format!( "{1:00$}", self.digits, - result % (10 as u32).pow(self.digits as u32) + result % 10_u32.pow(self.digits as u32) ) } @@ -303,10 +287,10 @@ impl> TOTP { pub fn from_url>(url: S) -> Result>, TotpUrlError> { let url = Url::parse(url.as_ref()).map_err(|err| TotpUrlError::Url(err))?; if url.scheme() != "otpauth" { - return Err(TotpUrlError::Scheme); + return Err(TotpUrlError::Scheme(url.scheme().to_string())); } if url.host() != Some(Host::Domain("totp")) { - return Err(TotpUrlError::Host); + return Err(TotpUrlError::Host(url.host().unwrap().to_string())); } let mut algorithm = Algorithm::SHA1; @@ -319,13 +303,13 @@ impl> TOTP { let path = url.path().trim_start_matches('/'); if path.contains(':') { let parts = path.split_once(':').unwrap(); - issuer = Some(urlencoding::decode(parts.0.to_owned().as_str()).map_err(|_| TotpUrlError::Issuer)?.to_string()); + issuer = Some(urlencoding::decode(parts.0.to_owned().as_str()).map_err(|_| TotpUrlError::IssuerDecoding(parts.0.to_owned().to_string()))?.to_string()); account_name = parts.1.trim_start_matches(':').to_owned(); } else { account_name = path.to_owned(); } - account_name = urlencoding::decode(account_name.as_str()).map_err(|_| TotpUrlError::AccountName)?.to_string(); + account_name = urlencoding::decode(account_name.as_str()).map_err(|_| TotpUrlError::AccountName(account_name.to_string()))?.to_string(); for (key, value) in url.query_pairs() { match key.as_ref() { @@ -334,24 +318,24 @@ impl> TOTP { "SHA1" => Algorithm::SHA1, "SHA256" => Algorithm::SHA256, "SHA512" => Algorithm::SHA512, - _ => return Err(TotpUrlError::Algorithm), + _ => return Err(TotpUrlError::Algorithm(value.to_string())), } } "digits" => { - digits = value.parse::().map_err(|_| TotpUrlError::Digits)?; + digits = value.parse::().map_err(|_| TotpUrlError::Digits(value.to_string()))?; } "period" => { - step = value.parse::().map_err(|_| TotpUrlError::Step)?; + step = value.parse::().map_err(|_| TotpUrlError::Step(value.to_string()))?; } "secret" => { secret = base32::decode(base32::Alphabet::RFC4648 { padding: false }, value.as_ref()) - .ok_or(TotpUrlError::Secret)?; + .ok_or(TotpUrlError::Secret(value.to_string()))?; } "issuer" => { - let param_issuer = value.parse::().map_err(|_| TotpUrlError::Issuer)?; + let param_issuer = value.parse::().map_err(|_| TotpUrlError::Issuer(value.to_string()))?; if issuer.is_some() && param_issuer.as_str() != issuer.as_ref().unwrap() { - return Err(TotpUrlError::Issuer); + return Err(TotpUrlError::IssuerMistmatch(issuer.as_ref().unwrap().to_string(), param_issuer)); } issuer = Some(param_issuer); } @@ -360,7 +344,7 @@ impl> TOTP { } if secret.is_empty() { - return Err(TotpUrlError::Secret); + return Err(TotpUrlError::Secret("".to_string())); } TOTP::new(algorithm, digits, 1, step, secret, issuer, account_name) @@ -468,114 +452,114 @@ mod tests { #[test] fn new_wrong_issuer() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github:".to_string()), "constantoine@github.com".to_string()); - assert_eq!(totp.is_err(), true); - assert_eq!(totp.unwrap_err(), TotpUrlError::Issuer); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github:".to_string()), "constantoine@github.com".to_string()); + assert!(totp.is_err()); + assert!(matches!(totp.unwrap_err(), TotpUrlError::Issuer(_))); } #[test] fn new_wrong_account_name() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine:github.com".to_string()); - assert_eq!(totp.is_err(), true); - assert_eq!(totp.unwrap_err(), TotpUrlError::AccountName); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine:github.com".to_string()); + assert!(totp.is_err()); + assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_))); } #[test] fn new_wrong_account_name_no_issuer() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", None, "constantoine:github.com".to_string()); - assert_eq!(totp.is_err(), true); - assert_eq!(totp.unwrap_err(), TotpUrlError::AccountName); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", None, "constantoine:github.com".to_string()); + assert!(totp.is_err()); + assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_))); } #[test] fn comparison_ok() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); assert_eq!(reference, test); } #[test] fn comparison_different_algo() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let test = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); assert_ne!(reference, test); } #[test] fn comparison_different_digits() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA1, 8, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let test = TOTP::new(Algorithm::SHA1, 8, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); assert_ne!(reference, test); } #[test] fn comparison_different_skew() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let test = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); assert_ne!(reference, test); } #[test] fn comparison_different_step() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let test = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); assert_ne!(reference, test); } #[test] fn comparison_different_secret() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretL", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretDifferentSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); assert_ne!(reference, test); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha1_without_issuer() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", None, "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", None, "constantoine@github.com".to_string()).unwrap(); let url = totp.get_url(); - assert_eq!(url.as_str(), "otpauth://totp/constantoine%40github.com?secret=KRSXG5CTMVRXEZLU&digits=6&algorithm=SHA1"); + assert_eq!(url.as_str(), "otpauth://totp/constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1"); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha1() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); let url = totp.get_url(); - assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLU&digits=6&algorithm=SHA1"); + assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1"); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha256() { - let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); let url = totp.get_url(); - assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLU&digits=6&algorithm=SHA256"); + assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA256"); } #[test] #[cfg(feature = "otpauth")] fn url_for_secret_matches_sha512() { - let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); let url = totp.get_url(); - assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLU&digits=6&algorithm=SHA512"); + assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA512"); } #[test] fn returns_base32() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - assert_eq!(totp.get_secret_base32().as_str(), "KRSXG5CTMVRXEZLU"); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + assert_eq!(totp.get_secret_base32().as_str(), "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ"); } #[test] fn generate_token() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - assert_eq!(totp.generate(1000).as_str(), "718996"); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + assert_eq!(totp.generate(1000).as_str(), "659761"); } #[test] fn generate_token_current() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); let time = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH).unwrap() .as_secs(); @@ -584,43 +568,40 @@ mod tests { #[test] fn generates_token_sha256() { - let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - assert_eq!(totp.generate(1000).as_str(), "480200"); + let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + assert_eq!(totp.generate(1000).as_str(), "076417"); } #[test] fn generates_token_sha512() { - let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - assert_eq!(totp.generate(1000).as_str(), "850500"); + let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + assert_eq!(totp.generate(1000).as_str(), "473536"); } #[test] fn checks_token() { - let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - assert!(totp.check("718996", 1000)); - assert!(totp.check("712039", 2000)); - assert!(!totp.check("527544", 2000)); - assert!(!totp.check("714250", 2000)); + let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + assert!(totp.check("659761", 1000)); } #[test] fn checks_token_current() { - let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); assert!(totp.check_current(&totp.generate_current().unwrap()).unwrap()); assert!(!totp.check_current("bogus").unwrap()); } #[test] fn checks_token_with_skew() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); assert!( - totp.check("527544", 2000) && totp.check("712039", 2000) && totp.check("714250", 2000) + totp.check("174269", 1000) && totp.check("659761", 1000) && totp.check("260393", 1000) ); } #[test] fn next_step() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); assert!(totp.next_step(0) == 30); assert!(totp.next_step(29) == 30); assert!(totp.next_step(30) == 60); @@ -628,7 +609,7 @@ mod tests { #[test] fn next_step_current() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); let t = system_time().unwrap(); assert!(totp.next_step_current().unwrap() == totp.next_step(t)); } @@ -639,13 +620,15 @@ mod tests { assert!(TOTP::>::from_url("otpauth://hotp/123").is_err()); assert!(TOTP::>::from_url("otpauth://totp/GitHub:test").is_err()); assert!(TOTP::>::from_url("otpauth://totp/GitHub:test:?secret=ABC&digits=8&period=60&algorithm=SHA256").is_err()); + assert!(TOTP::>::from_url("otpauth://totp/Github:constantoine%40github.com?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").is_err()) + } #[test] #[cfg(feature = "otpauth")] fn from_url_default() { - let totp = TOTP::>::from_url("otpauth://totp/GitHub:test?secret=ABC").unwrap(); - assert_eq!(totp.secret, base32::decode(base32::Alphabet::RFC4648 { padding: false }, "ABC").unwrap()); + let totp = TOTP::>::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap(); + assert_eq!(totp.secret, base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap()); assert_eq!(totp.algorithm, Algorithm::SHA1); assert_eq!(totp.digits, 6); assert_eq!(totp.skew, 1); @@ -655,8 +638,8 @@ mod tests { #[test] #[cfg(feature = "otpauth")] fn from_url_query() { - let totp = TOTP::>::from_url("otpauth://totp/GitHub:test?secret=ABC&digits=8&period=60&algorithm=SHA256").unwrap(); - assert_eq!(totp.secret, base32::decode(base32::Alphabet::RFC4648 { padding: false }, "ABC").unwrap()); + let totp = TOTP::>::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap(); + assert_eq!(totp.secret, base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap()); assert_eq!(totp.algorithm, Algorithm::SHA256); assert_eq!(totp.digits, 8); assert_eq!(totp.skew, 1); @@ -666,36 +649,38 @@ mod tests { #[test] #[cfg(feature = "otpauth")] fn from_url_to_url() { - let totp = TOTP::>::from_url("otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLU&digits=6&algorithm=SHA1").unwrap(); - let totp_bis = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::>::from_url("otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); + let totp_bis = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); } #[test] #[cfg(feature = "otpauth")] fn from_url_issuer_special() { - let totp = TOTP::>::from_url("otpauth://totp/Github%40:constantoine%40github.com?issuer=Github%40&secret=KRSXG5CTMVRXEZLU&digits=6&algorithm=SHA1").unwrap(); - let totp_bis = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github@".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::>::from_url("otpauth://totp/Github%40:constantoine%40github.com?issuer=Github%40&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); + let totp_bis = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github@".to_string()), "constantoine@github.com".to_string()).unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); + assert_eq!(totp.issuer.unwrap(), "Github@"); } #[test] #[cfg(feature = "otpauth")] fn from_url_query_issuer() { - let totp = TOTP::>::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=ABC&digits=8&period=60&algorithm=SHA256").unwrap(); - assert_eq!(totp.secret, base32::decode(base32::Alphabet::RFC4648 { padding: false }, "ABC").unwrap()); + let totp = TOTP::>::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap(); + assert_eq!(totp.secret, base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap()); assert_eq!(totp.algorithm, Algorithm::SHA256); assert_eq!(totp.digits, 8); assert_eq!(totp.skew, 1); assert_eq!(totp.step, 60); + assert_eq!(totp.issuer.unwrap(), "GitHub"); } #[test] #[cfg(feature = "otpauth")] fn from_url_query_different_issuers() { - let totp = TOTP::>::from_url("otpauth://totp/GitHub:test?issuer=Gitlab&secret=ABC&digits=8&period=60&algorithm=SHA256"); - assert_eq!(totp.is_err(), true); - assert_eq!(totp.unwrap_err(), TotpUrlError::Issuer); + let totp = TOTP::>::from_url("otpauth://totp/GitHub:test?issuer=Gitlab&secret=TestSecretSuperSecret&digits=8&period=60&algorithm=SHA256"); + assert!(totp.is_err()); + assert!(matches!(totp.unwrap_err(), TotpUrlError::IssuerMistmatch(_, _))); } #[test] @@ -703,14 +688,14 @@ mod tests { fn generates_qr() { use sha1::{Digest, Sha1}; - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); let qr = totp.get_qr().unwrap(); // Create hash from image let hash_digest = Sha1::digest(qr.as_bytes()); assert_eq!( format!("{:x}", hash_digest).as_str(), - "b21a9d4bbb5bd0800bb6bff83a92a2e3314266a5" + "3028f00bf1bd2898ce4d73b234ba087d3c5172f9" ); } } diff --git a/src/rfc.rs b/src/rfc.rs index b34bdaa..808a1e2 100644 --- a/src/rfc.rs +++ b/src/rfc.rs @@ -9,21 +9,23 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Eq, PartialEq)] pub enum Rfc6238Error { /// Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code - InvalidDigits, + InvalidDigits(usize), /// The length of the shared secret MUST be at least 128 bits - SecretTooSmall, + SecretTooSmall(usize), } impl std::fmt::Display for Rfc6238Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Rfc6238Error::InvalidDigits => write!( + Rfc6238Error::InvalidDigits(digits) => write!( f, - "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code" + "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. {} digits is not allowed", + digits, ), - Rfc6238Error::SecretTooSmall => write!( + Rfc6238Error::SecretTooSmall(bits) => write!( f, - "The length of the shared secret MUST be at least 128 bits" + "The length of the shared secret MUST be at least 128 bits. {} bits is not enough", + bits, ), } } @@ -31,7 +33,15 @@ impl std::fmt::Display for Rfc6238Error { pub fn assert_digits(digits: &usize) -> Result<(), Rfc6238Error> { if !(&6..=&8).contains(&digits) { - Err(Rfc6238Error::InvalidDigits) + Err(Rfc6238Error::InvalidDigits(*digits)) + } else { + Ok(()) + } +} + +pub fn assert_secret_length(secret: &[u8]) -> Result<(), Rfc6238Error> { + if secret.as_ref().len() < 16 { + Err(Rfc6238Error::SecretTooSmall(secret.as_ref().len() * 8)) } else { Ok(()) } @@ -91,19 +101,17 @@ impl> Rfc6238 { account_name: String, ) -> Result, Rfc6238Error> { assert_digits(&digits)?; - if secret.as_ref().len() < 16 { - Err(Rfc6238Error::SecretTooSmall) - } else { - Ok(Rfc6238 { - algorithm: Algorithm::SHA1, - digits, - skew: 1, - step: 30, - secret, - issuer, - account_name, - }) - } + assert_secret_length(secret.as_ref())?; + + Ok(Rfc6238 { + algorithm: Algorithm::SHA1, + digits, + skew: 1, + step: 30, + secret, + issuer, + account_name, + }) } /// Create an [rfc-6238](https://tools.ietf.org/html/rfc6238) compliant set of options that can be turned into a [TOTP](struct.TOTP.html), @@ -173,9 +181,9 @@ mod tests { ISSUER.map(str::to_string), ACCOUNT.to_string(), ); - if x < 6 || x > 8 { + if !(6..=8).contains(&x) { assert!(rfc.is_err()); - assert_eq!(rfc.unwrap_err(), Rfc6238Error::InvalidDigits) + assert!(matches!(rfc.unwrap_err(), Rfc6238Error::InvalidDigits(_))); } else { assert!(rfc.is_ok()); } @@ -196,9 +204,9 @@ mod tests { let rfc_default = Rfc6238::with_defaults(secret.clone()); if secret.len() < 16 { assert!(rfc.is_err()); - assert_eq!(rfc.unwrap_err(), Rfc6238Error::SecretTooSmall); + assert!(matches!(rfc.unwrap_err(), Rfc6238Error::SecretTooSmall(_))); assert!(rfc_default.is_err()); - assert_eq!(rfc_default.unwrap_err(), Rfc6238Error::SecretTooSmall); + assert!(matches!(rfc_default.unwrap_err(), Rfc6238Error::SecretTooSmall(_))); } else { assert!(rfc.is_ok()); assert!(rfc_default.is_ok()); @@ -238,7 +246,7 @@ mod tests { .unwrap(); let totp = TOTP::try_from(rfc); assert!(totp.is_err()); - assert_eq!(totp.unwrap_err(), TotpUrlError::AccountName) + assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_))) } #[test] @@ -252,7 +260,7 @@ mod tests { assert_eq!(rfc.account_name, new_account.to_string()); let fail = rfc.digits(4); assert!(fail.is_err()); - assert_eq!(fail.unwrap_err(), Rfc6238Error::InvalidDigits); + assert!(matches!(fail.unwrap_err(), Rfc6238Error::InvalidDigits(_))); assert_eq!(rfc.digits, 6); let ok = rfc.digits(8); assert!(ok.is_ok()); diff --git a/src/url_error.rs b/src/url_error.rs new file mode 100644 index 0000000..0161587 --- /dev/null +++ b/src/url_error.rs @@ -0,0 +1,195 @@ +#[cfg(feature = "otpauth")] +use url::ParseError; + +use crate::Rfc6238Error; + +#[derive(Debug, Eq, PartialEq)] +pub enum TotpUrlError { + #[cfg(feature = "otpauth")] + Url(ParseError), + Scheme(String), + Host(String), + Secret(String), + SecretSize(usize), + Algorithm(String), + Digits(String), + DigitsNumber(usize), + Step(String), + Issuer(String), + IssuerDecoding(String), + IssuerMistmatch(String, String), + AccountName(String), + AccountNameDecoding(String), +} + +impl std::fmt::Display for TotpUrlError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TotpUrlError::AccountName(name) => write!( + f, + "Account Name can't contain a colon. \"{}\" contains a colon", + name + ), + TotpUrlError::AccountNameDecoding(name) => write!( + f, + "Couldn't URL decode \"{}\"", + name + ), + TotpUrlError::Algorithm(algo) => write!( + f, + "Algorithm can only be SHA1, SHA256 or SHA512, not \"{}\"", + algo + ), + TotpUrlError::Digits(digits) => write!( + f, + "Could not parse \"{}\" as a number.", + digits, + ), + TotpUrlError::DigitsNumber(digits) => write!( + f, + "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. {} digits is not allowed", + digits, + ), + TotpUrlError::Host(host) => write!( + f, + "Host should be totp, not \"{}\"", + host + ), + TotpUrlError::Issuer(issuer) => write!( + f, + "Issuer can't contain a colon. \"{}\" contains a colon", + issuer + ), + TotpUrlError::IssuerDecoding(issuer) => write!( + f, + "Couldn't URL decode \"{}\"", + issuer + ), + TotpUrlError::IssuerMistmatch(path_issuer, issuer) => write!( + f, + "An issuer \"{}\" could be retrieved from the path, but a different issuer \"{}\" was found in the issuer URL parameter", + path_issuer, + issuer, + ), + TotpUrlError::Scheme(scheme) => write!( + f, + "Scheme should be otpauth, not \"{}\"", + scheme + ), + TotpUrlError::Secret(secret) => write!( + f, + "Secret \"{}\" is not a valid non-padded base32 string", + secret, + ), + TotpUrlError::SecretSize(bits) => write!( + f, + "The length of the shared secret MUST be at least 128 bits. {} bits is not enough", + bits, + ), + TotpUrlError::Step(step) => write!( + f, + "Could not parse \"{}\" as a number.", + step, + ), + #[cfg(feature = "otpauth")] + TotpUrlError::Url(e) => write!( + f, + "Error parsing URL: {}", + e + ) + } + } +} + +impl From for TotpUrlError { + fn from(e: Rfc6238Error) -> Self { + match e { + Rfc6238Error::InvalidDigits(digits) => TotpUrlError::DigitsNumber(digits), + Rfc6238Error::SecretTooSmall(bits) => TotpUrlError::SecretSize(bits), + } + } +} + +#[cfg(test)] +mod tests { + use crate::TotpUrlError; + + #[test] + fn account_name() { + let error = TotpUrlError::AccountName("Laziz:".to_string()); + assert_eq!(error.to_string(), "Account Name can't contain a colon. \"Laziz:\" contains a colon") + } + + #[test] + fn account_name_decoding() { + let error = TotpUrlError::AccountNameDecoding("Laz&iz".to_string()); + assert_eq!(error.to_string(), "Couldn't URL decode \"Laz&iz\"".to_string()) + } + + #[test] + fn algorithm() { + let error = TotpUrlError::Algorithm("SIKE".to_string()); + assert_eq!(error.to_string(), "Algorithm can only be SHA1, SHA256 or SHA512, not \"SIKE\"".to_string()) + } + + #[test] + fn digits() { + let error = TotpUrlError::Digits("six".to_string()); + assert_eq!(error.to_string(), "Could not parse \"six\" as a number.".to_string()) + } + + #[test] + fn digits_number() { + let error = TotpUrlError::DigitsNumber(5); + assert_eq!(error.to_string(), "Implementations MUST extract a 6-digit code at a minimum and possibly 7 and 8-digit code. 5 digits is not allowed".to_string()) + } + + #[test] + fn host() { + let error = TotpUrlError::Host("hotp".to_string()); + assert_eq!(error.to_string(), "Host should be totp, not \"hotp\"".to_string()) + } + + #[test] + fn issuer() { + let error = TotpUrlError::Issuer("Iss:uer".to_string()); + assert_eq!(error.to_string(), "Issuer can't contain a colon. \"Iss:uer\" contains a colon".to_string()) + } + + #[test] + fn issuer_decoding() { + let error = TotpUrlError::IssuerDecoding("iss&uer".to_string()); + assert_eq!(error.to_string(), "Couldn't URL decode \"iss&uer\"".to_string()) + } + + #[test] + fn issuer_mismatch() { + let error = TotpUrlError::IssuerMistmatch("Google".to_string(), "Github".to_string()); + assert_eq!(error.to_string(), "An issuer \"Google\" could be retrieved from the path, but a different issuer \"Github\" was found in the issuer URL parameter".to_string()) + } + + #[test] + fn scheme() { + let error = TotpUrlError::Scheme("https".to_string()); + assert_eq!(error.to_string(), "Scheme should be otpauth, not \"https\"".to_string()) + } + + #[test] + fn secret() { + let error = TotpUrlError::Secret("YoLo".to_string()); + assert_eq!(error.to_string(), "Secret \"YoLo\" is not a valid non-padded base32 string".to_string()) + } + + #[test] + fn secret_size() { + let error = TotpUrlError::SecretSize(112); + assert_eq!(error.to_string(), "The length of the shared secret MUST be at least 128 bits. 112 bits is not enough".to_string()) + } + + #[test] + #[cfg(feature = "otpauth")] + fn step() { + let error = TotpUrlError::Url(url::ParseError::EmptyHost); + assert_eq!(error.to_string(), "Error parsing URL: empty host".to_string()) + } +} \ No newline at end of file