diff --git a/src/messages.rs b/src/messages.rs index 79db8f0..45c5b36 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,10 +1,10 @@ pub const COUNTER_INVALID: &str = "Counter must be greater than or equal to 1"; pub const DRIFT_BEHIND_INVALID: &str = "Drift behind must be less than timestamp"; pub const INTERVAL_INVALID: &str = "Interval must be greater than or equal to 30"; -pub const OTP_LENGTH_INVALID: &str = "OTP length must be greater than or equal to 4"; +pub const OTP_LENGTH_INVALID: &str = "OTP length must be greater than or equal to 1"; pub const OTP_LENGTH_NOT_MATCHED: &str = "OTP length does not match the length of the configuration"; -pub const PROV_OTP_LENGTH_INVALID: &str = "HOTP length must be 6"; -pub const PROV_OTP_RADIX_INVALID: &str = "HOTP radix must be 10"; +pub const PROV_OTP_LENGTH_INVALID: &str = "OTP length must be 6"; +pub const PROV_OTP_RADIX_INVALID: &str = "Radix must be 10"; pub const RADIX_INVALID: &str = "Radix must be between 2 and 36 inclusive"; pub const SECRET_EMPTY: &str = "Secret must not be empty"; pub const TIMESTAMP_INVALID: &str = "Timestamp must be greater than or equal to 1"; diff --git a/src/otp/algorithm.rs b/src/otp/algorithm.rs index ed8669c..ff9b3e4 100644 --- a/src/otp/algorithm.rs +++ b/src/otp/algorithm.rs @@ -10,6 +10,7 @@ pub trait AlgorithmTrait { fn hash(&self, secret: Vec, data: u64) -> Result, String>; } +#[derive(Copy, Clone)] pub enum Algorithm { SHA1, SHA256, diff --git a/src/otp/hotp.rs b/src/otp/hotp.rs index dcb2f27..69566da 100644 --- a/src/otp/hotp.rs +++ b/src/otp/hotp.rs @@ -1,6 +1,6 @@ use crate::messages::{ - OTP_LENGTH_INVALID, OTP_LENGTH_NOT_MATCHED, PROV_OTP_LENGTH_INVALID, - PROV_OTP_RADIX_INVALID, RADIX_INVALID, SECRET_EMPTY, UNSUPPORTED_ALGORITHM, + OTP_LENGTH_INVALID, OTP_LENGTH_NOT_MATCHED, PROV_OTP_LENGTH_INVALID, PROV_OTP_RADIX_INVALID, + RADIX_INVALID, SECRET_EMPTY, UNSUPPORTED_ALGORITHM, }; use crate::otp::algorithm::Algorithm; use crate::otp::otp::otp; @@ -21,7 +21,7 @@ impl HOTP { ) -> Result { if secret.len() < 1 { Err(SECRET_EMPTY) - } else if length < 4 { + } else if length < 1 { Err(OTP_LENGTH_INVALID) } else if radix < 2 || radix > 36 { Err(RADIX_INVALID) @@ -45,12 +45,7 @@ impl HOTP { ) } - pub fn verify( - &self, - otp: &str, - counter: u64, - retries: u64, - ) -> Result, String> { + pub fn verify(&self, otp: &str, counter: u64, retries: u64) -> Result, String> { if self.length != otp.len() as u8 { Err(OTP_LENGTH_NOT_MATCHED.to_string()) } else { diff --git a/src/otp/otp.rs b/src/otp/otp.rs index 26d1cdb..67f4185 100644 --- a/src/otp/otp.rs +++ b/src/otp/otp.rs @@ -1,4 +1,4 @@ -use crate::messages::{COUNTER_INVALID, OTP_LENGTH_INVALID, RADIX_INVALID, SECRET_EMPTY}; +use crate::messages::COUNTER_INVALID; use crate::otp::algorithm::{Algorithm, AlgorithmTrait}; use num_bigint::BigUint; @@ -11,13 +11,7 @@ pub(crate) fn otp( radix: u8, counter: u64, ) -> Result { - if secret.len() < 1 { - Err(SECRET_EMPTY.to_string()) - } else if length < 4 { - Err(OTP_LENGTH_INVALID.to_string()) - } else if radix < 2 || radix > 36 { - Err(RADIX_INVALID.to_string()) - } else if counter < 1 { + if counter < 1 { Err(COUNTER_INVALID.to_string()) } else { match otp_bin_code(algorithm, secret, counter) { diff --git a/tests/hotp.rs b/tests/hotp.rs index f2972db..ba8037c 100644 --- a/tests/hotp.rs +++ b/tests/hotp.rs @@ -1,35 +1,70 @@ -use rusotp::Algorithm; +use rusotp::{generate_hotp, Algorithm, HOTP}; + +const SECRET: &str = "12345678901234567890"; #[test] fn fail_with_empty_secret() { - let result = rusotp::HOTP::new(Algorithm::SHA256, "", 1, 1); + let result = HOTP::new(Algorithm::SHA256, "", 1, 1); assert!(result.is_err(), "Expected an error"); assert_eq!(result.err().unwrap(), "Secret must not be empty"); } #[test] -fn fail_with_invalid_otp_length() { - let result = rusotp::HOTP::new(Algorithm::SHA256, "12312341234", 1, 10); +#[should_panic(expected = "Secret must not be empty")] +fn generate_hotp_should_fail_with_empty_secret() { + generate_hotp(Algorithm::SHA256, "", 1, 1, 0); +} + +#[test] +fn fail_with_otp_length_less_than_1() { + let result = HOTP::new(Algorithm::SHA256, SECRET, 0, 10); assert!(result.is_err(), "Expected an error"); assert_eq!( result.err().unwrap(), - "OTP length must be greater than or equal to 4" + "OTP length must be greater than or equal to 1" ); } #[test] -fn fail_with_invalid_radix() { - let result = rusotp::HOTP::new(Algorithm::SHA256, "12312341234", 4, 1); - assert!(result.is_err(), "Expected an error"); +#[should_panic(expected = "OTP length must be greater than or equal to 1")] +fn generate_hotp_should_fail_otp_length_less_than_1() { + generate_hotp(Algorithm::SHA256, SECRET, 0, 10, 0); +} + +#[test] +fn fail_with_radix_less_than_2() { + let lesser_radix = HOTP::new(Algorithm::SHA256, SECRET, 4, 1); + assert!(lesser_radix.is_err(), "Expected an error"); assert_eq!( - result.err().unwrap(), + lesser_radix.err().unwrap(), + "Radix must be between 2 and 36 inclusive" + ); +} + +#[test] +#[should_panic(expected = "Radix must be between 2 and 36 inclusive")] +fn generate_hotp_should_fail_radix_less_than_2() { + generate_hotp(Algorithm::SHA256, SECRET, 4, 1, 0); +} +#[test] +fn fail_with_radix_greater_than_36() { + let greater_radix = HOTP::new(Algorithm::SHA256, SECRET, 4, 37); + assert!(greater_radix.is_err(), "Expected an error"); + assert_eq!( + greater_radix.err().unwrap(), "Radix must be between 2 and 36 inclusive" ); } #[test] -fn fail_with_invalid_counter() { - let hotp = match rusotp::HOTP::new(Algorithm::SHA256, "12312341234", 4, 10) { +#[should_panic(expected = "Radix must be between 2 and 36 inclusive")] +fn generate_hotp_should_fail_radix_more_than_36() { + generate_hotp(Algorithm::SHA256, SECRET, 4, 37, 0); +} + +#[test] +fn fail_with_counter_less_than_1() { + let hotp = match HOTP::new(Algorithm::SHA256, SECRET, 4, 10) { Ok(hotp) => hotp, Err(e) => panic!("{}", e), }; @@ -44,12 +79,14 @@ fn fail_with_invalid_counter() { } #[test] -fn fail_with_otp_length_not_matched() { - let hotp = match rusotp::HOTP::new(Algorithm::SHA256, "12312341234", 4, 10) { - Ok(hotp) => hotp, - Err(e) => panic!("{}", e), - }; +#[should_panic(expected = "Counter must be greater than or equal to 1")] +fn generate_hotp_should_fail_with_counter_less_than_1() { + generate_hotp(Algorithm::SHA256, SECRET, 4, 10, 0); +} +#[test] +fn fail_with_otp_length_not_matched() { + let hotp = HOTP::new(Algorithm::SHA256, SECRET, 4, 10).unwrap(); let result = hotp.verify("12345", 10, 0); assert!(result.is_err(), "Expected an error"); @@ -61,8 +98,34 @@ fn fail_with_otp_length_not_matched() { #[test] fn generated_otp_is_correct() { - let secret = "12345678901234567890"; + let data = vec![ + (6, 10, 1, "247374"), + (6, 10, 2, "254785"), + (6, 10, 3, "496144"), + (6, 16, 1, "687B4E"), + (6, 24, 1, "N7C1B6"), + (6, 36, 1, "M16ONI"), + (8, 10, 100, "93583477"), + (8, 16, 100, "23615D75"), + (8, 24, 100, "032D2EKL"), + (8, 36, 100, "009TEJXX"), + (4, 36, 1, "6ONI"), + (4, 36, 2, "KYWX"), + (4, 36, 3, "ERBK"), + (4, 36, 4, "ROTO"), + ]; + + data.iter().for_each(|(length, radix, counter, otp)| { + let hotp = HOTP::new(Algorithm::SHA256, SECRET, *length, *radix).unwrap(); + + let result = hotp.generate(*counter); + assert!(result.is_ok(), "Expected a result"); + assert_eq!(result.unwrap(), otp.to_string()); + }); +} +#[test] +fn wrong_otp_does_not_get_verified() { let data = vec![ (6, 10, 1, "247374"), (6, 10, 2, "254785"), @@ -81,53 +144,143 @@ fn generated_otp_is_correct() { ]; data.iter().for_each(|(length, radix, counter, otp)| { - match rusotp::HOTP::new(Algorithm::SHA256, secret, *length, *radix) { - Ok(hotp) => { - let result = hotp.generate(*counter); - assert!(result.is_ok(), "Expected a result"); - assert_eq!(result.unwrap(), otp.to_string()); - } - Err(e) => panic!("{}", e), - }; + let hotp = HOTP::new(Algorithm::SHA256, SECRET, *length, *radix).unwrap(); + + let result = hotp.verify(otp, *counter + 1, 0); + assert!(result.is_ok(), "Expected a result"); + assert!(result.unwrap().is_none(), "Expected a failed verification"); }); } +#[test] +fn otp_get_verified_with_retries() { + let data = vec![ + (6, 10, 2, "254785", 1), + (6, 10, 3, "496144", 1), + (8, 10, 100, "93583477", 5), + (8, 16, 100, "23615D75", 1), + (8, 24, 100, "032D2EKL", 1), + (8, 36, 100, "009TEJXX", 1), + (4, 36, 2, "KYWX", 1), + (4, 36, 3, "ERBK", 1), + (4, 36, 4, "ROTO", 1), + ]; + + data.iter() + .for_each(|(length, radix, counter, otp, retries)| { + let hotp = HOTP::new(Algorithm::SHA256, SECRET, *length, *radix).unwrap(); + + let result = hotp.verify(otp, *counter - *retries, *retries); + assert!(result.is_ok(), "Expected a result"); + assert!( + result.unwrap().is_some(), + "Expected a successful verification" + ); + }); +} + #[test] fn generated_otp_gets_verified() { let secret = "12345678901234567890"; let data = vec![ - (6, 10, 1), - (6, 10, 2), - (6, 10, 3), - (6, 16, 1), - (6, 24, 1), - (6, 36, 1), - (8, 10, 100), - (8, 16, 100), - (8, 24, 100), - (8, 36, 100), - (4, 36, 1), - (4, 36, 2), - (4, 36, 3), + (Algorithm::SHA256, 6, 10, 1), + (Algorithm::SHA256, 6, 10, 2), + (Algorithm::SHA256, 6, 10, 3), + (Algorithm::SHA256, 6, 16, 1), + (Algorithm::SHA256, 6, 24, 1), + (Algorithm::SHA256, 6, 36, 1), + (Algorithm::SHA256, 8, 10, 100), + (Algorithm::SHA256, 8, 16, 100), + (Algorithm::SHA256, 8, 24, 100), + (Algorithm::SHA256, 8, 36, 100), + (Algorithm::SHA256, 4, 36, 1), + (Algorithm::SHA256, 4, 36, 2), + (Algorithm::SHA256, 4, 36, 3), ]; - data.iter().for_each(|(length, radix, counter)| { - match rusotp::HOTP::new(Algorithm::SHA256, secret, *length, *radix) { - Ok(hotp) => { - match hotp.generate(*counter) { - Ok(otp) => { - let result = hotp.verify(&otp, *counter, 0); - assert!(result.is_ok(), "Expected a result"); - assert!( - result.unwrap().is_some(), - "Expected a successful verification" - ); - } - Err(e) => panic!("{}", e), - }; - } - Err(e) => panic!("{}", e), - }; + data.iter().for_each(|(algorithm, length, radix, counter)| { + let hotp = HOTP::new(*algorithm, secret, *length, *radix).unwrap(); + let otp = hotp.generate(*counter).unwrap(); + + let result = hotp.verify(&otp, *counter, 0); + assert!(result.is_ok(), "Expected a result"); + assert!( + result.unwrap().is_some(), + "Expected a successful verification" + ); }); } + +#[test] +fn provisioning_uri_is_correct() { + let hotp_tool = HOTP::new(Algorithm::SHA1, SECRET, 6, 10).unwrap(); + + let result = hotp_tool.provisioning_uri("test", 0); + + assert!(result.is_ok(), "Expected a result"); + assert_eq!( + result.unwrap(), + "otpauth://hotp/test?secret=12345678901234567890&counter=0" + ); +} + +#[test] +fn fail_provisioning_uri_with_sha256() { + let hotp_tool = HOTP::new(Algorithm::SHA256, SECRET, 6, 10).unwrap(); + + let result = hotp_tool.provisioning_uri("test", 0); + + assert!(result.is_err(), "Expected an error"); + assert_eq!(result.err().unwrap(), "Unsupported algorithm"); +} + +#[test] +fn fail_provisioning_uri_with_sha512() { + let hotp_tool = HOTP::new(Algorithm::SHA512, SECRET, 6, 10).unwrap(); + + let result = hotp_tool.provisioning_uri("test", 0); + + assert!(result.is_err(), "Expected an error"); + assert_eq!(result.err().unwrap(), "Unsupported algorithm"); +} + +#[test] +fn fail_provisioning_uri_with_otp_length_less_than_6() { + let hotp_tool = HOTP::new(Algorithm::SHA512, SECRET, 4, 10).unwrap(); + + let result = hotp_tool.provisioning_uri("test", 0); + + assert!(result.is_err(), "Expected an error"); + assert_eq!(result.err().unwrap(), "OTP length must be 6"); +} + +#[test] +fn fail_provisioning_uri_with_otp_length_more_than_6() { + let hotp_tool = HOTP::new(Algorithm::SHA512, SECRET, 8, 10).unwrap(); + + let result = hotp_tool.provisioning_uri("test", 0); + + assert!(result.is_err(), "Expected an error"); + assert_eq!(result.err().unwrap(), "OTP length must be 6"); +} + +#[test] +fn fail_provisioning_uri_with_radix_less_than_10() { + let hotp_tool = HOTP::new(Algorithm::SHA512, SECRET, 6, 9).unwrap(); + + let result = hotp_tool.provisioning_uri("test", 0); + + assert!(result.is_err(), "Expected an error"); + assert_eq!(result.err().unwrap(), "Radix must be 10"); +} + +#[test] +fn fail_provisioning_uri_with_radix_more_than_10() { + let hotp_tool = HOTP::new(Algorithm::SHA512, SECRET, 6, 11).unwrap(); + + let result = hotp_tool.provisioning_uri("test", 0); + + assert!(result.is_err(), "Expected an error"); + assert_eq!(result.err().unwrap(), "Radix must be 10"); +}