From 3bdb91fad77d0809e12bad19d8160527a793f6c1 Mon Sep 17 00:00:00 2001 From: Steven Salaun Date: Sat, 6 Aug 2022 22:58:57 +0200 Subject: [PATCH 1/4] clarifies doc for `secret`: should be non-encoded --- src/lib.rs | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index be92b46..7927289 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! This library permits the creation of 2FA authentification tokens per TOTP, the verification of said tokens, with configurable time skew, validity time of each token, algorithm and number of digits! Default features are kept as low-dependency as possible to ensure small binaries and short compilation time //! -//! Be aware that some authenticator apps will accept the `SHA256` -//! and `SHA512` algorithms but silently fallback to `SHA1` which will +//! Be aware that some authenticator apps will accept the `SHA256` +//! and `SHA512` algorithms but silently fallback to `SHA1` which will //! make the `check()` function fail due to mismatched algorithms. //! //! Use the `SHA1` algorithm to avoid this problem. @@ -45,6 +45,8 @@ //! # } //! ``` +pub use base32; + use constant_time_eq::constant_time_eq; #[cfg(feature = "serde_support")] @@ -144,6 +146,8 @@ pub struct TOTP> { /// 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: T, /// The "Github" part of "Github:constantoine@github.com". Must not contain a colon `:` /// For example, the name of your service/website. @@ -177,8 +181,17 @@ impl > PartialEq for TOTP { impl> TOTP { /// 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, base32 encoded values should be decoded beforehand + /// ``` + /// use totp_rs::{base32, TOTP, Algorithm}; + /// let secret = String::from("NV4S243FMNZGK5A"); + /// let decoded = base32::decode(base32::Alphabet::RFC4648 { padding: false }, &secret).unwrap(); + /// let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, decoded, None, "".to_string()).unwrap(); + /// ``` + /// /// # 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, issuer: Option, account_name: String) -> Result, TotpUrlError> { if issuer.is_some() && issuer.as_ref().unwrap().contains(':') { @@ -244,7 +257,7 @@ impl> TOTP { let basestep = time / self.step - (self.skew as u64); for i in 0..self.skew * 2 + 1 { let step_time = (basestep + (i as u64)) * (self.step as u64); - + if constant_time_eq(self.generate(step_time).as_bytes(), token.as_bytes()) { return true; } @@ -265,7 +278,7 @@ impl> TOTP { self.secret.as_ref(), ) } - + /// Generate a TOTP from the standard otpauth URL #[cfg(feature = "otpauth")] pub fn from_url>(url: S) -> Result>, TotpUrlError> { @@ -276,7 +289,7 @@ impl> TOTP { if url.host() != Some(Host::Domain("totp")) { return Err(TotpUrlError::Host); } - + let mut algorithm = Algorithm::SHA1; let mut digits = 6; let mut step = 30; @@ -292,7 +305,7 @@ impl> TOTP { } else { account_name = path.to_owned(); } - + account_name = urlencoding::decode(account_name.as_str()).map_err(|_| TotpUrlError::AccountName)?.to_string(); for (key, value) in url.query_pairs() { @@ -341,7 +354,7 @@ impl> TOTP { } /// Will generate a standard URL used to automatically add TOTP auths. Usually used with qr codes - /// + /// /// Label and issuer will be URL-encoded if needed be /// Secret will be base 32'd without padding, as per RFC. #[cfg(feature = "otpauth")] @@ -383,12 +396,12 @@ impl> TOTP { let mut vec = Vec::new(); let qr = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Medium)?; 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 + // 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 { @@ -405,12 +418,12 @@ impl> TOTP { // 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 { From f65a2e840a8348c72f10ab7824dcdad062ce045e Mon Sep 17 00:00:00 2001 From: Steven Salaun Date: Sat, 6 Aug 2022 23:04:50 +0200 Subject: [PATCH 2/4] add `Secret` enum & `gen_secret` feature - Make the distinction between encoded/non-encoded secret clear, and allows for easy transformation betwen the two formats - add `gen_secret` feature to allow easy generation of CSPRNG secret, also add function to generate rfc recommended length secret --- Cargo.toml | 6 +- examples/gen_secret.rs | 26 ++++++ examples/secret.rs | 33 ++++++++ src/lib.rs | 2 + src/secret.rs | 185 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 examples/gen_secret.rs create mode 100644 examples/secret.rs create mode 100644 src/secret.rs diff --git a/Cargo.toml b/Cargo.toml index e428b89..c8ace63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ default = [] otpauth = ["url", "urlencoding"] qr = ["qrcodegen", "image", "base64", "otpauth"] serde_support = ["serde"] +gen_secret = ["rand"] [dependencies] serde = { version = "1.0", features = ["derive"], optional = true } @@ -27,8 +28,9 @@ sha-1 = "~0.10.0" hmac = "~0.12.1" base32 = "~0.4" urlencoding = { version = "^2.1.0", optional = true} -url = { version = "^2.2.2", optional = true } +url = { version = "^2.2.2", optional = true } 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 } \ No newline at end of file +base64 = { version = "~0.13", optional = true } +rand = { version = "~0.8.5", optional = true } \ No newline at end of file diff --git a/examples/gen_secret.rs b/examples/gen_secret.rs new file mode 100644 index 0000000..0d063c9 --- /dev/null +++ b/examples/gen_secret.rs @@ -0,0 +1,26 @@ +#[cfg(not(feature = "gen_secret"))] +compile_error!("requires feature gen_secret"); + +use totp_rs::{Secret, TOTP, Algorithm}; + +fn main () { + + let secret = Secret::generate_rfc_secret(); + + let totp = TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + secret.as_bytes().unwrap(), + None, + "account".to_string(), + ).unwrap(); + + println!( + "secret plain: {} ; secret base32 {} ; code: {}", + secret, + secret.as_base32(), + totp.generate_current().unwrap() + ) +} diff --git a/examples/secret.rs b/examples/secret.rs new file mode 100644 index 0000000..abdda1e --- /dev/null +++ b/examples/secret.rs @@ -0,0 +1,33 @@ +use totp_rs::{Secret, TOTP, Algorithm}; + +fn main () { + // create TOTP from base32 secret + let secret_b32 = Secret::Base32(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); + let totp_b32 = TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + secret_b32.as_bytes().unwrap(), + None, + "account".to_string(), + ).unwrap(); + + println!("base32 {} ; plain {}", secret_b32, secret_b32.as_plain().unwrap()); + println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); + + // create TOTP from plain text secret + let secret_plain = Secret::Plain(String::from("plain-string-secret-123")); + let totp_plain = TOTP::new( + Algorithm::SHA1, + 6, + 1, + 30, + secret_plain.as_bytes().unwrap(), + None, + "account".to_string(), + ).unwrap(); + + println!("plain {} ; base32 {}", secret_plain, secret_plain.as_base32()); + println!("code from plain text:\t{}", totp_plain.generate_current().unwrap()); +} diff --git a/src/lib.rs b/src/lib.rs index 7927289..1718439 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,8 @@ //! # } //! ``` +mod secret; +pub use secret::{Secret, SecretParseError}; pub use base32; use constant_time_eq::constant_time_eq; diff --git a/src/secret.rs b/src/secret.rs new file mode 100644 index 0000000..276ac78 --- /dev/null +++ b/src/secret.rs @@ -0,0 +1,185 @@ +use std::string::FromUtf8Error; + +use base32::{self, Alphabet}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SecretParseError { + ParseBase32, + Utf8Error(FromUtf8Error), +} + +/// Representation of a secret either in "plain text" or "base 32" encoded +/// +/// # Examples +/// +/// - Create a TOTP from a "plain text" secret +/// ``` +/// use totp_rs::{Secret, TOTP, Algorithm}; +/// +/// let secret = Secret::Plain(String::from("my-secret")); +/// let totp_plain = TOTP::new( +/// Algorithm::SHA1, +/// 6, +/// 1, +/// 30, +/// secret.as_bytes().unwrap(), +/// None, +/// "account".to_string(), +/// ).unwrap(); +/// +/// println!("code from plain text:\t{}", totp_plain.generate_current().unwrap()); +/// ``` +/// +/// - Create a TOTP from a base32 encoded secret +/// ``` +/// use totp_rs::{Secret, TOTP, Algorithm}; +/// +/// let secret = Secret::Base32(String::from("NV4S243FMNZGK5A")); +/// let totp_base32 = TOTP::new( +/// Algorithm::SHA1, +/// 6, +/// 1, +/// 30, +/// secret.as_bytes().unwrap(), +/// None, +/// "account".to_string(), +/// ).unwrap(); +/// +/// println!("code from base32:\t{}", totp_base32.generate_current().unwrap()); +/// +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Secret { + /// represent a non-encoded "plain text" secret + Plain(String), + /// represent a base32 encoded secret + Base32(String), +} + +impl Secret { + /// Get the inner String value of the enum variant + pub fn inner(&self) -> &String { + match self { + Secret::Plain(s) => s, + Secret::Base32(s) => s, + } + } + + /// Get the inner String value as a Vec of bytes + pub fn as_bytes(&self) -> Result, SecretParseError> { + match self { + Secret::Plain(s) => Ok(s.as_bytes().to_vec()), + Secret::Base32(s) => match base32::decode(Alphabet::RFC4648 { padding: false }, s) { + Some(bytes) => Ok(bytes), + None => Err(SecretParseError::ParseBase32), + }, + } + } + + /// Transforms a `Secret::Base32` into a `Secret::Plain` + pub fn as_plain(&self) -> Result { + match self { + Secret::Plain(_) => Ok(self.clone()), + Secret::Base32(s) => match base32::decode(Alphabet::RFC4648 { padding: false }, s) { + Some(buf) => match String::from_utf8(buf) { + Ok(str) => Ok(Secret::Plain(str)), + Err(e) => Err(SecretParseError::Utf8Error(e)), + }, + None => Err(SecretParseError::ParseBase32), + }, + } + } + + /// Transforms a `Secret::Plain` into a `Secret::Base32` + pub fn as_base32(&self) -> Self { + match self { + Secret::Plain(s) => Secret::Base32(base32::encode( + Alphabet::RFC4648 { padding: false }, + s.as_ref(), + )), + Secret::Base32(_) => self.clone(), + } + } + + /// ⚠️ requires feature `gen_secret` + /// + /// Generate a CSPRNG alpha-numeric string of length `size` + #[cfg(feature = "gen_secret")] + pub fn generate_secret(size: usize) -> Secret { + use rand::distributions::{Alphanumeric, DistString}; + Secret::Plain(Alphanumeric.sample_string(&mut rand::thread_rng(), size)) + } + + /// ⚠️ requires feature `gen_secret` + /// + /// Generate a CSPRNG alpha-numeric string of length 20, + /// the recomended size from [rfc-4226](https://tools.ietf.org/html/rfc4226) + /// + /// > The length of the shared secret MUST be at least 128 bits. + /// > This document RECOMMENDs a shared secret length of 160 bits. + #[cfg(feature = "gen_secret")] + pub fn generate_rfc_secret() -> Secret { + Secret::generate_secret(20) + } +} + +impl std::fmt::Display for Secret { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Secret::Plain(s) => write!(f, "{}", s), + Secret::Base32(s) => write!(f, "{}", s), + } + } +} + +#[cfg(test)] +mod tests { + use super::Secret; + + const PLAIN: &str = "plain-string-secret-123"; + const BASE32: &str = "OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"; + const BYTES: [u8; 23] = [ + 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33, + ]; + + #[test] + fn secret_convert_base32_plain() { + let plain_str = String::from(PLAIN); + let base32_str = String::from(BASE32); + let secret_plain = Secret::Plain(plain_str.clone()); + let secret_base32 = Secret::Base32(base32_str.clone()); + + assert_eq!(&secret_plain.as_base32(), &secret_base32); + assert_eq!(&secret_plain.as_plain().unwrap(), &secret_plain); + + assert_eq!(&secret_base32.as_plain().unwrap(), &secret_plain); + assert_eq!(&secret_base32.as_base32(), &secret_base32); + } + + #[test] + fn secret_as_bytes() { + let plain_str = String::from(PLAIN); + let base32_str = String::from(BASE32); + assert_eq!(Secret::Plain(plain_str).as_bytes().unwrap(), BYTES.to_vec()); + assert_eq!(Secret::Base32(base32_str).as_bytes().unwrap(), BYTES.to_vec()); + } + + #[test] + #[cfg(feature = "gen_secret")] + fn secret_gen_secret() { + match Secret::generate_secret(10) { + Secret::Plain(secret) => assert_eq!(secret.len(), 10), + Secret::Base32(_) => panic!("should be plain"), + } + } + + #[test] + #[cfg(feature = "gen_secret")] + fn secret_gen_rfc_secret() { + match Secret::generate_rfc_secret() { + Secret::Plain(secret) => assert_eq!(secret.len(), 20), + Secret::Base32(_) => panic!("should be plain"), + } + } +} From 3d61027d52b9acc107c4fb0c92de83ad26fbf130 Mon Sep 17 00:00:00 2001 From: Steven Salaun Date: Mon, 8 Aug 2022 20:19:04 +0200 Subject: [PATCH 3/4] Change Secret: Plain to Raw & Base32 to Encoded --- examples/gen_secret.rs | 8 +-- examples/secret.rs | 22 +++--- src/secret.rs | 149 ++++++++++++++++++++--------------------- 3 files changed, 89 insertions(+), 90 deletions(-) diff --git a/examples/gen_secret.rs b/examples/gen_secret.rs index 0d063c9..d8c0878 100644 --- a/examples/gen_secret.rs +++ b/examples/gen_secret.rs @@ -5,22 +5,22 @@ use totp_rs::{Secret, TOTP, Algorithm}; fn main () { - let secret = Secret::generate_rfc_secret(); + let secret = Secret::generate_secret(); let totp = TOTP::new( Algorithm::SHA1, 6, 1, 30, - secret.as_bytes().unwrap(), + secret.to_bytes().unwrap(), None, "account".to_string(), ).unwrap(); println!( - "secret plain: {} ; secret base32 {} ; code: {}", + "secret raw: {} ; secret base32 {} ; code: {}", secret, - secret.as_base32(), + secret.to_encoded(), totp.generate_current().unwrap() ) } diff --git a/examples/secret.rs b/examples/secret.rs index abdda1e..5ff7276 100644 --- a/examples/secret.rs +++ b/examples/secret.rs @@ -2,32 +2,36 @@ use totp_rs::{Secret, TOTP, Algorithm}; fn main () { // create TOTP from base32 secret - let secret_b32 = Secret::Base32(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); + let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); let totp_b32 = TOTP::new( Algorithm::SHA1, 6, 1, 30, - secret_b32.as_bytes().unwrap(), + secret_b32.to_bytes().unwrap(), None, "account".to_string(), ).unwrap(); - println!("base32 {} ; plain {}", secret_b32, secret_b32.as_plain().unwrap()); + println!("base32 {} ; raw {}", secret_b32, secret_b32.to_raw().unwrap()); println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); - // create TOTP from plain text secret - let secret_plain = Secret::Plain(String::from("plain-string-secret-123")); - let totp_plain = TOTP::new( + // 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_plain.as_bytes().unwrap(), + secret_raw.to_bytes().unwrap(), None, "account".to_string(), ).unwrap(); - println!("plain {} ; base32 {}", secret_plain, secret_plain.as_base32()); - println!("code from plain text:\t{}", totp_plain.generate_current().unwrap()); + println!("raw {} ; base32 {}", secret_raw, secret_raw.to_encoded()); + println!("code from raw secret:\t{}", totp_raw.generate_current().unwrap()); } diff --git a/src/secret.rs b/src/secret.rs index 276ac78..5bf517b 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -1,5 +1,4 @@ use std::string::FromUtf8Error; - use base32::{self, Alphabet}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -8,126 +7,123 @@ pub enum SecretParseError { Utf8Error(FromUtf8Error), } -/// Representation of a secret either in "plain text" or "base 32" encoded +/// Representation of a secret either a "raw" \[u8\] or "base 32" encoded String /// /// # Examples /// -/// - Create a TOTP from a "plain text" secret +/// - Create a TOTP from a "raw" secret /// ``` /// use totp_rs::{Secret, TOTP, Algorithm}; /// -/// let secret = Secret::Plain(String::from("my-secret")); -/// let totp_plain = TOTP::new( +/// 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.as_bytes().unwrap(), +/// secret_raw.to_bytes().unwrap(), /// None, /// "account".to_string(), /// ).unwrap(); /// -/// println!("code from plain text:\t{}", totp_plain.generate_current().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 = Secret::Base32(String::from("NV4S243FMNZGK5A")); -/// let totp_base32 = TOTP::new( +/// let secret_b32 = Secret::Encoded(String::from("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")); +/// let totp_b32 = TOTP::new( /// Algorithm::SHA1, /// 6, /// 1, /// 30, -/// secret.as_bytes().unwrap(), +/// secret_b32.to_bytes().unwrap(), /// None, /// "account".to_string(), /// ).unwrap(); /// -/// println!("code from base32:\t{}", totp_base32.generate_current().unwrap()); -/// +/// println!("code from base32:\t{}", totp_b32.generate_current().unwrap()); /// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub enum Secret { - /// represent a non-encoded "plain text" secret - Plain(String), + /// represent a non-encoded "raw" secret + Raw(Vec), /// represent a base32 encoded secret - Base32(String), + Encoded(String), } impl Secret { - /// Get the inner String value of the enum variant - pub fn inner(&self) -> &String { - match self { - Secret::Plain(s) => s, - Secret::Base32(s) => s, - } - } /// Get the inner String value as a Vec of bytes - pub fn as_bytes(&self) -> Result, SecretParseError> { + pub fn to_bytes(&self) -> Result, SecretParseError> { match self { - Secret::Plain(s) => Ok(s.as_bytes().to_vec()), - Secret::Base32(s) => match base32::decode(Alphabet::RFC4648 { padding: false }, s) { + Secret::Raw(s) => Ok(s.to_vec()), + Secret::Encoded(s) => match base32::decode(Alphabet::RFC4648 { padding: false }, s) { Some(bytes) => Ok(bytes), None => Err(SecretParseError::ParseBase32), }, } } - /// Transforms a `Secret::Base32` into a `Secret::Plain` - pub fn as_plain(&self) -> Result { + /// Try to transform a `Secret::Encoded` into a `Secret::Raw` + pub fn to_raw(&self) -> Result { match self { - Secret::Plain(_) => Ok(self.clone()), - Secret::Base32(s) => match base32::decode(Alphabet::RFC4648 { padding: false }, s) { - Some(buf) => match String::from_utf8(buf) { - Ok(str) => Ok(Secret::Plain(str)), - Err(e) => Err(SecretParseError::Utf8Error(e)), - }, + Secret::Raw(_) => Ok(self.clone()), + Secret::Encoded(s) => match base32::decode(Alphabet::RFC4648 { padding: false }, s) { + Some(buf) => Ok(Secret::Raw(buf)), None => Err(SecretParseError::ParseBase32), }, } } - /// Transforms a `Secret::Plain` into a `Secret::Base32` - pub fn as_base32(&self) -> Self { + /// Try to transforms a `Secret::Raw` into a `Secret::Encoded` + pub fn to_encoded(&self) -> Self { match self { - Secret::Plain(s) => Secret::Base32(base32::encode( + Secret::Raw(s) => Secret::Encoded(base32::encode( Alphabet::RFC4648 { padding: false }, - s.as_ref(), + &s, )), - Secret::Base32(_) => self.clone(), + Secret::Encoded(_) => self.clone(), } } /// ⚠️ requires feature `gen_secret` /// - /// Generate a CSPRNG alpha-numeric string of length `size` - #[cfg(feature = "gen_secret")] - pub fn generate_secret(size: usize) -> Secret { - use rand::distributions::{Alphanumeric, DistString}; - Secret::Plain(Alphanumeric.sample_string(&mut rand::thread_rng(), size)) - } - - /// ⚠️ requires feature `gen_secret` - /// - /// Generate a CSPRNG alpha-numeric string of length 20, - /// the recomended size from [rfc-4226](https://tools.ietf.org/html/rfc4226) + /// Generate a CSPRNG binary value of 160 bits, + /// the recomended size from [rfc-4226](https://www.rfc-editor.org/rfc/rfc4226#section-4) /// /// > The length of the shared secret MUST be at least 128 bits. /// > This document RECOMMENDs a shared secret length of 160 bits. + /// + /// ⚠️ The generated secret is not guaranteed to be a valid UTF-8 sequence #[cfg(feature = "gen_secret")] - pub fn generate_rfc_secret() -> Secret { - Secret::generate_secret(20) + pub fn generate_secret() -> Secret { + use rand::Rng; + + let mut rng = rand::thread_rng(); + let mut secret: [u8; 20] = Default::default(); + rng.fill(&mut secret); + Secret::Raw(secret.to_vec()) } } impl std::fmt::Display for Secret { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Secret::Plain(s) => write!(f, "{}", s), - Secret::Base32(s) => write!(f, "{}", s), + Secret::Raw(bytes) => { + let mut s: String = String::new(); + for b in bytes { + s = format!("{}{:02x}", &s, &b); + } + write!(f, "{}", s) + }, + Secret::Encoded(s) => write!(f, "{}", s), } } } @@ -136,50 +132,49 @@ impl std::fmt::Display for Secret { mod tests { use super::Secret; - const PLAIN: &str = "plain-string-secret-123"; const BASE32: &str = "OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG"; const BYTES: [u8; 23] = [ 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33, ]; + const BYTES_DISPLAY: &str = "706c61696e2d737472696e672d7365637265742d313233"; #[test] - fn secret_convert_base32_plain() { - let plain_str = String::from(PLAIN); + fn secret_display() { let base32_str = String::from(BASE32); - let secret_plain = Secret::Plain(plain_str.clone()); - let secret_base32 = Secret::Base32(base32_str.clone()); + let secret_raw = Secret::Raw(BYTES.to_vec()); + let secret_base32 = Secret::Encoded(base32_str.clone()); + println!("{}", secret_raw); + assert_eq!(secret_raw.to_string(), BYTES_DISPLAY.to_string()); + assert_eq!(secret_base32.to_string(), BASE32.to_string()); + } - assert_eq!(&secret_plain.as_base32(), &secret_base32); - assert_eq!(&secret_plain.as_plain().unwrap(), &secret_plain); + #[test] + 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()); - assert_eq!(&secret_base32.as_plain().unwrap(), &secret_plain); - assert_eq!(&secret_base32.as_base32(), &secret_base32); + assert_eq!(&secret_raw.to_encoded(), &secret_base32); + assert_eq!(&secret_raw.to_raw().unwrap(), &secret_raw); + + assert_eq!(&secret_base32.to_raw().unwrap(), &secret_raw); + assert_eq!(&secret_base32.to_encoded(), &secret_base32); } #[test] fn secret_as_bytes() { - let plain_str = String::from(PLAIN); let base32_str = String::from(BASE32); - assert_eq!(Secret::Plain(plain_str).as_bytes().unwrap(), BYTES.to_vec()); - assert_eq!(Secret::Base32(base32_str).as_bytes().unwrap(), BYTES.to_vec()); + assert_eq!(Secret::Raw(BYTES.to_vec()).to_bytes().unwrap(), BYTES.to_vec()); + assert_eq!(Secret::Encoded(base32_str).to_bytes().unwrap(), BYTES.to_vec()); } #[test] #[cfg(feature = "gen_secret")] fn secret_gen_secret() { - match Secret::generate_secret(10) { - Secret::Plain(secret) => assert_eq!(secret.len(), 10), - Secret::Base32(_) => panic!("should be plain"), - } - } - - #[test] - #[cfg(feature = "gen_secret")] - fn secret_gen_rfc_secret() { - match Secret::generate_rfc_secret() { - Secret::Plain(secret) => assert_eq!(secret.len(), 20), - Secret::Base32(_) => panic!("should be plain"), + match Secret::generate_secret() { + Secret::Raw(secret) => assert_eq!(secret.len(), 20), + Secret::Encoded(_) => panic!("should be raw"), } } } From 991a07bb8f3c3ef94c6400e4758ad1c62a462bf3 Mon Sep 17 00:00:00 2001 From: Steven Salaun Date: Mon, 8 Aug 2022 20:50:09 +0200 Subject: [PATCH 4/4] fix doc example --- src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 25c70d5..c33e800 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,12 +175,12 @@ impl> TOTP { /// 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, base32 encoded values should be decoded beforehand - /// ``` - /// use totp_rs::{base32, TOTP, Algorithm}; - /// let secret = String::from("NV4S243FMNZGK5A"); - /// let decoded = base32::decode(base32::Alphabet::RFC4648 { padding: false }, &secret).unwrap(); - /// let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, decoded, None, "".to_string()).unwrap(); + /// * `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 @@ -632,7 +632,7 @@ mod tests { assert!(TOTP::>::from_url("otpauth://totp/GitHub:test").is_err()); assert!(TOTP::>::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()) - + } #[test]