ruotp 0.0.1

RFC-complaint one-time password algorithms written in Rust
Documentation
use std::collections::HashMap;
use urlencoding::encode;

use crate::{Algorithm, OTPError};

pub fn generate_hotp_token(
    algorithm: &Algorithm,
    secret: &[u8],
    counter: u64,
    digits: u32,
) -> Result<String, OTPError> {
    let hash = algorithm.digest(secret, &counter.to_be_bytes())?;
    let offset: usize = (hash[hash.len() - 1] & 0xf) as usize;

    let binary = ((hash[offset] as u64) & 0x7f) << 24
        | ((hash[offset + 1] as u64) & 0xff) << 16
        | ((hash[offset + 2] as u64) & 0xff) << 8
        | ((hash[offset + 3] as u64) & 0xff);

    let mut token = (binary % 10_u64.pow(digits)).to_string();

    while token.len() < (digits as usize) {
        token = format!("0{}", token);
    }

    Ok(token)
}

#[derive(Debug, PartialEq, Eq)]
pub enum KeyUriType {
    HOTP,
    TOTP,
}

#[derive(Debug)]
pub struct KeyUriOptions {
    pub r#type: KeyUriType,
    pub secret: String,
    pub counter: Option<u64>,
    pub period: Option<u64>,
    pub algorithm: Option<Algorithm>,
    pub digits: Option<u32>,
    pub account_name: String,
    pub issuer: Option<String>,
}

pub fn to_key_uri(options: KeyUriOptions) -> Result<String, OTPError> {
    let mut params: HashMap<&str, String> = HashMap::new();
    params.insert("secret", options.secret);

    let key_uri_type = match options.r#type {
        KeyUriType::HOTP => {
            let counter = options.counter.ok_or(OTPError::ToKeyUriError(
                "counter to be a number when options.type is 'hotp'.".to_string(),
            ))?;
            params.insert("counter", counter.to_string());

            "hotp"
        }
        KeyUriType::TOTP => {
            if let Some(period) = options.period {
                params.insert("period", period.to_string());
            }

            "totp"
        }
    };

    if let Some(algorithm) = options.algorithm {
        params.insert("algorithm", algorithm.to_string().to_uppercase());
    }

    if let Some(digits) = options.digits {
        params.insert("digits", digits.to_string());
    }

    let mut label = encode(&options.account_name).to_string();
    if let Some(issuer) = options.issuer {
        let issuer_encoded = encode(&issuer).to_string();
        params.insert("issuer", issuer_encoded.clone());
        label = format!("{}:{}", issuer_encoded, label);
    }

    let query: Vec<String> = params.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
    let query = query.join("&");

    Ok(format!("otpauth://{}/{}?{}", key_uri_type, label, query))
}