digest_access/
digest_authenticator.rs

1use digest::{Digest, Output};
2use md5::Md5;
3use rand::{
4    distr::{Distribution, Uniform},
5    prelude::*,
6    rng,
7};
8use sha2::{Sha256, Sha512_256};
9use std::{fmt, ops::Range, str::FromStr};
10
11#[cfg(feature = "from-headers")]
12use http::{header::WWW_AUTHENTICATE, HeaderMap};
13#[cfg(feature = "from-headers")]
14use std::convert::TryFrom;
15
16#[derive(Debug, PartialEq)]
17enum DigestAlgorithm {
18    MD5,
19    SHA256,
20    SHA512_256,
21}
22
23impl DigestAlgorithm {
24    fn to_str(&self) -> &'static str {
25        match self {
26            DigestAlgorithm::MD5 => "MD5",
27            DigestAlgorithm::SHA256 => "SHA-256",
28            DigestAlgorithm::SHA512_256 => "SHA-512-256",
29        }
30    }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq)]
34enum QualityOfProtection {
35    None, // rfc2069
36    Auth,
37    AuthInt,
38}
39
40impl QualityOfProtection {
41    fn to_str(self) -> &'static str {
42        match self {
43            QualityOfProtection::Auth => "auth",
44            QualityOfProtection::AuthInt => "auth-int",
45            QualityOfProtection::None => "",
46        }
47    }
48}
49
50#[derive(Debug)]
51struct QualityOfProtectionData {
52    cnonce: String,
53    count_str: String,
54    qop: QualityOfProtection,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum DigestParseError {
59    Length,
60    InvalidEncoding,
61    MissingDigest,
62    MissingRealm,
63    MissingNonce,
64}
65
66impl DigestParseError {
67    fn description(&self) -> &str {
68        match self {
69            DigestParseError::Length => "Cannot parse Digest scheme from short string.",
70            DigestParseError::InvalidEncoding => "String doesn't match expected encoding.",
71            DigestParseError::MissingDigest => "String does not start with \"Digest \"",
72            DigestParseError::MissingNonce => "Digest scheme must contain a nonce value.",
73            DigestParseError::MissingRealm => "Digest scheme must contain a realm value.",
74        }
75    }
76}
77
78impl fmt::Display for DigestParseError {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        self.description().fmt(f)
81    }
82}
83
84#[derive(Debug, Default, Clone, Copy)]
85struct StrRange {
86    start: usize,
87    end: usize,
88}
89
90impl StrRange {
91    fn is_valid(&self) -> bool {
92        self.start < self.end
93    }
94}
95
96impl Into<Range<usize>> for StrRange {
97    fn into(self) -> Range<usize> {
98        self.start..self.end
99    }
100}
101
102#[derive(Debug)]
103pub struct DigestAccess {
104    authenticate: String,
105    nonce: StrRange,
106    domain: Vec<StrRange>,
107    realm: StrRange,
108    opaque: StrRange,
109    stale: bool,
110    nonce_count: u32,
111    algorithm: DigestAlgorithm,
112    session: bool,
113    userhash: bool,
114    qop: QualityOfProtection,
115    qop_data: Option<QualityOfProtectionData>,
116    username: Option<String>,
117    hashed_user_realm_pass: Option<Vec<u8>>,
118}
119
120impl FromStr for DigestAccess {
121    type Err = DigestParseError;
122
123    fn from_str(auth: &str) -> Result<Self, Self::Err> {
124        if auth.len() < Self::MIN_AUTH_LENGTH {
125            Err(DigestParseError::Length)
126        } else {
127            Self::create_from_www_auth(auth)
128        }
129    }
130}
131
132impl DigestAccess {
133    const MIN_AUTH_LENGTH: usize = 22;
134
135    pub fn set_username<A: Into<String>>(&mut self, username: A) {
136        self.username = Some(username.into());
137    }
138
139    pub fn set_password(&mut self, password: &str) {
140        if let Some(user) = self.username.as_ref() {
141            let hashed = match self.algorithm {
142                DigestAlgorithm::MD5 => {
143                    Self::hash_user_realm_password::<Md5>(user, self.realm(), password).to_vec()
144                }
145                DigestAlgorithm::SHA256 => {
146                    Self::hash_user_realm_password::<Sha256>(user, self.realm(), password).to_vec()
147                }
148                DigestAlgorithm::SHA512_256 => {
149                    Self::hash_user_realm_password::<Sha512_256>(user, self.realm(), password)
150                        .to_vec()
151                }
152            };
153            self.hashed_user_realm_pass = Some(hashed);
154        }
155    }
156
157    pub fn set_hashed_user_realm_password<A: Into<Vec<u8>>>(&mut self, hashed: A) {
158        self.hashed_user_realm_pass = Some(hashed.into());
159    }
160
161    /// Generate the Authorization header value
162    pub fn generate_authorization(
163        &mut self,
164        method: &str,
165        uri: &str,
166        body: Option<&[u8]>,
167        cnonce: Option<&str>,
168    ) -> Option<String> {
169        if self.username.is_none() || self.hashed_user_realm_pass.is_none() {
170            return None;
171        }
172
173        self.qop_data = if self.qop != QualityOfProtection::None {
174            let cnonce = match cnonce {
175                Some(c) => c.to_owned(),
176                None => Self::cnonce(),
177            };
178            self.nonce_count += 1;
179            let count_str = format!("{:08.x}", self.nonce_count);
180            let qop = if self.qop == QualityOfProtection::AuthInt && body.is_none() {
181                QualityOfProtection::Auth
182            } else {
183                self.qop
184            };
185            Some(QualityOfProtectionData {
186                cnonce,
187                count_str,
188                qop,
189            })
190        } else {
191            None
192        };
193        let response = match self.algorithm {
194            DigestAlgorithm::MD5 => self.generate_response_string::<Md5>(
195                self.hashed_user_realm_pass.as_ref().unwrap(),
196                method,
197                uri,
198                body,
199            ),
200            DigestAlgorithm::SHA256 => self.generate_response_string::<Sha256>(
201                self.hashed_user_realm_pass.as_ref().unwrap(),
202                method,
203                uri,
204                body,
205            ),
206            DigestAlgorithm::SHA512_256 => self.generate_response_string::<Sha512_256>(
207                self.hashed_user_realm_pass.as_ref().unwrap(),
208                method,
209                uri,
210                body,
211            ),
212        };
213        let username = self.username.as_ref().unwrap();
214        let mut auth_str_len = 90
215            + username.len()
216            + self.realm().len()
217            + self.nonce().len()
218            + uri.len()
219            + response.len();
220        if self.qop != QualityOfProtection::None {
221            let qop_data = self.qop_data.as_ref().unwrap();
222            auth_str_len +=
223                6 + qop_data.qop.to_str().len() + qop_data.count_str.len() + qop_data.cnonce.len();
224        }
225        if let Some(o) = self.opaque() {
226            auth_str_len += 11 + o.len();
227        }
228        if self.userhash {
229            auth_str_len += 15 + 64;
230        }
231        let mut auth = String::with_capacity(auth_str_len);
232        auth.push_str("Digest username=\"");
233        if self.userhash {
234            let user = match self.algorithm {
235                DigestAlgorithm::MD5 => self.hash_username::<Md5>(username),
236                DigestAlgorithm::SHA256 => self.hash_username::<Sha256>(username),
237                DigestAlgorithm::SHA512_256 => self.hash_username::<Sha512_256>(username),
238            };
239            auth.push_str(&user);
240        } else {
241            auth.push_str(username);
242        }
243        auth.push_str("\", realm=\"");
244        auth.push_str(self.realm());
245        auth.push_str("\", nonce=\"");
246        auth.push_str(self.nonce());
247        auth.push_str("\", uri=\"");
248        auth.push_str(uri);
249        if self.qop != QualityOfProtection::None {
250            let qop_data = self.qop_data.as_ref().unwrap();
251            auth.push_str("\", qop=");
252            auth.push_str(qop_data.qop.to_str());
253            auth.push_str(", algorithm=");
254            auth.push_str(self.algorithm.to_str());
255            auth.push_str(", nc=");
256            auth.push_str(&qop_data.count_str);
257            auth.push_str(", cnonce=\"");
258            auth.push_str(&qop_data.cnonce);
259        }
260        auth.push_str("\", response=\"");
261        auth.push_str(&response);
262        if let Some(o) = self.opaque() {
263            auth.push_str("\", opaque=\"");
264            auth.push_str(o);
265        }
266        auth.push('"');
267        if self.userhash {
268            auth.push_str(", userhash=true");
269        }
270        Some(auth)
271    }
272
273    fn create_from_www_auth(auth: &str) -> Result<Self, DigestParseError> {
274        let input = Self::digest_challenge(auth)?;
275        let mut res = Self {
276            authenticate: input.to_owned(),
277            nonce: StrRange::default(),
278            domain: Vec::new(),
279            realm: StrRange::default(),
280            opaque: StrRange::default(),
281            stale: false,
282            nonce_count: 0,
283            algorithm: DigestAlgorithm::MD5,
284            session: false,
285            userhash: false,
286            qop: QualityOfProtection::None,
287            qop_data: None,
288            username: None,
289            hashed_user_realm_pass: None,
290        };
291
292        #[derive(PartialEq)]
293        enum KeyVal {
294            PreKey,
295            Key,
296            PreVal,
297            QuoteVal,
298            Val,
299        }
300
301        let mut state = KeyVal::PreKey;
302        let mut key = StrRange::default();
303        let mut value = StrRange::default();
304
305        for (idx, ch) in input.char_indices() {
306            match state {
307                KeyVal::PreKey => {
308                    if !ch.is_ascii_whitespace() {
309                        key.start = idx;
310                        state = KeyVal::Key;
311                    }
312                }
313                KeyVal::Key => {
314                    if ch != '=' {
315                        continue;
316                    }
317                    key.end = idx;
318                    state = KeyVal::PreVal;
319                }
320                KeyVal::PreVal => {
321                    if ch == '"' {
322                        value.start = idx + 1;
323                        state = KeyVal::QuoteVal;
324                    } else {
325                        value.start = idx;
326                        state = KeyVal::Val;
327                    }
328                }
329                KeyVal::QuoteVal => {
330                    if ch != '"' {
331                        continue;
332                    }
333                    value.end = idx;
334                    let is_last = idx == input.len() - 1;
335                    if is_last {
336                        res.apply_directive(key, value);
337                    }
338                    state = KeyVal::Val;
339                }
340                KeyVal::Val => {
341                    let is_last = idx == input.len() - 1;
342                    if !is_last && ch != ',' {
343                        if value.end == 0 && ch.is_ascii_whitespace() {
344                            value.end = idx;
345                        }
346                        continue;
347                    }
348                    if value.end == 0 {
349                        value.end = idx;
350                    }
351                    if is_last {
352                        value.end = idx + 1;
353                    }
354                    res.apply_directive(key, value);
355                    value = StrRange::default();
356                    key = StrRange::default();
357                    state = KeyVal::PreKey;
358                }
359            }
360        }
361
362        match (res.nonce.is_valid(), res.realm.is_valid()) {
363            (_, false) => Err(DigestParseError::MissingRealm),
364            (false, _) => Err(DigestParseError::MissingNonce),
365            (true, true) => Ok(res),
366        }
367    }
368
369    #[inline(always)]
370    fn digest_challenge(input: &str) -> Result<&str, DigestParseError> {
371        if input.is_char_boundary(6) {
372            let (dig, input) = input.split_at(6);
373            if dig.eq_ignore_ascii_case("digest") {
374                let ret = input.trim_start_matches(|c: char| c.is_ascii_whitespace());
375                if input.len() == ret.len() {
376                    Err(DigestParseError::InvalidEncoding)
377                } else {
378                    Ok(ret)
379                }
380            } else {
381                Err(DigestParseError::MissingDigest)
382            }
383        } else {
384            Err(DigestParseError::InvalidEncoding)
385        }
386    }
387
388    fn apply_directive(&mut self, key: StrRange, val: StrRange) {
389        let key = self.authenticate_slice(key.into());
390        if key.eq_ignore_ascii_case("nonce") {
391            self.nonce = val;
392        } else if key.eq_ignore_ascii_case("realm") {
393            self.realm = val;
394        } else if key.eq_ignore_ascii_case("domain") {
395            // @todo solve splitting - this isn't commonly used
396            // res.domain = Some(value.as_str().split(' ').collect());
397        } else if key.eq_ignore_ascii_case("opaque") {
398            self.opaque = val;
399        } else if key.eq_ignore_ascii_case("stale")
400            && self
401                .authenticate_slice(val.into())
402                .eq_ignore_ascii_case("true")
403        {
404            self.stale = true;
405        } else if key.eq_ignore_ascii_case("algorithm") {
406            let alg_str = self.authenticate_slice(val.into()).to_ascii_lowercase();
407            if alg_str.contains("sha-256") {
408                self.algorithm = DigestAlgorithm::SHA256;
409                if alg_str.contains("sha-256-sess") {
410                    self.session = true;
411                }
412            } else if alg_str.contains("sha-512-256") {
413                self.algorithm = DigestAlgorithm::SHA512_256;
414                if alg_str.contains("sha-512-256-sess") {
415                    self.session = true;
416                }
417            } else if alg_str.contains("md5-sess") {
418                self.session = true;
419            }
420        } else if key.eq_ignore_ascii_case("qop") {
421            let qop_str = self.authenticate_slice(val.into()).to_ascii_lowercase();
422            if qop_str.contains(QualityOfProtection::AuthInt.to_str()) {
423                self.qop = QualityOfProtection::AuthInt;
424            } else {
425                self.qop = QualityOfProtection::Auth;
426            }
427        } else if key.eq_ignore_ascii_case("userhash")
428            && self
429                .authenticate_slice(val.into())
430                .eq_ignore_ascii_case("true")
431        {
432            self.userhash = true;
433        }
434    }
435
436    #[inline(always)]
437    fn authenticate_slice(&self, r: Range<usize>) -> &str {
438        &self.authenticate[r]
439    }
440
441    fn realm(&self) -> &str {
442        self.authenticate_slice(self.realm.into())
443    }
444
445    pub fn nonce(&self) -> &str {
446        self.authenticate_slice(self.nonce.into())
447    }
448
449    fn opaque(&self) -> Option<&str> {
450        if self.opaque.is_valid() {
451            Some(self.authenticate_slice(self.opaque.into()))
452        } else {
453            None
454        }
455    }
456
457    pub fn cnonce() -> String {
458        const HEX_CHARS: [char; 16] = [
459            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
460        ];
461        let mut rng = rng();
462        let size = Uniform::new_inclusive(8, 32)
463            .expect("Uniform distribution failure")
464            .sample(&mut rng);
465        let mut cnonce = String::with_capacity(size);
466        for _ in 0..size {
467            cnonce.push(*HEX_CHARS.choose(&mut rng).expect("Random choice failure"));
468        }
469        cnonce
470    }
471
472    pub fn hash_user_realm_password<T: Digest>(
473        username: &str,
474        realm: &str,
475        password: &str,
476    ) -> Output<T> {
477        let mut hasher = T::new();
478        hasher.update(username);
479        hasher.update(":");
480        hasher.update(realm);
481        hasher.update(":");
482        hasher.update(password);
483        hasher.finalize()
484    }
485
486    fn calculate_ha1<T: Digest>(&self, hashed_user_realm_pass: &[u8]) -> String {
487        if self.session {
488            let qop_data = self.qop_data.as_ref().unwrap();
489            let mut hasher = T::new();
490            hasher.update(hashed_user_realm_pass);
491            hasher.update(":");
492            hasher.update(self.nonce());
493            hasher.update(":");
494            hasher.update(&qop_data.cnonce);
495            hex::encode(hasher.finalize())
496        } else {
497            hex::encode(hashed_user_realm_pass)
498        }
499    }
500
501    fn calculate_ha2<T: Digest>(&self, method: &str, uri: &str, body: Option<&[u8]>) -> String {
502        let mut hasher = T::new();
503        hasher.update(method);
504        hasher.update(":");
505        hasher.update(uri);
506        if self.qop != QualityOfProtection::None
507            && self.qop_data.as_ref().unwrap().qop == QualityOfProtection::AuthInt
508        {
509            hasher.update(":");
510            let mut body_hasher = T::new();
511            body_hasher.update(body.unwrap());
512            hasher.update(hex::encode(body_hasher.finalize()));
513        }
514        let digest = hasher.finalize();
515        hex::encode(digest)
516    }
517
518    fn calculate_response<T: Digest>(&self, ha1: &str, ha2: &str) -> String {
519        let mut hasher = T::new();
520        hasher.update(ha1);
521        hasher.update(":");
522        hasher.update(self.nonce());
523        hasher.update(":");
524        if self.qop != QualityOfProtection::None {
525            let qop_data = self.qop_data.as_ref().unwrap();
526            hasher.update(&qop_data.count_str);
527            hasher.update(":");
528            hasher.update(&qop_data.cnonce);
529            hasher.update(":");
530            hasher.update(qop_data.qop.to_str());
531            hasher.update(":");
532        }
533        hasher.update(ha2);
534        let digest = hasher.finalize();
535        hex::encode(digest)
536    }
537
538    fn generate_response_string<T: Digest>(
539        &self,
540        hashed_user_realm_pass: &[u8],
541        method: &str,
542        uri: &str,
543        body: Option<&[u8]>,
544    ) -> String {
545        let ha1 = self.calculate_ha1::<T>(hashed_user_realm_pass);
546
547        let ha2 = self.calculate_ha2::<T>(method, uri, body);
548
549        self.calculate_response::<T>(&ha1, &ha2)
550    }
551
552    fn hash_username<T: Digest>(&self, username: &str) -> String {
553        let mut hasher = T::new();
554        hasher.update(username);
555        hasher.update(":");
556        hasher.update(self.realm());
557        hex::encode(hasher.finalize())
558    }
559}
560
561#[cfg(feature = "from-headers")]
562impl<'a> TryFrom<&'a HeaderMap> for DigestAccess {
563    type Error = DigestParseError;
564    /// Returns a DigestScheme object if the HTTP response HeaderMap contains a digest authenticate header
565    fn try_from(headers: &HeaderMap) -> Result<DigestAccess, Self::Error> {
566        let mut err = DigestParseError::MissingDigest;
567        let auth_headers = headers.get_all(WWW_AUTHENTICATE);
568        for a in auth_headers.iter() {
569            if a.len() > Self::MIN_AUTH_LENGTH {
570                if let Ok(b) = a.to_str() {
571                    return DigestAccess::create_from_www_auth(b);
572                }
573            } else {
574                err = DigestParseError::Length;
575            }
576        }
577        Err(err)
578    }
579}
580
581#[cfg(feature = "from-headers")]
582impl TryFrom<HeaderMap> for DigestAccess {
583    type Error = DigestParseError;
584
585    fn try_from(headers: HeaderMap) -> Result<Self, Self::Error> {
586        DigestAccess::try_from(&headers)
587    }
588}