1use std::fmt;
6
7#[derive(Debug, Clone)]
9pub struct DigestChallenge {
10 pub realm: String,
11 pub nonce: String,
12 pub opaque: Option<String>,
13 pub algorithm: DigestAlgorithm,
14 pub qop: Option<String>,
15 pub stale: bool,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum DigestAlgorithm {
20 Md5,
21 Md5Sess,
22}
23
24impl fmt::Display for DigestAlgorithm {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 DigestAlgorithm::Md5 => write!(f, "MD5"),
28 DigestAlgorithm::Md5Sess => write!(f, "MD5-sess"),
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct Credentials {
36 pub username: String,
37 pub password: String,
38}
39
40#[derive(Debug, Clone)]
42pub struct DigestResponse {
43 pub username: String,
44 pub realm: String,
45 pub nonce: String,
46 pub uri: String,
47 pub response: String,
48 pub algorithm: DigestAlgorithm,
49 pub opaque: Option<String>,
50 pub qop: Option<String>,
51 pub nc: Option<String>,
52 pub cnonce: Option<String>,
53}
54
55impl fmt::Display for DigestResponse {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "Digest username=\"{}\", realm=\"{}\", nonce=\"{}\", uri=\"{}\", response=\"{}\", algorithm={}",
58 self.username, self.realm, self.nonce, self.uri, self.response, self.algorithm)?;
59 if let Some(ref opaque) = self.opaque {
60 write!(f, ", opaque=\"{}\"", opaque)?;
61 }
62 if let Some(ref qop) = self.qop {
63 write!(f, ", qop={}", qop)?;
64 if let Some(ref nc) = self.nc {
65 write!(f, ", nc={}", nc)?;
66 }
67 if let Some(ref cnonce) = self.cnonce {
68 write!(f, ", cnonce=\"{}\"", cnonce)?;
69 }
70 }
71 Ok(())
72 }
73}
74
75pub fn parse_challenge(header_value: &str) -> Option<DigestChallenge> {
79 let value = header_value.strip_prefix("Digest ")
80 .or_else(|| header_value.strip_prefix("digest "))?;
81
82 let mut realm = None;
83 let mut nonce = None;
84 let mut opaque = None;
85 let mut algorithm = DigestAlgorithm::Md5;
86 let mut qop = None;
87 let mut stale = false;
88
89 for param in split_params(value) {
91 let param = param.trim();
92 if let Some((key, val)) = param.split_once('=') {
93 let key = key.trim().to_lowercase();
94 let val = val.trim().trim_matches('"');
95 match key.as_str() {
96 "realm" => realm = Some(val.to_string()),
97 "nonce" => nonce = Some(val.to_string()),
98 "opaque" => opaque = Some(val.to_string()),
99 "algorithm" => {
100 algorithm = match val.to_lowercase().as_str() {
101 "md5-sess" => DigestAlgorithm::Md5Sess,
102 _ => DigestAlgorithm::Md5,
103 };
104 }
105 "qop" => qop = Some(val.to_string()),
106 "stale" => stale = val.eq_ignore_ascii_case("true"),
107 _ => {}
108 }
109 }
110 }
111
112 Some(DigestChallenge {
113 realm: realm?,
114 nonce: nonce?,
115 opaque,
116 algorithm,
117 qop,
118 stale,
119 })
120}
121
122pub fn compute_digest(
130 challenge: &DigestChallenge,
131 creds: &Credentials,
132 method: &str,
133 uri: &str,
134) -> DigestResponse {
135 let ha1 = md5_hex(&format!("{}:{}:{}", creds.username, challenge.realm, creds.password));
136 let ha2 = md5_hex(&format!("{}:{}", method, uri));
137
138 let (response, qop, nc, cnonce) = if let Some(ref qop_val) = challenge.qop {
139 if qop_val.contains("auth") {
140 let cnonce = generate_cnonce();
141 let nc = "00000001".to_string();
142 let response = md5_hex(&format!("{}:{}:{}:{}:auth:{}", ha1, challenge.nonce, nc, cnonce, ha2));
143 (response, Some("auth".to_string()), Some(nc), Some(cnonce))
144 } else {
145 let response = md5_hex(&format!("{}:{}:{}", ha1, challenge.nonce, ha2));
146 (response, None, None, None)
147 }
148 } else {
149 let response = md5_hex(&format!("{}:{}:{}", ha1, challenge.nonce, ha2));
150 (response, None, None, None)
151 };
152
153 DigestResponse {
154 username: creds.username.clone(),
155 realm: challenge.realm.clone(),
156 nonce: challenge.nonce.clone(),
157 uri: uri.to_string(),
158 response,
159 algorithm: challenge.algorithm,
160 opaque: challenge.opaque.clone(),
161 qop,
162 nc,
163 cnonce,
164 }
165}
166
167fn split_params(s: &str) -> Vec<&str> {
169 let mut result = Vec::new();
170 let mut start = 0;
171 let mut in_quotes = false;
172 for (i, ch) in s.char_indices() {
173 match ch {
174 '"' => in_quotes = !in_quotes,
175 ',' if !in_quotes => {
176 result.push(&s[start..i]);
177 start = i + 1;
178 }
179 _ => {}
180 }
181 }
182 if start < s.len() {
183 result.push(&s[start..]);
184 }
185 result
186}
187
188fn md5_hex(input: &str) -> String {
190 let digest = md5_compute(input.as_bytes());
193 hex_encode(&digest)
194}
195
196fn hex_encode(bytes: &[u8]) -> String {
197 let mut s = String::with_capacity(bytes.len() * 2);
198 for &b in bytes {
199 s.push_str(&format!("{:02x}", b));
200 }
201 s
202}
203
204fn generate_cnonce() -> String {
205 use rand::Rng;
206 let mut rng = rand::thread_rng();
207 let bytes: [u8; 8] = rng.gen();
208 hex_encode(&bytes)
209}
210
211const S: [u32; 64] = [
214 7,12,17,22, 7,12,17,22, 7,12,17,22, 7,12,17,22,
215 5, 9,14,20, 5, 9,14,20, 5, 9,14,20, 5, 9,14,20,
216 4,11,16,23, 4,11,16,23, 4,11,16,23, 4,11,16,23,
217 6,10,15,21, 6,10,15,21, 6,10,15,21, 6,10,15,21,
218];
219
220const K: [u32; 64] = [
221 0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,
222 0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501,
223 0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,
224 0x6b901122,0xfd987193,0xa679438e,0x49b40821,
225 0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,
226 0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8,
227 0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,
228 0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a,
229 0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,
230 0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70,
231 0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,
232 0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665,
233 0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,
234 0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1,
235 0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,
236 0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391,
237];
238
239fn md5_compute(data: &[u8]) -> [u8; 16] {
240 let mut a0: u32 = 0x67452301;
241 let mut b0: u32 = 0xefcdab89;
242 let mut c0: u32 = 0x98badcfe;
243 let mut d0: u32 = 0x10325476;
244
245 let orig_len_bits = (data.len() as u64) * 8;
247 let mut msg = data.to_vec();
248 msg.push(0x80);
249 while msg.len() % 64 != 56 {
250 msg.push(0);
251 }
252 msg.extend_from_slice(&orig_len_bits.to_le_bytes());
253
254 for chunk in msg.chunks_exact(64) {
256 let mut m = [0u32; 16];
257 for (i, word) in chunk.chunks_exact(4).enumerate() {
258 m[i] = u32::from_le_bytes([word[0], word[1], word[2], word[3]]);
259 }
260
261 let mut a = a0;
262 let mut b = b0;
263 let mut c = c0;
264 let mut d = d0;
265
266 for i in 0..64 {
267 let (f, g) = match i {
268 0..=15 => ((b & c) | ((!b) & d), i),
269 16..=31 => ((d & b) | ((!d) & c), (5 * i + 1) % 16),
270 32..=47 => (b ^ c ^ d, (3 * i + 5) % 16),
271 _ => (c ^ (b | (!d)), (7 * i) % 16),
272 };
273
274 let f = f.wrapping_add(a).wrapping_add(K[i]).wrapping_add(m[g]);
275 a = d;
276 d = c;
277 c = b;
278 b = b.wrapping_add(f.rotate_left(S[i]));
279 }
280
281 a0 = a0.wrapping_add(a);
282 b0 = b0.wrapping_add(b);
283 c0 = c0.wrapping_add(c);
284 d0 = d0.wrapping_add(d);
285 }
286
287 let mut result = [0u8; 16];
288 result[0..4].copy_from_slice(&a0.to_le_bytes());
289 result[4..8].copy_from_slice(&b0.to_le_bytes());
290 result[8..12].copy_from_slice(&c0.to_le_bytes());
291 result[12..16].copy_from_slice(&d0.to_le_bytes());
292 result
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_md5_known_values() {
301 assert_eq!(md5_hex(""), "d41d8cd98f00b204e9800998ecf8427e");
303 assert_eq!(md5_hex("a"), "0cc175b9c0f1b6a831c399e269772661");
304 assert_eq!(md5_hex("abc"), "900150983cd24fb0d6963f7d28e17f72");
305 assert_eq!(md5_hex("message digest"), "f96b697d7cb7938d525a2f31aaf161d0");
306 }
307
308 #[test]
309 fn test_parse_challenge_basic() {
310 let header = r#"Digest realm="asterisk", nonce="abc123def""#;
311 let challenge = parse_challenge(header).unwrap();
312 assert_eq!(challenge.realm, "asterisk");
313 assert_eq!(challenge.nonce, "abc123def");
314 assert_eq!(challenge.algorithm, DigestAlgorithm::Md5);
315 assert!(challenge.opaque.is_none());
316 assert!(challenge.qop.is_none());
317 }
318
319 #[test]
320 fn test_parse_challenge_full() {
321 let header = r#"Digest realm="biloxi.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41", qop="auth", algorithm=MD5"#;
322 let challenge = parse_challenge(header).unwrap();
323 assert_eq!(challenge.realm, "biloxi.com");
324 assert_eq!(challenge.nonce, "dcd98b7102dd2f0e8b11d0f600bfb0c093");
325 assert_eq!(challenge.opaque.as_deref(), Some("5ccc069c403ebaf9f0171e9517f40e41"));
326 assert_eq!(challenge.qop.as_deref(), Some("auth"));
327 assert_eq!(challenge.algorithm, DigestAlgorithm::Md5);
328 }
329
330 #[test]
331 fn test_compute_digest_rfc2617_example() {
332 let challenge = DigestChallenge {
334 realm: "testrealm@host.com".to_string(),
335 nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093".to_string(),
336 opaque: Some("5ccc069c403ebaf9f0171e9517f40e41".to_string()),
337 algorithm: DigestAlgorithm::Md5,
338 qop: Some("auth".to_string()),
339 stale: false,
340 };
341 let creds = Credentials {
342 username: "Mufasa".to_string(),
343 password: "Circle Of Life".to_string(),
344 };
345 let resp = compute_digest(&challenge, &creds, "GET", "/dir/index.html");
346 assert_eq!(resp.realm, "testrealm@host.com");
347 assert_eq!(resp.username, "Mufasa");
348 assert_eq!(resp.response.len(), 32);
350 assert!(resp.response.chars().all(|c| c.is_ascii_hexdigit()));
351 assert_eq!(resp.qop.as_deref(), Some("auth"));
352 }
353
354 #[test]
355 fn test_compute_digest_no_qop() {
356 let challenge = DigestChallenge {
357 realm: "asterisk".to_string(),
358 nonce: "1234567890".to_string(),
359 opaque: None,
360 algorithm: DigestAlgorithm::Md5,
361 qop: None,
362 stale: false,
363 };
364 let creds = Credentials {
365 username: "alice".to_string(),
366 password: "secret".to_string(),
367 };
368 let resp = compute_digest(&challenge, &creds, "REGISTER", "sip:asterisk");
369
370 let ha1 = md5_hex("alice:asterisk:secret");
375 let ha2 = md5_hex("REGISTER:sip:asterisk");
376 let expected = md5_hex(&format!("{}:1234567890:{}", ha1, ha2));
377 assert_eq!(resp.response, expected);
378 assert!(resp.qop.is_none());
379 }
380
381 #[test]
382 fn test_digest_response_display() {
383 let resp = DigestResponse {
384 username: "alice".to_string(),
385 realm: "asterisk".to_string(),
386 nonce: "abc123".to_string(),
387 uri: "sip:asterisk".to_string(),
388 response: "deadbeef01234567890abcdef0123456".to_string(),
389 algorithm: DigestAlgorithm::Md5,
390 opaque: Some("xyz".to_string()),
391 qop: None,
392 nc: None,
393 cnonce: None,
394 };
395 let s = resp.to_string();
396 assert!(s.starts_with("Digest "));
397 assert!(s.contains("username=\"alice\""));
398 assert!(s.contains("opaque=\"xyz\""));
399 assert!(!s.contains("qop="));
400 }
401
402 #[test]
403 fn test_parse_challenge_stale() {
404 let header = r#"Digest realm="test", nonce="new_nonce", stale=true"#;
405 let challenge = parse_challenge(header).unwrap();
406 assert!(challenge.stale);
407 }
408
409 #[test]
410 fn test_split_params_with_quotes() {
411 let input = r#"realm="a,b", nonce="c""#;
412 let params = split_params(input);
413 assert_eq!(params.len(), 2);
414 assert!(params[0].contains("a,b"));
415 }
416}