libreauth 0.18.1

Collection of user authentication tools.
Documentation
use crate::oath::HashFunction;
use crate::oath::{
	DEFAULT_OTP_HASH, DEFAULT_OTP_OUT_BASE, DEFAULT_OTP_OUT_LEN, DEFAULT_TOTP_PERIOD,
	DEFAULT_TOTP_T0,
};
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()
	}
}