Skip to main content

ttpkit_auth/digest/
mod.rs

1//! HTTP Digest authentication.
2
3mod 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/// 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        // helper function
68        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/// Quality of Protection parameter.
124#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
125pub enum QualityOfProtection {
126    Auth,
127    AuthInt,
128}
129
130impl QualityOfProtection {
131    /// Parse a QOP list from a string.
132    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/// Helper type.
176#[derive(Debug)]
177struct AuthParam {
178    name: String,
179    value: String,
180}
181
182/// Helper type.
183trait StringReaderExt {
184    /// Try to parse an authorization parameter.
185    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        // helper function
191        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}