1use std::{
2 borrow::Cow,
3 fmt::{self, Display, Formatter},
4 str::FromStr,
5};
6
7use str_reader::StringReader;
8use ttpkit::header::HeaderFieldValue;
9
10use crate::{
11 DisplayEscaped, Error,
12 digest::{DigestAlgorithm, QualityOfProtection, StringReaderExt, challenge::DigestChallenge},
13};
14
15pub struct DigestResponseBuilder<'a> {
17 challenge: &'a DigestChallenge,
18 nc: u32,
19 cnonce: Option<String>,
20 echo_md5: bool,
21}
22
23impl DigestResponseBuilder<'_> {
24 #[inline]
26 pub(crate) fn new(challenge: &DigestChallenge) -> DigestResponseBuilder<'_> {
27 DigestResponseBuilder {
28 challenge,
29 nc: 1,
30 cnonce: None,
31 echo_md5: true,
32 }
33 }
34
35 #[inline]
41 pub fn nc(mut self, nc: u32) -> Self {
42 self.nc = nc;
43 self
44 }
45
46 pub fn cnonce<T>(mut self, cnonce: T) -> Self
50 where
51 T: Into<String>,
52 {
53 self.cnonce = Some(cnonce.into());
54 self
55 }
56
57 #[inline]
64 pub fn echo_md5(mut self, echo_md5: bool) -> Self {
65 self.echo_md5 = echo_md5;
66 self
67 }
68
69 pub fn build(
71 mut self,
72 method: &str,
73 uri: &str,
74 body: Option<&[u8]>,
75 username: &str,
76 password: &str,
77 ) -> Result<DigestResponse, Error> {
78 let realm = self.challenge.realm();
79 let nonce = self.challenge.nonce();
80 let algorithm = self.challenge.algorithm();
81
82 let cnonce = self
83 .cnonce
84 .take()
85 .unwrap_or_else(|| format!("{:016x}", rand::random::<u64>()));
86
87 let nc = format!("{:08x}", self.nc);
88
89 let a1 = self.get_a1(username, password, &cnonce);
90 let a2 = self.get_a2(method, uri, body)?;
91
92 let a1_hash = algorithm.digest(a1.as_bytes());
93 let a2_hash = algorithm.digest(a2.as_bytes());
94
95 let qop = self.get_qop(body);
96
97 let data = if let Some(qop) = qop {
98 format!("{a1_hash}:{nonce}:{nc}:{cnonce}:{qop}:{a2_hash}")
99 } else {
100 format!("{a1_hash}:{nonce}:{a2_hash}")
101 };
102
103 let algorithm_param = if !self.echo_md5 && algorithm.is_md5() {
104 None
105 } else {
106 self.challenge.algorithm_param()
107 };
108
109 let opaque = self.challenge.opaque();
110
111 let res = DigestResponse {
112 realm: realm.into(),
113 uri: uri.into(),
114 username: username.into(),
115 qop,
116 nonce: nonce.into(),
117 cnonce: Some(cnonce),
118 nc: Some(nc),
119 algorithm_param: algorithm_param.map(|p| Cow::Owned(p.into())),
120 algorithm,
121 response: algorithm.digest(data.as_bytes()),
122 opaque: opaque.map(String::from),
123 };
124
125 Ok(res)
126 }
127
128 fn get_a1(&self, username: &str, password: &str, cnonce: &str) -> String {
130 let realm = self.challenge.realm();
131 let nonce = self.challenge.nonce();
132 let algorithm = self.challenge.algorithm();
133
134 let res = format!("{username}:{realm}:{password}");
135
136 if algorithm.is_sess() {
137 let hash = algorithm.digest(res.as_bytes());
138
139 format!("{hash}:{nonce}:{cnonce}")
140 } else {
141 res
142 }
143 }
144
145 fn get_a2(&self, method: &str, uri: &str, body: Option<&[u8]>) -> Result<String, Error> {
147 if let Some(QualityOfProtection::AuthInt) = self.get_qop(body) {
148 let body = body.ok_or_else(|| Error::from_static_msg("request body is required"))?;
149
150 let algorithm = self.challenge.algorithm();
151
152 let hash = algorithm.digest(body);
153
154 Ok(format!("{method}:{uri}:{hash}"))
155 } else {
156 Ok(format!("{method}:{uri}"))
157 }
158 }
159
160 fn get_qop(&self, body: Option<&[u8]>) -> Option<QualityOfProtection> {
163 let qops = self.challenge.qops();
164
165 if qops.is_empty() {
166 return None;
167 }
168
169 let mut auth = false;
170 let mut auth_int = false;
171
172 for qop in qops {
173 match qop {
174 QualityOfProtection::Auth => auth = true,
175 QualityOfProtection::AuthInt => auth_int = true,
176 }
177 }
178
179 if auth_int && body.is_some() {
180 Some(QualityOfProtection::AuthInt)
181 } else if auth {
182 Some(QualityOfProtection::Auth)
183 } else {
184 Some(QualityOfProtection::AuthInt)
185 }
186 }
187}
188
189pub struct DigestResponse {
191 realm: String,
192 uri: String,
193 username: String,
194 qop: Option<QualityOfProtection>,
195 nonce: String,
196 cnonce: Option<String>,
197 nc: Option<String>,
198 algorithm_param: Option<Cow<'static, str>>,
199 algorithm: DigestAlgorithm,
200 opaque: Option<String>,
201 response: String,
202}
203
204impl DigestResponse {
205 #[inline]
207 pub fn realm(&self) -> &str {
208 &self.realm
209 }
210
211 #[inline]
213 pub fn username(&self) -> &str {
214 &self.username
215 }
216
217 #[inline]
219 pub fn algorithm(&self) -> DigestAlgorithm {
220 self.algorithm
221 }
222
223 pub fn verify<M>(&self, method: M, password_hash: &str) -> bool
230 where
231 M: Display,
232 {
233 self.verify_inner(&format!("{}:{}", method, self.uri), password_hash)
234 }
235
236 fn verify_inner(&self, a2: &str, password_hash: &str) -> bool {
243 if self.qop.unwrap_or(QualityOfProtection::Auth) == QualityOfProtection::AuthInt {
244 return false;
245 }
246
247 let nonce = self.nonce.as_str();
248
249 let a2_hash = self.algorithm.digest(a2.as_bytes());
250
251 let input = if let Some(qop) = self.qop {
252 let nc = self.nc.as_deref().unwrap_or("");
253 let cnonce = self.cnonce.as_deref().unwrap_or("");
254
255 format!("{password_hash}:{nonce}:{nc}:{cnonce}:{qop}:{a2_hash}",)
256 } else {
257 format!("{password_hash}:{nonce}:{a2_hash}")
258 };
259
260 let hash = self.algorithm.digest(input.as_bytes());
261
262 hash.eq_ignore_ascii_case(&self.response)
263 }
264}
265
266impl Display for DigestResponse {
267 fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
268 let username = DisplayEscaped::new(&self.username);
272 let realm = DisplayEscaped::new(&self.realm);
273 let nonce = DisplayEscaped::new(&self.nonce);
274 let uri = DisplayEscaped::new(&self.uri);
275 let response = DisplayEscaped::new(&self.response);
276
277 write!(
278 f,
279 "Digest username=\"{username}\", realm=\"{realm}\", nonce=\"{nonce}\", uri=\"{uri}\", response=\"{response}\"",
280 )?;
281
282 if let Some(opaque) = self.opaque.as_ref() {
283 write!(f, ", opaque=\"{}\"", DisplayEscaped::new(opaque))?;
284 }
285
286 if let Some(algorithm) = self.algorithm_param.as_ref() {
287 write!(f, ", algorithm={algorithm}")?;
288 }
289
290 if let Some(qop) = self.qop.as_ref() {
291 write!(f, ", qop={qop}")?;
292
293 if let Some(nc) = self.nc.as_ref() {
294 write!(f, ", nc={nc}")?;
295 }
296
297 if let Some(cnonce) = self.cnonce.as_ref() {
298 write!(f, ", cnonce=\"{}\"", DisplayEscaped::new(cnonce))?;
299 }
300 }
301
302 Ok(())
303 }
304}
305
306impl FromStr for DigestResponse {
307 type Err = Error;
308
309 fn from_str(s: &str) -> Result<Self, Self::Err> {
310 let mut reader = StringReader::new(s);
311
312 let auth_method = reader.read_word();
313
314 if !auth_method.eq_ignore_ascii_case("digest") {
315 return Err(Error::from_static_msg("not a Digest authorization"));
316 }
317
318 let mut realm = None;
319 let mut uri = None;
320 let mut username = None;
321 let mut qop = None;
322 let mut nonce = None;
323 let mut cnonce = None;
324 let mut nc = None;
325 let mut algorithm_param = None;
326 let mut response = None;
327 let mut opaque = None;
328
329 while let Some(p) = reader.parse_auth_param()? {
330 match p.name.as_str() {
331 "realm" => realm = Some(p.value),
332 "uri" => uri = Some(p.value),
333 "username" => username = Some(p.value),
334 "qop" => qop = Some(p.value),
335 "nonce" => nonce = Some(p.value),
336 "cnonce" => cnonce = Some(p.value),
337 "nc" => nc = Some(p.value),
338 "algorithm" => algorithm_param = Some(Cow::Owned(p.value)),
339 "response" => response = Some(p.value),
340 "opaque" => opaque = Some(p.value),
341 _ => (),
342 }
343 }
344
345 let realm =
346 realm.ok_or_else(|| Error::from_static_msg("the realm parameter is missing"))?;
347 let uri = uri.ok_or_else(|| Error::from_static_msg("the uri parameter is missing"))?;
348 let username =
349 username.ok_or_else(|| Error::from_static_msg("the username parameter is missing"))?;
350 let qop = qop.map(|qop| qop.parse()).transpose()?;
351 let nonce =
352 nonce.ok_or_else(|| Error::from_static_msg("the nonce parameter is missing"))?;
353 let algorithm = algorithm_param.as_deref().unwrap_or("MD5").parse()?;
354 let response =
355 response.ok_or_else(|| Error::from_static_msg("the response parameter is missing"))?;
356
357 if qop.is_some() {
358 if cnonce.is_none() {
359 return Err(Error::from_static_msg("the cnonce parameter is missing"));
360 } else if nc.is_none() {
361 return Err(Error::from_static_msg("the nc parameter is missing"));
362 }
363 } else if cnonce.is_some() {
364 return Err(Error::from_static_msg(
365 "the cnonce parameter is not expected",
366 ));
367 } else if nc.is_some() {
368 return Err(Error::from_static_msg("the nc parameter is not expected"));
369 }
370
371 let res = Self {
372 realm,
373 uri,
374 username,
375 qop,
376 nonce,
377 cnonce,
378 nc,
379 algorithm_param,
380 algorithm,
381 response,
382 opaque,
383 };
384
385 Ok(res)
386 }
387}
388
389impl From<DigestResponse> for HeaderFieldValue {
390 fn from(response: DigestResponse) -> Self {
391 HeaderFieldValue::from(response.to_string())
392 }
393}