use std::collections::HashMap;
pub(crate) fn authorization(
user: &str,
pass: &str,
method: &str,
uri: &str,
challenge: &str,
) -> Option<String> {
let body = challenge.trim().strip_prefix("Digest").or_else(|| {
challenge
.trim()
.get(..6)
.filter(|s| s.eq_ignore_ascii_case("Digest"))
.map(|_| &challenge.trim()[6..])
})?;
let p = parse_params(body);
let realm = p.get("realm")?;
let nonce = p.get("nonce")?;
if !is_safe_quoted(realm) || !is_safe_quoted(nonce) {
return None;
}
let raw_alg = p.get("algorithm").map(String::as_str);
let (algorithm, use_sha, sess) = match raw_alg {
None => ("MD5", false, false),
Some(a) if a.eq_ignore_ascii_case("MD5") => ("MD5", false, false),
Some(a) if a.eq_ignore_ascii_case("MD5-sess") => ("MD5-sess", false, true),
Some(a) if a.eq_ignore_ascii_case("SHA-256") => ("SHA-256", true, false),
Some(a) if a.eq_ignore_ascii_case("SHA-256-sess") => ("SHA-256-sess", true, true),
Some(_) => return None,
};
let qop_auth = p
.get("qop")
.map(|q| q.split(',').any(|t| t.trim().eq_ignore_ascii_case("auth")))
.unwrap_or(false);
let h = |s: &str| -> String {
if use_sha {
hex(&purecrypto::hash::sha256(s.as_bytes()))
} else {
hex(&purecrypto::hash::md5(s.as_bytes()))
}
};
let cnonce = hex(&rand_bytes());
let nc = "00000001";
let mut ha1 = h(&format!("{user}:{realm}:{pass}"));
if sess {
ha1 = h(&format!("{ha1}:{nonce}:{cnonce}"));
}
let ha2 = h(&format!("{method}:{uri}"));
let response = if qop_auth {
h(&format!("{ha1}:{nonce}:{nc}:{cnonce}:auth:{ha2}"))
} else {
h(&format!("{ha1}:{nonce}:{ha2}"))
};
let mut out = format!(
"Digest username=\"{user}\", realm=\"{realm}\", nonce=\"{nonce}\", \
uri=\"{uri}\", response=\"{response}\""
);
if let Some(opaque) = p.get("opaque") {
if !is_safe_quoted(opaque) {
return None;
}
out.push_str(&format!(", opaque=\"{opaque}\""));
}
if raw_alg.is_some() {
out.push_str(&format!(", algorithm={algorithm}"));
}
if qop_auth {
out.push_str(&format!(", qop=auth, nc={nc}, cnonce=\"{cnonce}\""));
}
Some(out)
}
fn parse_params(s: &str) -> HashMap<String, String> {
let mut map = HashMap::new();
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
while i < bytes.len() && (bytes[i] == b',' || bytes[i].is_ascii_whitespace()) {
i += 1;
}
let key_start = i;
while i < bytes.len() && bytes[i] != b'=' && bytes[i] != b',' {
i += 1;
}
let key = s[key_start..i].trim().to_ascii_lowercase();
if i >= bytes.len() || bytes[i] != b'=' {
if !key.is_empty() {
map.insert(key, String::new());
}
continue;
}
i += 1; let value = if i < bytes.len() && bytes[i] == b'"' {
i += 1;
let start = i;
while i < bytes.len() && bytes[i] != b'"' {
if bytes[i] == b'\\' && i + 1 < bytes.len() {
i += 1;
}
i += 1;
}
let v = s[start..i.min(bytes.len())].to_string();
if i < bytes.len() {
i += 1; }
v
} else {
let start = i;
while i < bytes.len() && bytes[i] != b',' {
i += 1;
}
s[start..i].trim().to_string()
};
if !key.is_empty() {
map.insert(key, value);
}
}
map
}
fn is_safe_quoted(v: &str) -> bool {
!v.bytes()
.any(|b| b == b'"' || b == b'\\' || b == b'\r' || b == b'\n' || b == 0)
}
fn hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
fn rand_bytes() -> [u8; 8] {
use purecrypto::rng::{OsRng, RngCore};
let mut out = [0u8; 8];
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
OsRng.fill_bytes(&mut out);
}))
.is_err()
{
out = *b"rsurlcli";
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_quoted_and_token_params() {
let p = parse_params(
" realm=\"test\", qop=\"auth,auth-int\", nonce=\"abc\", algorithm=MD5, stale=false",
);
assert_eq!(p.get("realm").unwrap(), "test");
assert_eq!(p.get("qop").unwrap(), "auth,auth-int");
assert_eq!(p.get("nonce").unwrap(), "abc");
assert_eq!(p.get("algorithm").unwrap(), "MD5");
assert_eq!(p.get("stale").unwrap(), "false");
}
#[test]
fn rfc2617_md5_response() {
let chal = "Digest realm=\"testrealm@host.com\", qop=\"auth,auth-int\", \
nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", \
opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"";
let h = authorization("Mufasa", "Circle Of Life", "GET", "/dir/index.html", chal)
.expect("digest header");
assert!(h.starts_with("Digest username=\"Mufasa\""));
assert!(h.contains("realm=\"testrealm@host.com\""));
assert!(h.contains("qop=auth"));
assert!(h.contains("uri=\"/dir/index.html\""));
assert!(h.contains("response=\""));
assert!(h.contains("opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""));
}
#[test]
fn unknown_algorithm_fails_closed() {
let chal = "Digest realm=\"r\", nonce=\"n\", algorithm=SHA-512-256";
assert!(authorization("u", "p", "GET", "/", chal).is_none());
let chal2 = "Digest realm=\"r\", nonce=\"n\", algorithm=MD6";
assert!(authorization("u", "p", "GET", "/", chal2).is_none());
}
#[test]
fn sha256_algorithm_echoed_as_selected() {
let chal = "Digest realm=\"r\", nonce=\"n\", algorithm=sha-256";
let h = authorization("u", "p", "GET", "/", chal).unwrap();
assert!(h.contains("algorithm=SHA-256"), "got: {h}");
}
#[test]
fn quoted_value_breakout_rejected() {
let chal = "Digest realm=\"r\\\"x\", nonce=\"n\"";
assert!(authorization("u", "p", "GET", "/", chal).is_none());
let chal2 = "Digest realm=\"r\", nonce=\"n\r\nX: y\"";
assert!(authorization("u", "p", "GET", "/", chal2).is_none());
}
#[test]
fn no_qop_form() {
let chal = "Digest realm=\"r\", nonce=\"n\"";
let h = authorization("u", "p", "GET", "/", chal).unwrap();
assert!(h.contains("response=\""));
assert!(!h.contains("qop="));
let ha1 = hex(&purecrypto::hash::md5(b"u:r:p"));
let ha2 = hex(&purecrypto::hash::md5(b"GET:/"));
let want = hex(&purecrypto::hash::md5(format!("{ha1}:n:{ha2}").as_bytes()));
assert!(h.contains(&format!("response=\"{want}\"")));
}
}