tfa: drop anyhow from totp module

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2023-04-21 10:44:46 +02:00
parent c45620b447
commit 5c39559cad

View File

@ -1,16 +1,69 @@
//! TOTP implementation. //! TOTP implementation.
use std::convert::TryFrom; use std::convert::TryFrom;
use std::error::Error as StdError;
use std::fmt; use std::fmt;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, Error};
use openssl::hash::MessageDigest; use openssl::hash::MessageDigest;
use openssl::pkey::PKey; use openssl::pkey::PKey;
use openssl::sign::Signer; use openssl::sign::Signer;
use percent_encoding::{percent_decode, percent_encode}; use percent_encoding::{percent_decode, percent_encode, PercentDecode};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
/// An error from the TOTP TFA submodule.
#[derive(Debug)]
pub enum Error {
Generic(String),
Decode(&'static str, Box<dyn StdError + Send + Sync + 'static>),
BadParameter(String, Box<dyn StdError + Send + Sync + 'static>),
Ssl(&'static str, openssl::error::ErrorStack),
UnsupportedAlgorithm(String),
UnknownParameter(String),
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Ssl(_m, e) => Some(e),
Self::Decode(_m, e) => Some(&**e),
Self::BadParameter(_m, e) => Some(&**e),
_ => None,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Generic(e) => f.write_str(&e),
Error::Decode(m, _e) => f.write_str(&m),
Error::Ssl(m, _e) => f.write_str(&m),
Error::UnsupportedAlgorithm(a) => write!(f, "unsupported algorithm: '{a}'"),
Error::UnknownParameter(p) => write!(f, "unknown otpauth uri parameter: '{p}'"),
Error::BadParameter(m, _e) => f.write_str(&m),
}
}
}
impl Error {
fn decode<E>(msg: &'static str, err: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Error::Decode(msg, Box::new(err))
}
fn msg<T: fmt::Display>(err: T) -> Self {
Error::Generic(err.to_string())
}
}
// for generic errors:
macro_rules! format_err {
($($msg:tt)*) => {{ Error::Generic(format!($($msg)*)) }};
}
/// Algorithms supported by the TOTP. This is simply an enum limited to the most common /// Algorithms supported by the TOTP. This is simply an enum limited to the most common
/// available implementations. /// available implementations.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -50,7 +103,7 @@ impl std::str::FromStr for Algorithm {
"SHA1" => Algorithm::Sha1, "SHA1" => Algorithm::Sha1,
"SHA256" => Algorithm::Sha256, "SHA256" => Algorithm::Sha256,
"SHA512" => Algorithm::Sha512, "SHA512" => Algorithm::Sha512,
_ => bail!("unsupported algorithm: {}", s), _ => return Err(Error::UnsupportedAlgorithm(s.to_string())),
}) })
} }
} }
@ -172,7 +225,10 @@ impl Totp {
/// Create a new OTP secret key builder using a secret specified in hexadecimal bytes. /// Create a new OTP secret key builder using a secret specified in hexadecimal bytes.
pub fn builder_from_hex(secret: &str) -> Result<TotpBuilder, Error> { pub fn builder_from_hex(secret: &str) -> Result<TotpBuilder, Error> {
Ok(Self::builder().secret(hex::decode(secret)?)) Ok(Self::builder().secret(
hex::decode(secret)
.map_err(|err| Error::decode("not a valid hexademical string", err))?,
))
} }
/// Get the secret key in binary form. /// Get the secret key in binary form.
@ -208,29 +264,29 @@ impl Totp {
/// Raw signing function. /// Raw signing function.
fn sign(&self, input_data: &[u8]) -> Result<TotpValue, Error> { fn sign(&self, input_data: &[u8]) -> Result<TotpValue, Error> {
let secret = PKey::hmac(&self.secret) let secret = PKey::hmac(&self.secret)
.map_err(|err| anyhow!("error instantiating hmac key: {}", err))?; .map_err(|err| Error::Ssl("error instantiating hmac key", err))?;
let mut signer = Signer::new(self.algorithm.into(), &secret) let mut signer = Signer::new(self.algorithm.into(), &secret)
.map_err(|err| anyhow!("error instantiating hmac signer: {}", err))?; .map_err(|err| Error::Ssl("error instantiating hmac signer", err))?;
signer signer
.update(input_data) .update(input_data)
.map_err(|err| anyhow!("error calculating hmac (error in update): {}", err))?; .map_err(|err| Error::Ssl("error updating hmac", err))?;
let hmac = signer let hmac = signer
.sign_to_vec() .sign_to_vec()
.map_err(|err| anyhow!("error calculating hmac (error in sign): {}", err))?; .map_err(|err| Error::Ssl("error finishing hmac", err))?;
let byte_offset = usize::from( let byte_offset = usize::from(
hmac.last() hmac.last()
.ok_or_else(|| anyhow!("error calculating hmac (too short)"))? .ok_or_else(|| format_err!("error calculating hmac (too short)"))?
& 0xF, & 0xF,
); );
let value = u32::from_be_bytes( let value = u32::from_be_bytes(
TryFrom::try_from( TryFrom::try_from(
hmac.get(byte_offset..(byte_offset + 4)) hmac.get(byte_offset..(byte_offset + 4))
.ok_or_else(|| anyhow!("error calculating hmac (too short)"))?, .ok_or_else(|| format_err!("error finalizing hmac (too short)"))?,
) )
.unwrap(), .unwrap(),
) & 0x7fffffff; ) & 0x7fffffff;
@ -254,7 +310,7 @@ impl Totp {
fn time_to_counter(&self, time: SystemTime) -> Result<u64, Error> { fn time_to_counter(&self, time: SystemTime) -> Result<u64, Error> {
match time.duration_since(SystemTime::UNIX_EPOCH) { match time.duration_since(SystemTime::UNIX_EPOCH) {
Ok(epoch) => Ok(epoch.as_secs() / (self.period as u64)), Ok(epoch) => Ok(epoch.as_secs() / (self.period as u64)),
Err(_) => bail!("refusing to create otp value for negative time"), Err(_) => Err(Error::msg("refusing to create otp value for negative time")),
} }
} }
@ -288,18 +344,22 @@ impl Totp {
let mut out = String::new(); let mut out = String::new();
write!(out, "otpauth://totp/")?; write!(out, "otpauth://totp/").map_err(Error::msg)?;
let account_name = match &self.account_name { let account_name = match &self.account_name {
Some(account_name) => account_name, Some(account_name) => account_name,
None => bail!("cannot create otpauth uri without an account name"), None => {
return Err(Error::msg(
"cannot create otpauth uri without an account name",
))
}
}; };
let issuer = match &self.issuer { let issuer = match &self.issuer {
Some(issuer) => { Some(issuer) => {
let issuer = percent_encode(issuer.as_bytes(), percent_encoding::NON_ALPHANUMERIC) let issuer = percent_encode(issuer.as_bytes(), percent_encoding::NON_ALPHANUMERIC)
.to_string(); .to_string();
write!(out, "{}:", issuer)?; write!(out, "{}:", issuer).map_err(Error::msg)?;
Some(issuer) Some(issuer)
} }
None => None, None => None,
@ -310,13 +370,14 @@ impl Totp {
"{}?secret={}", "{}?secret={}",
percent_encode(account_name.as_bytes(), percent_encoding::NON_ALPHANUMERIC), percent_encode(account_name.as_bytes(), percent_encoding::NON_ALPHANUMERIC),
base32::encode(base32::Alphabet::RFC4648 { padding: false }, &self.secret), base32::encode(base32::Alphabet::RFC4648 { padding: false }, &self.secret),
)?; )
write!(out, "&digits={}", self.digits)?; .map_err(Error::msg)?;
write!(out, "&algorithm={}", self.algorithm)?; write!(out, "&digits={}", self.digits).map_err(Error::msg)?;
write!(out, "&period={}", self.period)?; write!(out, "&algorithm={}", self.algorithm).map_err(Error::msg)?;
write!(out, "&period={}", self.period).map_err(Error::msg)?;
if let Some(issuer) = issuer { if let Some(issuer) = issuer {
write!(out, "&issuer={}", issuer)?; write!(out, "&issuer={}", issuer).map_err(Error::msg)?;
} }
Ok(out) Ok(out)
@ -328,14 +389,14 @@ impl std::str::FromStr for Totp {
fn from_str(uri: &str) -> Result<Self, Error> { fn from_str(uri: &str) -> Result<Self, Error> {
if !uri.starts_with("otpauth://totp/") { if !uri.starts_with("otpauth://totp/") {
bail!("not an otpauth uri"); return Err(Error::msg("not an otpauth uri"));
} }
let uri = &uri.as_bytes()[15..]; let uri = &uri.as_bytes()[15..];
let qmark = uri let qmark = uri
.iter() .iter()
.position(|&b| b == b'?') .position(|&b| b == b'?')
.ok_or_else(|| anyhow!("missing '?' in otp uri"))?; .ok_or_else(|| format_err!("missing '?' in otp uri"))?;
let account = &uri[..qmark]; let account = &uri[..qmark];
let uri = &uri[(qmark + 1)..]; let uri = &uri[(qmark + 1)..];
@ -345,7 +406,7 @@ impl std::str::FromStr for Totp {
let first_part = percent_decode( let first_part = percent_decode(
account account
.next() .next()
.ok_or_else(|| anyhow!("missing account in otpauth uri"))?, .ok_or_else(|| format_err!("missing account in otpauth uri"))?,
) )
.decode_utf8_lossy() .decode_utf8_lossy()
.into_owned(); .into_owned();
@ -363,36 +424,57 @@ impl std::str::FromStr for Totp {
for parts in uri.split(|&b| b == b'&') { for parts in uri.split(|&b| b == b'&') {
let mut parts = parts.splitn(2, |&b| b == b'='); let mut parts = parts.splitn(2, |&b| b == b'=');
let key = percent_decode( let key = percent_decode(
parts parts
.next() .next()
.ok_or_else(|| anyhow!("bad key in otpauth uri"))?, .ok_or_else(|| format_err!("bad key in otpauth uri"))?,
) )
.decode_utf8()?; .decode_utf8()
.map_err(|err| Error::decode("failed to decode key", err))?;
let value = percent_decode( let value = percent_decode(
parts parts
.next() .next()
.ok_or_else(|| anyhow!("bad value in otpauth uri"))?, .ok_or_else(|| format_err!("bad value in otpauth uri"))?,
); );
fn decode_utf8<T>(value: PercentDecode, n: &'static str) -> Result<T, Error>
where
T: std::str::FromStr,
T::Err: StdError + Send + Sync + 'static,
{
value
.decode_utf8()
.map_err(|err| {
Error::BadParameter(format!("failed to decode value '{n}'"), Box::new(err))
})?
.parse()
.map_err(|err| {
Error::BadParameter(format!("failed to parse value '{n}'"), Box::new(err))
})
}
match &*key { match &*key {
"secret" => { "secret" => {
totp.secret = base32::decode( totp.secret = base32::decode(
base32::Alphabet::RFC4648 { padding: false }, base32::Alphabet::RFC4648 { padding: false },
&value.decode_utf8()?, &value
.decode_utf8()
.map_err(|err| Error::decode("failed to decode value", err))?,
) )
.ok_or_else(|| anyhow!("failed to decode otp secret in otpauth url"))? .ok_or_else(|| format_err!("failed to decode otp secret in otpauth url"))?
} }
"digits" => totp.digits = value.decode_utf8()?.parse()?, "digits" => totp.digits = decode_utf8(value, "digits")?,
"algorithm" => totp.algorithm = value.decode_utf8()?.parse()?, "algorithm" => totp.algorithm = decode_utf8(value, "algorithm")?,
"period" => totp.period = value.decode_utf8()?.parse()?, "period" => totp.period = decode_utf8(value, "period")?,
"issuer" => totp.issuer = Some(value.decode_utf8_lossy().into_owned()), "issuer" => totp.issuer = Some(value.decode_utf8_lossy().into_owned()),
_other => bail!("unrecognized otpauth uri parameter: {}", key), _other => return Err(Error::UnknownParameter(key.to_string())),
} }
} }
if totp.secret.is_empty() { if totp.secret.is_empty() {
bail!("missing secret in otpauth url"); return Err(Error::msg("missing secret in otpauth url"));
} }
Ok(totp) Ok(totp)