Edit

thodg/totp-rs/src/secret.rs

Branch :

  • Show log

    Commit

  • Author : Steven Salaun
    Date : 2022-08-06 23:04:50
    Hash : f65a2e84
    Message : 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

  • src/secret.rs
  • 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<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) {
                    Some(bytes) => Ok(bytes),
                    None => Err(SecretParseError::ParseBase32),
                },
            }
        }
    
        /// Transforms a `Secret::Base32` into a `Secret::Plain`
        pub fn as_plain(&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)),
                    },
                    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"),
            }
        }
    }