1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package org.apache.hc.client5.http.impl.auth;
28
29 import java.io.IOException;
30 import java.io.ObjectInputStream;
31 import java.io.ObjectOutputStream;
32 import java.io.Serializable;
33 import java.nio.charset.Charset;
34 import java.nio.charset.StandardCharsets;
35 import java.security.MessageDigest;
36 import java.security.Principal;
37 import java.security.SecureRandom;
38 import java.util.ArrayList;
39 import java.util.Formatter;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.List;
43 import java.util.Locale;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.StringTokenizer;
47
48 import org.apache.hc.client5.http.auth.AuthChallenge;
49 import org.apache.hc.client5.http.auth.AuthScheme;
50 import org.apache.hc.client5.http.auth.AuthScope;
51 import org.apache.hc.client5.http.auth.AuthenticationException;
52 import org.apache.hc.client5.http.auth.Credentials;
53 import org.apache.hc.client5.http.auth.CredentialsProvider;
54 import org.apache.hc.client5.http.auth.MalformedChallengeException;
55 import org.apache.hc.client5.http.auth.StandardAuthScheme;
56 import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
57 import org.apache.hc.client5.http.protocol.HttpClientContext;
58 import org.apache.hc.client5.http.utils.ByteArrayBuilder;
59 import org.apache.hc.core5.annotation.Internal;
60 import org.apache.hc.core5.http.ClassicHttpRequest;
61 import org.apache.hc.core5.http.HttpEntity;
62 import org.apache.hc.core5.http.HttpHost;
63 import org.apache.hc.core5.http.HttpRequest;
64 import org.apache.hc.core5.http.NameValuePair;
65 import org.apache.hc.core5.http.message.BasicHeaderValueFormatter;
66 import org.apache.hc.core5.http.message.BasicNameValuePair;
67 import org.apache.hc.core5.http.protocol.HttpContext;
68 import org.apache.hc.core5.net.PercentCodec;
69 import org.apache.hc.core5.util.Args;
70 import org.apache.hc.core5.util.CharArrayBuffer;
71 import org.slf4j.Logger;
72 import org.slf4j.LoggerFactory;
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88 public class DigestScheme implements AuthScheme, Serializable {
89
90 private static final long serialVersionUID = 3883908186234566916L;
91
92 private static final Logger LOG = LoggerFactory.getLogger(DigestScheme.class);
93
94
95
96
97
98
99
100 private static final char[] HEXADECIMAL = {
101 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
102 'e', 'f'
103 };
104
105
106
107
108 private enum QualityOfProtection {
109 UNKNOWN, MISSING, AUTH_INT, AUTH
110 }
111
112 private transient Charset defaultCharset;
113 private final Map<String, String> paramMap;
114 private boolean complete;
115 private transient ByteArrayBuilder buffer;
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132 private boolean userhashSupported = false;
133
134
135 private String lastNonce;
136 private long nounceCount;
137 private String cnonce;
138 private byte[] a1;
139 private byte[] a2;
140
141 private UsernamePasswordCredentials credentials;
142
143 public DigestScheme() {
144 this.defaultCharset = StandardCharsets.UTF_8;
145 this.paramMap = new HashMap<>();
146 this.complete = false;
147 }
148
149
150
151
152
153
154
155 @Deprecated
156 public DigestScheme(final Charset charset) {
157 this();
158 }
159
160 public void initPreemptive(final Credentials credentials, final String cnonce, final String realm) {
161 Args.notNull(credentials, "Credentials");
162 Args.check(credentials instanceof UsernamePasswordCredentials,
163 "Unsupported credential type: " + credentials.getClass());
164 this.credentials = (UsernamePasswordCredentials) credentials;
165 this.paramMap.put("cnonce", cnonce);
166 this.paramMap.put("realm", realm);
167 }
168
169 @Override
170 public String getName() {
171 return StandardAuthScheme.DIGEST;
172 }
173
174 @Override
175 public boolean isConnectionBased() {
176 return false;
177 }
178
179 @Override
180 public String getRealm() {
181 return this.paramMap.get("realm");
182 }
183
184 @Override
185 public void processChallenge(
186 final AuthChallenge authChallenge,
187 final HttpContext context) throws MalformedChallengeException {
188 Args.notNull(authChallenge, "AuthChallenge");
189 this.paramMap.clear();
190 final List<NameValuePair> params = authChallenge.getParams();
191 if (params != null) {
192 for (final NameValuePair param: params) {
193 this.paramMap.put(param.getName().toLowerCase(Locale.ROOT), param.getValue());
194 }
195 }
196 if (this.paramMap.isEmpty()) {
197 throw new MalformedChallengeException("Missing digest auth parameters");
198 }
199
200 final String userHashValue = this.paramMap.get("userhash");
201 this.userhashSupported = "true".equalsIgnoreCase(userHashValue);
202
203 this.complete = true;
204 }
205
206 @Override
207 public boolean isChallengeComplete() {
208 final String s = this.paramMap.get("stale");
209 return !"true".equalsIgnoreCase(s) && this.complete;
210 }
211
212 @Override
213 public boolean isResponseReady(
214 final HttpHost host,
215 final CredentialsProvider credentialsProvider,
216 final HttpContext context) throws AuthenticationException {
217
218 Args.notNull(host, "Auth host");
219 Args.notNull(credentialsProvider, "CredentialsProvider");
220
221 final AuthScope authScope = new AuthScope(host, getRealm(), getName());
222 final Credentials credentials = credentialsProvider.getCredentials(
223 authScope, context);
224 if (credentials instanceof UsernamePasswordCredentials) {
225 this.credentials = (UsernamePasswordCredentials) credentials;
226 return true;
227 }
228
229 if (LOG.isDebugEnabled()) {
230 final HttpClientContext clientContext = HttpClientContext.cast(context);
231 final String exchangeId = clientContext.getExchangeId();
232 LOG.debug("{} No credentials found for auth scope [{}]", exchangeId, authScope);
233 }
234 this.credentials = null;
235 return false;
236 }
237
238 @Override
239 public Principal getPrincipal() {
240 return null;
241 }
242
243 @Override
244 public String generateAuthResponse(
245 final HttpHost host,
246 final HttpRequest request,
247 final HttpContext context) throws AuthenticationException {
248
249 Args.notNull(request, "HTTP request");
250 if (this.paramMap.get("realm") == null) {
251 throw new AuthenticationException("missing realm");
252 }
253 if (this.paramMap.get("nonce") == null) {
254 throw new AuthenticationException("missing nonce");
255 }
256 return createDigestResponse(request);
257 }
258
259 private static MessageDigest createMessageDigest(
260 final String digAlg) throws UnsupportedDigestAlgorithmException {
261 try {
262 return MessageDigest.getInstance(digAlg);
263 } catch (final Exception e) {
264 throw new UnsupportedDigestAlgorithmException(
265 "Unsupported algorithm in HTTP Digest authentication: "
266 + digAlg);
267 }
268 }
269
270 private String createDigestResponse(final HttpRequest request) throws AuthenticationException {
271 if (credentials == null) {
272 throw new AuthenticationException("User credentials have not been provided");
273 }
274 final String uri = request.getRequestUri();
275 final String method = request.getMethod();
276 final String realm = this.paramMap.get("realm");
277 final String nonce = this.paramMap.get("nonce");
278 final String opaque = this.paramMap.get("opaque");
279 final String algorithm = this.paramMap.get("algorithm");
280
281 final Set<String> qopset = new HashSet<>(8);
282 QualityOfProtection qop = QualityOfProtection.UNKNOWN;
283 final String qoplist = this.paramMap.get("qop");
284 if (qoplist != null) {
285 final StringTokenizer tok = new StringTokenizer(qoplist, ",");
286 while (tok.hasMoreTokens()) {
287 final String variant = tok.nextToken().trim();
288 qopset.add(variant.toLowerCase(Locale.ROOT));
289 }
290 final HttpEntity entity = request instanceof ClassicHttpRequest ? ((ClassicHttpRequest) request).getEntity() : null;
291 if (entity != null && qopset.contains("auth-int")) {
292 qop = QualityOfProtection.AUTH_INT;
293 } else if (qopset.contains("auth")) {
294 qop = QualityOfProtection.AUTH;
295 } else if (qopset.contains("auth-int")) {
296 qop = QualityOfProtection.AUTH_INT;
297 }
298 } else {
299 qop = QualityOfProtection.MISSING;
300 }
301
302 if (qop == QualityOfProtection.UNKNOWN) {
303 throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
304 }
305
306 final Charset charset = AuthSchemeSupport.parseCharset(paramMap.get("charset"), defaultCharset);
307 String digAlg = algorithm;
308
309 if (digAlg == null || digAlg.equalsIgnoreCase("MD5-sess")) {
310 digAlg = "MD5";
311 }
312
313 final MessageDigest digester;
314 try {
315 digester = createMessageDigest(digAlg);
316 } catch (final UnsupportedDigestAlgorithmException ex) {
317 throw new AuthenticationException("Unsupported digest algorithm: " + digAlg);
318 }
319
320 if (nonce.equals(this.lastNonce)) {
321 nounceCount++;
322 } else {
323 nounceCount = 1;
324 cnonce = null;
325 lastNonce = nonce;
326 }
327
328 final StringBuilder sb = new StringBuilder(8);
329 try (final Formatter formatter = new Formatter(sb, Locale.ROOT)) {
330 formatter.format("%08x", nounceCount);
331 }
332 final String nc = sb.toString();
333
334 if (cnonce == null) {
335 cnonce = formatHex(createCnonce());
336 }
337
338 if (buffer == null) {
339 buffer = new ByteArrayBuilder(128);
340 } else {
341 buffer.reset();
342 }
343 buffer.charset(charset);
344
345 a1 = null;
346 a2 = null;
347
348
349
350 String username = credentials.getUserName();
351 String encodedUsername = null;
352
353 if (username != null && containsInvalidABNFChars(username)) {
354 encodedUsername = "UTF-8''" + PercentCodec.RFC5987.encode(username);
355 }
356
357 final String usernameForDigest;
358 if (this.userhashSupported) {
359 final String usernameRealm = username + ":" + realm;
360 final byte[] hashedBytes = digester.digest(usernameRealm.getBytes(StandardCharsets.UTF_8));
361 usernameForDigest = formatHex(hashedBytes);
362 username = usernameForDigest;
363 } else if (encodedUsername != null) {
364 usernameForDigest = encodedUsername;
365 } else {
366 usernameForDigest = username;
367 }
368
369
370 if ("MD5-sess".equalsIgnoreCase(algorithm)) {
371
372
373
374
375
376 buffer.append(username).append(":").append(realm).append(":").append(credentials.getUserPassword());
377 final String checksum = formatHex(digester.digest(this.buffer.toByteArray()));
378 buffer.reset();
379 buffer.append(checksum).append(":").append(nonce).append(":").append(cnonce);
380 } else {
381
382 buffer.append(username).append(":").append(realm).append(":").append(credentials.getUserPassword());
383 }
384 a1 = buffer.toByteArray();
385
386 final String hasha1 = formatHex(digester.digest(a1));
387 buffer.reset();
388
389 if (qop == QualityOfProtection.AUTH) {
390
391 a2 = buffer.append(method).append(":").append(uri).toByteArray();
392 } else if (qop == QualityOfProtection.AUTH_INT) {
393
394 final HttpEntity entity = request instanceof ClassicHttpRequest ? ((ClassicHttpRequest) request).getEntity() : null;
395 if (entity != null && !entity.isRepeatable()) {
396
397 if (qopset.contains("auth")) {
398 qop = QualityOfProtection.AUTH;
399 a2 = buffer.append(method).append(":").append(uri).toByteArray();
400 } else {
401 throw new AuthenticationException("Qop auth-int cannot be used with " +
402 "a non-repeatable entity");
403 }
404 } else {
405 final HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
406 try {
407 if (entity != null) {
408 entity.writeTo(entityDigester);
409 }
410 entityDigester.close();
411 } catch (final IOException ex) {
412 throw new AuthenticationException("I/O error reading entity content", ex);
413 }
414 a2 = buffer.append(method).append(":").append(uri)
415 .append(":").append(formatHex(entityDigester.getDigest())).toByteArray();
416 }
417 } else {
418 a2 = buffer.append(method).append(":").append(uri).toByteArray();
419 }
420
421 final String hasha2 = formatHex(digester.digest(a2));
422 buffer.reset();
423
424
425
426 final byte[] digestInput;
427 if (qop == QualityOfProtection.MISSING) {
428 buffer.append(hasha1).append(":").append(nonce).append(":").append(hasha2);
429 } else {
430 buffer.append(hasha1).append(":").append(nonce).append(":").append(nc).append(":")
431 .append(cnonce).append(":").append(qop == QualityOfProtection.AUTH_INT ? "auth-int" : "auth")
432 .append(":").append(hasha2);
433 }
434 digestInput = buffer.toByteArray();
435 buffer.reset();
436
437 final String digest = formatHex(digester.digest(digestInput));
438
439 final CharArrayBuffer buffer = new CharArrayBuffer(128);
440 buffer.append(StandardAuthScheme.DIGEST + " ");
441
442 final List<BasicNameValuePair> params = new ArrayList<>(20);
443 if (this.userhashSupported) {
444
445 params.add(new BasicNameValuePair("username", usernameForDigest));
446 params.add(new BasicNameValuePair("userhash", "true"));
447 } else if (encodedUsername != null) {
448
449 params.add(new BasicNameValuePair("username*", encodedUsername));
450 } else {
451
452 params.add(new BasicNameValuePair("username", username));
453 }
454 params.add(new BasicNameValuePair("realm", realm));
455 params.add(new BasicNameValuePair("nonce", nonce));
456 params.add(new BasicNameValuePair("uri", uri));
457 params.add(new BasicNameValuePair("response", digest));
458
459 if (qop != QualityOfProtection.MISSING) {
460 params.add(new BasicNameValuePair("qop", qop == QualityOfProtection.AUTH_INT ? "auth-int" : "auth"));
461 params.add(new BasicNameValuePair("nc", nc));
462 params.add(new BasicNameValuePair("cnonce", cnonce));
463 }
464 if (algorithm != null) {
465 params.add(new BasicNameValuePair("algorithm", algorithm));
466 }
467 if (opaque != null) {
468 params.add(new BasicNameValuePair("opaque", opaque));
469 }
470
471 for (int i = 0; i < params.size(); i++) {
472 final BasicNameValuePair param = params.get(i);
473 if (i > 0) {
474 buffer.append(", ");
475 }
476 final String name = param.getName();
477 final boolean noQuotes = ("nc".equals(name) || "qop".equals(name)
478 || "algorithm".equals(name));
479 BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes);
480 }
481 return buffer.toString();
482 }
483
484 @Internal
485 public String getNonce() {
486 return lastNonce;
487 }
488
489 @Internal
490 public long getNounceCount() {
491 return nounceCount;
492 }
493
494 @Internal
495 public String getCnonce() {
496 return cnonce;
497 }
498
499 String getA1() {
500 return a1 != null ? new String(a1, StandardCharsets.US_ASCII) : null;
501 }
502
503 String getA2() {
504 return a2 != null ? new String(a2, StandardCharsets.US_ASCII) : null;
505 }
506
507
508
509
510
511
512
513 static String formatHex(final byte[] binaryData) {
514 final int n = binaryData.length;
515 final char[] buffer = new char[n * 2];
516 for (int i = 0; i < n; i++) {
517 final int low = (binaryData[i] & 0x0f);
518 final int high = ((binaryData[i] & 0xf0) >> 4);
519 buffer[i * 2] = HEXADECIMAL[high];
520 buffer[(i * 2) + 1] = HEXADECIMAL[low];
521 }
522
523 return new String(buffer);
524 }
525
526
527
528
529
530
531 static byte[] createCnonce() {
532 final SecureRandom rnd = new SecureRandom();
533 final byte[] tmp = new byte[8];
534 rnd.nextBytes(tmp);
535 return tmp;
536 }
537
538 private void writeObject(final ObjectOutputStream out) throws IOException {
539 out.defaultWriteObject();
540 out.writeUTF(defaultCharset.name());
541 }
542
543 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
544 in.defaultReadObject();
545 this.defaultCharset = Charset.forName(in.readUTF());
546 }
547
548 @Override
549 public String toString() {
550 return getName() + this.paramMap;
551 }
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567 private boolean containsInvalidABNFChars(final String value) {
568 if (value == null) {
569 throw new IllegalArgumentException("Input string should not be null.");
570 }
571
572 for (int i = 0; i < value.length(); i++) {
573 final char c = value.charAt(i);
574
575
576 if (c <= 0x1F || c == 0x7F) {
577 return true;
578 }
579
580
581 if (c > 0x7E) {
582 return true;
583 }
584
585
586 if (c == '"' || c == '\\') {
587 return true;
588 }
589 }
590 return false;
591 }
592 }