use std::{
borrow::Cow,
fmt::{self, Display, Formatter},
str::FromStr,
};
use str_reader::StringReader;
use ttpkit::header::HeaderFieldValue;
use crate::{
DisplayEscaped, Error,
digest::{DigestAlgorithm, QualityOfProtection, StringReaderExt, challenge::DigestChallenge},
};
pub struct DigestResponseBuilder<'a> {
challenge: &'a DigestChallenge,
nc: u32,
cnonce: Option<String>,
echo_md5: bool,
}
impl DigestResponseBuilder<'_> {
#[inline]
pub(crate) fn new(challenge: &DigestChallenge) -> DigestResponseBuilder<'_> {
DigestResponseBuilder {
challenge,
nc: 1,
cnonce: None,
echo_md5: true,
}
}
#[inline]
pub fn nc(mut self, nc: u32) -> Self {
self.nc = nc;
self
}
pub fn cnonce<T>(mut self, cnonce: T) -> Self
where
T: Into<String>,
{
self.cnonce = Some(cnonce.into());
self
}
#[inline]
pub fn echo_md5(mut self, echo_md5: bool) -> Self {
self.echo_md5 = echo_md5;
self
}
pub fn build(
mut self,
method: &str,
uri: &str,
body: Option<&[u8]>,
username: &str,
password: &str,
) -> Result<DigestResponse, Error> {
let realm = self.challenge.realm();
let nonce = self.challenge.nonce();
let algorithm = self.challenge.algorithm();
let cnonce = self
.cnonce
.take()
.unwrap_or_else(|| format!("{:016x}", rand::random::<u64>()));
let nc = format!("{:08x}", self.nc);
let a1 = self.get_a1(username, password, &cnonce);
let a2 = self.get_a2(method, uri, body)?;
let a1_hash = algorithm.digest(a1.as_bytes());
let a2_hash = algorithm.digest(a2.as_bytes());
let qop = self.get_qop(body);
let data = if let Some(qop) = qop {
format!("{a1_hash}:{nonce}:{nc}:{cnonce}:{qop}:{a2_hash}")
} else {
format!("{a1_hash}:{nonce}:{a2_hash}")
};
let algorithm_param = if !self.echo_md5 && algorithm.is_md5() {
None
} else {
self.challenge.algorithm_param()
};
let opaque = self.challenge.opaque();
let res = DigestResponse {
realm: realm.into(),
uri: uri.into(),
username: username.into(),
qop,
nonce: nonce.into(),
cnonce: Some(cnonce),
nc: Some(nc),
algorithm_param: algorithm_param.map(|p| Cow::Owned(p.into())),
algorithm,
response: algorithm.digest(data.as_bytes()),
opaque: opaque.map(String::from),
};
Ok(res)
}
fn get_a1(&self, username: &str, password: &str, cnonce: &str) -> String {
let realm = self.challenge.realm();
let nonce = self.challenge.nonce();
let algorithm = self.challenge.algorithm();
let res = format!("{username}:{realm}:{password}");
if algorithm.is_sess() {
let hash = algorithm.digest(res.as_bytes());
format!("{hash}:{nonce}:{cnonce}")
} else {
res
}
}
fn get_a2(&self, method: &str, uri: &str, body: Option<&[u8]>) -> Result<String, Error> {
if let Some(QualityOfProtection::AuthInt) = self.get_qop(body) {
let body = body.ok_or_else(|| Error::from_static_msg("request body is required"))?;
let algorithm = self.challenge.algorithm();
let hash = algorithm.digest(body);
Ok(format!("{method}:{uri}:{hash}"))
} else {
Ok(format!("{method}:{uri}"))
}
}
fn get_qop(&self, body: Option<&[u8]>) -> Option<QualityOfProtection> {
let qops = self.challenge.qops();
if qops.is_empty() {
return None;
}
let mut auth = false;
let mut auth_int = false;
for qop in qops {
match qop {
QualityOfProtection::Auth => auth = true,
QualityOfProtection::AuthInt => auth_int = true,
}
}
if auth_int && body.is_some() {
Some(QualityOfProtection::AuthInt)
} else if auth {
Some(QualityOfProtection::Auth)
} else {
Some(QualityOfProtection::AuthInt)
}
}
}
pub struct DigestResponse {
realm: String,
uri: String,
username: String,
qop: Option<QualityOfProtection>,
nonce: String,
cnonce: Option<String>,
nc: Option<String>,
algorithm_param: Option<Cow<'static, str>>,
algorithm: DigestAlgorithm,
opaque: Option<String>,
response: String,
}
impl DigestResponse {
#[inline]
pub fn realm(&self) -> &str {
&self.realm
}
#[inline]
pub fn username(&self) -> &str {
&self.username
}
#[inline]
pub fn algorithm(&self) -> DigestAlgorithm {
self.algorithm
}
pub fn verify<M>(&self, method: M, password_hash: &str) -> bool
where
M: Display,
{
self.verify_inner(&format!("{}:{}", method, self.uri), password_hash)
}
fn verify_inner(&self, a2: &str, password_hash: &str) -> bool {
if self.qop.unwrap_or(QualityOfProtection::Auth) == QualityOfProtection::AuthInt {
return false;
}
let nonce = self.nonce.as_str();
let a2_hash = self.algorithm.digest(a2.as_bytes());
let input = if let Some(qop) = self.qop {
let nc = self.nc.as_deref().unwrap_or("");
let cnonce = self.cnonce.as_deref().unwrap_or("");
format!("{password_hash}:{nonce}:{nc}:{cnonce}:{qop}:{a2_hash}",)
} else {
format!("{password_hash}:{nonce}:{a2_hash}")
};
let hash = self.algorithm.digest(input.as_bytes());
hash.eq_ignore_ascii_case(&self.response)
}
}
impl Display for DigestResponse {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
let username = DisplayEscaped::new(&self.username);
let realm = DisplayEscaped::new(&self.realm);
let nonce = DisplayEscaped::new(&self.nonce);
let uri = DisplayEscaped::new(&self.uri);
let response = DisplayEscaped::new(&self.response);
write!(
f,
"Digest username=\"{username}\", realm=\"{realm}\", nonce=\"{nonce}\", uri=\"{uri}\", response=\"{response}\"",
)?;
if let Some(opaque) = self.opaque.as_ref() {
write!(f, ", opaque=\"{}\"", DisplayEscaped::new(opaque))?;
}
if let Some(algorithm) = self.algorithm_param.as_ref() {
write!(f, ", algorithm={algorithm}")?;
}
if let Some(qop) = self.qop.as_ref() {
write!(f, ", qop={qop}")?;
if let Some(nc) = self.nc.as_ref() {
write!(f, ", nc={nc}")?;
}
if let Some(cnonce) = self.cnonce.as_ref() {
write!(f, ", cnonce=\"{}\"", DisplayEscaped::new(cnonce))?;
}
}
Ok(())
}
}
impl FromStr for DigestResponse {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut reader = StringReader::new(s);
let auth_method = reader.read_word();
if !auth_method.eq_ignore_ascii_case("digest") {
return Err(Error::from_static_msg("not a Digest authorization"));
}
let mut realm = None;
let mut uri = None;
let mut username = None;
let mut qop = None;
let mut nonce = None;
let mut cnonce = None;
let mut nc = None;
let mut algorithm_param = None;
let mut response = None;
let mut opaque = None;
while let Some(p) = reader.parse_auth_param()? {
match p.name.as_str() {
"realm" => realm = Some(p.value),
"uri" => uri = Some(p.value),
"username" => username = Some(p.value),
"qop" => qop = Some(p.value),
"nonce" => nonce = Some(p.value),
"cnonce" => cnonce = Some(p.value),
"nc" => nc = Some(p.value),
"algorithm" => algorithm_param = Some(Cow::Owned(p.value)),
"response" => response = Some(p.value),
"opaque" => opaque = Some(p.value),
_ => (),
}
}
let realm =
realm.ok_or_else(|| Error::from_static_msg("the realm parameter is missing"))?;
let uri = uri.ok_or_else(|| Error::from_static_msg("the uri parameter is missing"))?;
let username =
username.ok_or_else(|| Error::from_static_msg("the username parameter is missing"))?;
let qop = qop.map(|qop| qop.parse()).transpose()?;
let nonce =
nonce.ok_or_else(|| Error::from_static_msg("the nonce parameter is missing"))?;
let algorithm = algorithm_param.as_deref().unwrap_or("MD5").parse()?;
let response =
response.ok_or_else(|| Error::from_static_msg("the response parameter is missing"))?;
if qop.is_some() {
if cnonce.is_none() {
return Err(Error::from_static_msg("the cnonce parameter is missing"));
} else if nc.is_none() {
return Err(Error::from_static_msg("the nc parameter is missing"));
}
} else if cnonce.is_some() {
return Err(Error::from_static_msg(
"the cnonce parameter is not expected",
));
} else if nc.is_some() {
return Err(Error::from_static_msg("the nc parameter is not expected"));
}
let res = Self {
realm,
uri,
username,
qop,
nonce,
cnonce,
nc,
algorithm_param,
algorithm,
response,
opaque,
};
Ok(res)
}
}
impl From<DigestResponse> for HeaderFieldValue {
fn from(response: DigestResponse) -> Self {
HeaderFieldValue::from(response.to_string())
}
}