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, 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 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 } 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 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}