diff --git a/examples/gen_secret.rs b/examples/gen_secret.rs index a536b20..cfb2c60 100644 --- a/examples/gen_secret.rs +++ b/examples/gen_secret.rs @@ -1,16 +1,18 @@ #[cfg(all(feature = "gen_secret", feature = "otpauth"))] -use totp_rs::{Algorithm, Secret, TOTP}; +use totp_rs::{Algorithm, Secret, TOTP, LabeledTOTP}; #[cfg(all(feature = "gen_secret", feature = "otpauth"))] fn main() { let secret = Secret::generate_secret(); - let totp = TOTP::new( - Algorithm::SHA1, - 6, - 1, - 30, - secret.to_bytes().unwrap(), + let totp = LabeledTOTP::new( + TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + secret.to_bytes().unwrap(), + ).unwrap(), None, "account".to_string(), ) diff --git a/examples/rfc-6238.rs b/examples/rfc-6238.rs index ba4ef40..5ce6229 100644 --- a/examples/rfc-6238.rs +++ b/examples/rfc-6238.rs @@ -1,24 +1,8 @@ use totp_rs::{Rfc6238, TOTP}; -#[cfg(feature = "otpauth")] fn main() { let mut rfc = Rfc6238::with_defaults("totp-sercret-123".as_bytes().to_vec()).unwrap(); - // optional, set digits, issuer, account_name - rfc.digits(8).unwrap(); - rfc.issuer("issuer".to_string()); - rfc.account_name("user-account".to_string()); - - // create a TOTP from rfc - let totp = TOTP::from_rfc6238(rfc).unwrap(); - let code = totp.generate_current().unwrap(); - println!("code: {}", code); -} - -#[cfg(not(feature = "otpauth"))] -fn main() { - let mut rfc = Rfc6238::with_defaults("totp-sercret-123".into()).unwrap(); - // optional, set digits, issuer, account_name rfc.digits(8).unwrap(); diff --git a/examples/secret.rs b/examples/secret.rs index a5bb2a7..8566e1d 100644 --- a/examples/secret.rs +++ b/examples/secret.rs @@ -1,6 +1,5 @@ -use totp_rs::{Algorithm, Secret, TOTP}; +use totp_rs::{Algorithm, Secret, TOTP, LabeledTOTP}; -#[cfg(feature = "otpauth")] fn main() { // create TOTP from base32 secret let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); @@ -10,9 +9,12 @@ fn main() { 1, 30, secret_b32.to_bytes().unwrap(), - Some("issuer".to_string()), - "user-account".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "issuer".to_string(), + "user-account".to_string(), + )) .unwrap(); println!( @@ -37,9 +39,12 @@ fn main() { 1, 30, secret_raw.to_bytes().unwrap(), - Some("issuer".to_string()), - "user-account".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "issuer".to_string(), + "user-account".to_string(), + )) .unwrap(); println!("raw {} ; base32 {}", secret_raw, secret_raw.to_encoded()); @@ -48,34 +53,3 @@ fn main() { 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() - ); -} diff --git a/examples/steam.rs b/examples/steam.rs index 5c2d20a..b07c6cf 100644 --- a/examples/steam.rs +++ b/examples/steam.rs @@ -1,12 +1,12 @@ #[cfg(feature = "steam")] -use totp_rs::{Secret, TOTP}; +use totp_rs::{Secret, LabeledTOTP}; #[cfg(feature = "steam")] #[cfg(feature = "otpauth")] fn main() { // create TOTP from base32 secret let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); - let totp_b32 = TOTP::new_steam(secret_b32.to_bytes().unwrap(), "user-account".to_string()); + let totp_b32 = LabeledTOTP::new_steam(secret_b32.to_bytes().unwrap(), "user-account".to_string()); println!( "base32 {} ; raw {}", @@ -24,7 +24,7 @@ fn main() { fn main() { // create TOTP from base32 secret let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); - let totp_b32 = TOTP::new_steam(secret_b32.to_bytes().unwrap()); + let totp_b32 = LabeledTOTP::new_steam(secret_b32.to_bytes().unwrap()); println!( "base32 {} ; raw {}", diff --git a/examples/ttl.rs b/examples/ttl.rs index 806f6c4..88ba421 100644 --- a/examples/ttl.rs +++ b/examples/ttl.rs @@ -1,21 +1,5 @@ -use totp_rs::{Algorithm, TOTP}; +use totp_rs::{Algorithm, TOTP, LabeledTOTP}; -#[cfg(not(feature = "otpauth"))] -fn main() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "my-secret".as_bytes().to_vec()).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, @@ -23,9 +7,12 @@ fn main() { 1, 30, "my-secret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine@github.com".to_string(), + )) .unwrap(); loop { diff --git a/src/custom_providers.rs b/src/custom_providers.rs index 08ec2e8..cdc50cc 100644 --- a/src/custom_providers.rs +++ b/src/custom_providers.rs @@ -1,45 +1,31 @@ #[cfg(feature = "steam")] -use crate::{Algorithm, TOTP}; +use crate::{Algorithm, TOTP, LabeledTOTP}; #[cfg(feature = "steam")] -impl TOTP { - #[cfg(feature = "otpauth")] +impl LabeledTOTP { /// Will create a new instance of TOTP using the Steam algorithm 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}; + /// use totp_rs::{Secret, LabeledTOTP}; /// let secret = Secret::Encoded("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".into()); - /// let totp = TOTP::new_steam(secret.to_bytes().unwrap(), "username".into()); + /// let totp = LabeledTOTP::new_steam(secret.to_bytes().unwrap(), "username".into()); /// ``` - pub fn new_steam(secret: Vec, account_name: String) -> TOTP { + pub fn new_steam(secret: Vec, account_name: String) -> Self { Self::new_unchecked( - Algorithm::Steam, - 5, - 1, - 30, - secret, - Some("Steam".into()), + TOTP::new_unchecked( + Algorithm::Steam, + 5, + 1, + 30, + secret, + ), + "Steam".to_owned(), account_name, ) } - - #[cfg(not(feature = "otpauth"))] - /// Will create a new instance of TOTP using the Steam algorithm 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}; - /// let secret = Secret::Encoded("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".to_string()); - /// let totp = TOTP::new_steam(secret.to_bytes().unwrap()); - /// ``` - pub fn new_steam(secret: Vec) -> TOTP { - Self::new_unchecked(Algorithm::Steam, 5, 1, 30, secret) - } } #[cfg(all(test, feature = "steam"))] @@ -50,7 +36,7 @@ mod test { #[test] #[cfg(feature = "otpauth")] fn get_url_steam() { - let totp = TOTP::new_steam("TestSecretSuperSecret".into(), "constantoine".into()); + let totp = LabeledTOTP::new_steam("TestSecretSuperSecret".into(), "constantoine".into()); let url = totp.get_url(); assert_eq!(url.as_str(), "otpauth://steam/Steam:constantoine?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=5&algorithm=SHA1&issuer=Steam"); } diff --git a/src/lib.rs b/src/lib.rs index c6a9b14..9e7cff2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,6 @@ //! # Examples //! //! ```rust -//! # #[cfg(feature = "otpauth")] { //! use std::time::SystemTime; //! use totp_rs::{Algorithm, TOTP, Secret}; //! @@ -19,17 +18,14 @@ //! 1, //! 30, //! 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 //! # #[cfg(feature = "qr")] { -//! use totp_rs::{Algorithm, TOTP}; +//! use totp_rs::{Algorithm, TOTP, LabeledTOTP}; //! //! let totp = TOTP::new( //! Algorithm::SHA1, @@ -37,7 +33,10 @@ //! 1, //! 30, //! "supersecret_topsecret".as_bytes().to_vec(), -//! Some("Github".to_string()), +//! ).unwrap(); +//! let totp = LabeledTOTP::new( +//! totp, +//! "Github".to_string(), //! "constantoine@github.com".to_string(), //! ).unwrap(); //! let url = totp.get_url(); @@ -81,7 +80,8 @@ type HmacSha512 = hmac::Hmac; const STEAM_CHARS: &str = "23456789BCDFGHJKMNPQRTVWXY"; /// Algorithm enum holds the three standards algorithms for TOTP as per the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A) -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize))] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub enum Algorithm { SHA1, @@ -134,13 +134,10 @@ fn system_time() -> Result { Ok(t) } -/// 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, Copy, Eq, Hash)] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] -pub struct TOTP { +pub struct TOTPRef<'a> { /// SHA-1 is the most widespread algorithm used, and for totp pursposes, SHA-1 hash collisions are [not a problem](https://tools.ietf.org/html/rfc4226#appendix-B.2) as HMAC-SHA-1 is not impacted. It's also the main one cited in [rfc-6238](https://tools.ietf.org/html/rfc6238#section-3) even though the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A) permits the use of SHA-1, SHA-256 and SHA-512. Not all clients support other algorithms then SHA-1 - #[cfg_attr(feature = "zeroize", zeroize(skip))] pub algorithm: Algorithm, /// The number of digits composing the auth code. Per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-5.3), this can oscilate between 6 and 8 digits pub digits: usize, @@ -151,21 +148,13 @@ pub struct TOTP { /// 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 /// /// non-encoded value - pub secret: Vec, - #[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, + pub secret: &'a [u8], } -impl PartialEq for TOTP { +impl<'a> PartialEq for TOTPRef<'a> { /// Will not check for issuer and account_name equality /// As they aren't taken in account for token generation/token checking + #[inline] fn eq(&self, other: &Self) -> bool { if self.algorithm != other.algorithm { return false; @@ -179,27 +168,11 @@ impl PartialEq for TOTP { if self.step != other.step { return false; } - constant_time_eq(self.secret.as_ref(), other.secret.as_ref()) + constant_time_eq(self.secret, other.secret) } } -#[cfg(feature = "otpauth")] -impl core::fmt::Display for TOTP { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "digits: {}; step: {}; alg: {}; issuer: <{}>({})", - self.digits, - self.step, - self.algorithm, - self.issuer.clone().unwrap_or_else(|| "None".to_string()), - self.account_name - ) - } -} - -#[cfg(not(feature = "otpauth"))] -impl core::fmt::Display for TOTP { +impl<'a> core::fmt::Display for TOTPRef<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -209,116 +182,7 @@ impl core::fmt::Display 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 { - 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 - /// * `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(), None, "".to_string()).unwrap(); - /// ``` - /// * `digits`: MUST be between 6 & 8 - /// * `secret`: Must have bitsize of at least 128 - /// * `account_name`: Must not contain `:` - /// * `issuer`: Must not contain `:` - /// - /// # Errors - /// - /// Will return an error if the `digit` or `secret` size is invalid or if `issuer` or `label` contain the character ':' - pub fn new( - algorithm: Algorithm, - digits: usize, - skew: u8, - step: u64, - secret: Vec, - issuer: Option, - account_name: String, - ) -> Result { - crate::rfc::assert_digits(&digits)?; - crate::rfc::assert_secret_length(secret.as_ref())?; - if issuer.is_some() && issuer.as_ref().unwrap().contains(':') { - return Err(TotpUrlError::Issuer(issuer.as_ref().unwrap().to_string())); - } - if account_name.contains(':') { - return Err(TotpUrlError::AccountName(account_name)); - } - Ok(Self::new_unchecked( - algorithm, - digits, - skew, - step, - secret, - issuer, - account_name, - )) - } - - #[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. This is unchecked and does not check the `digits` and `secret` size - /// - /// # 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_unchecked(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap(), None, "".to_string()); - /// ``` - pub fn new_unchecked( - algorithm: Algorithm, - digits: usize, - skew: u8, - step: u64, - secret: Vec, - issuer: Option, - account_name: String, - ) -> TOTP { - TOTP { - algorithm, - digits, - skew, - step, - secret, - issuer, - account_name, - } - } - - #[cfg(not(feature = "otpauth"))] +impl<'a> TOTPRef<'a> { /// 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 @@ -340,14 +204,13 @@ impl TOTP { digits: usize, skew: u8, step: u64, - secret: Vec, - ) -> Result { + secret: &'a [u8], + ) -> Result { crate::rfc::assert_digits(&digits)?; crate::rfc::assert_secret_length(secret.as_ref())?; Ok(Self::new_unchecked(algorithm, digits, skew, step, secret)) } - #[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. This is unchecked and does not check the `digits` and `secret` size /// /// # Description @@ -358,14 +221,15 @@ impl TOTP { /// let secret = Secret::Encoded("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG".to_string()); /// let totp = TOTP::new_unchecked(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap()); /// ``` + #[inline] pub fn new_unchecked( algorithm: Algorithm, digits: usize, skew: u8, step: u64, - secret: Vec, - ) -> TOTP { - TOTP { + secret: &'a [u8], + ) -> Self { + Self { algorithm, digits, skew, @@ -374,19 +238,10 @@ impl TOTP { } } - /// Will create a new instance of TOTP from the given [Rfc6238](struct.Rfc6238.html) struct - /// - /// # Errors - /// - /// Will return an error in case issuer or label contain the character ':' - pub fn from_rfc6238(rfc: Rfc6238) -> Result { - TOTP::try_from(rfc) - } - /// Will sign the given timestamp pub fn sign(&self, time: u64) -> Vec { self.algorithm.sign( - self.secret.as_ref(), + self.secret, (time / self.step).to_be_bytes().as_ref(), ) } @@ -469,45 +324,338 @@ impl TOTP { pub fn get_secret_base32(&self) -> String { base32::encode( base32::Alphabet::RFC4648 { padding: false }, - self.secret.as_ref(), + self.secret, ) } - /// Generate a TOTP from the standard otpauth URL - #[cfg(feature = "otpauth")] - pub fn from_url>(url: S) -> Result { - let (algorithm, digits, skew, step, secret, issuer, account_name) = - Self::parts_from_url(url)?; - TOTP::new(algorithm, digits, skew, step, secret, issuer, account_name) + #[cfg(feature = "qr")] + fn get_qr_draw_canvas(&self, qr: qrcodegen::QrCode) -> image::ImageBuffer, Vec> { + let size = qr.size() as u32; + // "+ 8 * 8" is here to add padding (the white border around the QRCode) + // As some QRCode readers don't work without padding + let image_size = size * 8 + 8 * 8; + let mut canvas = image::GrayImage::new(image_size, image_size); + + // Draw the border + for x in 0..image_size { + for y in 0..image_size { + if (y < 8 * 4 || y >= image_size - 8 * 4) || (x < 8 * 4 || x >= image_size - 8 * 4) + { + canvas.put_pixel(x, y, Luma([255])); + } + } + } + + // The QR inside the white border + for x_qr in 0..size { + for y_qr in 0..size { + // The canvas is a grayscale image without alpha. Hence it's only one 8-bits byte longs + // This clever trick to one-line the value was achieved with advanced mathematics + // And deep understanding of Boolean algebra. + let val = !qr.get_module(x_qr as i32, y_qr as i32) as u8 * 255; + + // Multiply coordinates by width of pixels + // And take into account the 8*4 padding on top and left side + let x_start = x_qr * 8 + 8 * 4; + let y_start = y_qr * 8 + 8 * 4; + + // Draw a 8-pixels-wide square + for x_img in x_start..x_start + 8 { + for y_img in y_start..y_start + 8 { + canvas.put_pixel(x_img, y_img, Luma([val])); + } + } + } + } + canvas + } +} + +/// 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, Eq, Hash)] +#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] +pub struct TOTP { + /// SHA-1 is the most widespread algorithm used, and for totp pursposes, SHA-1 hash collisions are [not a problem](https://tools.ietf.org/html/rfc4226#appendix-B.2) as HMAC-SHA-1 is not impacted. It's also the main one cited in [rfc-6238](https://tools.ietf.org/html/rfc6238#section-3) even though the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A) permits the use of SHA-1, SHA-256 and SHA-512. Not all clients support other algorithms then SHA-1 + pub algorithm: Algorithm, + /// The number of digits composing the auth code. Per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-5.3), this can oscilate between 6 and 8 digits + pub digits: usize, + /// Number of steps allowed as network delay. 1 would mean one step before current step and one step after are valids. The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 1. Anything more is sketchy, and anyone recommending more is, by definition, ugly and stupid + pub skew: u8, + /// Duration in seconds of a step. The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 30 seconds + 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 + /// + /// non-encoded value + pub secret: Vec, +} + +impl PartialEq for TOTP { + /// Will not check for issuer and account_name equality + /// As they aren't taken in account for token generation/token checking + #[inline] + fn eq(&self, other: &Self) -> bool { + self.as_ref().eq(&other.as_ref()) + } +} + +impl core::fmt::Display for TOTP { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + core::fmt::Display::fmt(&self.as_ref(), f) + } +} + +#[cfg(feature = "gen_secret")] +impl Default for TOTP { + fn default() -> Self { + return TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + Secret::generate_secret().to_bytes().unwrap(), + ) + .unwrap(); + } +} + +impl TOTP { + #[inline(always)] + pub fn as_ref(&self) -> TOTPRef { + TOTPRef { algorithm: self.algorithm, digits: self.digits, skew: self.skew, step: self.step, secret: self.secret.as_slice() } } - /// Generate a TOTP from the standard otpauth URL, using `TOTP::new_unchecked` internally - #[cfg(feature = "otpauth")] - pub fn from_url_unchecked>(url: S) -> Result { - let (algorithm, digits, skew, step, secret, issuer, account_name) = - Self::parts_from_url(url)?; - Ok(TOTP::new_unchecked( + /// 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 if the `digit` or `secret` size is invalid + pub fn new( + algorithm: Algorithm, + digits: usize, + skew: u8, + step: u64, + secret: Vec, + ) -> Result { + crate::rfc::assert_digits(&digits)?; + crate::rfc::assert_secret_length(secret.as_ref())?; + Ok(Self::new_unchecked(algorithm, digits, skew, step, secret)) + } + + /// 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. This is unchecked and does not check the `digits` and `secret` size + /// + /// # 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_unchecked(Algorithm::SHA1, 6, 1, 30, secret.to_bytes().unwrap()); + /// ``` + #[inline] + pub fn new_unchecked( + algorithm: Algorithm, + digits: usize, + skew: u8, + step: u64, + secret: Vec, + ) -> TOTP { + TOTP { algorithm, digits, skew, step, secret, - issuer, - account_name, - )) + } + } + + /// Will create a new instance of TOTP from the given [Rfc6238](struct.Rfc6238.html) struct + /// + /// # Errors + /// + /// Will return an error in case issuer or label contain the character ':' + pub fn from_rfc6238(rfc: Rfc6238) -> Result { + Self::try_from(rfc) + } + + /// Will sign the given timestamp + #[inline(always)] + pub fn sign(&self, time: u64) -> Vec { + self.as_ref().sign(time) + } + + /// Will generate a token given the provided timestamp in seconds + #[inline(always)] + pub fn generate(&self, time: u64) -> String { + self.as_ref().generate(time) + } + + /// Returns the timestamp of the first second for the next step + /// given the provided timestamp in seconds + #[inline(always)] + pub fn next_step(&self, time: u64) -> u64 { + self.as_ref().next_step(time) + } + + /// Returns the timestamp of the first second of the next step + /// According to system time + #[inline(always)] + pub fn next_step_current(&self) -> Result { + self.as_ref().next_step_current() + } + + /// Give the ttl (in seconds) of the current token + #[inline(always)] + pub fn ttl(&self) -> Result { + self.as_ref().ttl() + } + + /// Generate a token from the current system time + #[inline(always)] + pub fn generate_current(&self) -> Result { + self.as_ref().generate_current() + } + + /// Will check if token is valid given the provided timestamp in seconds, accounting [skew](struct.TOTP.html#structfield.skew) + #[inline(always)] + pub fn check(&self, token: &str, time: u64) -> bool { + self.as_ref().check(token, time) + } + + /// Will check if token is valid by current system time, accounting [skew](struct.TOTP.html#structfield.skew) + #[inline(always)] + pub fn check_current(&self, token: &str) -> Result { + self.as_ref().check_current(token) + } + + /// Will return the base32 representation of the secret, which might be useful when users want to manually add the secret to their authenticator + #[inline(always)] + pub fn get_secret_base32(&self) -> String { + self.as_ref().get_secret_base32() + } + + #[cfg(feature = "qr")] + #[inline(always)] + fn get_qr_draw_canvas(&self, qr: qrcodegen::QrCode) -> image::ImageBuffer, Vec> { + self.as_ref().get_qr_draw_canvas(qr) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))] +pub struct LabeledTOTP { + pub totp: TOTP, + pub issuer: Option, + pub account_name: Option, +} + +impl From for LabeledTOTP { + #[inline] + fn from(value: TOTP) -> Self { + Self { + issuer: None, + account_name: None, + totp: value, + } + } +} + +impl From for TOTP { + #[inline] + fn from(mut value: LabeledTOTP) -> Self { + Self { + algorithm: value.totp.algorithm, + digits: value.totp.digits, + skew: value.totp.skew, + step: value.totp.step, + secret: std::mem::take(&mut value.totp.secret), + } + } +} + +impl std::ops::Deref for LabeledTOTP { + type Target = TOTP; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.totp + } +} + +impl LabeledTOTP { + pub fn new(totp: TOTP, issuer: impl Into>, account_name: impl Into>) -> Result { + let mut issuer = issuer.into(); + let mut account_name = account_name.into(); + if let Some(issuer) = &mut issuer { + if issuer.contains(':') { + return Err(TotpUrlError::Issuer(std::mem::take(issuer))); + } + } + if let Some(account_name) = &mut account_name { + if account_name.contains(':') { + return Err(TotpUrlError::AccountName(std::mem::take(account_name))); + } + } + Ok(Self::new_unchecked(totp, issuer, account_name)) + } + + #[inline] + pub fn new_unchecked(totp: TOTP, issuer: impl Into>, account_name: impl Into>) -> Self { + Self { + totp, + issuer: issuer.into(), + account_name: account_name.into(), + } + } + + /// Will create a new instance of TOTP from the given [Rfc6238](struct.Rfc6238.html) struct + /// + /// # Errors + /// + /// Will return an error in case issuer or label contain the character ':' + pub fn from_rfc6238(rfc: Rfc6238) -> Result { + Self::try_from(rfc) + } + + /// Generate a TOTP from the standard otpauth URL + #[cfg(feature = "otpauth")] + pub fn from_url>(url: S) -> Result { + let (algorithm, digits, skew, step, secret, issuer, account_name) = + Self::parts_from_url(url)?; + Self::new(TOTP::new(algorithm, digits, skew, step, secret)?, issuer, account_name) + } + + /// Generate a TOTP from the standard otpauth URL, using `TOTP::new_unchecked` internally + #[cfg(feature = "otpauth")] + pub fn from_url_unchecked>(url: S) -> Result { + let (algorithm, digits, skew, step, secret, issuer, account_name) = + Self::parts_from_url(url)?; + Ok(Self::new_unchecked(TOTP::new_unchecked(algorithm, digits, skew, step, secret), issuer, account_name)) } /// Parse the TOTP parts from the standard otpauth URL #[cfg(feature = "otpauth")] fn parts_from_url>( url: S, - ) -> Result<(Algorithm, usize, u8, u64, Vec, Option, String), TotpUrlError> { + ) -> Result<(Algorithm, usize, u8, u64, Vec, Option, Option), TotpUrlError> { let mut algorithm = Algorithm::SHA1; let mut digits = 6; let mut step = 30; let mut secret = Vec::new(); let mut issuer: Option = None; - let mut account_name: String; + let mut account_name: Option = None; let url = Url::parse(url.as_ref()).map_err(TotpUrlError::Url)?; if url.scheme() != "otpauth" { @@ -525,21 +673,23 @@ impl TOTP { } let path = url.path().trim_start_matches('/'); - let path = urlencoding::decode(path) - .map_err(|_| TotpUrlError::AccountNameDecoding(path.to_string()))? - .to_string(); - if path.contains(':') { - let parts = path.split_once(':').unwrap(); - issuer = Some(parts.0.to_owned()); - account_name = parts.1.to_owned(); - } else { - account_name = path; + if !path.is_empty() { + let path = urlencoding::decode(path) + .map_err(|_| TotpUrlError::AccountNameDecoding(path.to_string()))? + .to_string(); + if path.contains(':') { + let parts = path.split_once(':').unwrap(); + issuer = Some(parts.0.to_owned()); + account_name = Some(parts.1.to_owned()); + } else { + account_name = Some(path); + } + let acc_name = account_name.unwrap(); + account_name = Some(urlencoding::decode(acc_name.as_str()) + .map_err(|_| TotpUrlError::AccountName(acc_name.to_string()))? + .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() { match key.as_ref() { #[cfg(feature = "steam")] @@ -618,16 +768,18 @@ impl TOTP { #[allow(unused_mut)] let mut host = "totp"; #[cfg(feature = "steam")] - if self.algorithm == Algorithm::Steam { + if self.totp.algorithm == Algorithm::Steam { host = "steam"; } - let account_name = urlencoding::encode(self.account_name.as_str()).to_string(); - let mut params = vec![format!("secret={}", self.get_secret_base32())]; - if self.digits != 6 { - params.push(format!("digits={}", self.digits)); + let account_name = self.account_name.as_ref() + .map(|account_name| urlencoding::encode(account_name.as_str()).to_string()) + .unwrap_or_else(String::new); + let mut params = vec![format!("secret={}", self.totp.get_secret_base32())]; + if self.totp.digits != 6 { + params.push(format!("digits={}", self.totp.digits)); } - if self.algorithm != Algorithm::SHA1 { - params.push(format!("algorithm={}", self.algorithm)); + if self.totp.algorithm != Algorithm::SHA1 { + params.push(format!("algorithm={}", self.totp.algorithm)); } let label = if let Some(issuer) = &self.issuer { let issuer = urlencoding::encode(issuer); @@ -636,55 +788,13 @@ impl TOTP { } else { account_name }; - if self.step != 30 { - params.push(format!("period={}", self.step)); + if self.totp.step != 30 { + params.push(format!("period={}", self.totp.step)); } format!("otpauth://{}/{}?{}", host, label, params.join("&")) } - #[cfg(feature = "qr")] - fn get_qr_draw_canvas(&self, qr: qrcodegen::QrCode) -> image::ImageBuffer, Vec> { - let size = qr.size() as u32; - // "+ 8 * 8" is here to add padding (the white border around the QRCode) - // As some QRCode readers don't work without padding - let image_size = size * 8 + 8 * 8; - let mut canvas = image::GrayImage::new(image_size, image_size); - - // Draw the border - for x in 0..image_size { - for y in 0..image_size { - if (y < 8 * 4 || y >= image_size - 8 * 4) || (x < 8 * 4 || x >= image_size - 8 * 4) - { - canvas.put_pixel(x, y, Luma([255])); - } - } - } - - // The QR inside the white border - for x_qr in 0..size { - for y_qr in 0..size { - // The canvas is a grayscale image without alpha. Hence it's only one 8-bits byte longs - // This clever trick to one-line the value was achieved with advanced mathematics - // And deep understanding of Boolean algebra. - let val = !qr.get_module(x_qr as i32, y_qr as i32) as u8 * 255; - - // Multiply coordinates by width of pixels - // And take into account the 8*4 padding on top and left side - let x_start = x_qr * 8 + 8 * 4; - let y_start = y_qr * 8 + 8 * 4; - - // Draw a 8-pixels-wide square - for x_img in x_start..x_start + 8 { - for y_img in y_start..y_start + 8 { - canvas.put_pixel(x_img, y_img, Luma([val])); - } - } - } - } - canvas - } - /// Will return a qrcode to automatically add a TOTP as a base64 string. Needs feature `qr` to be enabled! /// Result will be in the form of a string containing a base64-encoded png, which you can embed in HTML without needing /// To store the png as a file. @@ -751,23 +861,24 @@ mod tests { } #[test] - #[cfg(feature = "otpauth")] fn new_wrong_issuer() { let totp = TOTP::new( Algorithm::SHA1, 6, 1, 1, - "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github:".to_string()), - "constantoine@github.com".to_string(), - ); + "TestSecretSuperSecret".as_bytes().to_vec() + ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github:".to_string(), + "constantoine@github.com".to_string() + )); assert!(totp.is_err()); assert!(matches!(totp.unwrap_err(), TotpUrlError::Issuer(_))); } #[test] - #[cfg(feature = "otpauth")] fn new_wrong_account_name() { let totp = TOTP::new( Algorithm::SHA1, @@ -775,15 +886,17 @@ mod tests { 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine:github.com".to_string(), - ); + ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine:github.com".to_string(), + )); assert!(totp.is_err()); assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_))); } #[test] - #[cfg(feature = "otpauth")] fn new_wrong_account_name_no_issuer() { let totp = TOTP::new( Algorithm::SHA1, @@ -791,15 +904,17 @@ mod tests { 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), - None, - "constantoine:github.com".to_string(), - ); + ) + .and_then(|totp| LabeledTOTP::new( + totp, + None, + "constantoine:github.com".to_string(), + )); assert!(totp.is_err()); assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_))); } #[test] - #[cfg(feature = "otpauth")] fn comparison_ok() { let reference = TOTP::new( Algorithm::SHA1, @@ -807,25 +922,30 @@ mod tests { 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine@github.com".to_string(), ) - .unwrap(); + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine@github.com".to_string(), + )) + .unwrap(); let test = TOTP::new( Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine@github.com".to_string(), + )) .unwrap(); assert_eq!(reference, test); } #[test] - #[cfg(not(feature = "otpauth"))] fn comparison_different_algo() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); @@ -834,7 +954,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn comparison_different_digits() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); @@ -843,7 +962,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn comparison_different_skew() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); @@ -852,7 +970,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn comparison_different_step() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); @@ -861,7 +978,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn comparison_different_secret() { let reference = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); @@ -878,9 +994,12 @@ mod tests { 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), - None, - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + None, + "constantoine@github.com".to_string(), + )) .unwrap(); let url = totp.get_url(); assert_eq!( @@ -898,9 +1017,12 @@ mod tests { 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine@github.com".to_string(), + )) .unwrap(); let url = totp.get_url(); assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&issuer=Github"); @@ -915,9 +1037,12 @@ mod tests { 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine@github.com".to_string(), + )) .unwrap(); let url = totp.get_url(); assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&algorithm=SHA256&issuer=Github"); @@ -932,9 +1057,12 @@ mod tests { 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine@github.com".to_string(), + )) .unwrap(); let url = totp.get_url(); assert_eq!(url.as_str(), "otpauth://totp/Github:constantoine%40github.com?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&algorithm=SHA512&issuer=Github"); @@ -950,7 +1078,6 @@ mod tests { } #[test] - #[cfg(feature = "otpauth")] fn ttl_ok() { let totp = TOTP::new( Algorithm::SHA512, @@ -958,15 +1085,17 @@ mod tests { 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine@github.com".to_string(), + )) .unwrap(); - assert!(totp.ttl().is_ok()); + assert!(totp.totp.ttl().is_ok()); } #[test] - #[cfg(not(feature = "otpauth"))] fn returns_base32() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); assert_eq!( @@ -976,14 +1105,12 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn generate_token() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).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".into()).unwrap(); let time = SystemTime::now() @@ -997,28 +1124,24 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn generates_token_sha256() { let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecretSuperSecret".into()).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".into()).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".into()).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".into()).unwrap(); assert!(totp @@ -1028,7 +1151,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn checks_token_with_skew() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecretSuperSecret".into()).unwrap(); assert!( @@ -1037,7 +1159,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn next_step() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".into()).unwrap(); assert!(totp.next_step(0) == 30); @@ -1046,7 +1167,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn next_step_current() { let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".into()).unwrap(); let t = system_time().unwrap(); @@ -1056,119 +1176,125 @@ mod tests { #[test] #[cfg(feature = "otpauth")] fn from_url_err() { - assert!(TOTP::from_url("otpauth://hotp/123").is_err()); - assert!(TOTP::from_url("otpauth://totp/GitHub:test").is_err()); - assert!(TOTP::from_url( + assert!(LabeledTOTP::from_url("otpauth://hotp/123").is_err()); + assert!(LabeledTOTP::from_url("otpauth://totp/GitHub:test").is_err()); + assert!(LabeledTOTP::from_url( "otpauth://totp/GitHub:test:?secret=ABC&digits=8&period=60&algorithm=SHA256" ) .is_err()); - assert!(TOTP::from_url("otpauth://totp/Github:constantoine%40github.com?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").is_err()) + assert!(LabeledTOTP::from_url("otpauth://totp/Github:constantoine%40github.com?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").is_err()) } #[test] #[cfg(feature = "otpauth")] fn from_url_default() { let totp = - TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ") + LabeledTOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ") .unwrap(); assert_eq!( - totp.secret, + totp.totp.secret, base32::decode( base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ) .unwrap() ); - assert_eq!(totp.algorithm, Algorithm::SHA1); - assert_eq!(totp.digits, 6); - assert_eq!(totp.skew, 1); - assert_eq!(totp.step, 30); + assert_eq!(totp.totp.algorithm, Algorithm::SHA1); + assert_eq!(totp.totp.digits, 6); + assert_eq!(totp.totp.skew, 1); + assert_eq!(totp.totp.step, 30); } #[test] #[cfg(feature = "otpauth")] fn from_url_query() { - let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap(); + let totp = LabeledTOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap(); assert_eq!( - totp.secret, + totp.totp.secret, base32::decode( base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ) .unwrap() ); - assert_eq!(totp.algorithm, Algorithm::SHA256); - assert_eq!(totp.digits, 8); - assert_eq!(totp.skew, 1); - assert_eq!(totp.step, 60); + assert_eq!(totp.totp.algorithm, Algorithm::SHA256); + assert_eq!(totp.totp.digits, 8); + assert_eq!(totp.totp.skew, 1); + assert_eq!(totp.totp.step, 60); } #[test] #[cfg(feature = "otpauth")] fn from_url_query_sha512() { - let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA512").unwrap(); + let totp = LabeledTOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA512").unwrap(); assert_eq!( - totp.secret, + totp.totp.secret, base32::decode( base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ) .unwrap() ); - assert_eq!(totp.algorithm, Algorithm::SHA512); - assert_eq!(totp.digits, 8); - assert_eq!(totp.skew, 1); - assert_eq!(totp.step, 60); + assert_eq!(totp.totp.algorithm, Algorithm::SHA512); + assert_eq!(totp.totp.digits, 8); + assert_eq!(totp.totp.skew, 1); + assert_eq!(totp.totp.step, 60); } #[test] #[cfg(feature = "otpauth")] fn from_url_to_url() { - let totp = TOTP::from_url("otpauth://totp/Github:constantoine%40github.com?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); + let totp = LabeledTOTP::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, 30, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine@github.com".to_string(), + )) .unwrap(); - assert_eq!(totp.get_url(), totp_bis.get_url()); + assert_eq!(totp.get_url(), LabeledTOTP::from(totp_bis).get_url()); } #[test] #[cfg(feature = "otpauth")] fn from_url_unknown_param() { - let totp = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256&foo=bar").unwrap(); + let totp = LabeledTOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256&foo=bar").unwrap(); assert_eq!( - totp.secret, + totp.totp.secret, base32::decode( base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ) .unwrap() ); - assert_eq!(totp.algorithm, Algorithm::SHA256); - assert_eq!(totp.digits, 8); - assert_eq!(totp.skew, 1); - assert_eq!(totp.step, 60); + assert_eq!(totp.totp.algorithm, Algorithm::SHA256); + assert_eq!(totp.totp.digits, 8); + assert_eq!(totp.totp.skew, 1); + assert_eq!(totp.totp.step, 60); } #[test] #[cfg(feature = "otpauth")] fn from_url_issuer_special() { - let totp = TOTP::from_url("otpauth://totp/Github%40:constantoine%40github.com?issuer=Github%40&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); + let totp = LabeledTOTP::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, 30, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github@".to_string()), - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github@".to_string(), + "constantoine@github.com".to_string(), + )) .unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); assert_eq!(totp.issuer.as_ref().unwrap(), "Github@"); @@ -1177,64 +1303,70 @@ mod tests { #[test] #[cfg(feature = "otpauth")] fn from_url_account_name_issuer() { - let totp = TOTP::from_url("otpauth://totp/Github:constantoine?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); + let totp = LabeledTOTP::from_url("otpauth://totp/Github:constantoine?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); let totp_bis = TOTP::new( Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine".to_string(), + )) .unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); - assert_eq!(totp.account_name, "constantoine"); + assert_eq!(totp.account_name, Some("constantoine".to_owned())); assert_eq!(totp.issuer.as_ref().unwrap(), "Github"); } #[test] #[cfg(feature = "otpauth")] fn from_url_account_name_issuer_encoded() { - let totp = TOTP::from_url("otpauth://totp/Github%3Aconstantoine?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); + let totp = LabeledTOTP::from_url("otpauth://totp/Github%3Aconstantoine?issuer=Github&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=6&algorithm=SHA1").unwrap(); let totp_bis = TOTP::new( Algorithm::SHA1, 6, 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine".to_string(), + )) .unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); - assert_eq!(totp.account_name, "constantoine"); + assert_eq!(totp.account_name, Some("constantoine".to_owned())); assert_eq!(totp.issuer.as_ref().unwrap(), "Github"); } #[test] #[cfg(feature = "otpauth")] fn from_url_query_issuer() { - let totp = TOTP::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap(); + let totp = LabeledTOTP::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap(); assert_eq!( - totp.secret, + totp.totp.secret, base32::decode( base32::Alphabet::RFC4648 { padding: false }, "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ" ) .unwrap() ); - assert_eq!(totp.algorithm, Algorithm::SHA256); - assert_eq!(totp.digits, 8); - assert_eq!(totp.skew, 1); - assert_eq!(totp.step, 60); + assert_eq!(totp.totp.algorithm, Algorithm::SHA256); + assert_eq!(totp.totp.digits, 8); + assert_eq!(totp.totp.skew, 1); + assert_eq!(totp.totp.step, 60); assert_eq!(totp.issuer.as_ref().unwrap(), "GitHub"); } #[test] #[cfg(feature = "otpauth")] fn from_url_wrong_scheme() { - let totp = TOTP::from_url("http://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256"); + let totp = LabeledTOTP::from_url("http://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256"); assert!(totp.is_err()); let err = totp.unwrap_err(); assert!(matches!(err, TotpUrlError::Scheme(_))); @@ -1243,7 +1375,7 @@ mod tests { #[test] #[cfg(feature = "otpauth")] fn from_url_wrong_algo() { - let totp = TOTP::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=MD5"); + let totp = LabeledTOTP::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=MD5"); assert!(totp.is_err()); let err = totp.unwrap_err(); assert!(matches!(err, TotpUrlError::Algorithm(_))); @@ -1252,7 +1384,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=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256"); + let totp = LabeledTOTP::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(), @@ -1271,9 +1403,12 @@ mod tests { 1, 30, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine@github.com".to_string(), + )) .unwrap(); let url = totp.get_url(); let qr = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Medium) @@ -1297,9 +1432,12 @@ mod tests { 1, 1, "TestSecretSuperSecret".as_bytes().to_vec(), - Some("Github".to_string()), - "constantoine@github.com".to_string(), ) + .and_then(|totp| LabeledTOTP::new( + totp, + "Github".to_string(), + "constantoine@github.com".to_string(), + )) .unwrap(); let qr = totp.get_qr(); assert!(qr.is_ok()); diff --git a/src/rfc.rs b/src/rfc.rs index 30ba33a..d8b5adf 100644 --- a/src/rfc.rs +++ b/src/rfc.rs @@ -1,4 +1,5 @@ use crate::Algorithm; +use crate::LabeledTOTP; use crate::TotpUrlError; use crate::TOTP; @@ -77,15 +78,6 @@ 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: Vec, - #[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, } impl Rfc6238 { @@ -96,27 +88,6 @@ 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: Vec, - issuer: Option, - account_name: String, - ) -> Result { - assert_digits(&digits)?; - assert_secret_length(secret.as_ref())?; - - Ok(Rfc6238 { - algorithm: Algorithm::SHA1, - digits, - skew: 1, - step: 30, - secret, - issuer, - account_name, - }) - } - #[cfg(not(feature = "otpauth"))] pub fn new(digits: usize, secret: Vec) -> Result { assert_digits(&digits)?; assert_secret_length(secret.as_ref())?; @@ -138,12 +109,6 @@ 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: Vec) -> Result { - Rfc6238::new(6, secret, Some("".to_string()), "".to_string()) - } - - #[cfg(not(feature = "otpauth"))] pub fn with_defaults(secret: Vec) -> Result { Rfc6238::new(6, secret) } @@ -154,21 +119,8 @@ impl Rfc6238 { self.digits = value; Ok(()) } - - #[cfg(feature = "otpauth")] - /// Set the `issuer` - pub fn issuer(&mut self, value: String) { - self.issuer = Some(value); - } - - #[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; @@ -178,8 +130,7 @@ impl TryFrom for TOTP { } } -#[cfg(feature = "otpauth")] -impl TryFrom for TOTP { +impl TryFrom for LabeledTOTP { type Error = TotpUrlError; /// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config @@ -190,35 +141,21 @@ impl TryFrom for TOTP { rfc.skew, rfc.step, rfc.secret, - rfc.issuer, - rfc.account_name, - ) + ).map(LabeledTOTP::from) } } #[cfg(test)] mod tests { - #[cfg(feature = "otpauth")] - use crate::TotpUrlError; - 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.into()); @@ -232,7 +169,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn new_rfc_secret() { let mut secret = String::from(""); for _ in 0..=20 { @@ -255,7 +191,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn rfc_to_totp_ok() { let rfc = Rfc6238::new(8, GOOD_SECRET.into()).unwrap(); let totp = TOTP::try_from(rfc); @@ -269,7 +204,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn rfc_to_totp_ok_2() { let rfc = Rfc6238::with_defaults( Secret::Encoded("KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ".to_string()) @@ -287,51 +221,6 @@ mod tests { } #[test] - #[cfg(feature = "otpauth")] - fn rfc_to_totp_fail() { - let rfc = Rfc6238::new( - 8, - GOOD_SECRET.as_bytes().to_vec(), - ISSUER.map(str::to_string), - INVALID_ACCOUNT.to_string(), - ) - .unwrap(); - let totp = TOTP::try_from(rfc); - assert!(totp.is_err()); - assert!(matches!(totp.unwrap_err(), TotpUrlError::AccountName(_))) - } - - #[test] - #[cfg(feature = "otpauth")] - fn rfc_to_totp_ok() { - let rfc = Rfc6238::new( - 8, - GOOD_SECRET.as_bytes().to_vec(), - ISSUER.map(str::to_string), - ACCOUNT.to_string(), - ) - .unwrap(); - let totp = TOTP::try_from(rfc); - assert!(totp.is_ok()); - } - - #[test] - #[cfg(feature = "otpauth")] - fn rfc_with_default_set_values() { - let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.as_bytes().to_vec()).unwrap(); - let ok = rfc.digits(8); - assert!(ok.is_ok()); - assert_eq!(rfc.account_name, ""); - assert_eq!(rfc.issuer, Some("".to_string())); - rfc.issuer("Github".to_string()); - rfc.account_name("constantoine".to_string()); - assert_eq!(rfc.account_name, "constantoine"); - assert_eq!(rfc.issuer, Some("Github".to_string())); - assert_eq!(rfc.digits, 8) - } - - #[test] - #[cfg(not(feature = "otpauth"))] fn rfc_with_default_set_values() { let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.as_bytes().to_vec()).unwrap(); let fail = rfc.digits(4); @@ -344,7 +233,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn digits_error() { let error = crate::Rfc6238Error::InvalidDigits(9); assert_eq!( @@ -354,7 +242,6 @@ mod tests { } #[test] - #[cfg(not(feature = "otpauth"))] fn secret_length_error() { let error = Rfc6238Error::SecretTooSmall(120); assert_eq!(