Commit 263299be1477668e5b26704e582012de054dca2a

constantoine 2022-06-16T11:48:34

Fix url related bugs * Bug where your issuer would be incorrectly prefixed with a /, and comparison with the issuer parameter would fail * Bug where the issuer and account name in path would not be correctly url decoded in path, but correctly decoded in url query Signed-off-by: constantoine <cleo.rebert-ext@treezor.com>

diff --git a/Cargo.toml b/Cargo.toml
index 93bd8bc..e428b89 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "totp-rs"
-version = "2.0.0"
+version = "2.0.1"
 authors = ["Cleo Rebert <cleo.rebert@gmail.com>"]
 edition = "2021"
 readme = "README.md"
@@ -12,11 +12,11 @@ keywords = ["authentication", "2fa", "totp", "hmac", "otp"]
 categories = ["authentication", "web-programming"]
 
 [package.metadata.docs.rs]
-features = [ "qr", "serde_support" ]
+features = [ "qr", "serde_support", "otpauth" ]
 
 [features]
 default = []
-otpauth = ["url"]
+otpauth = ["url", "urlencoding"]
 qr = ["qrcodegen", "image", "base64", "otpauth"]
 serde_support = ["serde"]
 
@@ -26,7 +26,8 @@ sha2 = "~0.10.2"
 sha-1 = "~0.10.0"
 hmac = "~0.12.1"
 base32 = "~0.4"
-url = { version = "2.2.2", optional = true } 
+urlencoding = { version = "^2.1.0", optional = true}
+url = { version = "^2.2.2", optional = true } 
 constant_time_eq = "~0.2.1"
 qrcodegen = { version = "~1.8", optional = true }
 image = { version = "~0.24.2", features = ["png"], optional = true, default-features = false}
diff --git a/src/lib.rs b/src/lib.rs
index f29533d..be92b46 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -57,6 +57,8 @@ use {base64, image::Luma, qrcodegen};
 
 #[cfg(feature = "otpauth")]
 use url::{Host, ParseError, Url};
+#[cfg(feature = "otpauth")]
+use urlencoding;
 
 use hmac::Mac;
 use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH};
@@ -280,16 +282,18 @@ impl<T: AsRef<[u8]>> TOTP<T> {
         let mut step = 30;
         let mut secret = Vec::new();
         let mut issuer: Option<String> = None;
-        let account_name: String;
+        let mut account_name: String;
 
-        let path = url.path();
+        let path = url.path().trim_start_matches('/');
         if path.contains(':') {
             let parts = path.split_once(':').unwrap();
-            issuer = Some(parts.0.to_owned());
+            issuer = Some(urlencoding::decode(parts.0.to_owned().as_str()).map_err(|_| TotpUrlError::Issuer)?.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();
 
         for (key, value) in url.query_pairs() {
             match key.as_ref() {
@@ -343,9 +347,9 @@ impl<T: AsRef<[u8]>> TOTP<T> {
     #[cfg(feature = "otpauth")]
     pub fn get_url(&self) -> String {
         let label: String;
-        let account_name: String = url::form_urlencoded::byte_serialize(self.account_name.as_bytes()).collect();
+        let account_name: String = urlencoding::encode(self.account_name.as_str()).to_string();
         if self.issuer.is_some() {
-            let issuer: String = url::form_urlencoded::byte_serialize(self.issuer.as_ref().unwrap().as_bytes()).collect();
+            let issuer: String = urlencoding::encode(self.issuer.as_ref().unwrap().as_str()).to_string();
             label = format!("{0}:{1}?issuer={0}&", issuer, account_name);
         } else {
             label = format!("{}?", account_name);
@@ -635,6 +639,33 @@ mod tests {
 
     #[test]
     #[cfg(feature = "otpauth")]
+    fn from_url_to_url() {
+        let totp = TOTP::<Vec<u8>>::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();
+        assert_eq!(totp.get_url(), totp_bis.get_url());
+    }
+
+    #[test]
+    #[cfg(feature = "otpauth")]
+    fn from_url_issuer_special() {
+        let totp = TOTP::<Vec<u8>>::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();
+        assert_eq!(totp.get_url(), totp_bis.get_url());
+    }
+
+    #[test]
+    #[cfg(feature = "otpauth")]
+    fn from_url_query_issuer() {
+        let totp = TOTP::<Vec<u8>>::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());
+        assert_eq!(totp.algorithm, Algorithm::SHA256);
+        assert_eq!(totp.digits, 8);
+        assert_eq!(totp.skew, 1);
+        assert_eq!(totp.step, 60);
+    }
+
+    #[test]
+    #[cfg(feature = "otpauth")]
     fn from_url_query_different_issuers() {
         let totp = TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test?issuer=Gitlab&secret=ABC&digits=8&period=60&algorithm=SHA256");
         assert_eq!(totp.is_err(), true);