Finish work on v2

Signed-off-by: constantoine <cleo.rebert@gmail.com>
This commit is contained in:
Cléo Rebert 2022-05-15 13:41:19 +02:00
parent d6d08b1a1e
commit eba97c0ffa
No known key found for this signature in database
GPG Key ID: 74E461C12B6038A3
4 changed files with 173 additions and 110 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea
/target /target
Cargo.lock Cargo.lock
.vscode/settings.json .vscode/settings.json

View File

@ -1,6 +1,6 @@
[package] [package]
name = "totp-rs" name = "totp-rs"
version = "1.4.0" version = "2.0.0"
authors = ["Cleo Rebert <cleo.rebert@gmail.com>"] authors = ["Cleo Rebert <cleo.rebert@gmail.com>"]
edition = "2021" edition = "2021"
readme = "README.md" readme = "README.md"
@ -12,13 +12,12 @@ keywords = ["authentication", "2fa", "totp", "hmac", "otp"]
categories = ["authentication", "web-programming"] categories = ["authentication", "web-programming"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [ "qr", "serde_support", "otpauth" ] features = [ "qr", "serde_support" ]
[features] [features]
default = ["otpauth"] default = []
qr = ["qrcodegen", "image", "base64"] qr = ["qrcodegen", "image", "base64"]
serde_support = ["serde"] serde_support = ["serde"]
otpauth = ["url"]
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true }
@ -26,8 +25,8 @@ sha2 = "~0.10.2"
sha-1 = "~0.10.0" sha-1 = "~0.10.0"
hmac = "~0.12.1" hmac = "~0.12.1"
base32 = "~0.4" base32 = "~0.4"
url = "2.2.2"
constant_time_eq = "~0.2.1" constant_time_eq = "~0.2.1"
qrcodegen = { version = "~1.8", optional = true } qrcodegen = { version = "~1.8", optional = true }
image = { version = "~0.24.2", features = ["png"], optional = true, default-features = false} image = { version = "~0.24.2", features = ["png"], optional = true, default-features = false}
base64 = { version = "~0.13", optional = true } base64 = { version = "~0.13", optional = true }
url = { version = "2.2.2", optional = true }

View File

@ -1,7 +1,9 @@
# totp-rs # totp-rs
![Build Status](https://github.com/constantoine/totp-rs/workflows/Rust/badge.svg) [![docs](https://docs.rs/totp-rs/badge.svg)](https://docs.rs/totp-rs) [![](https://img.shields.io/crates/v/totp-rs.svg)](https://crates.io/crates/totp-rs) [![codecov](https://codecov.io/gh/constantoine/totp-rs/branch/master/graph/badge.svg?token=Q50RAIFVWZ)](https://codecov.io/gh/constantoine/totp-rs) [![cargo-audit](https://github.com/constantoine/totp-rs/actions/workflows/security.yml/badge.svg)](https://github.com/constantoine/totp-rs/actions/workflows/security.yml) ![Build Status](https://github.com/constantoine/totp-rs/workflows/Rust/badge.svg) [![docs](https://docs.rs/totp-rs/badge.svg)](https://docs.rs/totp-rs) [![](https://img.shields.io/crates/v/totp-rs.svg)](https://crates.io/crates/totp-rs) [![codecov](https://codecov.io/gh/constantoine/totp-rs/branch/master/graph/badge.svg?token=Q50RAIFVWZ)](https://codecov.io/gh/constantoine/totp-rs) [![cargo-audit](https://github.com/constantoine/totp-rs/actions/workflows/security.yml/badge.svg)](https://github.com/constantoine/totp-rs/actions/workflows/security.yml)
This library permits the creation of 2FA authentification tokens per TOTP, the verification of said tokens, with configurable time skew, validity time of each token, algorithm and number of digits! Default features are kept as lightweight as possible to ensure small binaries and short compilation time This library permits the creation of 2FA authentification tokens per TOTP, the verification of said tokens, with configurable time skew, validity time of each token, algorithm and number of digits! Default features are kept as lightweight as possible to ensure small binaries and short compilation time.
It now supports parsing [otpauth URLs](https://github.com/google/google-authenticator/wiki/Key-Uri-Format) into a totp object, with sane default values.
Be aware that some authenticator apps will accept the `SHA256` and `SHA512` algorithms but silently fallback to `SHA1` which will make the `check()` function fail due to mismatched algorithms. Be aware that some authenticator apps will accept the `SHA256` and `SHA512` algorithms but silently fallback to `SHA1` which will make the `check()` function fail due to mismatched algorithms.
@ -12,33 +14,33 @@ With optional feature "qr", you can use it to generate a base64 png qrcode
### serde_support ### serde_support
With optional feature "serde_support", library-defined types will be Deserialize-able and Serialize-able With optional feature "serde_support", library-defined types will be Deserialize-able and Serialize-able
### otpauth
With optional feature "otpauth", Support to parse the TOTP parameter from `otpauth` URL
## How to use ## How to use
--- ---
Add it to your `Cargo.toml`: Add it to your `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
totp-rs = "~1.4" totp-rs = "^2.0"
``` ```
You can then do something like: You can then do something like:
```Rust ```Rust
use std::time::SystemTime; use std::time::SystemTime;
use totp_rs::{Algorithm, TOTP}; use totp_rs::{Algorithm, TOTP};
let totp = TOTP::new( fn main() {
let totp = TOTP::new(
Algorithm::SHA1, Algorithm::SHA1,
6, 6,
1, 1,
30, 30,
"supersecret", "supersecret",
); Some("Github".to_string()),
let url = totp.get_url("user@example.com", "my-org.com"); "constantoine@github.com".to_string(),
println!("{}", url); ).unwrap();
let token = totp.generate_current().unwrap(); let url = totp.get_url();
println!("{}", token); println!("{}", url);
let token = totp.generate_current().unwrap();
println!("{}", token);
}
``` ```
### With qrcode generation ### With qrcode generation
@ -46,29 +48,33 @@ println!("{}", token);
Add it to your `Cargo.toml`: Add it to your `Cargo.toml`:
```toml ```toml
[dependencies.totp-rs] [dependencies.totp-rs]
version = "~1.4" version = "^2.0"
features = ["qr"] features = ["qr"]
``` ```
You can then do something like: You can then do something like:
```Rust ```Rust
use totp_rs::{Algorithm, TOTP}; use totp_rs::{Algorithm, TOTP};
let totp = TOTP::new( fn main() {
let totp = TOTP::new(
Algorithm::SHA1, Algorithm::SHA1,
6, 6,
1, 1,
30, 30,
"supersecret", "supersecret",
); Some("Github".to_string()),
let code = totp.get_qr("user@example.com", "my-org.com")?; "constantoine@github.com".to_string(),
println!("{}", code); ).unwrap();
let code = totp.get_qr("user@example.com", "my-org.com")?;
println!("{}", code);
}
``` ```
### With serde support ### With serde support
Add it to your `Cargo.toml`: Add it to your `Cargo.toml`:
```toml ```toml
[dependencies.totp-rs] [dependencies.totp-rs]
version = "~1.4" version = "^2.0"
features = ["serde_support"] features = ["serde_support"]
``` ```
@ -76,15 +82,16 @@ features = ["serde_support"]
Add it to your `Cargo.toml`: Add it to your `Cargo.toml`:
```toml ```toml
[dependencies.totp-rs] [dependencies]
version = "~1.4" totp-rs = "^2.0"
features = ["otpauth"]
``` ```
You can then do something like: You can then do something like:
```Rust ```Rust
use totp_rs::TOTP; use totp_rs::TOTP;
let otpauth = "otpauth://totp/GitHub:test?secret=ABC&issuer=GitHub"; fn main() {
let totp = TOTP::from_url(otpauth).unwrap(); let otpauth = "otpauth://totp/GitHub:constantoine@github.com?secret=ABC&issuer=GitHub";
println!("{}", totp.generate_current().unwrap()); let totp = TOTP::from_url(otpauth).unwrap();
println!("{}", totp.generate_current().unwrap());
}
``` ```

View File

@ -18,8 +18,10 @@
//! 1, //! 1,
//! 30, //! 30,
//! "supersecret", //! "supersecret",
//! ); //! Some("Github".to_string()),
//! let url = totp.get_url("user@example.com", "my-org.com"); //! "constantoine@github.com".to_string(),
//! ).unwrap();
//! let url = totp.get_url();
//! println!("{}", url); //! println!("{}", url);
//! let token = totp.generate_current().unwrap(); //! let token = totp.generate_current().unwrap();
//! println!("{}", token); //! println!("{}", token);
@ -35,8 +37,10 @@
//! 1, //! 1,
//! 30, //! 30,
//! "supersecret", //! "supersecret",
//! ); //! Some("Github".to_string()),
//! let code = totp.get_qr("user@example.com", "my-org.com").unwrap(); //! "constantoine@github.com".to_string(),
//! ).unwrap();
//! let code = totp.get_qr().unwrap();
//! println!("{}", code); //! println!("{}", code);
//! # } //! # }
//! ``` //! ```
@ -51,7 +55,6 @@ use core::fmt;
#[cfg(feature = "qr")] #[cfg(feature = "qr")]
use {base64, image::Luma, qrcodegen}; use {base64, image::Luma, qrcodegen};
#[cfg(feature = "otpauth")]
use url::{Host, ParseError, Url}; use url::{Host, ParseError, Url};
use hmac::Mac; use hmac::Mac;
@ -72,15 +75,15 @@ pub enum Algorithm {
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 {
match *self { return match *self {
Algorithm::SHA1 => { Algorithm::SHA1 => {
return f.write_str("SHA1"); f.write_str("SHA1")
} }
Algorithm::SHA256 => { Algorithm::SHA256 => {
return f.write_str("SHA256"); f.write_str("SHA256")
} }
Algorithm::SHA512 => { Algorithm::SHA512 => {
return f.write_str("SHA512"); f.write_str("SHA512")
} }
} }
} }
@ -89,7 +92,7 @@ impl fmt::Display for Algorithm {
impl Algorithm { impl Algorithm {
fn hash<D>(mut digest: D, data: &[u8]) -> Vec<u8> fn hash<D>(mut digest: D, data: &[u8]) -> Vec<u8>
where where
D: hmac::Mac, D: Mac,
{ {
digest.update(data); digest.update(data);
digest.finalize().into_bytes().to_vec() digest.finalize().into_bytes().to_vec()
@ -111,7 +114,7 @@ fn system_time() -> Result<u64, SystemTimeError> {
Ok(t) Ok(t)
} }
#[derive(Debug)] #[derive(Debug, Eq, PartialEq)]
pub enum TotpUrlError { pub enum TotpUrlError {
Url(ParseError), Url(ParseError),
Scheme, Scheme,
@ -121,7 +124,7 @@ pub enum TotpUrlError {
Digits, Digits,
Step, Step,
Issuer, Issuer,
Label, AccountName,
} }
/// 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
@ -138,15 +141,18 @@ pub struct TOTP<T = Vec<u8>> {
pub step: u64, pub step: u64,
/// As per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-4) the secret should come from a strong source, most likely a CSPRNG. It should be at least 128 bits, but 160 are recommended /// As per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-4) the secret should come from a strong source, most likely a CSPRNG. It should be at least 128 bits, but 160 are recommended
pub secret: T, pub secret: T,
/// The "Github" part of "Github:constantoine". Must not contain a color `:` /// The "Github" part of "Github:constantoine@github.com". Must not contain a colon `:`
/// For example, the name of your service/website. /// For example, the name of your service/website.
pub issuer: String, /// Not mandatory, but strongly recommended!
/// The "alice@google.com" part of "Github:constantoine". Must not contain a color `:` pub issuer: Option<String>,
/// The "constantoine@github.com" part of "Github:constantoine@github.com". Must not contain a colon `:`
/// For example, the name of your user's account. /// For example, the name of your user's account.
pub label: String pub account_name: String
} }
impl <T: AsRef<[u8]>> PartialEq for TOTP<T> { impl <T: AsRef<[u8]>> PartialEq for TOTP<T> {
/// Will not check for issuer and account_name equality
/// As they aren't taken in account for token generation/token checking
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
if self.algorithm != other.algorithm { if self.algorithm != other.algorithm {
return false; return false;
@ -170,12 +176,12 @@ impl<T: AsRef<[u8]>> TOTP<T> {
/// # 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: String, label: 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> {
if issuer.contains(':') { if issuer.is_some() && issuer.as_ref().unwrap().contains(':') {
return Err(TotpUrlError::Issuer); return Err(TotpUrlError::Issuer);
} }
if label.contains(':') { if account_name.contains(':') {
return Err(TotpUrlError::Label); return Err(TotpUrlError::AccountName);
} }
Ok(TOTP { Ok(TOTP {
algorithm, algorithm,
@ -184,7 +190,7 @@ impl<T: AsRef<[u8]>> TOTP<T> {
step, step,
secret, secret,
issuer, issuer,
label, account_name,
}) })
} }
@ -255,10 +261,17 @@ impl<T: AsRef<[u8]>> TOTP<T> {
let mut digits = 6; let mut digits = 6;
let mut step = 30; let mut step = 30;
let mut secret = Vec::new(); let mut secret = Vec::new();
let issuer: String; let mut issuer: Option<String> = None;
let label: String; let account_name: String;
let path = url.path();
if path.contains(':') {
let parts = path.split_once(':').unwrap();
issuer = Some(parts.0.to_owned());
account_name = parts.1.trim_start_matches(':').to_owned();
} else {
account_name = path.to_owned();
}
for (key, value) in url.query_pairs() { for (key, value) in url.query_pairs() {
match key.as_ref() { match key.as_ref() {
@ -282,23 +295,27 @@ impl<T: AsRef<[u8]>> TOTP<T> {
.ok_or(TotpUrlError::Secret)?; .ok_or(TotpUrlError::Secret)?;
} }
"issuer" => { "issuer" => {
issuer = value.parse::<String>().map_err(|_| TotpUrlError::Issuer)?; let param_issuer = value.parse::<String>().map_err(|_| TotpUrlError::Issuer)?;
if issuer.is_some() && param_issuer.as_str() != issuer.as_ref().unwrap() {
return Err(TotpUrlError::Issuer);
}
issuer = Some(param_issuer);
} }
_ => {} _ => {}
} }
} }
if issuer.contains(':') { if issuer.is_some() && issuer.as_ref().unwrap().contains(':') {
return Err(TotpUrlError::Issuer); return Err(TotpUrlError::Issuer);
} }
if label.contains(':') { if account_name.contains(':') {
return Err(TotpUrlError::Label); return Err(TotpUrlError::AccountName);
} }
if secret.is_empty() { if secret.is_empty() {
return Err(TotpUrlError::Secret); return Err(TotpUrlError::Secret);
} }
Ok(TOTP::new(algorithm, digits, 1, step, secret)) TOTP::new(algorithm, digits, 1, step, secret, issuer, account_name)
} }
/// Will generate a standard URL used to automatically add TOTP auths. Usually used with qr codes /// Will generate a standard URL used to automatically add TOTP auths. Usually used with qr codes
@ -306,13 +323,19 @@ impl<T: AsRef<[u8]>> TOTP<T> {
/// Label and issuer will be URL-encoded if needed be /// Label and issuer will be URL-encoded if needed be
/// Secret will be base 32'd without padding, as per RFC. /// Secret will be base 32'd without padding, as per RFC.
pub fn get_url(&self) -> String { pub fn get_url(&self) -> String {
let label: String = url::form_urlencoded::byte_serialize(self.label.as_bytes()).collect(); let label: String;
let issuer: String = url::form_urlencoded::byte_serialize(self.issuer.as_bytes()).collect(); let account_name: String = url::form_urlencoded::byte_serialize(self.account_name.as_bytes()).collect();
if self.issuer.is_some() {
let issuer: String = url::form_urlencoded::byte_serialize(self.issuer.as_ref().unwrap().as_bytes()).collect();
label = format!("{0}:{1}?issuer={0}&", issuer, account_name);
} else {
label = format!("{}?", account_name);
}
format!( format!(
"otpauth://totp/{}?secret={}&issuer={}&digits={}&algorithm={}", "otpauth://totp/{}secret={}&digits={}&algorithm={}",
label, label,
self.get_secret_base32(), self.get_secret_base32(),
issuer,
self.digits.to_string(), self.digits.to_string(),
self.algorithm, self.algorithm,
) )
@ -330,10 +353,10 @@ impl<T: AsRef<[u8]>> TOTP<T> {
/// ///
/// It will also return an error in case it can't encode the qr into a png. This shouldn't happen unless either the qrcode library returns malformed data, or the image library doesn't encode the data correctly /// It will also return an error in case it can't encode the qr into a png. This shouldn't happen unless either the qrcode library returns malformed data, or the image library doesn't encode the data correctly
#[cfg(feature = "qr")] #[cfg(feature = "qr")]
pub fn get_qr(&self, label: &str, issuer: &str) -> Result<String, Box<dyn std::error::Error>> { pub fn get_qr(&self) -> Result<String, Box<dyn std::error::Error>> {
use image::ImageEncoder; use image::ImageEncoder;
let url = self.get_url(label, issuer); let url = self.get_url();
let mut vec = Vec::new(); let mut vec = Vec::new();
let qr = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Medium)?; let qr = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Medium)?;
let size = qr.size() as u32; let size = qr.size() as u32;
@ -394,84 +417,112 @@ impl<T: AsRef<[u8]>> TOTP<T> {
mod tests { mod tests {
use super::*; use super::*;
#[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);
}
#[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);
}
#[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);
}
#[test] #[test]
fn comparison_ok() { fn comparison_ok() {
let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret"); 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"); let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", 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"); 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"); let test = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecret", 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"); 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"); let test = TOTP::new(Algorithm::SHA1, 8, 1, 1, "TestSecret", 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"); 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"); let test = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecret", 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"); 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"); let test = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecret", 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"); 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"); let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretL", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap();
assert_ne!(reference, test); assert_ne!(reference, test);
} }
#[test]
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 url = totp.get_url();
assert_eq!(url.as_str(), "otpauth://totp/constantoine%40github.com?secret=KRSXG5CTMVRXEZLU&digits=6&algorithm=SHA1");
}
#[test] #[test]
fn url_for_secret_matches_sha1() { fn url_for_secret_matches_sha1() {
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret"); let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap();
let url = totp.get_url("test_url", "totp-rs"); let url = totp.get_url();
assert_eq!(url.as_str(), "otpauth://totp/test_url?secret=KRSXG5CTMVRXEZLU&issuer=totp-rs&digits=6&algorithm=SHA1"); assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLU&digits=6&algorithm=SHA1");
} }
#[test] #[test]
fn url_for_secret_matches_sha256() { fn url_for_secret_matches_sha256() {
let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecret"); let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap();
let url = totp.get_url("test_url", "totp-rs"); let url = totp.get_url();
assert_eq!(url.as_str(), "otpauth://totp/test_url?secret=KRSXG5CTMVRXEZLU&issuer=totp-rs&digits=6&algorithm=SHA256"); assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLU&digits=6&algorithm=SHA256");
} }
#[test] #[test]
fn url_for_secret_matches_sha512() { fn url_for_secret_matches_sha512() {
let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecret"); let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap();
let url = totp.get_url("test_url", "totp-rs"); let url = totp.get_url();
assert_eq!(url.as_str(), "otpauth://totp/test_url?secret=KRSXG5CTMVRXEZLU&issuer=totp-rs&digits=6&algorithm=SHA512"); assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLU&digits=6&algorithm=SHA512");
} }
#[test] #[test]
fn returns_base32() { fn returns_base32() {
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret"); 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"); assert_eq!(totp.get_secret_base32().as_str(), "KRSXG5CTMVRXEZLU");
} }
#[test] #[test]
fn generate_token() { fn generate_token() {
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret"); 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"); assert_eq!(totp.generate(1000).as_str(), "718996");
} }
#[test] #[test]
fn generate_token_current() { fn generate_token_current() {
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret"); let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", 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();
@ -480,19 +531,19 @@ mod tests {
#[test] #[test]
fn generates_token_sha256() { fn generates_token_sha256() {
let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecret"); 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"); assert_eq!(totp.generate(1000).as_str(), "480200");
} }
#[test] #[test]
fn generates_token_sha512() { fn generates_token_sha512() {
let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecret"); 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"); assert_eq!(totp.generate(1000).as_str(), "850500");
} }
#[test] #[test]
fn checks_token() { fn checks_token() {
let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecret"); 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("718996", 1000));
assert!(totp.check("712039", 2000)); assert!(totp.check("712039", 2000));
assert!(!totp.check("527544", 2000)); assert!(!totp.check("527544", 2000));
@ -501,28 +552,27 @@ mod tests {
#[test] #[test]
fn checks_token_current() { fn checks_token_current() {
let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecret"); let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecret", 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"); let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", 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("527544", 2000) && totp.check("712039", 2000) && totp.check("714250", 2000)
); );
} }
#[test] #[test]
#[cfg(feature = "otpauth")]
fn from_url_err() { fn from_url_err() {
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());
} }
#[test] #[test]
#[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=ABC").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 }, "ABC").unwrap());
@ -533,7 +583,6 @@ mod tests {
} }
#[test] #[test]
#[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=ABC&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 }, "ABC").unwrap());
@ -543,19 +592,26 @@ mod tests {
assert_eq!(totp.step, 60); assert_eq!(totp.step, 60);
} }
#[test]
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);
assert_eq!(totp.unwrap_err(), TotpUrlError::Issuer);
}
#[test] #[test]
#[cfg(feature = "qr")] #[cfg(feature = "qr")]
fn generates_qr() { fn generates_qr() {
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret"); let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap();
let qr = totp.get_qr("test_url", "totp-rs").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(),
"f671a5a553227a9565c6132024808123f2c9e5e3" "b21a9d4bbb5bd0800bb6bff83a92a2e3314266a5"
); );
} }
} }