diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index be67e20..ee7fe1b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -21,8 +21,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Test + - name: All features run: cargo test --all-features + - name: No feature + run: cargo test + - name: otpauth feature + run: cargo test --features=otpauth + - name: gen_secret feature + run: cargo test --features=gen_secret + - name: otpauth+gensecret feature + run: cargo test --features=gen_secret,otpauth coverage: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index c8ace63..0600991 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "totp-rs" -version = "2.0.1" +version = "3.0.0" authors = ["Cleo Rebert "] edition = "2021" readme = "README.md" @@ -33,4 +33,4 @@ constant_time_eq = "~0.2.1" qrcodegen = { version = "~1.8", optional = true } image = { version = "~0.24.2", features = ["png"], optional = true, default-features = false} base64 = { version = "~0.13", optional = true } -rand = { version = "~0.8.5", optional = true } \ No newline at end of file +rand = { version = "~0.8.5", features = ["std_rng", "std"], optional = true, default-features = false } \ No newline at end of file diff --git a/README.md b/README.md index 8ef67f5..39a170c 100644 --- a/README.md +++ b/README.md @@ -10,23 +10,48 @@ Be aware that some authenticator apps will accept the `SHA256` and `SHA512` algo ## Features --- ### qr -With optional feature "qr", you can use it to generate a base64 png qrcode. This will enable feature `otpauth` +With optional feature "qr", you can use it to generate a base64 png qrcode. This will enable feature `otpauth`. ### otpauth -With optional feature "otpauth", support parsing the TOTP parameters from an `otpauth` URL, and generating an `otpauth` URL +With optional feature "otpauth", support parsing the TOTP parameters from an `otpauth` URL, and generating an `otpauth` URL. It adds 2 fields to `TOTP`. ### serde_support -With optional feature "serde_support", library-defined types `TOTP` and `Algorithm` and will be Deserialize-able and Serialize-able +With optional feature "serde_support", library-defined types `TOTP` and `Algorithm` and will be Deserialize-able and Serialize-able. +### gen_secret +With optional feature "gen_secret", a secret will be generated for you to store in database. -## How to use + +# Examples + +## Summarry + +0. [Understanding Secret](#understanding-secret) +1. [Generate a token](#generate-a-token) +2. [Enable qrcode generation](#with-qrcode-generation) +3. [Enable serde support](#with-serde-support) +4. [Enable otpauth url support](#with-otpauth-url-support) +5. [Enable gen_secret support](#with-gensecret) +6. [With RFC-6238 compliant default](#with-rfc-6238-compliant-default) + +### Understanding Secret +--- +This new type was added as a disambiguation between Raw and already base32 encoded secrets. +```Rust + Secret::Raw("TestSecretSuperSecret".as_bytes().to_vec()) +``` +Is equivalent to +```Rust + Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()) +``` +### Generate a token --- Add it to your `Cargo.toml`: ```toml [dependencies] -totp-rs = "^2.0" +totp-rs = "^3.0" ``` You can then do something like: ```Rust use std::time::SystemTime; -use totp_rs::{Algorithm, TOTP}; +use totp_rs::{Algorithm, TOTP, Secret}; fn main() { let totp = TOTP::new( @@ -34,26 +59,16 @@ fn main() { 6, 1, 30, - "supersecret", - Some("Github".to_string()), - "constantoine@github.com".to_string(), + Secret::Raw("TestSecretSuperSecret".as_bytes().to_vec()).to_bytes().unwrap(), ).unwrap(); let token = totp.generate_current().unwrap(); println!("{}", token); } ``` - -### With qrcode generation - -Add it to your `Cargo.toml`: -```toml -[dependencies.totp-rs] -version = "^2.0" -features = ["qr"] -``` -You can then do something like: +Which is equivalent to: ```Rust -use totp_rs::{Algorithm, TOTP}; +use std::time::SystemTime; +use totp_rs::{Algorithm, TOTP, Secret}; fn main() { let totp = TOTP::new( @@ -61,29 +76,54 @@ fn main() { 6, 1, 30, - "supersecret", + Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()).to_bytes().unwrap(), + ).unwrap(); + let token = totp.generate_current().unwrap(); + println!("{}", token); +} +``` +### With qrcode generation +--- +Add it to your `Cargo.toml`: +```toml +[dependencies.totp-rs] +version = "^3.0" +features = ["qr"] +``` +You can then do something like: +```Rust +use totp_rs::{Algorithm, TOTP, Secret}; + +fn main() { + let totp = TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()).to_bytes().unwrap(), Some("Github".to_string()), "constantoine@github.com".to_string(), ).unwrap(); - let code = totp.get_qr("user@example.com", "my-org.com")?; + let code = totp.get_qr()?; println!("{}", code); } ``` ### With serde support +--- Add it to your `Cargo.toml`: ```toml [dependencies.totp-rs] -version = "^2.0" +version = "^3.0" features = ["serde_support"] ``` ### With otpauth url support - +--- Add it to your `Cargo.toml`: ```toml [dependencies.totp-rs] -version = "^2.0" +version = "^3.0" features = ["otpauth"] ``` You can then do something like: @@ -91,8 +131,84 @@ You can then do something like: use totp_rs::TOTP; fn main() { - let otpauth = "otpauth://totp/GitHub:constantoine@github.com?secret=ABC&issuer=GitHub"; + let otpauth = "otpauth://totp/GitHub:constantoine@github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&issuer=GitHub"; let totp = TOTP::from_url(otpauth).unwrap(); println!("{}", totp.generate_current().unwrap()); } +``` + +### With gen_secret +--- +Add it to your `Cargo.toml`: +```toml +[dependencies.totp-rs] +version = "^3.0" +features = ["gen_secret"] +``` +You can then do something like: +```Rust +use totp_rs::{Algorithm, TOTP, Secret}; + +fn main() { + let totp = TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + Secret::default().to_bytes().unwrap(), + Some("Github".to_string()), + "constantoine@github.com".to_string(), + ).unwrap(); + let code = totp.get_qr()?; + println!("{}", code); +} +``` +Which is equivalent to +```Rust +use totp_rs::{Algorithm, TOTP, Secret}; + +fn main() { + let totp = TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + Secret::generate_secret().to_bytes().unwrap(), + Some("Github".to_string()), + "constantoine@github.com".to_string(), + ).unwrap(); + let code = totp.get_qr()?; + println!("{}", code); +} +``` + +### With RFC-6238 compliant default +--- +You can do something like this +```Rust +use totp_rs::{Algorithm, TOTP, Secret, Rfc6238}; + +fn main () { + let mut rfc = Rfc6238::with_defaults( + Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()).to_bytes().unwrap(), + ) + .unwrap(); + + // optional, set digits + rfc.digits(8).unwrap(); + + // create a TOTP from rfc + let totp = TOTP::from_rfc6238(rfc).unwrap(); + let code = totp.generate_current().unwrap(); + println!("code: {}", code); +} +``` +With `gen_secret` feature, you can go even further and have all values by default and a secure secret. + +Note: With `otpauth` feature, `TOTP.issuer` will be `None`, and `TOTP.account_name` will be `""`. Be sure to set those fields before generating an URL/QRCode +```Rust +fn main() { + let totp = TOTP::default(); + println!("code: {}", code); +} ``` \ No newline at end of file diff --git a/examples/gen_secret.rs b/examples/gen_secret.rs index d8c0878..527fc10 100644 --- a/examples/gen_secret.rs +++ b/examples/gen_secret.rs @@ -1,8 +1,7 @@ -#[cfg(not(feature = "gen_secret"))] -compile_error!("requires feature gen_secret"); - +#[cfg(all(feature = "gen_secret", feature = "otpauth"))] use totp_rs::{Secret, TOTP, Algorithm}; +#[cfg(all(feature = "gen_secret", feature = "otpauth"))] fn main () { let secret = Secret::generate_secret(); @@ -24,3 +23,6 @@ fn main () { totp.generate_current().unwrap() ) } + +#[cfg(not(all(feature = "gen_secret", feature = "otpauth")))] +fn main () {} \ No newline at end of file diff --git a/examples/rfc-6238.rs b/examples/rfc-6238.rs index 12a9d59..4e3975c 100644 --- a/examples/rfc-6238.rs +++ b/examples/rfc-6238.rs @@ -1,5 +1,6 @@ use totp_rs::{Rfc6238, TOTP}; +#[cfg(feature = "otpauth")] fn main () { let mut rfc = Rfc6238::with_defaults( "totp-sercret-123" @@ -15,3 +16,18 @@ fn main () { let code = totp.generate_current().unwrap(); println!("code: {}", code); } + +#[cfg(not(feature = "otpauth"))] +fn main () { + let mut rfc = Rfc6238::with_defaults( + "totp-sercret-123" + ).unwrap(); + + // optional, set digits, issuer, account_name + rfc.digits(8).unwrap(); + + // create a TOTP from rfc + let totp = TOTP::from_rfc6238(rfc).unwrap(); + let code = totp.generate_current().unwrap(); + println!("code: {}", code); +} \ No newline at end of file diff --git a/examples/secret.rs b/examples/secret.rs index 5ff7276..adf1eaa 100644 --- a/examples/secret.rs +++ b/examples/secret.rs @@ -1,5 +1,6 @@ use totp_rs::{Secret, TOTP, Algorithm}; +#[cfg(feature = "otpauth")] fn main () { // create TOTP from base32 secret let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); @@ -9,8 +10,8 @@ fn main () { 1, 30, secret_b32.to_bytes().unwrap(), - None, - "account".to_string(), + Some("issuer".to_string()), + "user-account".to_string(), ).unwrap(); println!("base32 {} ; raw {}", secret_b32, secret_b32.to_raw().unwrap()); @@ -28,10 +29,43 @@ fn main () { 1, 30, secret_raw.to_bytes().unwrap(), - None, - "account".to_string(), + Some("issuer".to_string()), + "user-account".to_string(), ).unwrap(); println!("raw {} ; base32 {}", secret_raw, secret_raw.to_encoded()); println!("code from raw secret:\t{}", totp_raw.generate_current().unwrap()); } + +#[cfg(not(feature = "otpauth"))] +fn main () { + // create TOTP from base32 secret + let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); + let totp_b32 = TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + secret_b32.to_bytes().unwrap(), + ).unwrap(); + + println!("base32 {} ; raw {}", secret_b32, secret_b32.to_raw().unwrap()); + println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); + + // create TOTP from raw binary value + let secret = [ + 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33, + ]; + let secret_raw = Secret::Raw(secret.to_vec()); + let totp_raw = TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + secret_raw.to_bytes().unwrap(), + ).unwrap(); + + println!("raw {} ; base32 {}", secret_raw, secret_raw.to_encoded()); + println!("code from raw secret:\t{}", totp_raw.generate_current().unwrap()); +} \ No newline at end of file diff --git a/examples/ttl.rs b/examples/ttl.rs index 7554303..ec2c1a8 100644 --- a/examples/ttl.rs +++ b/examples/ttl.rs @@ -1,5 +1,6 @@ use totp_rs::{Algorithm, TOTP}; +#[cfg(not(feature = "otpauth"))] fn main() { let totp = TOTP::new( Algorithm::SHA1, @@ -7,8 +8,30 @@ fn main() { 1, 30, "my-secret".to_string(), - None, - "account".to_string(), + ).unwrap(); + + loop { + println!( + "code {}\t ttl {}\t valid until: {}", + totp.generate_current().unwrap(), + totp.ttl().unwrap(), + totp.next_step_current().unwrap() + ); + std::thread::sleep(std::time::Duration::from_secs(1)); + } +} + + +#[cfg(feature = "otpauth")] +fn main() { + let totp = TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + "my-secret".to_string(), + Some("Github".to_string()), + "constantoine@github.com".to_string() ).unwrap(); loop { diff --git a/src/lib.rs b/src/lib.rs index c33e800..382585c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,20 +9,22 @@ //! # Examples //! //! ```rust +//! # #[cfg(feature = "otpauth")] { //! use std::time::SystemTime; -//! use totp_rs::{Algorithm, TOTP}; +//! use totp_rs::{Algorithm, TOTP, Secret}; //! //! let totp = TOTP::new( //! Algorithm::SHA1, //! 6, //! 1, //! 30, -//! "supersecret_topsecret", +//! Secret::Raw("TestSecretSuperSecret".as_bytes().to_vec()).to_bytes().unwrap(), //! Some("Github".to_string()), //! "constantoine@github.com".to_string(), //! ).unwrap(); //! let token = totp.generate_current().unwrap(); //! println!("{}", token); +//! # } //! ``` //! //! ```rust @@ -142,10 +144,12 @@ pub struct TOTP> { /// /// non-encoded value pub secret: T, + #[cfg(feature = "otpauth")] /// The "Github" part of "Github:constantoine@github.com". Must not contain a colon `:` /// For example, the name of your service/website. /// Not mandatory, but strongly recommended! pub issuer: Option, + #[cfg(feature = "otpauth")] /// The "constantoine@github.com" part of "Github:constantoine@github.com". Must not contain a colon `:` /// For example, the name of your user's account. pub account_name: String @@ -171,7 +175,23 @@ impl > PartialEq for TOTP { } } +#[cfg(all(feature = "gen_secret", not(feature = "otpauth")))] +impl Default for TOTP { + fn default() -> Self { + return TOTP::new(Algorithm::SHA1, 6, 1, 30, Secret::generate_secret().to_bytes().unwrap()).unwrap() + } +} + +#[cfg(all(feature = "gen_secret", feature = "otpauth"))] +impl Default for TOTP { + fn default() -> Self { + return TOTP::new(Algorithm::SHA1, 6, 1, 30, Secret::generate_secret().to_bytes().unwrap(), None, "".to_string()).unwrap() + } +} + impl> TOTP { + + #[cfg(feature = "otpauth")] /// Will create a new instance of TOTP with given parameters. See [the doc](struct.TOTP.html#fields) for reference as to how to choose those values /// /// # Description @@ -210,6 +230,35 @@ impl> TOTP { }) } + #[cfg(not(feature = "otpauth"))] + /// Will create a new instance of TOTP with given parameters. See [the doc](struct.TOTP.html#fields) for reference as to how to choose those values + /// + /// # Description + /// * `secret`: expect a non-encoded value, to pass in base32 string use `Secret::Encoded(String)` + /// + /// ```rust + /// use totp_rs::{Secret, TOTP, Algorithm}; + /// let secret = Secret::Encoded("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG".to_string()); + /// let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap()).unwrap(); + /// ``` + /// * `digits`: MUST be between 6 & 8 + /// * `secret`: Must have bitsize of at least 128 + /// + /// # Errors + /// + /// Will return an error in case issuer or label contain the character ':' + pub fn new(algorithm: Algorithm, digits: usize,skew: u8, step: u64, secret: T) -> Result, TotpUrlError> { + crate::rfc::assert_digits(&digits)?; + crate::rfc::assert_secret_length(secret.as_ref())?; + Ok(TOTP { + algorithm, + digits, + skew, + step, + secret, + }) + } + /// Will create a new instance of TOTP from the given [Rfc6238](struct.Rfc6238.html) struct /// /// # Errors @@ -462,6 +511,17 @@ mod tests { use super::*; #[test] + #[cfg(feature = "gen_secret")] + fn default_values() { + let totp = TOTP::default(); + assert_eq!(totp.algorithm, Algorithm::SHA1); + assert_eq!(totp.digits, 6); + assert_eq!(totp.skew, 1); + assert_eq!(totp.step, 30) + } + + #[test] + #[cfg(feature = "otpauth")] fn new_wrong_issuer() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github:".to_string()), "constantoine@github.com".to_string()); assert!(totp.is_err()); @@ -469,6 +529,7 @@ mod tests { } #[test] + #[cfg(feature = "otpauth")] fn new_wrong_account_name() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine:github.com".to_string()); assert!(totp.is_err()); @@ -476,6 +537,7 @@ mod tests { } #[test] + #[cfg(feature = "otpauth")] fn new_wrong_account_name_no_issuer() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", None, "constantoine:github.com".to_string()); assert!(totp.is_err()); @@ -483,6 +545,7 @@ mod tests { } #[test] + #[cfg(feature = "otpauth")] fn comparison_ok() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); @@ -490,37 +553,42 @@ mod tests { } #[test] + #[cfg(not(feature = "otpauth"))] fn comparison_different_algo() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); + let test = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret").unwrap(); assert_ne!(reference, test); } #[test] + #[cfg(not(feature = "otpauth"))] fn comparison_different_digits() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA1, 8, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); + let test = TOTP::new(Algorithm::SHA1, 8, 1, 1, "TestSecretSuperSecret").unwrap(); assert_ne!(reference, test); } #[test] + #[cfg(not(feature = "otpauth"))] fn comparison_different_skew() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); + let test = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret").unwrap(); assert_ne!(reference, test); } #[test] + #[cfg(not(feature = "otpauth"))] fn comparison_different_step() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); + let test = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret").unwrap(); assert_ne!(reference, test); } #[test] + #[cfg(not(feature = "otpauth"))] fn comparison_different_secret() { - let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); - let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretDifferentSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); + let test = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretDifferentSecret").unwrap(); assert_ne!(reference, test); } @@ -557,20 +625,23 @@ mod tests { } #[test] + #[cfg(not(feature = "otpauth"))] fn returns_base32() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); assert_eq!(totp.get_secret_base32().as_str(), "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ"); } #[test] + #[cfg(not(feature = "otpauth"))] fn generate_token() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); assert_eq!(totp.generate(1000).as_str(), "659761"); } #[test] + #[cfg(not(feature = "otpauth"))] fn generate_token_current() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); let time = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH).unwrap() .as_secs(); @@ -578,49 +649,56 @@ mod tests { } #[test] + #[cfg(not(feature = "otpauth"))] fn generates_token_sha256() { - let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret").unwrap(); assert_eq!(totp.generate(1000).as_str(), "076417"); } #[test] + #[cfg(not(feature = "otpauth"))] fn generates_token_sha512() { - let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecretSuperSecret").unwrap(); assert_eq!(totp.generate(1000).as_str(), "473536"); } #[test] + #[cfg(not(feature = "otpauth"))] fn checks_token() { - let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret").unwrap(); assert!(totp.check("659761", 1000)); } #[test] + #[cfg(not(feature = "otpauth"))] fn checks_token_current() { - let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 0, 1, "TestSecretSuperSecret").unwrap(); assert!(totp.check_current(&totp.generate_current().unwrap()).unwrap()); assert!(!totp.check_current("bogus").unwrap()); } #[test] + #[cfg(not(feature = "otpauth"))] fn checks_token_with_skew() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret").unwrap(); assert!( totp.check("174269", 1000) && totp.check("659761", 1000) && totp.check("260393", 1000) ); } #[test] + #[cfg(not(feature = "otpauth"))] fn next_step() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret").unwrap(); assert!(totp.next_step(0) == 30); assert!(totp.next_step(29) == 30); assert!(totp.next_step(30) == 60); } #[test] + #[cfg(not(feature = "otpauth"))] fn next_step_current() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret", Some("Github".to_string()), "constantoine@github.com".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret").unwrap(); let t = system_time().unwrap(); assert!(totp.next_step_current().unwrap() == totp.next_step(t)); } @@ -689,7 +767,7 @@ mod tests { #[test] #[cfg(feature = "otpauth")] fn from_url_query_different_issuers() { - let totp = TOTP::>::from_url("otpauth://totp/GitHub:test?issuer=Gitlab&secret=TestSecretSuperSecret&digits=8&period=60&algorithm=SHA256"); + let totp = TOTP::>::from_url("otpauth://totp/GitHub:test?issuer=Gitlab&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256"); assert!(totp.is_err()); assert!(matches!(totp.unwrap_err(), TotpUrlError::IssuerMistmatch(_, _))); } diff --git a/src/rfc.rs b/src/rfc.rs index 808a1e2..f5f763c 100644 --- a/src/rfc.rs +++ b/src/rfc.rs @@ -59,8 +59,6 @@ pub fn assert_secret_length(secret: &[u8]) -> Result<(), Rfc6238Error> { /// /// // optional, set digits, issuer, account_name /// rfc.digits(8).unwrap(); -/// rfc.issuer("issuer".to_string()); -/// rfc.account_name("user-account".to_string()); /// /// let totp = TOTP::from_rfc6238(rfc).unwrap(); /// ``` @@ -77,10 +75,12 @@ pub struct Rfc6238> { 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 secret: T, + #[cfg(feature = "otpauth")] /// The "Github" part of "Github:constantoine@github.com". Must not contain a colon `:` /// For example, the name of your service/website. /// Not mandatory, but strongly recommended! issuer: Option, + #[cfg(feature = "otpauth")] /// The "constantoine@github.com" part of "Github:constantoine@github.com". Must not contain a colon `:` /// For example, the name of your user's account. account_name: String, @@ -94,6 +94,7 @@ impl> Rfc6238 { /// will return a [Rfc6238Error](enum.Rfc6238Error.html) when /// - `digits` is lower than 6 or higher than 8 /// - `secret` is smaller than 128 bits (16 characters) + #[cfg(feature = "otpauth")] pub fn new( digits: usize, secret: T, @@ -113,6 +114,22 @@ impl> Rfc6238 { account_name, }) } + #[cfg(not(feature = "otpauth"))] + pub fn new( + digits: usize, + secret: T, + ) -> Result, Rfc6238Error> { + assert_digits(&digits)?; + assert_secret_length(secret.as_ref())?; + + Ok(Rfc6238 { + algorithm: Algorithm::SHA1, + digits, + skew: 1, + step: 30, + secret, + }) + } /// 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 @@ -122,8 +139,14 @@ impl> Rfc6238 { /// will return a [Rfc6238Error](enum.Rfc6238Error.html) when /// - `digits` is lower than 6 or higher than 8 /// - `secret` is smaller than 128 bits (16 characters) + #[cfg(feature = "otpauth")] pub fn with_defaults(secret: T) -> Result, Rfc6238Error> { - Rfc6238::new(6, secret, None, "".to_string()) + Rfc6238::new(6, secret, Some("".to_string()), "".to_string()) + } + + #[cfg(not(feature = "otpauth"))] + pub fn with_defaults(secret: T) -> Result, Rfc6238Error> { + Rfc6238::new(6, secret) } /// Set the `digits` @@ -133,17 +156,36 @@ impl> Rfc6238 { Ok(()) } + #[cfg(feature = "otpauth")] /// Set the `issuer` pub fn issuer(&mut self, value: String) { self.issuer = Some(value); } - /// Seet the `account_name` + #[cfg(feature = "otpauth")] + /// Set the `account_name` pub fn account_name(&mut self, value: String) { self.account_name = value; } } +#[cfg(not(feature = "otpauth"))] +impl> TryFrom> for TOTP { + type Error = TotpUrlError; + + /// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config + fn try_from(rfc: Rfc6238) -> Result { + TOTP::new( + rfc.algorithm, + rfc.digits, + rfc.skew, + rfc.step, + rfc.secret, + ) + } +} + +#[cfg(feature = "otpauth")] impl> TryFrom> for TOTP { type Error = TotpUrlError; @@ -156,30 +198,39 @@ impl> TryFrom> for TOTP { rfc.step, rfc.secret, rfc.issuer, - rfc.account_name, + rfc.account_name ) } } #[cfg(test)] mod tests { + #[cfg(feature = "otpauth")] use crate::TotpUrlError; - use super::{Rfc6238, Rfc6238Error, TOTP}; + use super::{Rfc6238, TOTP}; + + #[cfg(not(feature = "otpauth"))] + use super::Rfc6238Error; + + #[cfg(not(feature = "otpauth"))] + use crate::Secret; const GOOD_SECRET: &str = "01234567890123456789"; + #[cfg(feature = "otpauth")] const ISSUER: Option<&str> = None; + #[cfg(feature = "otpauth")] const ACCOUNT: &str = "valid-account"; + #[cfg(feature = "otpauth")] const INVALID_ACCOUNT: &str = ":invalid-account"; #[test] + #[cfg(not(feature = "otpauth"))] fn new_rfc_digits() { for x in 0..=20 { let rfc = Rfc6238::new( x, GOOD_SECRET.to_string(), - ISSUER.map(str::to_string), - ACCOUNT.to_string(), ); if !(6..=8).contains(&x) { assert!(rfc.is_err()); @@ -191,6 +242,7 @@ mod tests { } #[test] + #[cfg(not(feature = "otpauth"))] fn new_rfc_secret() { let mut secret = String::from(""); for _ in 0..=20 { @@ -198,8 +250,6 @@ mod tests { let rfc = Rfc6238::new( 6, secret.clone(), - ISSUER.map(str::to_string), - ACCOUNT.to_string(), ); let rfc_default = Rfc6238::with_defaults(secret.clone()); if secret.len() < 16 { @@ -215,12 +265,11 @@ mod tests { } #[test] + #[cfg(not(feature = "otpauth"))] fn rfc_to_totp_ok() { let rfc = Rfc6238::new( 8, GOOD_SECRET.to_string(), - ISSUER.map(str::to_string), - ACCOUNT.to_string(), ) .unwrap(); let totp = TOTP::try_from(rfc); @@ -228,14 +277,29 @@ mod tests { let otp = totp.unwrap(); assert_eq!(&otp.secret, GOOD_SECRET); assert_eq!(otp.algorithm, crate::Algorithm::SHA1); - assert_eq!(&otp.account_name, ACCOUNT); assert_eq!(otp.digits, 8); - assert_eq!(otp.issuer, ISSUER.map(str::to_string)); assert_eq!(otp.skew, 1); assert_eq!(otp.step, 30) } #[test] + #[cfg(not(feature = "otpauth"))] + fn rfc_to_totp_ok_2() { + let rfc = Rfc6238::with_defaults( + Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()).to_bytes().unwrap(), + ) + .unwrap(); + let totp = TOTP::try_from(rfc); + assert!(totp.is_ok()); + let otp = totp.unwrap(); + assert_eq!(otp.algorithm, crate::Algorithm::SHA1); + assert_eq!(otp.digits, 6); + assert_eq!(otp.skew, 1); + assert_eq!(otp.step, 30) + } + + #[test] + #[cfg(feature = "otpauth")] fn rfc_to_totp_fail() { let rfc = Rfc6238::new( 8, @@ -250,14 +314,23 @@ mod tests { } #[test] + #[cfg(feature = "otpauth")] + fn rfc_to_totp_ok() { + let rfc = Rfc6238::new( + 8, + GOOD_SECRET.to_string(), + ISSUER.map(str::to_string), + ACCOUNT.to_string(), + ) + .unwrap(); + let totp = TOTP::try_from(rfc); + assert!(!totp.is_err()); + } + + #[test] + #[cfg(not(feature = "otpauth"))] fn rfc_with_default_set_values() { - let new_account = "new-account"; - let new_issuer = String::from("new-issuer"); let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.to_string()).unwrap(); - rfc.issuer(new_issuer.clone()); - assert_eq!(rfc.issuer, Some(new_issuer)); - rfc.account_name(new_account.to_string()); - assert_eq!(rfc.account_name, new_account.to_string()); let fail = rfc.digits(4); assert!(fail.is_err()); assert!(matches!(fail.unwrap_err(), Rfc6238Error::InvalidDigits(_))); diff --git a/src/secret.rs b/src/secret.rs index 5bf517b..68ba340 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -1,3 +1,82 @@ +//! Representation of a secret either a "raw" \[u8\] or "base 32" encoded String +//! +//! # Examples +//! +//! - Create a TOTP from a "raw" secret +//! ``` +//! # #[cfg(not(feature = "otpauth"))] { +//! use totp_rs::{Secret, TOTP, Algorithm}; +//! +//! let secret = [ +//! 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65, +//! 0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33, +//! ]; +//! let secret_raw = Secret::Raw(secret.to_vec()); +//! let totp_raw = TOTP::new( +//! Algorithm::SHA1, +//! 6, +//! 1, +//! 30, +//! secret_raw.to_bytes().unwrap(), +//! ).unwrap(); +//! +//! println!("code from raw secret:\t{}", totp_raw.generate_current().unwrap()); +//! # } +//! ``` +//! +//! - Create a TOTP from a base32 encoded secret +//! ``` +//! # #[cfg(not(feature = "otpauth"))] { +//! use totp_rs::{Secret, TOTP, Algorithm}; +//! +//! let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); +//! let totp_b32 = TOTP::new( +//! Algorithm::SHA1, +//! 6, +//! 1, +//! 30, +//! secret_b32.to_bytes().unwrap(), +//! ).unwrap(); +//! +//! println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); +//! # } +//! +//! ``` +//! - Create a TOTP from a Generated Secret +//! ``` +//! # #[cfg(all(feature = "gen_secret", not(feature = "otpauth")))] { +//! use totp_rs::{Secret, TOTP, Algorithm}; +//! +//! let secret_b32 = Secret::default(); +//! let totp_b32 = TOTP::new( +//! Algorithm::SHA1, +//! 6, +//! 1, +//! 30, +//! secret_b32.to_bytes().unwrap(), +//! ).unwrap(); +//! +//! println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); +//! # } +//! ``` +//! - Create a TOTP from a Generated Secret 2 +//! ``` +//! # #[cfg(all(feature = "gen_secret", not(feature = "otpauth")))] { +//! use totp_rs::{Secret, TOTP, Algorithm}; +//! +//! let secret_b32 = Secret::generate_secret(); +//! let totp_b32 = TOTP::new( +//! Algorithm::SHA1, +//! 6, +//! 1, +//! 30, +//! secret_b32.to_bytes().unwrap(), +//! ).unwrap(); +//! +//! println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); +//! # } +//! ``` + use std::string::FromUtf8Error; use base32::{self, Alphabet}; @@ -7,49 +86,6 @@ pub enum SecretParseError { Utf8Error(FromUtf8Error), } -/// Representation of a secret either a "raw" \[u8\] or "base 32" encoded String -/// -/// # Examples -/// -/// - Create a TOTP from a "raw" secret -/// ``` -/// use totp_rs::{Secret, TOTP, Algorithm}; -/// -/// let secret = [ -/// 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65, -/// 0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33, -/// ]; -/// let secret_raw = Secret::Raw(secret.to_vec()); -/// let totp_raw = TOTP::new( -/// Algorithm::SHA1, -/// 6, -/// 1, -/// 30, -/// secret_raw.to_bytes().unwrap(), -/// None, -/// "account".to_string(), -/// ).unwrap(); -/// -/// println!("code from raw secret:\t{}", totp_raw.generate_current().unwrap()); -/// ``` -/// -/// - Create a TOTP from a base32 encoded secret -/// ``` -/// use totp_rs::{Secret, TOTP, Algorithm}; -/// -/// let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); -/// let totp_b32 = TOTP::new( -/// Algorithm::SHA1, -/// 6, -/// 1, -/// 30, -/// secret_b32.to_bytes().unwrap(), -/// None, -/// "account".to_string(), -/// ).unwrap(); -/// -/// println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); -/// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum Secret { /// represent a non-encoded "raw" secret @@ -58,6 +94,13 @@ pub enum Secret { Encoded(String), } +#[cfg(feature = "gen_secret")] +impl Default for Secret { + fn default() -> Self { + return Secret::generate_secret() + } +} + impl Secret { /// Get the inner String value as a Vec of bytes @@ -87,7 +130,7 @@ impl Secret { match self { Secret::Raw(s) => Secret::Encoded(base32::encode( Alphabet::RFC4648 { padding: false }, - &s, + s, )), Secret::Encoded(_) => self.clone(), } @@ -108,7 +151,7 @@ impl Secret { let mut rng = rand::thread_rng(); let mut secret: [u8; 20] = Default::default(); - rng.fill(&mut secret); + rng.fill(&mut secret[..]); Secret::Raw(secret.to_vec()) } } @@ -143,7 +186,7 @@ mod tests { fn secret_display() { let base32_str = String::from(BASE32); let secret_raw = Secret::Raw(BYTES.to_vec()); - let secret_base32 = Secret::Encoded(base32_str.clone()); + let secret_base32 = Secret::Encoded(base32_str); println!("{}", secret_raw); assert_eq!(secret_raw.to_string(), BYTES_DISPLAY.to_string()); assert_eq!(secret_base32.to_string(), BASE32.to_string()); @@ -153,7 +196,7 @@ mod tests { fn secret_convert_base32_raw() { let base32_str = String::from(BASE32); let secret_raw = Secret::Raw(BYTES.to_vec()); - let secret_base32 = Secret::Encoded(base32_str.clone()); + let secret_base32 = Secret::Encoded(base32_str); assert_eq!(&secret_raw.to_encoded(), &secret_base32); assert_eq!(&secret_raw.to_raw().unwrap(), &secret_raw); @@ -169,6 +212,14 @@ mod tests { assert_eq!(Secret::Encoded(base32_str).to_bytes().unwrap(), BYTES.to_vec()); } + #[test] + fn secret_from_string() { + let raw: Secret = Secret::Raw("TestSecretSuperSecret".as_bytes().to_vec()); + let encoded: Secret = Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()); + assert_eq!(raw.to_encoded(), encoded); + assert_eq!(raw, encoded.to_raw().unwrap()); + } + #[test] #[cfg(feature = "gen_secret")] fn secret_gen_secret() {