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