Merge branch 'master' into secret
This commit is contained in:
commit
ab7bde0866
212
src/lib.rs
212
src/lib.rs
|
@ -17,7 +17,7 @@
|
||||||
//! 6,
|
//! 6,
|
||||||
//! 1,
|
//! 1,
|
||||||
//! 30,
|
//! 30,
|
||||||
//! "supersecret",
|
//! "supersecret_topsecret",
|
||||||
//! Some("Github".to_string()),
|
//! Some("Github".to_string()),
|
||||||
//! "constantoine@github.com".to_string(),
|
//! "constantoine@github.com".to_string(),
|
||||||
//! ).unwrap();
|
//! ).unwrap();
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
//! 6,
|
//! 6,
|
||||||
//! 1,
|
//! 1,
|
||||||
//! 30,
|
//! 30,
|
||||||
//! "supersecret",
|
//! "supersecret_topsecret",
|
||||||
//! Some("Github".to_string()),
|
//! Some("Github".to_string()),
|
||||||
//! "constantoine@github.com".to_string(),
|
//! "constantoine@github.com".to_string(),
|
||||||
//! ).unwrap();
|
//! ).unwrap();
|
||||||
|
@ -47,8 +47,10 @@
|
||||||
|
|
||||||
mod secret;
|
mod secret;
|
||||||
mod rfc;
|
mod rfc;
|
||||||
|
mod url_error;
|
||||||
|
|
||||||
pub use secret::{Secret, SecretParseError};
|
pub use secret::{Secret, SecretParseError};
|
||||||
|
use url_error::TotpUrlError;
|
||||||
pub use rfc::{Rfc6238, Rfc6238Error};
|
pub use rfc::{Rfc6238, Rfc6238Error};
|
||||||
pub use base32;
|
pub use base32;
|
||||||
|
|
||||||
|
@ -63,7 +65,7 @@ use core::fmt;
|
||||||
use {base64, image::Luma, qrcodegen};
|
use {base64, image::Luma, qrcodegen};
|
||||||
|
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
use url::{Host, ParseError, Url};
|
use url::{Host, Url};
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
use urlencoding;
|
use urlencoding;
|
||||||
|
|
||||||
|
@ -83,18 +85,18 @@ pub enum Algorithm {
|
||||||
SHA512,
|
SHA512,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for Algorithm {
|
||||||
|
fn default() -> Self {
|
||||||
|
Algorithm::SHA1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for Algorithm {
|
impl fmt::Display for Algorithm {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
return match *self {
|
match self {
|
||||||
Algorithm::SHA1 => {
|
Algorithm::SHA1 => f.write_str("SHA1"),
|
||||||
f.write_str("SHA1")
|
Algorithm::SHA256 => f.write_str("SHA256"),
|
||||||
}
|
Algorithm::SHA512 => f.write_str("SHA512"),
|
||||||
Algorithm::SHA256 => {
|
|
||||||
f.write_str("SHA256")
|
|
||||||
}
|
|
||||||
Algorithm::SHA512 => {
|
|
||||||
f.write_str("SHA512")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,7 +111,7 @@ impl Algorithm {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sign(&self, key: &[u8], data: &[u8]) -> Vec<u8> {
|
fn sign(&self, key: &[u8], data: &[u8]) -> Vec<u8> {
|
||||||
match *self {
|
match self {
|
||||||
Algorithm::SHA1 => Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data),
|
Algorithm::SHA1 => Algorithm::hash(HmacSha1::new_from_slice(key).unwrap(), data),
|
||||||
Algorithm::SHA256 => Algorithm::hash(HmacSha256::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),
|
Algorithm::SHA512 => Algorithm::hash(HmacSha512::new_from_slice(key).unwrap(), data),
|
||||||
|
@ -124,29 +126,6 @@ fn system_time() -> Result<u64, SystemTimeError> {
|
||||||
Ok(t)
|
Ok(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
pub enum TotpUrlError {
|
|
||||||
#[cfg(feature = "otpauth")]
|
|
||||||
Url(ParseError),
|
|
||||||
Scheme,
|
|
||||||
Host,
|
|
||||||
Secret,
|
|
||||||
Algorithm,
|
|
||||||
Digits,
|
|
||||||
Step,
|
|
||||||
Issuer,
|
|
||||||
AccountName,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Rfc6238Error> 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
|
/// 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)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
|
||||||
|
@ -204,17 +183,21 @@ impl<T: AsRef<[u8]>> TOTP<T> {
|
||||||
/// let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, decoded, None, "".to_string()).unwrap();
|
/// let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, decoded, None, "".to_string()).unwrap();
|
||||||
/// ```
|
/// ```
|
||||||
/// * `digits`: MUST be between 6 & 8
|
/// * `digits`: MUST be between 6 & 8
|
||||||
|
/// * `secret`: Must have bitsize of at least 128
|
||||||
|
/// * `account_name`: Must not contain `:`
|
||||||
|
/// * `issuer`: Must not contain `:`
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Will return an error in case issuer or label contain the character ':'
|
/// 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<String>, account_name: String) -> Result<TOTP<T>, TotpUrlError> {
|
pub fn new(algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: T, issuer: Option<String>, account_name: String) -> Result<TOTP<T>, TotpUrlError> {
|
||||||
crate::rfc::assert_digits(&digits)?;
|
crate::rfc::assert_digits(&digits)?;
|
||||||
|
crate::rfc::assert_secret_length(secret.as_ref())?;
|
||||||
if issuer.is_some() && issuer.as_ref().unwrap().contains(':') {
|
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(':') {
|
if account_name.contains(':') {
|
||||||
return Err(TotpUrlError::AccountName);
|
return Err(TotpUrlError::AccountName(account_name));
|
||||||
}
|
}
|
||||||
Ok(TOTP {
|
Ok(TOTP {
|
||||||
algorithm,
|
algorithm,
|
||||||
|
@ -252,7 +235,7 @@ impl<T: AsRef<[u8]>> TOTP<T> {
|
||||||
format!(
|
format!(
|
||||||
"{1:00$}",
|
"{1:00$}",
|
||||||
self.digits,
|
self.digits,
|
||||||
result % (10 as u32).pow(self.digits as u32)
|
result % 10_u32.pow(self.digits as u32)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,10 +298,10 @@ impl<T: AsRef<[u8]>> TOTP<T> {
|
||||||
pub fn from_url<S: AsRef<str>>(url: S) -> Result<TOTP<Vec<u8>>, TotpUrlError> {
|
pub fn from_url<S: AsRef<str>>(url: S) -> Result<TOTP<Vec<u8>>, TotpUrlError> {
|
||||||
let url = Url::parse(url.as_ref()).map_err(|err| TotpUrlError::Url(err))?;
|
let url = Url::parse(url.as_ref()).map_err(|err| TotpUrlError::Url(err))?;
|
||||||
if url.scheme() != "otpauth" {
|
if url.scheme() != "otpauth" {
|
||||||
return Err(TotpUrlError::Scheme);
|
return Err(TotpUrlError::Scheme(url.scheme().to_string()));
|
||||||
}
|
}
|
||||||
if url.host() != Some(Host::Domain("totp")) {
|
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;
|
let mut algorithm = Algorithm::SHA1;
|
||||||
|
@ -331,13 +314,13 @@ impl<T: AsRef<[u8]>> TOTP<T> {
|
||||||
let path = url.path().trim_start_matches('/');
|
let path = url.path().trim_start_matches('/');
|
||||||
if path.contains(':') {
|
if path.contains(':') {
|
||||||
let parts = path.split_once(':').unwrap();
|
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();
|
account_name = parts.1.trim_start_matches(':').to_owned();
|
||||||
} else {
|
} else {
|
||||||
account_name = path.to_owned();
|
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() {
|
for (key, value) in url.query_pairs() {
|
||||||
match key.as_ref() {
|
match key.as_ref() {
|
||||||
|
@ -346,24 +329,24 @@ impl<T: AsRef<[u8]>> TOTP<T> {
|
||||||
"SHA1" => Algorithm::SHA1,
|
"SHA1" => Algorithm::SHA1,
|
||||||
"SHA256" => Algorithm::SHA256,
|
"SHA256" => Algorithm::SHA256,
|
||||||
"SHA512" => Algorithm::SHA512,
|
"SHA512" => Algorithm::SHA512,
|
||||||
_ => return Err(TotpUrlError::Algorithm),
|
_ => return Err(TotpUrlError::Algorithm(value.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"digits" => {
|
"digits" => {
|
||||||
digits = value.parse::<usize>().map_err(|_| TotpUrlError::Digits)?;
|
digits = value.parse::<usize>().map_err(|_| TotpUrlError::Digits(value.to_string()))?;
|
||||||
}
|
}
|
||||||
"period" => {
|
"period" => {
|
||||||
step = value.parse::<u64>().map_err(|_| TotpUrlError::Step)?;
|
step = value.parse::<u64>().map_err(|_| TotpUrlError::Step(value.to_string()))?;
|
||||||
}
|
}
|
||||||
"secret" => {
|
"secret" => {
|
||||||
secret =
|
secret =
|
||||||
base32::decode(base32::Alphabet::RFC4648 { padding: false }, value.as_ref())
|
base32::decode(base32::Alphabet::RFC4648 { padding: false }, value.as_ref())
|
||||||
.ok_or(TotpUrlError::Secret)?;
|
.ok_or(TotpUrlError::Secret(value.to_string()))?;
|
||||||
}
|
}
|
||||||
"issuer" => {
|
"issuer" => {
|
||||||
let param_issuer = value.parse::<String>().map_err(|_| TotpUrlError::Issuer)?;
|
let param_issuer = value.parse::<String>().map_err(|_| TotpUrlError::Issuer(value.to_string()))?;
|
||||||
if issuer.is_some() && param_issuer.as_str() != issuer.as_ref().unwrap() {
|
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);
|
issuer = Some(param_issuer);
|
||||||
}
|
}
|
||||||
|
@ -372,7 +355,7 @@ impl<T: AsRef<[u8]>> TOTP<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if secret.is_empty() {
|
if secret.is_empty() {
|
||||||
return Err(TotpUrlError::Secret);
|
return Err(TotpUrlError::Secret("".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
TOTP::new(algorithm, digits, 1, step, secret, issuer, account_name)
|
TOTP::new(algorithm, digits, 1, step, secret, issuer, account_name)
|
||||||
|
@ -480,114 +463,114 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_wrong_issuer() {
|
fn new_wrong_issuer() {
|
||||||
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github:".to_string()), "constantoine@github.com".to_string());
|
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github:".to_string()), "constantoine@github.com".to_string());
|
||||||
assert_eq!(totp.is_err(), true);
|
assert!(totp.is_err());
|
||||||
assert_eq!(totp.unwrap_err(), TotpUrlError::Issuer);
|
assert!(matches!(totp.unwrap_err(), TotpUrlError::Issuer(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_wrong_account_name() {
|
fn new_wrong_account_name() {
|
||||||
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine:github.com".to_string());
|
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine:github.com".to_string());
|
||||||
assert_eq!(totp.is_err(), true);
|
assert!(totp.is_err());
|
||||||
assert_eq!(totp.unwrap_err(), TotpUrlError::AccountName);
|
assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_wrong_account_name_no_issuer() {
|
fn new_wrong_account_name_no_issuer() {
|
||||||
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", None, "constantoine:github.com".to_string());
|
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", None, "constantoine:github.com".to_string());
|
||||||
assert_eq!(totp.is_err(), true);
|
assert!(totp.is_err());
|
||||||
assert_eq!(totp.unwrap_err(), TotpUrlError::AccountName);
|
assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn comparison_ok() {
|
fn comparison_ok() {
|
||||||
let reference = 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, "TestSecret", 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);
|
assert_eq!(reference, test);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn comparison_different_algo() {
|
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 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, "TestSecret", 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);
|
assert_ne!(reference, test);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn comparison_different_digits() {
|
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 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, "TestSecret", 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);
|
assert_ne!(reference, test);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn comparison_different_skew() {
|
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 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, "TestSecret", 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);
|
assert_ne!(reference, test);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn comparison_different_step() {
|
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 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, "TestSecret", 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);
|
assert_ne!(reference, test);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn comparison_different_secret() {
|
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 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, "TestSecretL", 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);
|
assert_ne!(reference, test);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
fn url_for_secret_matches_sha1_without_issuer() {
|
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();
|
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]
|
#[test]
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
fn url_for_secret_matches_sha1() {
|
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();
|
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]
|
#[test]
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
fn url_for_secret_matches_sha256() {
|
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();
|
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]
|
#[test]
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
fn url_for_secret_matches_sha512() {
|
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();
|
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]
|
#[test]
|
||||||
fn returns_base32() {
|
fn returns_base32() {
|
||||||
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_eq!(totp.get_secret_base32().as_str(), "KRSXG5CTMVRXEZLU");
|
assert_eq!(totp.get_secret_base32().as_str(), "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generate_token() {
|
fn generate_token() {
|
||||||
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_eq!(totp.generate(1000).as_str(), "718996");
|
assert_eq!(totp.generate(1000).as_str(), "659761");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generate_token_current() {
|
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()
|
let time = SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH).unwrap()
|
.duration_since(SystemTime::UNIX_EPOCH).unwrap()
|
||||||
.as_secs();
|
.as_secs();
|
||||||
|
@ -596,43 +579,40 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generates_token_sha256() {
|
fn generates_token_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();
|
||||||
assert_eq!(totp.generate(1000).as_str(), "480200");
|
assert_eq!(totp.generate(1000).as_str(), "076417");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generates_token_sha512() {
|
fn generates_token_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();
|
||||||
assert_eq!(totp.generate(1000).as_str(), "850500");
|
assert_eq!(totp.generate(1000).as_str(), "473536");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn checks_token() {
|
fn checks_token() {
|
||||||
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("718996", 1000));
|
assert!(totp.check("659761", 1000));
|
||||||
assert!(totp.check("712039", 2000));
|
|
||||||
assert!(!totp.check("527544", 2000));
|
|
||||||
assert!(!totp.check("714250", 2000));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn checks_token_current() {
|
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(&totp.generate_current().unwrap()).unwrap());
|
||||||
assert!(!totp.check_current("bogus").unwrap());
|
assert!(!totp.check_current("bogus").unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn checks_token_with_skew() {
|
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!(
|
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]
|
#[test]
|
||||||
fn next_step() {
|
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(0) == 30);
|
||||||
assert!(totp.next_step(29) == 30);
|
assert!(totp.next_step(29) == 30);
|
||||||
assert!(totp.next_step(30) == 60);
|
assert!(totp.next_step(30) == 60);
|
||||||
|
@ -640,7 +620,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn next_step_current() {
|
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();
|
let t = system_time().unwrap();
|
||||||
assert!(totp.next_step_current().unwrap() == totp.next_step(t));
|
assert!(totp.next_step_current().unwrap() == totp.next_step(t));
|
||||||
}
|
}
|
||||||
|
@ -651,13 +631,15 @@ mod tests {
|
||||||
assert!(TOTP::<Vec<u8>>::from_url("otpauth://hotp/123").is_err());
|
assert!(TOTP::<Vec<u8>>::from_url("otpauth://hotp/123").is_err());
|
||||||
assert!(TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test").is_err());
|
assert!(TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test").is_err());
|
||||||
assert!(TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test:?secret=ABC&digits=8&period=60&algorithm=SHA256").is_err());
|
assert!(TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test:?secret=ABC&digits=8&period=60&algorithm=SHA256").is_err());
|
||||||
|
assert!(TOTP::<Vec<u8>>::from_url("otpauth://totp/Github:constantoine%40github.com?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").is_err())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
fn from_url_default() {
|
fn from_url_default() {
|
||||||
let totp = TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test?secret=ABC").unwrap();
|
let totp = TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap();
|
||||||
assert_eq!(totp.secret, base32::decode(base32::Alphabet::RFC4648 { padding: false }, "ABC").unwrap());
|
assert_eq!(totp.secret, base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap());
|
||||||
assert_eq!(totp.algorithm, Algorithm::SHA1);
|
assert_eq!(totp.algorithm, Algorithm::SHA1);
|
||||||
assert_eq!(totp.digits, 6);
|
assert_eq!(totp.digits, 6);
|
||||||
assert_eq!(totp.skew, 1);
|
assert_eq!(totp.skew, 1);
|
||||||
|
@ -667,8 +649,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
fn from_url_query() {
|
fn from_url_query() {
|
||||||
let totp = TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test?secret=ABC&digits=8&period=60&algorithm=SHA256").unwrap();
|
let totp = TOTP::<Vec<u8>>::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 }, "ABC").unwrap());
|
assert_eq!(totp.secret, base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap());
|
||||||
assert_eq!(totp.algorithm, Algorithm::SHA256);
|
assert_eq!(totp.algorithm, Algorithm::SHA256);
|
||||||
assert_eq!(totp.digits, 8);
|
assert_eq!(totp.digits, 8);
|
||||||
assert_eq!(totp.skew, 1);
|
assert_eq!(totp.skew, 1);
|
||||||
|
@ -678,36 +660,38 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
fn from_url_to_url() {
|
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 = TOTP::<Vec<u8>>::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, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).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.get_url(), totp_bis.get_url());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
fn from_url_issuer_special() {
|
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 = TOTP::<Vec<u8>>::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, "TestSecret", Some("Github@".to_string()), "constantoine@github.com".to_string()).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.get_url(), totp_bis.get_url());
|
||||||
|
assert_eq!(totp.issuer.unwrap(), "Github@");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
fn from_url_query_issuer() {
|
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();
|
let totp = TOTP::<Vec<u8>>::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 }, "ABC").unwrap());
|
assert_eq!(totp.secret, base32::decode(base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ").unwrap());
|
||||||
assert_eq!(totp.algorithm, Algorithm::SHA256);
|
assert_eq!(totp.algorithm, Algorithm::SHA256);
|
||||||
assert_eq!(totp.digits, 8);
|
assert_eq!(totp.digits, 8);
|
||||||
assert_eq!(totp.skew, 1);
|
assert_eq!(totp.skew, 1);
|
||||||
assert_eq!(totp.step, 60);
|
assert_eq!(totp.step, 60);
|
||||||
|
assert_eq!(totp.issuer.unwrap(), "GitHub");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "otpauth")]
|
#[cfg(feature = "otpauth")]
|
||||||
fn from_url_query_different_issuers() {
|
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");
|
let totp = TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test?issuer=Gitlab&secret=TestSecretSuperSecret&digits=8&period=60&algorithm=SHA256");
|
||||||
assert_eq!(totp.is_err(), true);
|
assert!(totp.is_err());
|
||||||
assert_eq!(totp.unwrap_err(), TotpUrlError::Issuer);
|
assert!(matches!(totp.unwrap_err(), TotpUrlError::IssuerMistmatch(_, _)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -715,14 +699,14 @@ mod tests {
|
||||||
fn generates_qr() {
|
fn generates_qr() {
|
||||||
use sha1::{Digest, Sha1};
|
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();
|
let qr = totp.get_qr().unwrap();
|
||||||
|
|
||||||
// Create hash from image
|
// Create hash from image
|
||||||
let hash_digest = Sha1::digest(qr.as_bytes());
|
let hash_digest = Sha1::digest(qr.as_bytes());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format!("{:x}", hash_digest).as_str(),
|
format!("{:x}", hash_digest).as_str(),
|
||||||
"b21a9d4bbb5bd0800bb6bff83a92a2e3314266a5"
|
"3028f00bf1bd2898ce4d73b234ba087d3c5172f9"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
42
src/rfc.rs
42
src/rfc.rs
|
@ -9,21 +9,23 @@ use serde::{Deserialize, Serialize};
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
pub enum Rfc6238Error {
|
pub enum Rfc6238Error {
|
||||||
/// 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
|
||||||
InvalidDigits,
|
InvalidDigits(usize),
|
||||||
/// The length of the shared secret MUST be at least 128 bits
|
/// The length of the shared secret MUST be at least 128 bits
|
||||||
SecretTooSmall,
|
SecretTooSmall(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Rfc6238Error {
|
impl std::fmt::Display for Rfc6238Error {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Rfc6238Error::InvalidDigits => write!(
|
Rfc6238Error::InvalidDigits(digits) => write!(
|
||||||
f,
|
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,
|
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> {
|
pub fn assert_digits(digits: &usize) -> Result<(), Rfc6238Error> {
|
||||||
if !(&6..=&8).contains(&digits) {
|
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 {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -91,9 +101,8 @@ impl<T: AsRef<[u8]>> Rfc6238<T> {
|
||||||
account_name: String,
|
account_name: String,
|
||||||
) -> Result<Rfc6238<T>, Rfc6238Error> {
|
) -> Result<Rfc6238<T>, Rfc6238Error> {
|
||||||
assert_digits(&digits)?;
|
assert_digits(&digits)?;
|
||||||
if secret.as_ref().len() < 16 {
|
assert_secret_length(secret.as_ref())?;
|
||||||
Err(Rfc6238Error::SecretTooSmall)
|
|
||||||
} else {
|
|
||||||
Ok(Rfc6238 {
|
Ok(Rfc6238 {
|
||||||
algorithm: Algorithm::SHA1,
|
algorithm: Algorithm::SHA1,
|
||||||
digits,
|
digits,
|
||||||
|
@ -104,7 +113,6 @@ impl<T: AsRef<[u8]>> Rfc6238<T> {
|
||||||
account_name,
|
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),
|
/// Create an [rfc-6238](https://tools.ietf.org/html/rfc6238) compliant set of options that can be turned into a [TOTP](struct.TOTP.html),
|
||||||
/// with a default value of 6 for `digits`, None `issuer` and an empty account
|
/// with a default value of 6 for `digits`, None `issuer` and an empty account
|
||||||
|
@ -173,9 +181,9 @@ mod tests {
|
||||||
ISSUER.map(str::to_string),
|
ISSUER.map(str::to_string),
|
||||||
ACCOUNT.to_string(),
|
ACCOUNT.to_string(),
|
||||||
);
|
);
|
||||||
if x < 6 || x > 8 {
|
if !(6..=8).contains(&x) {
|
||||||
assert!(rfc.is_err());
|
assert!(rfc.is_err());
|
||||||
assert_eq!(rfc.unwrap_err(), Rfc6238Error::InvalidDigits)
|
assert!(matches!(rfc.unwrap_err(), Rfc6238Error::InvalidDigits(_)));
|
||||||
} else {
|
} else {
|
||||||
assert!(rfc.is_ok());
|
assert!(rfc.is_ok());
|
||||||
}
|
}
|
||||||
|
@ -196,9 +204,9 @@ mod tests {
|
||||||
let rfc_default = Rfc6238::with_defaults(secret.clone());
|
let rfc_default = Rfc6238::with_defaults(secret.clone());
|
||||||
if secret.len() < 16 {
|
if secret.len() < 16 {
|
||||||
assert!(rfc.is_err());
|
assert!(rfc.is_err());
|
||||||
assert_eq!(rfc.unwrap_err(), Rfc6238Error::SecretTooSmall);
|
assert!(matches!(rfc.unwrap_err(), Rfc6238Error::SecretTooSmall(_)));
|
||||||
assert!(rfc_default.is_err());
|
assert!(rfc_default.is_err());
|
||||||
assert_eq!(rfc_default.unwrap_err(), Rfc6238Error::SecretTooSmall);
|
assert!(matches!(rfc_default.unwrap_err(), Rfc6238Error::SecretTooSmall(_)));
|
||||||
} else {
|
} else {
|
||||||
assert!(rfc.is_ok());
|
assert!(rfc.is_ok());
|
||||||
assert!(rfc_default.is_ok());
|
assert!(rfc_default.is_ok());
|
||||||
|
@ -238,7 +246,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let totp = TOTP::try_from(rfc);
|
let totp = TOTP::try_from(rfc);
|
||||||
assert!(totp.is_err());
|
assert!(totp.is_err());
|
||||||
assert_eq!(totp.unwrap_err(), TotpUrlError::AccountName)
|
assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_)))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -252,7 +260,7 @@ mod tests {
|
||||||
assert_eq!(rfc.account_name, new_account.to_string());
|
assert_eq!(rfc.account_name, new_account.to_string());
|
||||||
let fail = rfc.digits(4);
|
let fail = rfc.digits(4);
|
||||||
assert!(fail.is_err());
|
assert!(fail.is_err());
|
||||||
assert_eq!(fail.unwrap_err(), Rfc6238Error::InvalidDigits);
|
assert!(matches!(fail.unwrap_err(), Rfc6238Error::InvalidDigits(_)));
|
||||||
assert_eq!(rfc.digits, 6);
|
assert_eq!(rfc.digits, 6);
|
||||||
let ok = rfc.digits(8);
|
let ok = rfc.digits(8);
|
||||||
assert!(ok.is_ok());
|
assert!(ok.is_ok());
|
||||||
|
|
|
@ -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<Rfc6238Error> 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())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue