libreauth 0.15.0

Collection of tools for user authentication.
Documentation
use super::{
    DEFAULT_OTP_HASH, DEFAULT_OTP_OUT_BASE, DEFAULT_OTP_OUT_LEN, DEFAULT_TOTP_PERIOD,
    DEFAULT_TOTP_T0,
};
use crate::oath::HashFunction;
use std::collections::HashMap;
use url::Url;

macro_rules! do_insert_param {
    ($s: ident, $uri: ident, $elem: expr, $name: expr, $def: expr, $is_gauth: expr, $force_param: expr) => {{
        let incl = if $force_param {
            true
        } else {
            match $s.parameters_visibility {
                ParametersVisibility::ShowAll => true,
                ParametersVisibility::ShowNonDefault => $elem != $def,
                ParametersVisibility::GAuthOnly => $is_gauth,
                ParametersVisibility::GAuthNonDefaultExt => $is_gauth || $elem != $def,
                ParametersVisibility::HideAll => false,
            }
        };
        if incl {
            $uri.query_pairs_mut()
                .append_pair($name, &$elem.to_string());
        }
    }};
}

macro_rules! insert_param {
    ($s: ident, $uri: ident, $elem: expr, $name: expr, $def: expr, $is_gauth: expr) => {{
        do_insert_param!($s, $uri, $elem, $name, $def, $is_gauth, false)
    }};
}

macro_rules! insert_param_opt {
    ($s: ident, $uri: ident, $elem: expr, $name: expr, $def: expr, $is_gauth: expr) => {{
        if let Some(e) = $elem {
            do_insert_param!($s, $uri, e, $name, $def, $is_gauth, false);
        }
    }};
}

macro_rules! insert_param_opt_f {
    ($s: ident, $uri: ident, $elem: expr, $name: expr, $def: expr, $is_gauth: expr) => {{
        if let Some(e) = $elem {
            do_insert_param!($s, $uri, e, $name, $def, $is_gauth, true);
        }
    }};
}

#[derive(Eq, PartialEq)]
pub(crate) enum UriType {
    TOTP,
    HOTP,
}

/// Defines the base policy for showing or hiding parameters in a Key URI.
#[derive(Eq, PartialEq)]
pub enum ParametersVisibility {
    /// Shows all possible parameters.
    ShowAll,
    /// Shows only parameters with non-default values.
    ShowNonDefault,
    /// Shows all parameters defined in the Google's Key Uri Format and hide extensions.
    GAuthOnly,
    /// Shows all parameters except those with default values that are not part of the Google's Key Uri Format.
    GAuthNonDefaultExt,
    /// Hides all parameters except `secret` and, for HOTP, `counter`.
    HideAll,
}

/// Creates the Key Uri Format according to the [Google authenticator
/// specification](https://github.com/google/google-authenticator/wiki/Key-Uri-Format) by calling
/// `key_uri_format()` on [`HOTP`](crate::oath::HOTP::key_uri_format) or [`TOTP`](crate::oath::TOTP::key_uri_format).
/// This value can be used to generete QR codes which allow easy scanning by the end user.
///
/// **WARNING**: The finalized value contains the secret key of the authentication process and
/// should only be displayed to the corresponding user!
///
/// ## Example
///
/// ```
/// let key_ascii = "12345678901234567890".to_owned();
/// let mut totp = libreauth::oath::TOTPBuilder::new()
///     .ascii_key(&key_ascii)
///     .finalize()
///     .unwrap();
///
/// let uri = totp
///     .key_uri_format("Provider1", "alice@example.com")
///     .finalize();
///
/// assert_eq!(
///     uri,
///     "otpauth://totp/Provider1:alice@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Provider1"
/// );
/// ```
pub struct KeyUriBuilder<'a> {
    pub(crate) parameters_visibility: ParametersVisibility,
    pub(crate) uri_type: UriType,
    pub(crate) key: &'a Vec<u8>,
    pub(crate) issuer: &'a str,
    pub(crate) account_name: &'a str,
    pub(crate) custom_label: Option<&'a str>,
    pub(crate) custom_parameters: HashMap<&'a str, &'a str>,
    pub(crate) algo: HashFunction,
    pub(crate) output_len: usize,
    pub(crate) output_base: &'a str,
    pub(crate) counter: Option<u64>,
    pub(crate) period: Option<u32>,
    pub(crate) initial_time: Option<u64>,
}

