Commit 3d61027d52b9acc107c4fb0c92de83ad26fbf130

Steven Salaun 2022-08-08T20:19:04

Change Secret: Plain to Raw & Base32 to Encoded

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<u8>),
     /// 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<Vec<u8>, SecretParseError> {
+    pub fn to_bytes(&self) -> Result<Vec<u8>, 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<Self, SecretParseError> {
+    /// Try to transform a `Secret::Encoded` into a `Secret::Raw`
+    pub fn to_raw(&self) -> Result<Self, SecretParseError> {
         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_display() {
+        let base32_str = String::from(BASE32);
+        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());
+    }
 
     #[test]
-    fn secret_convert_base32_plain() {
-        let plain_str = String::from(PLAIN);
+    fn secret_convert_base32_raw() {
         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());
 
-        assert_eq!(&secret_plain.as_base32(), &secret_base32);
-        assert_eq!(&secret_plain.as_plain().unwrap(), &secret_plain);
+        assert_eq!(&secret_raw.to_encoded(), &secret_base32);
+        assert_eq!(&secret_raw.to_raw().unwrap(), &secret_raw);
 
-        assert_eq!(&secret_base32.as_plain().unwrap(), &secret_plain);
-        assert_eq!(&secret_base32.as_base32(), &secret_base32);
+        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"),
         }
     }
 }