thotp 0.1.11

An RFC based implementation of TOTPs and HOTPs
Documentation
//! Contains a function to generate a qr code ready to be scanned by an authenticator app.
//! Tested and working on Google Authenticator and FreeOTP.
//! OTP uris generated by this module (which are encoded in the QR itself) are done so
//! using [this specification](https://github.com/google/google-authenticator/wiki/Key-Uri-Format).

pub use qrcode::EcLevel;

use super::ThotpError;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use qrcode::{self, render::svg::Color, QrCode};
use std::fmt::Write;

/// Generates a QR code SVG ready to be scanned by an authenticator app.
/// The parameters for OTPs and the secret get stored in the otp_uri
///
/// The `width` and `height` parameters are used to adjust the size of the
/// generated SVG and if not provided default to 200.
///
/// The `ec_level` indicates how much wrong blocks are allowed in the generated QR code.
pub fn generate_code_svg(
    otp_uri: &str,
    width: Option<u32>,
    height: Option<u32>,
    ec_level: EcLevel,
) -> Result<String, ThotpError> {
    let width = if let Some(width) = width { width } else { 200 };
    let height = if let Some(height) = height {
        height
    } else {
        200
    };

    let code = QrCode::with_error_correction_level(otp_uri, ec_level)?;

    Ok(code
        .render()
        .min_dimensions(width, height)
        .dark_color(Color("#000000"))
        .light_color(Color("#ffffff"))
        .build())
}

/// Generates an otp uri following [this specification](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
///
/// The `otp_type` must be either be `"totp"` or `"hotp"` or the function will return an error.
///
/// The `secret` is what gets appended to the otp uri as described in the spec along with `label` and `issuer`.
///
/// The `counter` can be used to set the initial counter value for HOTP passwords. If not provided
/// when generating HOTPs it will default to 0, omitted when generating TOTps.
pub fn otp_uri(
    otp_type: &str,
    secret: &str,
    label: &str,
    issuer: &str,
    counter: Option<u64>,
) -> Result<String, ThotpError> {
    if otp_type != "totp" && otp_type != "hotp" {
        return Err(ThotpError::InvalidUri(String::from(
            "Invalid otp type provided, accepted values are \"hotp\" and \"totp\"",
        )));
    }

    let label = utf8_percent_encode(label, NON_ALPHANUMERIC);
    let issuer = utf8_percent_encode(issuer, NON_ALPHANUMERIC);

    let mut uri = format!(
        "otpauth://{}/{}?secret={}&issuer={}",
        otp_type, label, secret, issuer
    );

    if otp_type == "hotp" {
        if let Some(counter) = counter {
            write!(uri, "&counter={}", counter)?;
        } else {
            write!(uri, "&counter=0")?;
        }
    }

    Ok(uri)
}

#[cfg(feature = "custom")]
/// Appends optional parameters to OTP uris. Mutates the string in place. The URI generated by
/// this module's `otp_uri` function is usually sufficient, however if you need to fully
/// customize your OTP uri you may choose to do so with this function.
/// #### Warning!
/// **Google authenticator ignores the algorithm, digits and time_step (period) parameters.**
pub fn uri_append_params(
    otp_uri: &mut String,
    algorithm: Option<&str>,
    digits: Option<u8>,
    time_step: Option<u8>,
) -> Result<(), ThotpError> {
    if let Some(algorithm) = algorithm {
        if algorithm != "SHA1" && algorithm != "SHA256" && algorithm != "SHA512" {
            return Err(ThotpError::InvalidUri(String::from(
                "Invalid algorithm provided, accepted values are \"SHA1\", \"SHA256\" and \"SHA512\"",
            )));
        }
        write!(otp_uri, "&algorithm={}", algorithm)?;
    }

    if let Some(digits) = digits {
        if !(6..=10).contains(&digits) {
            return Err(ThotpError::InvalidDigits);
        }
        write!(otp_uri, "&digits={}", digits)?;
    }

    if let Some(time_step) = time_step {
        write!(otp_uri, "&period={}", time_step)?;
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::super::encoding::{decode, encode};
    use super::super::ThotpError;
    use super::super::{generate_secret, otp, TIME_STEP};
    use super::*;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn uri() -> Result<(), ThotpError> {
        let mut uri = otp_uri("totp", "super_secret", "biblius", "bedgalopolis", None)?;
        assert_eq!(
            uri,
            "otpauth://totp/biblius?secret=super_secret&issuer=bedgalopolis"
        );
        uri_append_params(&mut uri, Some("SHA256"), Some(7), None)?;

        assert_eq!(uri, "otpauth://totp/biblius?secret=super_secret&issuer=bedgalopolis&algorithm=SHA256&digits=7");
        Ok(())
    }

    // These tests don't assert anything, but are useful for debugging qr codes and codes from
    // the authenticator
    #[test]
    fn qr() -> Result<(), ThotpError> {
        let secret = generate_secret(160);
        let secret = &encode(&secret, data_encoding::BASE32);

        let uri = otp_uri("totp", secret, "biblius", "bedgalopolis", None)?;

        let _code = generate_code_svg(&uri, Some(400), Some(400), EcLevel::H).unwrap();
        // std::fs::write("./temp_secret", secret).unwrap();
        // std::fs::write("./qr.html", _code).unwrap();
        Ok(())
    }

    // Used to check the current time totp, useful to compare to https://totp.danhersam.com/
    #[test]
    fn totp_now() -> Result<(), ThotpError> {
        let secret = "6RPFBC2M7HKNEAMQ435XFIEGBGW4ZLN2NT6DPYNVMV7R7REDIBGTXCKIN5S7BSNZTUBPXT6ZILNU6Q5LW6UZGUJZNJMWF55QE67PIZ3KRBWYS35FDKQ6I34XWQOHVR76NEFLZEBBEWGY2UK2KRMGI3BC7AXAV3OBO3J2DXXGVNBLHB5VFCFJF65CZCXUYP5LVAMHJSV4M6ZJ5FD3D5OU66NBE3M2FAXFBKQBHNMWCVZ7NZXAG3DVHMORDMJNDFWT";
        let secret = decode(secret, data_encoding::BASE32)?;
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs()
            / TIME_STEP as u64;
        let _totp = otp(&secret, nonce)?;
        // println!("TOTP: {}", _totp);
        // let _res = verify_totp("789705", &secret, 0);
        Ok(())
    }

    #[test]
    fn hotp_now() -> Result<(), ThotpError> {
        let secret = "ACTGTGXN6K5SIAWMTDPAUULYEZI2RFA3NFJC27U4EO4PNL6UEMUB3ZOD7BGOIRAFF54RDGBAKAZKTCX2CDRLPQ3GPW42AXVD4SEKLWNTBM56O4EXP7HUBBGKEEUHM4IF";
        let secret = decode(secret, data_encoding::BASE32)?;
        let nonce = 3;
        let _totp = otp(&secret, nonce)?;
        // println!("HOTP: {}", _totp);
        // let _res = verify_totp("789705", &secret, 0);
        Ok(())
    }
}