ttpkit_auth/digest/
mod.rs

1//! HTTP Digest authentication.
2
3mod 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/// Digest algorithm.
24#[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    /// Get the name of the digest algorithm.
39    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    /// Check if the algorithm is a "sess" variant.
53    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    /// Check if the algorithm is MD5.
61    const fn is_md5(&self) -> bool {
62        matches!(self, Self::Md5 | Self::Md5_SESS)
63    }
64
65    /// Create a hash from a given input using the digest algorithm.
66    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/// Quality of Protection parameter.
119#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
120pub enum QualityOfProtection {
121    Auth,
122    AuthInt,
123}
124
125impl QualityOfProtection {
126    /// Parse a QOP list from a string.
127    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/// Helper type.
171#[derive(Debug)]
172struct AuthParam {
173    name: String,
174    value: String,
175}
176
177/// Helper type.
178trait StringReaderExt {
179    /// Try to parse an authorization parameter.
180    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        // helper function
186        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}