Merge pull request #7 from evenorog/master

Be generic over secret type
This commit is contained in:
Cléo Rebert 2020-08-08 17:18:03 +02:00 committed by GitHub
commit 54116c0e45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 39 additions and 37 deletions

View File

@ -27,7 +27,7 @@ let totp = TOTP::new(
6, 6,
1, 1,
30, 30,
"supersecret".to_owned().into_bytes(), "supersecret",
); );
let time = SystemTime::now() let time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH).unwrap() .duration_since(SystemTime::UNIX_EPOCH).unwrap()
@ -55,7 +55,7 @@ let totp = TOTP::new(
6, 6,
1, 1,
30, 30,
"supersecret".to_owned().into_bytes(), "supersecret",
); );
let code = totp.get_qr("user@example.com", "my-org.com")?; let code = totp.get_qr("user@example.com", "my-org.com")?;
println!("{}", code); println!("{}", code);

View File

@ -11,7 +11,7 @@
//! 6, //! 6,
//! 1, //! 1,
//! 30, //! 30,
//! "supersecret".to_owned().into_bytes(), //! "supersecret",
//! ); //! );
//! let time = SystemTime::now() //! let time = SystemTime::now()
//! .duration_since(SystemTime::UNIX_EPOCH).unwrap() //! .duration_since(SystemTime::UNIX_EPOCH).unwrap()
@ -30,7 +30,7 @@
//! 6, //! 6,
//! 1, //! 1,
//! 30, //! 30,
//! "supersecret".to_owned().into_bytes(), //! "supersecret",
//! ); //! );
//! let code = totp.get_qr("user@example.com", "my-org.com").unwrap(); //! let code = totp.get_qr("user@example.com", "my-org.com").unwrap();
//! println!("{}", code); //! println!("{}", code);
@ -39,8 +39,6 @@
#[cfg(feature = "serde_support")] #[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use base32;
use byteorder::{BigEndian, ReadBytesExt}; use byteorder::{BigEndian, ReadBytesExt};
use std::io::Cursor; use std::io::Cursor;
@ -67,7 +65,7 @@ pub enum Algorithm {
/// 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 /// 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)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct TOTP { pub struct TOTP<T = Vec<u8>> {
/// 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 /// 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
pub algorithm: Algorithm, 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 /// 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
@ -77,18 +75,18 @@ 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 /// 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, 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 /// 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
pub secret: Vec<u8>, pub secret: T,
} }
impl TOTP { impl<T: AsRef<[u8]>> TOTP<T> {
/// 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 /// 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
pub fn new(algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: Vec<u8>) -> TOTP { pub fn new(algorithm: Algorithm, digits: usize, skew: u8, step: u64, secret: T) -> TOTP<T> {
TOTP { TOTP {
algorithm: algorithm, algorithm,
digits: digits, digits,
skew: skew, skew,
step: step, step,
secret: secret, secret,
} }
} }
@ -97,17 +95,17 @@ impl TOTP {
let ctr = (time / self.step).to_be_bytes().to_vec(); let ctr = (time / self.step).to_be_bytes().to_vec();
match self.algorithm { match self.algorithm {
Algorithm::SHA1 => { Algorithm::SHA1 => {
let mut mac = HmacSha1::new_varkey(&self.secret).expect("no key"); let mut mac = HmacSha1::new_varkey(self.secret.as_ref()).expect("no key");
mac.update(&ctr); mac.update(&ctr);
mac.finalize().into_bytes().to_vec() mac.finalize().into_bytes().to_vec()
} }
Algorithm::SHA256 => { Algorithm::SHA256 => {
let mut mac = HmacSha256::new_varkey(&self.secret).expect("no key"); let mut mac = HmacSha256::new_varkey(self.secret.as_ref()).expect("no key");
mac.update(&ctr); mac.update(&ctr);
mac.finalize().into_bytes().to_vec() mac.finalize().into_bytes().to_vec()
} }
Algorithm::SHA512 => { Algorithm::SHA512 => {
let mut mac = HmacSha512::new_varkey(&self.secret).expect("no key"); let mut mac = HmacSha512::new_varkey(self.secret.as_ref()).expect("no key");
mac.update(&ctr); mac.update(&ctr);
mac.finalize().into_bytes().to_vec() mac.finalize().into_bytes().to_vec()
} }
@ -117,8 +115,8 @@ impl TOTP {
/// Will generate a token according to the provided timestamp in seconds /// Will generate a token according to the provided timestamp in seconds
pub fn generate(&self, time: u64) -> String { pub fn generate(&self, time: u64) -> String {
let result: &[u8] = &self.sign(time); let result: &[u8] = &self.sign(time);
let offset = (result.as_ref()[19] & 15) as usize; let offset = (result[19] & 15) as usize;
let mut rdr = Cursor::new(result.as_ref()[offset..offset + 4].to_vec()); let mut rdr = Cursor::new(result[offset..offset + 4].to_vec());
let result = rdr.read_u32::<BigEndian>().unwrap() & 0x7fff_ffff; let result = rdr.read_u32::<BigEndian>().unwrap() & 0x7fff_ffff;
format!( format!(
"{1:00$}", "{1:00$}",
@ -141,7 +139,10 @@ impl TOTP {
/// Will return the base32 representation of the secret, which might be useful when users want to manually add the secret to their authenticator /// Will return the base32 representation of the secret, which might be useful when users want to manually add the secret to their authenticator
pub fn get_secret_base32(&self) -> String { pub fn get_secret_base32(&self) -> String {
base32::encode(base32::Alphabet::RFC4648 { padding: false }, &self.secret) base32::encode(
base32::Alphabet::RFC4648 { padding: false },
self.secret.as_ref(),
)
} }
/// Will generate a standard URL used to automatically add TOTP auths. Usually used with qr codes /// Will generate a standard URL used to automatically add TOTP auths. Usually used with qr codes
@ -169,11 +170,7 @@ impl TOTP {
/// ///
/// It will also return an error in case it can't encode the qr into a png. This shouldn't happen unless either the qrcode library returns malformed data, or the image library doesn't encode the data correctly /// It will also return an error in case it can't encode the qr into a png. This shouldn't happen unless either the qrcode library returns malformed data, or the image library doesn't encode the data correctly
#[cfg(feature = "qr")] #[cfg(feature = "qr")]
pub fn get_qr( pub fn get_qr(&self, label: &str, issuer: &str) -> Result<String, Box<dyn std::error::Error>> {
&self,
label: &str,
issuer: &str,
) -> Result<String, Box<dyn std::error::Error>> {
let url = self.get_url(label, issuer); let url = self.get_url(label, issuer);
let code = QrCode::new(&url)?; let code = QrCode::new(&url)?;
let mut vec = Vec::new(); let mut vec = Vec::new();
@ -195,57 +192,62 @@ mod tests {
#[test] #[test]
fn url_for_secret_matches() { fn url_for_secret_matches() {
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, String::from("TestSecret").into_bytes()); let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret");
let url = totp.get_url("test_url", "totp-rs"); let url = totp.get_url("test_url", "totp-rs");
assert_eq!(url.as_str(), "otpauth://totp/test_url?secret=KRSXG5CTMVRXEZLU&issuer=totp-rs&digits=6&algorithm=SHA1"); assert_eq!(url.as_str(), "otpauth://totp/test_url?secret=KRSXG5CTMVRXEZLU&issuer=totp-rs&digits=6&algorithm=SHA1");
} }
#[test] #[test]
fn returns_base32() { fn returns_base32() {
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, String::from("TestSecret").into_bytes()); let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret");
assert_eq!(totp.get_secret_base32().as_str(), "KRSXG5CTMVRXEZLU"); assert_eq!(totp.get_secret_base32().as_str(), "KRSXG5CTMVRXEZLU");
} }
#[test] #[test]
fn generates_token() { fn generates_token() {
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, String::from("TestSecret").into_bytes()); let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret");
assert_eq!(totp.generate(1000).as_str(), "718996"); assert_eq!(totp.generate(1000).as_str(), "718996");
} }
#[test] #[test]
fn generates_token_sha256() { fn generates_token_sha256() {
let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, String::from("TestSecret").into_bytes()); let totp = TOTP::new(Algorithm::SHA256, 6, 1, 1, "TestSecret");
assert_eq!(totp.generate(1000).as_str(), "423657"); assert_eq!(totp.generate(1000).as_str(), "423657");
} }
#[test] #[test]
fn generates_token_sha512() { fn generates_token_sha512() {
let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, String::from("TestSecret").into_bytes()); let totp = TOTP::new(Algorithm::SHA512, 6, 1, 1, "TestSecret");
assert_eq!(totp.generate(1000).as_str(), "416767"); assert_eq!(totp.generate(1000).as_str(), "416767");
} }
#[test] #[test]
fn checks_token() { fn checks_token() {
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, String::from("TestSecret").into_bytes()); let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret");
assert!(totp.check("718996", 1000)); assert!(totp.check("718996", 1000));
} }
#[test] #[test]
fn checks_token_with_skew() { fn checks_token_with_skew() {
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, String::from("TestSecret").into_bytes()); let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret");
assert!(totp.check("527544", 2000) && totp.check("712039", 2000) && totp.check("714250", 2000)); assert!(
totp.check("527544", 2000) && totp.check("712039", 2000) && totp.check("714250", 2000)
);
} }
#[test] #[test]
#[cfg(feature = "qr")] #[cfg(feature = "qr")]
fn generates_qr() { fn generates_qr() {
use sha1::{Sha1, Digest}; use sha1::{Digest, Sha1};
let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, String::from("TestSecret").into_bytes()); let totp = TOTP::new(Algorithm::SHA1, 6, 1, 1, "TestSecret");
let qr = totp.get_qr("test_url", "totp-rs").unwrap(); let qr = totp.get_qr("test_url", "totp-rs").unwrap();
// Create hash from image // Create hash from image
let hash_digest = Sha1::digest(qr.as_bytes()); let hash_digest = Sha1::digest(qr.as_bytes());
assert_eq!(format!("{:x}", hash_digest).as_str(), "3abc0127e7a2b1013fb25c97ef14422c1fe9e878"); assert_eq!(
format!("{:x}", hash_digest).as_str(),
"3abc0127e7a2b1013fb25c97ef14422c1fe9e878"
);
} }
} }