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