ttpkit_auth/digest/
response.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Display, Formatter},
4    str::FromStr,
5};
6
7use str_reader::StringReader;
8use ttpkit::header::HeaderFieldValue;
9
10use crate::{
11    DisplayEscaped, Error,
12    digest::{DigestAlgorithm, QualityOfProtection, StringReaderExt, challenge::DigestChallenge},
13};
14
15/// Digest response builder.
16pub struct DigestResponseBuilder<'a> {
17    challenge: &'a DigestChallenge,
18    nc: u32,
19    cnonce: Option<String>,
20    echo_md5: bool,
21}
22
23impl DigestResponseBuilder<'_> {
24    /// Create a new Digest response builder for a given challenge.
25    #[inline]
26    pub(crate) fn new(challenge: &DigestChallenge) -> DigestResponseBuilder<'_> {
27        DigestResponseBuilder {
28            challenge,
29            nc: 1,
30            cnonce: None,
31            echo_md5: true,
32        }
33    }
34
35    /// Set the nonce count.
36    ///
37    /// The first request using the same nonce value must start with nonce
38    /// count 1 and increase by one for each subsequent request. The default
39    /// value is 1.
40    #[inline]
41    pub fn nc(mut self, nc: u32) -> Self {
42        self.nc = nc;
43        self
44    }
45
46    /// Set the client nonce.
47    ///
48    /// It will be generated automatically if not set.
49    pub fn cnonce<T>(mut self, cnonce: T) -> Self
50    where
51        T: Into<String>,
52    {
53        self.cnonce = Some(cnonce.into());
54        self
55    }
56
57    /// Set this to `false` to omit the `algorithm=MD5` parameter in the
58    /// response.
59    ///
60    /// This omits the parameter even if it was present in the challenge (it
61    /// can be used to deal with some buggy servers). The default value is
62    /// `true`.
63    #[inline]
64    pub fn echo_md5(mut self, echo_md5: bool) -> Self {
65        self.echo_md5 = echo_md5;
66        self
67    }
68
69    /// Build the Digest response.
70    pub fn build(
71        mut self,
72        method: &str,
73        uri: &str,
74        body: Option<&[u8]>,
75        username: &str,
76        password: &str,
77    ) -> Result<DigestResponse, Error> {
78        let realm = self.challenge.realm();
79        let nonce = self.challenge.nonce();
80        let algorithm = self.challenge.algorithm();
81
82        let cnonce = self
83            .cnonce
84            .take()
85            .unwrap_or_else(|| format!("{:016x}", rand::random::<u64>()));
86
87        let nc = format!("{:08x}", self.nc);
88
89        let a1 = self.get_a1(username, password, &cnonce);
90        let a2 = self.get_a2(method, uri, body)?;
91
92        let a1_hash = algorithm.digest(a1.as_bytes());
93        let a2_hash = algorithm.digest(a2.as_bytes());
94
95        let qop = self.get_qop(body);
96
97        let data = if let Some(qop) = qop {
98            format!("{a1_hash}:{nonce}:{nc}:{cnonce}:{qop}:{a2_hash}")
99        } else {
100            format!("{a1_hash}:{nonce}:{a2_hash}")
101        };
102
103        let algorithm_param = if !self.echo_md5 && algorithm.is_md5() {
104            None
105        } else {
106            self.challenge.algorithm_param()
107        };
108
109        let opaque = self.challenge.opaque();
110
111        let res = DigestResponse {
112            realm: realm.into(),
113            uri: uri.into(),
114            username: username.into(),
115            qop,
116            nonce: nonce.into(),
117            cnonce: Some(cnonce),
118            nc: Some(nc),
119            algorithm_param: algorithm_param.map(|p| Cow::Owned(p.into())),
120            algorithm,
121            response: algorithm.digest(data.as_bytes()),
122            opaque: opaque.map(String::from),
123        };
124
125        Ok(res)
126    }
127
128    /// Get the A1 value as defined in RFC 7616.
129    fn get_a1(&self, username: &str, password: &str, cnonce: &str) -> String {
130        let realm = self.challenge.realm();
131        let nonce = self.challenge.nonce();
132        let algorithm = self.challenge.algorithm();
133
134        let res = format!("{username}:{realm}:{password}");
135
136        if algorithm.is_sess() {
137            let hash = algorithm.digest(res.as_bytes());
138
139            format!("{hash}:{nonce}:{cnonce}")
140        } else {
141            res
142        }
143    }
144
145    /// Get the A2 value as defined in RFC 7616.
146    fn get_a2(&self, method: &str, uri: &str, body: Option<&[u8]>) -> Result<String, Error> {
147        if let Some(QualityOfProtection::AuthInt) = self.get_qop(body) {
148            let body = body.ok_or_else(|| Error::from_static_msg("request body is required"))?;
149
150            let algorithm = self.challenge.algorithm();
151
152            let hash = algorithm.digest(body);
153
154            Ok(format!("{method}:{uri}:{hash}"))
155        } else {
156            Ok(format!("{method}:{uri}"))
157        }
158    }
159
160    /// Select a QOP from available QOPs or return None if there are no
161    /// available QOPs.
162    fn get_qop(&self, body: Option<&[u8]>) -> Option<QualityOfProtection> {
163        let qops = self.challenge.qops();
164
165        if qops.is_empty() {
166            return None;
167        }
168
169        let mut auth = false;
170        let mut auth_int = false;
171
172        for qop in qops {
173            match qop {
174                QualityOfProtection::Auth => auth = true,
175                QualityOfProtection::AuthInt => auth_int = true,
176            }
177        }
178
179        if auth_int && body.is_some() {
180            Some(QualityOfProtection::AuthInt)
181        } else if auth {
182            Some(QualityOfProtection::Auth)
183        } else {
184            Some(QualityOfProtection::AuthInt)
185        }
186    }
187}
188
189/// Digest response.
190pub struct DigestResponse {
191    realm: String,
192    uri: String,
193    username: String,
194    qop: Option<QualityOfProtection>,
195    nonce: String,
196    cnonce: Option<String>,
197    nc: Option<String>,
198    algorithm_param: Option<Cow<'static, str>>,
199    algorithm: DigestAlgorithm,
200    opaque: Option<String>,
201    response: String,
202}
203
204impl DigestResponse {
205    /// Get realm.
206    #[inline]
207    pub fn realm(&self) -> &str {
208        &self.realm
209    }
210
211    /// Get username.
212    #[inline]
213    pub fn username(&self) -> &str {
214        &self.username
215    }
216
217    /// Get the Digest algorithm used.
218    #[inline]
219    pub fn algorithm(&self) -> DigestAlgorithm {
220        self.algorithm
221    }
222
223    /// Verify the response.
224    ///
225    /// # Arguments
226    /// * `method` - request method
227    /// * `password_hash` - digest password hash (i.e. hashed realm, username
228    ///   and password)
229    pub fn verify<M>(&self, method: M, password_hash: &str) -> bool
230    where
231        M: Display,
232    {
233        self.verify_inner(&format!("{}:{}", method, self.uri), password_hash)
234    }
235
236    /// Verify the response.
237    ///
238    /// # Arguments
239    /// * `a2` - method:uri
240    /// * `password_hash` - digest password hash (i.e. hashed realm, username
241    ///   and password)
242    fn verify_inner(&self, a2: &str, password_hash: &str) -> bool {
243        if self.qop.unwrap_or(QualityOfProtection::Auth) == QualityOfProtection::AuthInt {
244            return false;
245        }
246
247        let nonce = self.nonce.as_str();
248
249        let a2_hash = self.algorithm.digest(a2.as_bytes());
250
251        let input = if let Some(qop) = self.qop {
252            let nc = self.nc.as_deref().unwrap_or("");
253            let cnonce = self.cnonce.as_deref().unwrap_or("");
254
255            format!("{password_hash}:{nonce}:{nc}:{cnonce}:{qop}:{a2_hash}",)
256        } else {
257            format!("{password_hash}:{nonce}:{a2_hash}")
258        };
259
260        let hash = self.algorithm.digest(input.as_bytes());
261
262        hash.eq_ignore_ascii_case(&self.response)
263    }
264}
265
266impl Display for DigestResponse {
267    fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
268        // TODO: use username hash if the server supports it
269        // TODO: use username* if the username cannot be encoded in ASCII
270
271        let username = DisplayEscaped::new(&self.username);
272        let realm = DisplayEscaped::new(&self.realm);
273        let nonce = DisplayEscaped::new(&self.nonce);
274        let uri = DisplayEscaped::new(&self.uri);
275        let response = DisplayEscaped::new(&self.response);
276
277        write!(
278            f,
279            "Digest username=\"{username}\", realm=\"{realm}\", nonce=\"{nonce}\", uri=\"{uri}\", response=\"{response}\"",
280        )?;
281
282        if let Some(opaque) = self.opaque.as_ref() {
283            write!(f, ", opaque=\"{}\"", DisplayEscaped::new(opaque))?;
284        }
285
286        if let Some(algorithm) = self.algorithm_param.as_ref() {
287            write!(f, ", algorithm={algorithm}")?;
288        }
289
290        if let Some(qop) = self.qop.as_ref() {
291            write!(f, ", qop={qop}")?;
292
293            if let Some(nc) = self.nc.as_ref() {
294                write!(f, ", nc={nc}")?;
295            }
296
297            if let Some(cnonce) = self.cnonce.as_ref() {
298                write!(f, ", cnonce=\"{}\"", DisplayEscaped::new(cnonce))?;
299            }
300        }
301
302        Ok(())
303    }
304}
305
306impl FromStr for DigestResponse {
307    type Err = Error;
308
309    fn from_str(s: &str) -> Result<Self, Self::Err> {
310        let mut reader = StringReader::new(s);
311
312        let auth_method = reader.read_word();
313
314        if !auth_method.eq_ignore_ascii_case("digest") {
315            return Err(Error::from_static_msg("not a Digest authorization"));
316        }
317
318        let mut realm = None;
319        let mut uri = None;
320        let mut username = None;
321        let mut qop = None;
322        let mut nonce = None;
323        let mut cnonce = None;
324        let mut nc = None;
325        let mut algorithm_param = None;
326        let mut response = None;
327        let mut opaque = None;
328
329        while let Some(p) = reader.parse_auth_param()? {
330            match p.name.as_str() {
331                "realm" => realm = Some(p.value),
332                "uri" => uri = Some(p.value),
333                "username" => username = Some(p.value),
334                "qop" => qop = Some(p.value),
335                "nonce" => nonce = Some(p.value),
336                "cnonce" => cnonce = Some(p.value),
337                "nc" => nc = Some(p.value),
338                "algorithm" => algorithm_param = Some(Cow::Owned(p.value)),
339                "response" => response = Some(p.value),
340                "opaque" => opaque = Some(p.value),
341                _ => (),
342            }
343        }
344
345        let realm =
346            realm.ok_or_else(|| Error::from_static_msg("the realm parameter is missing"))?;
347        let uri = uri.ok_or_else(|| Error::from_static_msg("the uri parameter is missing"))?;
348        let username =
349            username.ok_or_else(|| Error::from_static_msg("the username parameter is missing"))?;
350        let qop = qop.map(|qop| qop.parse()).transpose()?;
351        let nonce =
352            nonce.ok_or_else(|| Error::from_static_msg("the nonce parameter is missing"))?;
353        let algorithm = algorithm_param.as_deref().unwrap_or("MD5").parse()?;
354        let response =
355            response.ok_or_else(|| Error::from_static_msg("the response parameter is missing"))?;
356
357        if qop.is_some() {
358            if cnonce.is_none() {
359                return Err(Error::from_static_msg("the cnonce parameter is missing"));
360            } else if nc.is_none() {
361                return Err(Error::from_static_msg("the nc parameter is missing"));
362            }
363        } else if cnonce.is_some() {
364            return Err(Error::from_static_msg(
365                "the cnonce parameter is not expected",
366            ));
367        } else if nc.is_some() {
368            return Err(Error::from_static_msg("the nc parameter is not expected"));
369        }
370
371        let res = Self {
372            realm,
373            uri,
374            username,
375            qop,
376            nonce,
377            cnonce,
378            nc,
379            algorithm_param,
380            algorithm,
381            response,
382            opaque,
383        };
384
385        Ok(res)
386    }
387}
388
389impl From<DigestResponse> for HeaderFieldValue {
390    fn from(response: DigestResponse) -> Self {
391        HeaderFieldValue::from(response.to_string())
392    }
393}