Commit e4915a6446f33d8882ee1a11f56d21c7cbe71cf9

Cleo Rebert 2020-04-13T16:39:57

Finally, my own library. With a fix on the QR code generation

diff --git a/Cargo.toml b/Cargo.toml
index a43c562..896cd94 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,13 +1,16 @@
 [package]
 name = "totp"
-version = "0.1.0"
+version = "0.2.0"
 authors = ["Cleo Rebert <cleo.rebert@gmail.com>"]
 edition = "2018"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+ring = "0.16.12"
+byteorder = "1.3.4"
 otpauth = "0.4.1"
+base32 = "0.4.0"
 qrcode = "0.12.0"
 image = "0.23.3"
 base64 = "0.12.0"
diff --git a/README.md b/README.md
index a488b7a..a926a15 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # totp-rs
 
-This library is a cheap wrapper around otpauth, qrcode and image to seamlessly manage Time-based One-Time Password authentification
+This library permits the creation of authentification tokens per TOTP, the verification of said tokens, with configurable time skew, validity time of each token, algorithm and number of digits!
 
 ## How to use
 
diff --git a/src/lib.rs b/src/lib.rs
index 932ccf3..559a384 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,31 +1,111 @@
-//! This library is a cheap wrapper around otpauth, qrcode and image to seamlessly manage
-//! Time-based One-Time Password authentification
+//! This library permits the creation of authentification tokens per TOTP, the verification of said tokens, with configurable time skew, and validity time of each toke, algorithm and number of digits!
 
-use std::time::{SystemTime, UNIX_EPOCH};
-use otpauth::TOTP;
+use base32;
+use ring::hmac;
+use std::io::Cursor;
+use byteorder::{BigEndian, ReadBytesExt};
 
 use qrcode::QrCode;
 use image::Luma;
 use base64;
 
-/// Will check if provided code is valid with provided secret, with a tolerance of 15 seconds offest
-pub fn verify(code: u32, secret: String) -> bool {
-    let auth = TOTP::new(secret);
-    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
-    for i in 0..2 {
-        if auth.verify(code, 30, now - 15 + i * 15) {
-            return true;
+/// Algorithm enum holds the three standards algorithms for TOTP as per the [reference implementation](https://tools.ietf.org/html/rfc6238#appendix-A)
+#[derive(Debug)]
+pub enum Algorithm {
+    SHA1,
+    SHA256,
+    SHA512,
+}
+
+/// 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)]
+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
+    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
+    digits: usize,
+    /// Number of steps allowed as network delay. 1 would mean one step before current step and one step after are valids. The recommended value per [rfc-6238](https://tools.ietf.org/html/rfc6238#section-5.2) is 1. Anything more is sketchy, and anyone recommending more is, by definition, ugly and stuTOTPpid
+    skew: u8,
+    /// Duration in seconds of a step. 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: Vec<u8>,
+}
+
+impl TOTP {
+    /// 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 {
+        TOTP {
+            algorithm: algorithm,
+            digits: digits,
+            skew: skew,
+            step: step,
+            secret: secret,
         }
     }
-    false
-}
 
-/// Will return a qrcode to automatically add a TOTP as a base64 string
-pub fn get_qr(secret: String, mail: String) -> String {
-    let auth = TOTP::new(secret);
-    let code = QrCode::new(auth.to_uri(format!("account:{}@42l.fr", mail), "42l.fr".to_string())).unwrap();
-    let mut vec = Vec::new();
-    let encoder = image::png::PNGEncoder::new(&mut vec);
-    encoder.encode(&code.render::<Luma<u8>>().build().to_vec(), 360, 360, image::ColorType::L8).unwrap();
-    base64::encode(vec)
+    /// Will generate a token according to the provided timestamp in seconds
+    pub fn generate(&self, time: u64) -> String {
+        let key: hmac::Key;
+        match self.algorithm {
+            Algorithm::SHA1 => key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, &self.secret),
+            Algorithm::SHA256 => key = hmac::Key::new(hmac::HMAC_SHA256, &self.secret),
+            Algorithm::SHA512 => key = hmac::Key::new(hmac::HMAC_SHA512, &self.secret),
+        }
+        let ctr = (time / self.step).to_be_bytes().to_vec();
+        let result = hmac::sign(&key, &ctr);
+        let offset = (result.as_ref()[19] & 15) as usize;
+        let mut rdr = Cursor::new(result.as_ref()[offset..offset + 4].to_vec());
+        let result = rdr.read_u32::<BigEndian>().unwrap() & 0x7fff_ffff;
+        format!("{1:00$}", self.digits, result % (10 as u32).pow(self.digits as u32))
+    }
+
+    /// Will check if token is valid by current time, accounting [skew](struct.TOTP.html#structfield.skew)
+    pub fn check(&self, token: String, time: u64) -> bool {
+        let key: hmac::Key;
+        match self.algorithm {
+            Algorithm::SHA1 => key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, &self.secret),
+            Algorithm::SHA256 => key = hmac::Key::new(hmac::HMAC_SHA256, &self.secret),
+            Algorithm::SHA512 => key = hmac::Key::new(hmac::HMAC_SHA512, &self.secret),
+        }
+        let basestep = time / 30 - (self.skew as u64);
+        for _i in 0..self.skew * 2 + 1 {
+            let result = hmac::sign(&key, &basestep.to_be_bytes().to_vec());
+            let offset = (result.as_ref()[19] & 15) as usize;
+            let mut rdr = Cursor::new(result.as_ref()[offset..offset + 4].to_vec());
+            let result = rdr.read_u32::<BigEndian>().unwrap() & 0x7fffffff;
+            if format!("{1:00$}", self.digits, result % (10 as u32).pow(self.digits as u32)) == token {
+                return true;
+            }
+        }
+        false
+    }
+
+    /// Will generate a standard URL used to automatically add TOTP auths. Usually used with qr codes
+    pub fn get_url(&self, label: String, issuer: String) -> String {
+        let algorithm: String;
+        match self.algorithm {
+            Algorithm::SHA1 => algorithm = "SHA1".to_string(),
+            Algorithm::SHA256 => algorithm = "SHA256".to_string(),
+            Algorithm::SHA512 => algorithm = "SHA512".to_string(),
+        }
+        format!("otpauth://totp/{}?secret={}&issuer={}&digits={}&algorithm={}",
+            label,
+            base32::encode(base32::Alphabet::RFC4648{padding: false}, &self.secret),
+            issuer,
+            self.digits.to_string(),
+            algorithm,
+        )
+    }
+
+    /// Will return a qrcode to automatically add a TOTP as a base64 string
+    pub fn get_qr(&self, label: String, issuer: String) -> Result<String, Box<dyn std::error::Error>> {
+        let url = self.get_url(label, issuer);
+        let code = QrCode::new(&url)?;
+        let mut vec = Vec::new();
+        let size: u32 = ((code.width() + 8) * 8) as u32;
+        let encoder = image::png::PNGEncoder::new(&mut vec);
+        encoder.encode(&code.render::<Luma<u8>>().build().to_vec(), size, size, image::ColorType::L8)?;
+        Ok(base64::encode(vec))
+    }
 }