ttpkit_auth/
digest.rs

1//! HTTP Digest authentication.
2
3use std::{
4    fmt::{self, Display, Formatter},
5    str::FromStr,
6};
7
8use sha2::{Digest, Sha256};
9use str_reader::StringReader;
10use ttpkit::header::HeaderFieldValue;
11
12use crate::Error;
13
14/// Digest algorithm.
15#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
16pub enum DigestAlgorithm {
17    Md5,
18    Sha256,
19}
20
21impl DigestAlgorithm {
22    /// Create a hash from a given input using the digest algorithm.
23    pub fn digest(&self, input: &[u8]) -> String {
24        match self {
25            Self::Md5 => format!("{:x}", md5::compute(input)),
26            Self::Sha256 => format!("{:x}", Sha256::digest(input)),
27        }
28    }
29}
30
31impl Display for DigestAlgorithm {
32    #[inline]
33    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
34        let s = match self {
35            Self::Md5 => "MD5",
36            Self::Sha256 => "SHA-256",
37        };
38
39        f.write_str(s)
40    }
41}
42
43impl FromStr for DigestAlgorithm {
44    type Err = Error;
45
46    fn from_str(s: &str) -> Result<Self, Self::Err> {
47        let res = match s {
48            "MD5" => Self::Md5,
49            "SHA-256" => Self::Sha256,
50            _ => {
51                return Err(Error::from_static_msg(
52                    "unknown/unsupported digest algorithm",
53                ));
54            }
55        };
56
57        Ok(res)
58    }
59}
60
61/// Quality of Protection parameter.
62#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
63pub enum QualityOfProtection {
64    Auth,
65    AuthInt,
66}
67
68impl Display for QualityOfProtection {
69    #[inline]
70    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
71        let s = match self {
72            Self::Auth => "auth",
73            Self::AuthInt => "auth-int",
74        };
75
76        f.write_str(s)
77    }
78}
79
80impl FromStr for QualityOfProtection {
81    type Err = Error;
82
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        let res = match s {
85            "auth" => Self::Auth,
86            "auth-int" => Self::AuthInt,
87            _ => return Err(Error::from_static_msg("unknown qop")),
88        };
89
90        Ok(res)
91    }
92}
93
94/// Digest challenge.
95#[derive(Clone)]
96pub struct DigestChallenge {
97    realm: String,
98    qops: Vec<QualityOfProtection>,
99    algorithm: DigestAlgorithm,
100    nonce: u64,
101}
102
103impl DigestChallenge {
104    /// Create a new Digest challenge for a given realm.
105    pub fn new<R, Q>(realm: R, qops: Q, algorithm: DigestAlgorithm) -> Self
106    where
107        R: Into<String>,
108        Q: IntoIterator<Item = QualityOfProtection>,
109    {
110        let realm = realm.into();
111        let qops = qops.into_iter();
112
113        let nonce = rand::random();
114
115        Self {
116            realm,
117            qops: qops.collect(),
118            algorithm,
119            nonce,
120        }
121    }
122}
123
124impl Display for DigestChallenge {
125    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
126        write!(f, "Digest realm=\"{}\"", self.realm)?;
127
128        if !self.qops.is_empty() {
129            let mut qops = self.qops.iter();
130
131            write!(f, ",qop=\"{}", qops.next().unwrap())?;
132
133            for qop in qops {
134                write!(f, ", {qop}")?;
135            }
136
137            write!(f, "\"")?;
138        }
139
140        if self.algorithm != DigestAlgorithm::Md5 {
141            write!(f, ",algorithm={}", self.algorithm)?;
142        }
143
144        write!(f, ",nonce=\"{:016x}\"", self.nonce)
145    }
146}
147
148impl From<DigestChallenge> for HeaderFieldValue {
149    fn from(challenge: DigestChallenge) -> Self {
150        HeaderFieldValue::from(challenge.to_string())
151    }
152}
153
154/// Digest response.
155pub struct DigestResponse {
156    realm: String,
157    uri: String,
158    username: String,
159    qop: Option<QualityOfProtection>,
160    nonce: String,
161    cnonce: Option<String>,
162    nc: Option<String>,
163    algorithm: DigestAlgorithm,
164    response: String,
165}
166
167impl DigestResponse {
168    /// Get realm.
169    #[inline]
170    pub fn realm(&self) -> &str {
171        &self.realm
172    }
173
174    /// Get username.
175    #[inline]
176    pub fn username(&self) -> &str {
177        &self.username
178    }
179
180    /// Get the Digest algorithm used.
181    #[inline]
182    pub fn algorithm(&self) -> DigestAlgorithm {
183        self.algorithm
184    }
185
186    /// Verify the response.
187    ///
188    /// # Arguments
189    /// * `method` - request method
190    /// * `password_hash` - digest password hash (i.e. hashed realm, username
191    ///   and password)
192    pub fn verify<M>(&self, method: M, password_hash: &str) -> bool
193    where
194        M: Display,
195    {
196        self.verify_inner(&format!("{}:{}", method, self.uri), password_hash)
197    }
198
199    /// Verify the response.
200    ///
201    /// # Arguments
202    /// * `a2` - method:uri
203    /// * `password_hash` - digest password hash (i.e. hashed realm, username
204    ///   and password)
205    fn verify_inner(&self, a2: &str, password_hash: &str) -> bool {
206        if self.qop.unwrap_or(QualityOfProtection::Auth) == QualityOfProtection::AuthInt {
207            return false;
208        }
209
210        let a2_hash = self.algorithm.digest(a2.as_bytes());
211
212        let input = if let Some(qop) = self.qop {
213            let nc = self.nc.as_deref().unwrap_or("");
214            let cnonce = self.cnonce.as_deref().unwrap_or("");
215
216            format!(
217                "{}:{}:{}:{}:{}:{}",
218                password_hash, self.nonce, nc, cnonce, qop, a2_hash
219            )
220        } else {
221            format!("{}:{}:{}", password_hash, self.nonce, a2_hash)
222        };
223
224        let hash = self.algorithm.digest(input.as_bytes());
225
226        hash.eq_ignore_ascii_case(&self.response)
227    }
228}
229
230impl FromStr for DigestResponse {
231    type Err = Error;
232
233    fn from_str(s: &str) -> Result<Self, Self::Err> {
234        let mut reader = StringReader::new(s);
235
236        let auth_method = reader.read_word();
237
238        if !auth_method.eq_ignore_ascii_case("digest") {
239            return Err(Error::from_static_msg("not a Digest authorization"));
240        }
241
242        let mut realm = None;
243        let mut uri = None;
244        let mut username = None;
245        let mut qop = None;
246        let mut nonce = None;
247        let mut cnonce = None;
248        let mut nc = None;
249        let mut algorithm = None;
250        let mut response = None;
251
252        while let Some((name, value)) = parse_auth_param(&mut reader)? {
253            match &*name {
254                "realm" => realm = Some(value),
255                "uri" => uri = Some(value),
256                "username" => username = Some(value),
257                "qop" => qop = Some(value),
258                "nonce" => nonce = Some(value),
259                "cnonce" => cnonce = Some(value),
260                "nc" => nc = Some(value),
261                "algorithm" => algorithm = Some(value),
262                "response" => response = Some(value),
263                _ => (),
264            }
265        }
266
267        let res = Self {
268            realm: realm.ok_or_else(|| Error::from_static_msg("the realm parameter is missing"))?,
269            uri: uri.ok_or_else(|| Error::from_static_msg("the uri parameter is missing"))?,
270            username: username
271                .ok_or_else(|| Error::from_static_msg("the username parameter is missing"))?,
272            qop: qop.map(|qop| qop.parse()).transpose()?,
273            nonce: nonce.ok_or_else(|| Error::from_static_msg("the nonce parameter is missing"))?,
274            cnonce,
275            nc,
276            algorithm: algorithm.as_deref().unwrap_or("MD5").parse()?,
277            response: response
278                .ok_or_else(|| Error::from_static_msg("the response parameter is missing"))?,
279        };
280
281        if res.qop.is_some() {
282            if res.cnonce.is_none() {
283                return Err(Error::from_static_msg("the cnonce parameter is missing"));
284            } else if res.nc.is_none() {
285                return Err(Error::from_static_msg("the nc parameter is missing"));
286            }
287        } else if res.cnonce.is_some() {
288            return Err(Error::from_static_msg(
289                "the cnonce parameter is not expected",
290            ));
291        } else if res.nc.is_some() {
292            return Err(Error::from_static_msg("the nc parameter is not expected"));
293        }
294
295        Ok(res)
296    }
297}
298
299/// Try to parse an authorization parameter.
300fn parse_auth_param(reader: &mut StringReader) -> Result<Option<(String, String)>, Error> {
301    let mut tmp = StringReader::new(reader.as_str());
302
303    while !tmp.is_empty() {
304        let name = tmp.read_until(|c| c == ',' || c == '=').trim();
305
306        if name.is_empty() {
307            match tmp.read_char() {
308                Ok(',') => continue,
309                Ok('=') => return Err(Error::from_static_msg("empty auth parameter name")),
310                Ok(_) => panic!("unexpected character"),
311                Err(_) => break,
312            }
313        }
314
315        tmp.match_char('=')
316            .map_err(|_| Error::from_static_msg("invalid auth parameter"))?;
317
318        tmp.skip_whitespace();
319
320        let value = if tmp.current_char() == Some('"') {
321            parse_quoted_string(&mut tmp)?
322        } else {
323            tmp.read_until(|c| c == ',').trim().into()
324        };
325
326        tmp.skip_whitespace();
327
328        if !tmp.is_empty() {
329            tmp.match_char(',')
330                .map_err(|_| Error::from_static_msg("invalid auth parameter"))?;
331        }
332
333        *reader = tmp;
334
335        return Ok(Some((name.to_ascii_lowercase(), value)));
336    }
337
338    *reader = tmp;
339
340    Ok(None)
341}
342
343/// Parse quoted string.
344fn parse_quoted_string(reader: &mut StringReader) -> Result<String, Error> {
345    reader.match_char('"').map_err(|_| {
346        Error::from_static_msg("quoted string does not start with the double quote sign")
347    })?;
348
349    let mut res = String::new();
350
351    while let Some(c) = reader.current_char() {
352        if c == '\\' {
353            reader.skip_char();
354
355            let c = reader
356                .current_char()
357                .ok_or_else(|| Error::from_static_msg("end of string within an escape sequence"))?;
358
359            res.push(c);
360        } else if c == '"' {
361            break;
362        } else {
363            res.push(c);
364        }
365
366        reader.skip_char();
367    }
368
369    reader.match_char('"').map_err(|_| {
370        Error::from_static_msg("quoted string does not end with the double quote sign")
371    })?;
372
373    Ok(res)
374}
375
376#[cfg(test)]
377mod tests {
378    use std::str::FromStr;
379
380    use str_reader::StringReader;
381
382    use super::{DigestResponse, parse_auth_param};
383
384    #[test]
385    fn test_parse_auth_params() {
386        let mut reader = StringReader::new(" foo = bar ");
387
388        match parse_auth_param(&mut reader) {
389            Ok(Some((name, value))) => {
390                assert_eq!(name, "foo");
391                assert_eq!(value, "bar");
392            }
393            v => panic!("unexpected result: {:?}", v),
394        }
395
396        assert!(matches!(parse_auth_param(&mut reader), Ok(None)));
397        assert!(reader.is_empty());
398
399        let mut reader = StringReader::new(" foo = bar, ");
400
401        match parse_auth_param(&mut reader) {
402            Ok(Some((name, value))) => {
403                assert_eq!(name, "foo");
404                assert_eq!(value, "bar");
405            }
406            v => panic!("unexpected result: {:?}", v),
407        }
408
409        assert!(matches!(parse_auth_param(&mut reader), Ok(None)));
410        assert!(reader.is_empty());
411
412        let mut reader = StringReader::new("foo=\" bar, barr \", aaa=bbb");
413
414        match parse_auth_param(&mut reader) {
415            Ok(Some((name, value))) => {
416                assert_eq!(name, "foo");
417                assert_eq!(value, " bar, barr ");
418            }
419            v => panic!("unexpected result: {:?}", v),
420        }
421
422        match parse_auth_param(&mut reader) {
423            Ok(Some((name, value))) => {
424                assert_eq!(name, "aaa");
425                assert_eq!(value, "bbb");
426            }
427            v => panic!("unexpected result: {:?}", v),
428        }
429
430        assert!(matches!(parse_auth_param(&mut reader), Ok(None)));
431        assert!(reader.is_empty());
432
433        let mut reader = StringReader::new("foo=bar,, , aaa=bbb");
434
435        match parse_auth_param(&mut reader) {
436            Ok(Some((name, value))) => {
437                assert_eq!(name, "foo");
438                assert_eq!(value, "bar");
439            }
440            v => panic!("unexpected result: {:?}", v),
441        }
442
443        match parse_auth_param(&mut reader) {
444            Ok(Some((name, value))) => {
445                assert_eq!(name, "aaa");
446                assert_eq!(value, "bbb");
447            }
448            v => panic!("unexpected result: {:?}", v),
449        }
450
451        assert!(matches!(parse_auth_param(&mut reader), Ok(None)));
452        assert!(reader.is_empty());
453
454        let mut reader = StringReader::new(" = bar ");
455
456        assert!(parse_auth_param(&mut reader).is_err());
457
458        let mut reader = StringReader::new(" foo ");
459
460        assert!(parse_auth_param(&mut reader).is_err());
461
462        let mut reader = StringReader::new(" foo, ");
463
464        assert!(parse_auth_param(&mut reader).is_err());
465
466        let mut reader = StringReader::new("foo=\"bar\\");
467
468        assert!(parse_auth_param(&mut reader).is_err());
469
470        let mut reader = StringReader::new("foo=\"bar");
471
472        assert!(parse_auth_param(&mut reader).is_err());
473
474        let mut reader = StringReader::new("foo=\"bar, barr\" aaa, bbb=ccc");
475
476        assert!(parse_auth_param(&mut reader).is_err());
477    }
478
479    #[test]
480    fn test_parse_digest_response() {
481        let response = "Digest realm=foo, username=user, \
482                        uri=\"http://1.1.1.1/\", qop=auth, algorithm=MD5, \
483                        nonce=1, cnonce=1, nc=1, response=foo";
484
485        let response = DigestResponse::from_str(response);
486
487        assert!(response.is_ok());
488    }
489}