1mod challenge;
4mod response;
5
6use std::{
7 fmt::{self, Display, Formatter},
8 str::FromStr,
9};
10
11use digest::Digest;
12use sha1::Sha1;
13use sha2::{Sha256, Sha512_256};
14use str_reader::StringReader;
15
16use crate::{Error, StringReaderExt as _};
17
18pub use self::{
19 challenge::{DigestChallenge, DigestChallengeBuilder},
20 response::{DigestResponse, DigestResponseBuilder},
21};
22
23#[allow(non_camel_case_types)]
25#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
26pub enum DigestAlgorithm {
27 Md5,
28 Md5_SESS,
29 Sha,
30 Sha_SESS,
31 Sha256,
32 Sha256_SESS,
33 Sha512_256,
34 Sha512_256_SESS,
35}
36
37impl DigestAlgorithm {
38 pub const fn name(&self) -> &'static str {
40 match self {
41 Self::Md5 => "MD5",
42 Self::Md5_SESS => "MD5-sess",
43 Self::Sha => "SHA",
44 Self::Sha_SESS => "SHA-sess",
45 Self::Sha256 => "SHA-256",
46 Self::Sha256_SESS => "SHA-256-sess",
47 Self::Sha512_256 => "SHA-512-256",
48 Self::Sha512_256_SESS => "SHA-512-256-sess",
49 }
50 }
51
52 pub const fn is_sess(&self) -> bool {
54 matches!(
55 self,
56 Self::Md5_SESS | Self::Sha_SESS | Self::Sha256_SESS | Self::Sha512_256_SESS
57 )
58 }
59
60 const fn is_md5(&self) -> bool {
62 matches!(self, Self::Md5 | Self::Md5_SESS)
63 }
64
65 pub fn digest(&self, input: &[u8]) -> String {
67 match self {
68 Self::Md5 => format!("{:x}", md5::compute(input)),
69 Self::Md5_SESS => format!("{:x}", md5::compute(input)),
70 Self::Sha => format!("{:x}", Sha1::digest(input)),
71 Self::Sha_SESS => format!("{:x}", Sha1::digest(input)),
72 Self::Sha256 => format!("{:x}", Sha256::digest(input)),
73 Self::Sha256_SESS => format!("{:x}", Sha256::digest(input)),
74 Self::Sha512_256 => format!("{:x}", Sha512_256::digest(input)),
75 Self::Sha512_256_SESS => format!("{:x}", Sha512_256::digest(input)),
76 }
77 }
78}
79
80impl AsRef<str> for DigestAlgorithm {
81 #[inline]
82 fn as_ref(&self) -> &str {
83 self.name()
84 }
85}
86
87impl Display for DigestAlgorithm {
88 #[inline]
89 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
90 f.write_str(self.name())
91 }
92}
93
94impl FromStr for DigestAlgorithm {
95 type Err = Error;
96
97 fn from_str(s: &str) -> Result<Self, Self::Err> {
98 let res = match s {
99 "MD5" => Self::Md5,
100 "MD5-SESS" => Self::Md5_SESS,
101 "SHA" => Self::Sha,
102 "SHA-SESS" => Self::Sha_SESS,
103 "SHA-256" => Self::Sha256,
104 "SHA-256-SESS" => Self::Sha256_SESS,
105 "SHA-512-256" => Self::Sha512_256,
106 "SHA-512-256-SESS" => Self::Sha512_256_SESS,
107 _ => {
108 return Err(Error::from_static_msg(
109 "unknown/unsupported digest algorithm",
110 ));
111 }
112 };
113
114 Ok(res)
115 }
116}
117
118#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
120pub enum QualityOfProtection {
121 Auth,
122 AuthInt,
123}
124
125impl QualityOfProtection {
126 fn parse_many(s: &str) -> Vec<Self> {
128 let mut reader = StringReader::new(s);
129
130 let mut res = Vec::new();
131
132 while !reader.is_empty() {
133 if let Ok(qop) = QualityOfProtection::from_str(reader.read_word()) {
134 res.push(qop);
135 }
136 }
137
138 res.sort_unstable();
139 res.dedup();
140
141 res
142 }
143}
144
145impl Display for QualityOfProtection {
146 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
147 let s = match self {
148 Self::Auth => "auth",
149 Self::AuthInt => "auth-int",
150 };
151
152 f.write_str(s)
153 }
154}
155
156impl FromStr for QualityOfProtection {
157 type Err = Error;
158
159 fn from_str(s: &str) -> Result<Self, Self::Err> {
160 let res = match s {
161 "auth" => Self::Auth,
162 "auth-int" => Self::AuthInt,
163 _ => return Err(Error::from_static_msg("unknown qop")),
164 };
165
166 Ok(res)
167 }
168}
169
170#[derive(Debug)]
172struct AuthParam {
173 name: String,
174 value: String,
175}
176
177trait StringReaderExt {
179 fn parse_auth_param(&mut self) -> Result<Option<AuthParam>, Error>;
181}
182
183impl StringReaderExt for StringReader<'_> {
184 fn parse_auth_param(&mut self) -> Result<Option<AuthParam>, Error> {
185 fn inner(reader: &mut StringReader<'_>) -> Result<Option<AuthParam>, Error> {
187 while !reader.is_empty() {
188 let name = reader.read_until(|c| c == ',' || c == '=').trim();
189
190 if name.is_empty() {
191 match reader.read_char() {
192 Ok(',') => continue,
193 Ok('=') => return Err(Error::from_static_msg("empty auth parameter name")),
194 Ok(_) => panic!("unexpected character"),
195 Err(_) => break,
196 }
197 }
198
199 reader
200 .match_char('=')
201 .map_err(|_| Error::from_static_msg("invalid auth parameter"))?;
202
203 reader.skip_whitespace();
204
205 let value = if reader.current_char() == Some('"') {
206 reader.parse_quoted_string()?
207 } else {
208 reader.read_until(|c| c == ',').trim().into()
209 };
210
211 reader.skip_whitespace();
212
213 if !reader.is_empty() {
214 reader
215 .match_char(',')
216 .map_err(|_| Error::from_static_msg("invalid auth parameter"))?;
217 }
218
219 let res = AuthParam {
220 name: name.to_ascii_lowercase(),
221 value,
222 };
223
224 return Ok(Some(res));
225 }
226
227 Ok(None)
228 }
229
230 let mut reader = StringReader::new(self.as_str());
231
232 let res = inner(&mut reader)?;
233
234 *self = reader;
235
236 Ok(res)
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use std::str::FromStr;
243
244 use str_reader::StringReader;
245 use ttpkit::header::HeaderFieldValue;
246
247 use crate::AuthChallenge;
248
249 use super::{
250 DigestAlgorithm, DigestChallenge, DigestResponse, QualityOfProtection, StringReaderExt,
251 };
252
253 #[test]
254 fn test_parse_auth_params() {
255 let mut reader = StringReader::new(" foo = bar ");
256
257 match reader.parse_auth_param() {
258 Ok(Some(p)) => {
259 assert_eq!(p.name, "foo");
260 assert_eq!(p.value, "bar");
261 }
262 v => panic!("unexpected result: {:?}", v),
263 }
264
265 assert!(matches!(reader.parse_auth_param(), Ok(None)));
266 assert!(reader.is_empty());
267
268 let mut reader = StringReader::new(" foo = bar, ");
269
270 match reader.parse_auth_param() {
271 Ok(Some(p)) => {
272 assert_eq!(p.name, "foo");
273 assert_eq!(p.value, "bar");
274 }
275 v => panic!("unexpected result: {:?}", v),
276 }
277
278 assert!(matches!(reader.parse_auth_param(), Ok(None)));
279 assert!(reader.is_empty());
280
281 let mut reader = StringReader::new("foo=\" bar, barr \", aaa=bbb");
282
283 match reader.parse_auth_param() {
284 Ok(Some(p)) => {
285 assert_eq!(p.name, "foo");
286 assert_eq!(p.value, " bar, barr ");
287 }
288 v => panic!("unexpected result: {:?}", v),
289 }
290
291 match reader.parse_auth_param() {
292 Ok(Some(p)) => {
293 assert_eq!(p.name, "aaa");
294 assert_eq!(p.value, "bbb");
295 }
296 v => panic!("unexpected result: {:?}", v),
297 }
298
299 assert!(matches!(reader.parse_auth_param(), Ok(None)));
300 assert!(reader.is_empty());
301
302 let mut reader = StringReader::new("foo=bar,, , aaa=bbb");
303
304 match reader.parse_auth_param() {
305 Ok(Some(p)) => {
306 assert_eq!(p.name, "foo");
307 assert_eq!(p.value, "bar");
308 }
309 v => panic!("unexpected result: {:?}", v),
310 }
311
312 match reader.parse_auth_param() {
313 Ok(Some(p)) => {
314 assert_eq!(p.name, "aaa");
315 assert_eq!(p.value, "bbb");
316 }
317 v => panic!("unexpected result: {:?}", v),
318 }
319
320 assert!(matches!(reader.parse_auth_param(), Ok(None)));
321 assert!(reader.is_empty());
322
323 let mut reader = StringReader::new(" = bar ");
324
325 assert!(reader.parse_auth_param().is_err());
326
327 let mut reader = StringReader::new(" foo ");
328
329 assert!(reader.parse_auth_param().is_err());
330
331 let mut reader = StringReader::new(" foo, ");
332
333 assert!(reader.parse_auth_param().is_err());
334
335 let mut reader = StringReader::new("foo=\"bar\\");
336
337 assert!(reader.parse_auth_param().is_err());
338
339 let mut reader = StringReader::new("foo=\"bar");
340
341 assert!(reader.parse_auth_param().is_err());
342
343 let mut reader = StringReader::new("foo=\"bar, barr\" aaa, bbb=ccc");
344
345 assert!(reader.parse_auth_param().is_err());
346 }
347
348 #[test]
349 fn test_parse_digest_response() {
350 let response = "Digest realm=foo, username=user, \
351 uri=\"http://1.1.1.1/\", qop=auth, algorithm=MD5, \
352 nonce=1, cnonce=1, nc=1, response=foo";
353
354 let response = DigestResponse::from_str(response);
355
356 assert!(response.is_ok());
357 }
358
359 #[test]
360 fn test_building_digest_response() {
361 let alg = DigestAlgorithm::Md5;
362
363 let challenge = DigestChallenge::builder("my_realm")
364 .nonce("93dcebf46e4243cc=b235f016e3c64d9")
365 .qops([QualityOfProtection::Auth])
366 .algorithm(alg)
367 .build()
368 .to_string();
369
370 assert_eq!(
371 challenge,
372 "Digest realm=\"my_realm\", nonce=\"93dcebf46e4243cc=b235f016e3c64d9\", algorithm=MD5, qop=\"auth\""
373 );
374
375 let header = HeaderFieldValue::from(challenge);
376
377 let challenge = AuthChallenge::parse(&header).unwrap().pop().unwrap();
378
379 let response = DigestChallenge::try_from(&challenge)
380 .unwrap()
381 .response_builder()
382 .nc(1)
383 .cnonce("3090917bf7ecd130dfd11d12c839e131")
384 .build("GET", "http://192.168.1.1/", Some(&[]), "root", "pass")
385 .unwrap()
386 .to_string();
387
388 assert_eq!(
389 response.as_str(),
390 "Digest username=\"root\", realm=\"my_realm\", nonce=\"93dcebf46e4243cc=b235f016e3c64d9\", uri=\"http://192.168.1.1/\", response=\"3cfb965fe7eaa03618c5c8d14aa77800\", algorithm=MD5, qop=auth, nc=00000001, cnonce=\"3090917bf7ecd130dfd11d12c839e131\""
391 );
392
393 let password_hash = alg.digest(b"root:my_realm:pass");
394
395 let valid = DigestResponse::from_str(&response)
396 .unwrap()
397 .verify("GET", &password_hash);
398
399 assert!(valid);
400 }
401
402 #[test]
403 fn test_multiple_algorithm_response() {
404 let header = HeaderFieldValue::from(
405 "Digest realm=\"my_realm\", nonce=\"93dcebf46e4243cc=b235f016e3c64d9\", algorithm=\"MD5,SHA-256\", qop=\"auth\"",
406 );
407
408 let challenge = AuthChallenge::parse(&header).unwrap().pop().unwrap();
409
410 let response = DigestChallenge::try_from(&challenge)
411 .unwrap()
412 .response_builder()
413 .nc(1)
414 .cnonce("3090917bf7ecd130dfd11d12c839e131")
415 .build("GET", "http://192.168.1.1/", Some(&[]), "root", "pass")
416 .unwrap()
417 .to_string();
418
419 assert_eq!(
420 response.as_str(),
421 "Digest username=\"root\", realm=\"my_realm\", nonce=\"93dcebf46e4243cc=b235f016e3c64d9\", uri=\"http://192.168.1.1/\", response=\"3cfb965fe7eaa03618c5c8d14aa77800\", algorithm=MD5, qop=auth, nc=00000001, cnonce=\"3090917bf7ecd130dfd11d12c839e131\""
422 );
423 }
424
425 #[test]
426 fn test_no_qop_response() {
427 let alg = DigestAlgorithm::Md5;
428
429 let challenge = DigestChallenge::builder("my_realm")
430 .nonce("93dcebf46e4243cc=b235f016e3c64d9")
431 .algorithm(alg)
432 .build()
433 .to_string();
434
435 assert_eq!(
436 challenge,
437 "Digest realm=\"my_realm\", nonce=\"93dcebf46e4243cc=b235f016e3c64d9\", algorithm=MD5"
438 );
439
440 let header = HeaderFieldValue::from(challenge);
441
442 let challenge = AuthChallenge::parse(&header).unwrap().pop().unwrap();
443
444 let response = DigestChallenge::try_from(&challenge)
445 .unwrap()
446 .response_builder()
447 .build("GET", "http://192.168.1.1/", Some(&[]), "root", "pass")
448 .unwrap()
449 .to_string();
450
451 assert_eq!(
452 response.as_str(),
453 "Digest username=\"root\", realm=\"my_realm\", nonce=\"93dcebf46e4243cc=b235f016e3c64d9\", uri=\"http://192.168.1.1/\", response=\"9ae725b16537f446b7120a479d3cd2e2\", algorithm=MD5"
454 );
455
456 let password_hash = alg.digest(b"root:my_realm:pass");
457
458 let valid = DigestResponse::from_str(&response)
459 .unwrap()
460 .verify("GET", &password_hash);
461
462 assert!(valid);
463 }
464
465 #[test]
466 fn test_no_alg_challenge() {
467 let alg = DigestAlgorithm::Md5;
468
469 let challenge = DigestChallenge::builder("my_realm")
470 .nonce("93dcebf46e4243cc=b235f016e3c64d9")
471 .algorithm(alg)
472 .emit_md5(false)
473 .build()
474 .to_string();
475
476 assert_eq!(
477 challenge,
478 "Digest realm=\"my_realm\", nonce=\"93dcebf46e4243cc=b235f016e3c64d9\""
479 );
480
481 let header = HeaderFieldValue::from(challenge);
482
483 let challenge = AuthChallenge::parse(&header).unwrap().pop().unwrap();
484
485 let response = DigestChallenge::try_from(&challenge)
486 .unwrap()
487 .response_builder()
488 .build("GET", "http://192.168.1.1/", Some(&[]), "root", "pass")
489 .unwrap()
490 .to_string();
491
492 assert_eq!(
493 response.as_str(),
494 "Digest username=\"root\", realm=\"my_realm\", nonce=\"93dcebf46e4243cc=b235f016e3c64d9\", uri=\"http://192.168.1.1/\", response=\"9ae725b16537f446b7120a479d3cd2e2\""
495 );
496
497 let password_hash = alg.digest(b"root:my_realm:pass");
498
499 let valid = DigestResponse::from_str(&response)
500 .unwrap()
501 .verify("GET", &password_hash);
502
503 assert!(valid);
504 }
505
506 #[test]
507 fn test_no_md5_response() {
508 let header = HeaderFieldValue::from(
509 "Digest realm=\"my_realm\", nonce=\"93dcebf46e4243cc=b235f016e3c64d9\", algorithm=\"MD5\"",
510 );
511
512 let challenge = AuthChallenge::parse(&header).unwrap().pop().unwrap();
513
514 let response = DigestChallenge::try_from(&challenge)
515 .unwrap()
516 .response_builder()
517 .echo_md5(false)
518 .build("GET", "http://192.168.1.1/", Some(&[]), "root", "pass")
519 .unwrap()
520 .to_string();
521
522 assert_eq!(
523 response.as_str(),
524 "Digest username=\"root\", realm=\"my_realm\", nonce=\"93dcebf46e4243cc=b235f016e3c64d9\", uri=\"http://192.168.1.1/\", response=\"9ae725b16537f446b7120a479d3cd2e2\""
525 );
526 }
527}