diff --git a/Cargo.toml b/Cargo.toml index 6a25c01..ea546f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ otpauth = ["url", "urlencoding"] qr = ["qrcodegen", "image", "base64", "otpauth"] serde_support = ["serde"] gen_secret = ["rand"] +zeroize = ["dep:zeroize"] [dependencies] serde = { version = "1.0", features = ["derive"], optional = true } @@ -33,4 +34,5 @@ constant_time_eq = "~0.2.1" qrcodegen = { version = "~1.8", optional = true } image = { version = "~0.24.2", features = ["png"], optional = true, default-features = false} base64 = { version = "~0.13", optional = true } -rand = { version = "~0.8.5", features = ["std_rng", "std"], optional = true, default-features = false } \ No newline at end of file +rand = { version = "~0.8.5", features = ["std_rng", "std"], optional = true, default-features = false } +zeroize = { version = "1.5.7", features = ["alloc", "derive"], optional = true } \ No newline at end of file diff --git a/README.md b/README.md index ec36ab6..7aea502 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ With optional feature "otpauth", support parsing the TOTP parameters from an `ot With optional feature "serde_support", library-defined types `TOTP` and `Algorithm` and will be Deserialize-able and Serialize-able. ### gen_secret With optional feature "gen_secret", a secret will be generated for you to store in database. +### zeroize +Securely zero secret information when the TOTP struct is dropped. # Examples diff --git a/examples/rfc-6238.rs b/examples/rfc-6238.rs index 0dc9912..eefe4ed 100644 --- a/examples/rfc-6238.rs +++ b/examples/rfc-6238.rs @@ -2,7 +2,7 @@ use totp_rs::{Rfc6238, TOTP}; #[cfg(feature = "otpauth")] fn main() { - let mut rfc = Rfc6238::with_defaults("totp-sercret-123").unwrap(); + let mut rfc = Rfc6238::with_defaults("totp-sercret-123".as_bytes().to_vec()).unwrap(); // optional, set digits, issuer, account_name rfc.digits(8).unwrap(); diff --git a/examples/ttl.rs b/examples/ttl.rs index 3de5891..806f6c4 100644 --- a/examples/ttl.rs +++ b/examples/ttl.rs @@ -2,7 +2,7 @@ use totp_rs::{Algorithm, TOTP}; #[cfg(not(feature = "otpauth"))] fn main() { - let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "my-secret".to_string()).unwrap(); + let totp = TOTP::new(Algorithm::SHA1, 6, 1, 30, "my-secret".as_bytes().to_vec()).unwrap(); loop { println!( @@ -22,7 +22,7 @@ fn main() { 6, 1, 30, - "my-secret".to_string(), + "my-secret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) diff --git a/src/lib.rs b/src/lib.rs index 329002d..962cc81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ //! 6, //! 1, //! 30, -//! "supersecret_topsecret", +//! "supersecret_topsecret".as_bytes().to_vec(), //! Some("Github".to_string()), //! "constantoine@github.com".to_string(), //! ).unwrap(); @@ -126,8 +126,10 @@ fn system_time() -> Result { /// 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)] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] -pub struct TOTP> { +#[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 + #[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, @@ -138,7 +140,7 @@ 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: T, + 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. @@ -150,7 +152,7 @@ pub struct TOTP> { pub account_name: String, } -impl> PartialEq for TOTP { +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 fn eq(&self, other: &Self) -> bool { @@ -200,7 +202,7 @@ impl Default for TOTP { } } -impl> TOTP { +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 /// @@ -225,10 +227,10 @@ impl> TOTP { digits: usize, skew: u8, step: u64, - secret: T, + secret: Vec, issuer: Option, account_name: String, - ) -> Result, TotpUrlError> { + ) -> Result { crate::rfc::assert_digits(&digits)?; crate::rfc::assert_secret_length(secret.as_ref())?; if issuer.is_some() && issuer.as_ref().unwrap().contains(':') { @@ -288,7 +290,7 @@ impl> TOTP { /// # Errors /// /// Will return an error in case issuer or label contain the character ':' - pub fn from_rfc6238(rfc: Rfc6238) -> Result, TotpUrlError> { + pub fn from_rfc6238(rfc: Rfc6238) -> Result { TOTP::try_from(rfc) } @@ -369,7 +371,7 @@ impl> TOTP { /// Generate a TOTP from the standard otpauth URL #[cfg(feature = "otpauth")] - pub fn from_url>(url: S) -> Result>, TotpUrlError> { + pub fn from_url>(url: S) -> Result { let url = Url::parse(url.as_ref()).map_err(TotpUrlError::Url)?; if url.scheme() != "otpauth" { return Err(TotpUrlError::Scheme(url.scheme().to_string())); @@ -576,7 +578,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github:".to_string()), "constantoine@github.com".to_string(), ); @@ -592,7 +594,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine:github.com".to_string(), ); @@ -608,7 +610,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), None, "constantoine:github.com".to_string(), ); @@ -624,7 +626,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) @@ -634,7 +636,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) @@ -690,7 +692,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), None, "constantoine@github.com".to_string(), ) @@ -707,7 +709,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) @@ -724,7 +726,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) @@ -741,7 +743,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) @@ -767,7 +769,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) @@ -866,19 +868,19 @@ 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!(TOTP::from_url("otpauth://hotp/123").is_err()); + 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()) + assert!(TOTP::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( + let totp = TOTP::from_url( "otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ", ) .unwrap(); @@ -899,7 +901,7 @@ mod tests { #[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 = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap(); assert_eq!( totp.secret, base32::decode( @@ -917,7 +919,7 @@ mod tests { #[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 = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA512").unwrap(); assert_eq!( totp.secret, base32::decode( @@ -935,13 +937,13 @@ mod tests { #[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 = TOTP::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, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) @@ -952,7 +954,7 @@ mod tests { #[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 = TOTP::from_url("otpauth://totp/GitHub:test?secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256&foo=bar").unwrap(); assert_eq!( totp.secret, base32::decode( @@ -970,25 +972,25 @@ mod tests { #[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 = TOTP::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, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github@".to_string()), "constantoine@github.com".to_string(), ) .unwrap(); assert_eq!(totp.get_url(), totp_bis.get_url()); - assert_eq!(totp.issuer.unwrap(), "Github@"); + 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 = TOTP::from_url("otpauth://totp/GitHub:test?issuer=GitHub&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256").unwrap(); assert_eq!( totp.secret, base32::decode( @@ -1001,13 +1003,13 @@ mod tests { assert_eq!(totp.digits, 8); assert_eq!(totp.skew, 1); assert_eq!(totp.step, 60); - assert_eq!(totp.issuer.unwrap(), "GitHub"); + 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 = TOTP::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(_))); @@ -1016,7 +1018,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 = TOTP::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(_))); @@ -1025,7 +1027,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 = TOTP::from_url("otpauth://totp/GitHub:test?issuer=Gitlab&secret=KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ&digits=8&period=60&algorithm=SHA256"); assert!(totp.is_err()); assert!(matches!( totp.unwrap_err(), @@ -1043,7 +1045,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) @@ -1069,7 +1071,7 @@ mod tests { 6, 1, 1, - "TestSecretSuperSecret", + "TestSecretSuperSecret".as_bytes().to_vec(), Some("Github".to_string()), "constantoine@github.com".to_string(), ) diff --git a/src/rfc.rs b/src/rfc.rs index 34204a7..988503d 100644 --- a/src/rfc.rs +++ b/src/rfc.rs @@ -54,7 +54,7 @@ pub fn assert_secret_length(secret: &[u8]) -> Result<(), Rfc6238Error> { /// use totp_rs::{Rfc6238, TOTP}; /// /// let mut rfc = Rfc6238::with_defaults( -/// "totp-sercret-123" +/// "totp-sercret-123".as_bytes().to_vec() /// ).unwrap(); /// /// // optional, set digits, issuer, account_name @@ -64,7 +64,7 @@ pub fn assert_secret_length(secret: &[u8]) -> Result<(), Rfc6238Error> { /// ``` #[derive(Debug, Clone)] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] -pub struct Rfc6238> { +pub struct Rfc6238 { /// SHA-1 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 @@ -74,7 +74,7 @@ pub struct Rfc6238> { /// The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 30 seconds step: u64, /// As per [rfc-4226](https://tools.ietf.org/html/rfc4226#section-4) the secret should come from a strong source, most likely a CSPRNG. It should be at least 128 bits, but 160 are recommended - secret: T, + 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. @@ -86,7 +86,7 @@ pub struct Rfc6238> { account_name: String, } -impl> Rfc6238 { +impl Rfc6238 { /// Create an [rfc-6238](https://tools.ietf.org/html/rfc6238) compliant set of options that can be turned into a [TOTP](struct.TOTP.html) /// /// # Errors @@ -97,10 +97,10 @@ impl> Rfc6238 { #[cfg(feature = "otpauth")] pub fn new( digits: usize, - secret: T, + secret: Vec, issuer: Option, account_name: String, - ) -> Result, Rfc6238Error> { + ) -> Result { assert_digits(&digits)?; assert_secret_length(secret.as_ref())?; @@ -137,7 +137,7 @@ impl> Rfc6238 { /// - `digits` is lower than 6 or higher than 8 /// - `secret` is smaller than 128 bits (16 characters) #[cfg(feature = "otpauth")] - pub fn with_defaults(secret: T) -> Result, Rfc6238Error> { + pub fn with_defaults(secret: Vec) -> Result { Rfc6238::new(6, secret, Some("".to_string()), "".to_string()) } @@ -177,11 +177,11 @@ impl> TryFrom> for TOTP { } #[cfg(feature = "otpauth")] -impl> TryFrom> for TOTP { +impl TryFrom for TOTP { type Error = TotpUrlError; /// Try to create a [TOTP](struct.TOTP.html) from a [Rfc6238](struct.Rfc6238.html) config - fn try_from(rfc: Rfc6238) -> Result { + fn try_from(rfc: Rfc6238) -> Result { TOTP::new( rfc.algorithm, rfc.digits, @@ -289,7 +289,7 @@ mod tests { fn rfc_to_totp_fail() { let rfc = Rfc6238::new( 8, - GOOD_SECRET.to_string(), + GOOD_SECRET.as_bytes().to_vec(), ISSUER.map(str::to_string), INVALID_ACCOUNT.to_string(), ) @@ -304,7 +304,7 @@ mod tests { fn rfc_to_totp_ok() { let rfc = Rfc6238::new( 8, - GOOD_SECRET.to_string(), + GOOD_SECRET.as_bytes().to_vec(), ISSUER.map(str::to_string), ACCOUNT.to_string(), ) @@ -316,7 +316,7 @@ mod tests { #[test] #[cfg(feature = "otpauth")] fn rfc_with_default_set_values() { - let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.to_string()).unwrap(); + 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, ""); @@ -331,7 +331,7 @@ mod tests { #[test] #[cfg(not(feature = "otpauth"))] fn rfc_with_default_set_values() { - let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.to_string()).unwrap(); + let mut rfc = Rfc6238::with_defaults(GOOD_SECRET.as_bytes().to_vec()).unwrap(); let fail = rfc.digits(4); assert!(fail.is_err()); assert!(matches!(fail.unwrap_err(), Rfc6238Error::InvalidDigits(_)));