Support zeroize feature.

This commit is contained in:
muji 2022-11-02 11:14:18 +08:00
parent c36b3a9507
commit 28ebb0e97c
No known key found for this signature in database
GPG Key ID: A0AAA34940015B01
6 changed files with 62 additions and 56 deletions

View File

@ -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 }
rand = { version = "~0.8.5", features = ["std_rng", "std"], optional = true, default-features = false }
zeroize = { version = "1.5.7", features = ["alloc", "derive"], optional = true }

View File

@ -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

View File

@ -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();

View File

@ -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(),
)

View File

@ -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<u64, SystemTimeError> {
/// 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<T = Vec<u8>> {
#[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<T = Vec<u8>> {
/// 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<u8>,
#[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<T = Vec<u8>> {
pub account_name: String,
}
impl<T: AsRef<[u8]>> PartialEq for TOTP<T> {
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<T: AsRef<[u8]>> TOTP<T> {
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<T: AsRef<[u8]>> TOTP<T> {
digits: usize,
skew: u8,
step: u64,
secret: T,
secret: Vec<u8>,
issuer: Option<String>,
account_name: String,
) -> Result<TOTP<T>, TotpUrlError> {
) -> Result<TOTP, TotpUrlError> {
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<T: AsRef<[u8]>> TOTP<T> {
/// # Errors
///
/// Will return an error in case issuer or label contain the character ':'
pub fn from_rfc6238(rfc: Rfc6238<T>) -> Result<TOTP<T>, TotpUrlError> {
pub fn from_rfc6238(rfc: Rfc6238) -> Result<TOTP, TotpUrlError> {
TOTP::try_from(rfc)
}
@ -369,7 +371,7 @@ impl<T: AsRef<[u8]>> TOTP<T> {
/// Generate a TOTP from the standard otpauth URL
#[cfg(feature = "otpauth")]
pub fn from_url<S: AsRef<str>>(url: S) -> Result<TOTP<Vec<u8>>, TotpUrlError> {
pub fn from_url<S: AsRef<str>>(url: S) -> Result<TOTP, TotpUrlError> {
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::<Vec<u8>>::from_url("otpauth://hotp/123").is_err());
assert!(TOTP::<Vec<u8>>::from_url("otpauth://totp/GitHub:test").is_err());
assert!(TOTP::<Vec<u8>>::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::<Vec<u8>>::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::<Vec<u8>>::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::<Vec<u8>>::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::<Vec<u8>>::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::<Vec<u8>>::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::<Vec<u8>>::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::<Vec<u8>>::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::<Vec<u8>>::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::<Vec<u8>>::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::<Vec<u8>>::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::<Vec<u8>>::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(),
)

View File

@ -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<T = Vec<u8>> {
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<T = Vec<u8>> {
/// 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<u8>,
#[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<T = Vec<u8>> {
account_name: String,
}
impl<T: AsRef<[u8]>> Rfc6238<T> {
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<T: AsRef<[u8]>> Rfc6238<T> {
#[cfg(feature = "otpauth")]
pub fn new(
digits: usize,
secret: T,
secret: Vec<u8>,
issuer: Option<String>,
account_name: String,
) -> Result<Rfc6238<T>, Rfc6238Error> {
) -> Result<Rfc6238, Rfc6238Error> {
assert_digits(&digits)?;
assert_secret_length(secret.as_ref())?;
@ -137,7 +137,7 @@ impl<T: AsRef<[u8]>> Rfc6238<T> {
/// - `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<Rfc6238<T>, Rfc6238Error> {
pub fn with_defaults(secret: Vec<u8>) -> Result<Rfc6238, Rfc6238Error> {
Rfc6238::new(6, secret, Some("".to_string()), "".to_string())
}
@ -177,11 +177,11 @@ impl<T: AsRef<[u8]>> TryFrom<Rfc6238<T>> for TOTP<T> {
}
#[cfg(feature = "otpauth")]
impl<T: AsRef<[u8]>> TryFrom<Rfc6238<T>> for TOTP<T> {
impl TryFrom<Rfc6238> 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<T>) -> Result<Self, Self::Error> {
fn try_from(rfc: Rfc6238) -> Result<Self, Self::Error> {
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(_)));