impl<'a> KeyUriBuilder<'a> {
    /// Set the visibility policy for parameters.
    ///
    /// ## Example
    ///
    /// ```
    /// use libreauth::oath::{ParametersVisibility, TOTPBuilder };
    ///
    /// let key_ascii = "12345678901234567890".to_owned();
    /// let mut totp = TOTPBuilder::new()
    ///     .ascii_key(&key_ascii)
    ///     .finalize()
    ///     .unwrap();
    ///
    /// let uri = totp
    ///     .key_uri_format("Provider1", "alice@example.com")
    ///     .parameters_visibility_policy(ParametersVisibility::HideAll)
    ///     .finalize();
    ///
    /// assert_eq!(
    ///     uri,
    ///     "otpauth://totp/Provider1:alice@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
    /// );
    /// ```
    pub fn parameters_visibility_policy(mut self, policy: ParametersVisibility) -> Self {
        self.parameters_visibility = policy;
        self
    }

    /// Completely overwrite the default `{issuer}:{account_name}` label with a custom one.
    ///
    /// ## Example
    ///
    /// ```
    /// let key_ascii = "12345678901234567890".to_owned();
    /// let mut totp = libreauth::oath::TOTPBuilder::new()
    ///     .ascii_key(&key_ascii)
    ///     .finalize()
    ///     .unwrap();
    ///
    /// let uri = totp
    ///     .key_uri_format("Provider1", "alice@example.com")
    ///     .overwrite_label("Provider1Label")
    ///     .finalize();
    ///
    /// assert_eq!(
    ///     uri,
    ///     "otpauth://totp/Provider1Label?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Provider1"
    /// );
    /// ```
    pub fn overwrite_label(mut self, label: &'a str) -> Self {
        self.custom_label = Some(label);
        self
    }

    /// Add a custom key/value parameter.
    ///
    /// ## Example
    ///
    /// ```
    /// let key_ascii = "12345678901234567890".to_owned();
    /// let mut totp = libreauth::oath::TOTPBuilder::new()
    ///     .ascii_key(&key_ascii)
    ///     .finalize()
    ///     .unwrap();
    ///
    /// let uri = totp
    ///     .key_uri_format("Provider1", "alice@example.com")
    ///     .add_parameter("foo", "bar")
    ///     .finalize();
    ///
    /// assert_eq!(
    ///     uri,
    ///     "otpauth://totp/Provider1:alice@example.com?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Provider1&foo=bar"
    /// );
    /// ```
    pub fn add_parameter(mut self, key: &'a str, value: &'a str) -> Self {
        self.custom_parameters.insert(key, value);
        self
    }

    /// Generate the final format.
    #[allow(clippy::cognitive_complexity)]
    pub fn finalize(&self) -> String {
        let mut uri = Url::parse("otpauth://").unwrap();

        let uri_type_final = match self.uri_type {
            UriType::TOTP => "totp",
            UriType::HOTP => "hotp",
        };
        uri.set_host(Some(uri_type_final)).unwrap();

        let label_final = match self.custom_label {
            Some(label) => format!("/{}", label),
            None => format!("/{}:{}", self.issuer, self.account_name),
        };
        uri.set_path(&label_final);

        let secret_final = base32::encode(
            base32::Alphabet::RFC4648 { padding: false },
            self.key.as_slice(),
        );
        uri.query_pairs_mut().append_pair("secret", &secret_final);

        insert_param!(self, uri, self.issuer, "issuer", "", true);
        insert_param!(self, uri, self.algo, "algorithm", DEFAULT_OTP_HASH, true);
        insert_param!(
            self,
            uri,
            self.output_len,
            "digits",
            DEFAULT_OTP_OUT_LEN,
            true
        );
        insert_param!(
            self,
            uri,
            self.output_base,
            "base",
            DEFAULT_OTP_OUT_BASE,
            false
        );
        insert_param_opt_f!(self, uri, self.counter, "counter", 0, true);
        insert_param_opt!(self, uri, self.period, "period", DEFAULT_TOTP_PERIOD, true);
        insert_param_opt!(self, uri, self.initial_time, "t0", DEFAULT_TOTP_T0, false);
        if !self.custom_parameters.is_empty() {
            for (k, v) in &self.custom_parameters {
                uri.query_pairs_mut().append_pair(k, v);
            }
            return uri.into();
        }

        uri.into()
    }
}