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.nio.charset.UnsupportedCharsetException;
36 import java.security.MessageDigest;
37 import java.security.Principal;
38 import java.security.SecureRandom;
39 import java.util.ArrayList;
40 import java.util.Formatter;
41 import java.util.HashMap;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Locale;
45 import java.util.Map;
46 import java.util.Set;
47 import java.util.StringTokenizer;
48
49 import org.apache.hc.client5.http.auth.AuthChallenge;
50 import org.apache.hc.client5.http.auth.AuthScheme;
51 import org.apache.hc.client5.http.auth.AuthScope;
52 import org.apache.hc.client5.http.auth.AuthenticationException;
53 import org.apache.hc.client5.http.auth.Credentials;
54 import org.apache.hc.client5.http.auth.CredentialsProvider;
55 import org.apache.hc.client5.http.auth.MalformedChallengeException;
56 import org.apache.hc.client5.http.auth.StandardAuthScheme;
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.util.Args;
69 import org.apache.hc.core5.util.CharArrayBuffer;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87 public class DigestScheme implements AuthScheme, Serializable {
88
89 private static final long serialVersionUID = 3883908186234566916L;
90
91 private static final Logger LOG = LoggerFactory.getLogger(DigestScheme.class);
92
93
94
95
96
97
98
99 private static final char[] HEXADECIMAL = {
100 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
101 'e', 'f'
102 };
103
104 private static final int QOP_UNKNOWN = -1;
105 private static final int QOP_MISSING = 0;
106 private static final int QOP_AUTH_INT = 1;
107 private static final int QOP_AUTH = 2;
108
109 private transient Charset defaultCharset;
110 private final Map<String, String> paramMap;
111 private boolean complete;
112 private transient ByteArrayBuilder buffer;
113
114 private String lastNonce;
115 private long nounceCount;
116 private String cnonce;
117 private byte[] a1;
118 private byte[] a2;
119
120 private String username;
121 private char[] password;
122
123 public DigestScheme() {
124 this(StandardCharsets.ISO_8859_1);
125 }
126
127 public DigestScheme(final Charset charset) {
128 this.defaultCharset = charset != null ? charset : StandardCharsets.ISO_8859_1;
129 this.paramMap = new HashMap<>();
130 this.complete = false;
131 }
132
133 public void initPreemptive(final Credentials credentials, final String cnonce, final String realm) {
134 Args.notNull(credentials, "Credentials");
135 this.username = credentials.getUserPrincipal().getName();
136 this.password = credentials.getPassword();
137 this.paramMap.put("cnonce", cnonce);
138 this.paramMap.put("realm", realm);
139 }
140
141 @Override
142 public String getName() {
143 return StandardAuthScheme.DIGEST;
144 }
145
146 @Override
147 public boolean isConnectionBased() {
148 return false;
149 }
150
151 @Override
152 public String getRealm() {
153 return this.paramMap.get("realm");
154 }
155
156 @Override
157 public void processChallenge(
158 final AuthChallenge authChallenge,
159 final HttpContext context) throws MalformedChallengeException {
160 Args.notNull(authChallenge, "AuthChallenge");
161 this.paramMap.clear();
162 final List<NameValuePair> params = authChallenge.getParams();
163 if (params != null) {
164 for (final NameValuePair param: params) {
165 this.paramMap.put(param.getName().toLowerCase(Locale.ROOT), param.getValue());
166 }
167 }
168 if (this.paramMap.isEmpty()) {
169 throw new MalformedChallengeException("Missing digest auth parameters");
170 }
171 this.complete = true;
172 }
173
174 @Override
175 public boolean isChallengeComplete() {
176 final String s = this.paramMap.get("stale");
177 return !"true".equalsIgnoreCase(s) && this.complete;
178 }
179
180 @Override
181 public boolean isResponseReady(
182 final HttpHost host,
183 final CredentialsProvider credentialsProvider,
184 final HttpContext context) throws AuthenticationException {
185
186 Args.notNull(host, "Auth host");
187 Args.notNull(credentialsProvider, "CredentialsProvider");
188
189 final AuthScopep/auth/AuthScope.html#AuthScope">AuthScope authScope = new AuthScope(host, getRealm(), getName());
190 final Credentials credentials = credentialsProvider.getCredentials(
191 authScope, context);
192 if (credentials != null) {
193 this.username = credentials.getUserPrincipal().getName();
194 this.password = credentials.getPassword();
195 return true;
196 }
197
198 if (LOG.isDebugEnabled()) {
199 final HttpClientContext clientContext = HttpClientContext.adapt(context);
200 final String exchangeId = clientContext.getExchangeId();
201 LOG.debug("{} No credentials found for auth scope [{}]", exchangeId, authScope);
202 }
203 this.username = null;
204 this.password = null;
205 return false;
206 }
207
208 @Override
209 public Principal getPrincipal() {
210 return null;
211 }
212
213 @Override
214 public String generateAuthResponse(
215 final HttpHost host,
216 final HttpRequest request,
217 final HttpContext context) throws AuthenticationException {
218
219 Args.notNull(request, "HTTP request");
220 if (this.paramMap.get("realm") == null) {
221 throw new AuthenticationException("missing realm");
222 }
223 if (this.paramMap.get("nonce") == null) {
224 throw new AuthenticationException("missing nonce");
225 }
226 return createDigestResponse(request);
227 }
228
229 private static MessageDigest createMessageDigest(
230 final String digAlg) throws UnsupportedDigestAlgorithmException {
231 try {
232 return MessageDigest.getInstance(digAlg);
233 } catch (final Exception e) {
234 throw new UnsupportedDigestAlgorithmException(
235 "Unsupported algorithm in HTTP Digest authentication: "
236 + digAlg);
237 }
238 }
239
240 private String createDigestResponse(final HttpRequest request) throws AuthenticationException {
241
242 final String uri = request.getRequestUri();
243 final String method = request.getMethod();
244 final String realm = this.paramMap.get("realm");
245 final String nonce = this.paramMap.get("nonce");
246 final String opaque = this.paramMap.get("opaque");
247 String algorithm = this.paramMap.get("algorithm");
248
249 if (algorithm == null) {
250 algorithm = "MD5";
251 }
252
253 final Set<String> qopset = new HashSet<>(8);
254 int qop = QOP_UNKNOWN;
255 final String qoplist = this.paramMap.get("qop");
256 if (qoplist != null) {
257 final StringTokenizer tok = new StringTokenizer(qoplist, ",");
258 while (tok.hasMoreTokens()) {
259 final String variant = tok.nextToken().trim();
260 qopset.add(variant.toLowerCase(Locale.ROOT));
261 }
262 final HttpEntity entity = request instanceof ClassicHttpRequest ? ((ClassicHttpRequest) request).getEntity() : null;
263 if (entity != null && qopset.contains("auth-int")) {
264 qop = QOP_AUTH_INT;
265 } else if (qopset.contains("auth")) {
266 qop = QOP_AUTH;
267 } else if (qopset.contains("auth-int")) {
268 qop = QOP_AUTH_INT;
269 }
270 } else {
271 qop = QOP_MISSING;
272 }
273
274 if (qop == QOP_UNKNOWN) {
275 throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
276 }
277
278 final String charsetName = this.paramMap.get("charset");
279 final Charset charset;
280 try {
281 charset = charsetName != null ? Charset.forName(charsetName) : defaultCharset;
282 } catch (final UnsupportedCharsetException ex) {
283 throw new AuthenticationException("Unsupported charset: " + charsetName);
284 }
285
286 String digAlg = algorithm;
287 if (digAlg.equalsIgnoreCase("MD5-sess")) {
288 digAlg = "MD5";
289 }
290
291 final MessageDigest digester;
292 try {
293 digester = createMessageDigest(digAlg);
294 } catch (final UnsupportedDigestAlgorithmException ex) {
295 throw new AuthenticationException("Unsupported digest algorithm: " + digAlg);
296 }
297
298 if (nonce.equals(this.lastNonce)) {
299 nounceCount++;
300 } else {
301 nounceCount = 1;
302 cnonce = null;
303 lastNonce = nonce;
304 }
305
306 final StringBuilder sb = new StringBuilder(8);
307 try (final Formatter formatter = new Formatter(sb, Locale.ROOT)) {
308 formatter.format("%08x", nounceCount);
309 }
310 final String nc = sb.toString();
311
312 if (cnonce == null) {
313 cnonce = formatHex(createCnonce());
314 }
315
316 if (buffer == null) {
317 buffer = new ByteArrayBuilder(128);
318 } else {
319 buffer.reset();
320 }
321 buffer.charset(charset);
322
323 a1 = null;
324 a2 = null;
325
326 if (algorithm.equalsIgnoreCase("MD5-sess")) {
327
328
329
330
331
332 buffer.append(username).append(":").append(realm).append(":").append(password);
333 final String checksum = formatHex(digester.digest(this.buffer.toByteArray()));
334 buffer.reset();
335 buffer.append(checksum).append(":").append(nonce).append(":").append(cnonce);
336 } else {
337
338 buffer.append(username).append(":").append(realm).append(":").append(password);
339 }
340 a1 = buffer.toByteArray();
341
342 final String hasha1 = formatHex(digester.digest(a1));
343 buffer.reset();
344
345 if (qop == QOP_AUTH) {
346
347 a2 = buffer.append(method).append(":").append(uri).toByteArray();
348 } else if (qop == QOP_AUTH_INT) {
349
350 final HttpEntity entity = request instanceof ClassicHttpRequest ? ((ClassicHttpRequest) request).getEntity() : null;
351 if (entity != null && !entity.isRepeatable()) {
352
353 if (qopset.contains("auth")) {
354 qop = QOP_AUTH;
355 a2 = buffer.append(method).append(":").append(uri).toByteArray();
356 } else {
357 throw new AuthenticationException("Qop auth-int cannot be used with " +
358 "a non-repeatable entity");
359 }
360 } else {
361 final HttpEntityDigestertpEntityDigester.html#HttpEntityDigester">HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
362 try {
363 if (entity != null) {
364 entity.writeTo(entityDigester);
365 }
366 entityDigester.close();
367 } catch (final IOException ex) {
368 throw new AuthenticationException("I/O error reading entity content", ex);
369 }
370 a2 = buffer.append(method).append(":").append(uri)
371 .append(":").append(formatHex(entityDigester.getDigest())).toByteArray();
372 }
373 } else {
374 a2 = buffer.append(method).append(":").append(uri).toByteArray();
375 }
376
377 final String hasha2 = formatHex(digester.digest(a2));
378 buffer.reset();
379
380
381
382 final byte[] digestInput;
383 if (qop == QOP_MISSING) {
384 buffer.append(hasha1).append(":").append(nonce).append(":").append(hasha2);
385 } else {
386 buffer.append(hasha1).append(":").append(nonce).append(":").append(nc).append(":")
387 .append(cnonce).append(":").append(qop == QOP_AUTH_INT ? "auth-int" : "auth")
388 .append(":").append(hasha2);
389 }
390 digestInput = buffer.toByteArray();
391 buffer.reset();
392
393 final String digest = formatHex(digester.digest(digestInput));
394
395 final CharArrayBuffer buffer = new CharArrayBuffer(128);
396 buffer.append(StandardAuthScheme.DIGEST + " ");
397
398 final List<BasicNameValuePair> params = new ArrayList<>(20);
399 params.add(new BasicNameValuePair("username", username));
400 params.add(new BasicNameValuePair("realm", realm));
401 params.add(new BasicNameValuePair("nonce", nonce));
402 params.add(new BasicNameValuePair("uri", uri));
403 params.add(new BasicNameValuePair("response", digest));
404
405 if (qop != QOP_MISSING) {
406 params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth"));
407 params.add(new BasicNameValuePair("nc", nc));
408 params.add(new BasicNameValuePair("cnonce", cnonce));
409 }
410
411 params.add(new BasicNameValuePair("algorithm", algorithm));
412 if (opaque != null) {
413 params.add(new BasicNameValuePair("opaque", opaque));
414 }
415
416 for (int i = 0; i < params.size(); i++) {
417 final BasicNameValuePair param = params.get(i);
418 if (i > 0) {
419 buffer.append(", ");
420 }
421 final String name = param.getName();
422 final boolean noQuotes = ("nc".equals(name) || "qop".equals(name)
423 || "algorithm".equals(name));
424 BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes);
425 }
426 return buffer.toString();
427 }
428
429 @Internal
430 public String getNonce() {
431 return lastNonce;
432 }
433
434 @Internal
435 public long getNounceCount() {
436 return nounceCount;
437 }
438
439 @Internal
440 public String getCnonce() {
441 return cnonce;
442 }
443
444 String getA1() {
445 return a1 != null ? new String(a1, StandardCharsets.US_ASCII) : null;
446 }
447
448 String getA2() {
449 return a2 != null ? new String(a2, StandardCharsets.US_ASCII) : null;
450 }
451
452
453
454
455
456
457
458
459 static String formatHex(final byte[] binaryData) {
460 final int n = binaryData.length;
461 final char[] buffer = new char[n * 2];
462 for (int i = 0; i < n; i++) {
463 final int low = (binaryData[i] & 0x0f);
464 final int high = ((binaryData[i] & 0xf0) >> 4);
465 buffer[i * 2] = HEXADECIMAL[high];
466 buffer[(i * 2) + 1] = HEXADECIMAL[low];
467 }
468
469 return new String(buffer);
470 }
471
472
473
474
475
476
477 static byte[] createCnonce() {
478 final SecureRandom rnd = new SecureRandom();
479 final byte[] tmp = new byte[8];
480 rnd.nextBytes(tmp);
481 return tmp;
482 }
483
484 private void writeObject(final ObjectOutputStream out) throws IOException {
485 out.defaultWriteObject();
486 out.writeUTF(defaultCharset.name());
487 }
488
489 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
490 in.defaultReadObject();
491 this.defaultCharset = Charset.forName(in.readUTF());
492 }
493
494 @Override
495 public String toString() {
496 return getName() + this.paramMap;
497 }
498
499 }