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 String algorithm = this.paramMap.get("algorithm");
249
250 if (algorithm == null) {
251 algorithm = "MD5";
252 }
253
254 final Set<String> qopset = new HashSet<>(8);
255 QualityOfProtection qop = QualityOfProtection.UNKNOWN;
256 final String qoplist = this.paramMap.get("qop");
257 if (qoplist != null) {
258 final StringTokenizer tok = new StringTokenizer(qoplist, ",");
259 while (tok.hasMoreTokens()) {
260 final String variant = tok.nextToken().trim();
261 qopset.add(variant.toLowerCase(Locale.ROOT));
262 }
263 final HttpEntity entity = request instanceof ClassicHttpRequest ? ((ClassicHttpRequest) request).getEntity() : null;
264 if (entity != null && qopset.contains("auth-int")) {
265 qop = QualityOfProtection.AUTH_INT;
266 } else if (qopset.contains("auth")) {
267 qop = QualityOfProtection.AUTH;
268 } else if (qopset.contains("auth-int")) {
269 qop = QualityOfProtection.AUTH_INT;
270 }
271 } else {
272 qop = QualityOfProtection.MISSING;
273 }
274
275 if (qop == QualityOfProtection.UNKNOWN) {
276 throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
277 }
278
279 final Charset charset = AuthSchemeSupport.parseCharset(paramMap.get("charset"), defaultCharset);
280 String digAlg = algorithm;
281 if (digAlg.equalsIgnoreCase("MD5-sess")) {
282 digAlg = "MD5";
283 }
284
285 final MessageDigest digester;
286 try {
287 digester = createMessageDigest(digAlg);
288 } catch (final UnsupportedDigestAlgorithmException ex) {
289 throw new AuthenticationException("Unsupported digest algorithm: " + digAlg);
290 }
291
292 if (nonce.equals(this.lastNonce)) {
293 nounceCount++;
294 } else {
295 nounceCount = 1;
296 cnonce = null;
297 lastNonce = nonce;
298 }
299
300 final StringBuilder sb = new StringBuilder(8);
301 try (final Formatter formatter = new Formatter(sb, Locale.ROOT)) {
302 formatter.format("%08x", nounceCount);
303 }
304 final String nc = sb.toString();
305
306 if (cnonce == null) {
307 cnonce = formatHex(createCnonce());
308 }
309
310 if (buffer == null) {
311 buffer = new ByteArrayBuilder(128);
312 } else {
313 buffer.reset();
314 }
315 buffer.charset(charset);
316
317 a1 = null;
318 a2 = null;
319
320 if (algorithm.equalsIgnoreCase("MD5-sess")) {
321
322
323
324
325
326 buffer.append(username).append(":").append(realm).append(":").append(password);
327 final String checksum = formatHex(digester.digest(this.buffer.toByteArray()));
328 buffer.reset();
329 buffer.append(checksum).append(":").append(nonce).append(":").append(cnonce);
330 } else {
331
332 buffer.append(username).append(":").append(realm).append(":").append(password);
333 }
334 a1 = buffer.toByteArray();
335
336 final String hasha1 = formatHex(digester.digest(a1));
337 buffer.reset();
338
339 if (qop == QualityOfProtection.AUTH) {
340
341 a2 = buffer.append(method).append(":").append(uri).toByteArray();
342 } else if (qop == QualityOfProtection.AUTH_INT) {
343
344 final HttpEntity entity = request instanceof ClassicHttpRequest ? ((ClassicHttpRequest) request).getEntity() : null;
345 if (entity != null && !entity.isRepeatable()) {
346
347 if (qopset.contains("auth")) {
348 qop = QualityOfProtection.AUTH;
349 a2 = buffer.append(method).append(":").append(uri).toByteArray();
350 } else {
351 throw new AuthenticationException("Qop auth-int cannot be used with " +
352 "a non-repeatable entity");
353 }
354 } else {
355 final HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
356 try {
357 if (entity != null) {
358 entity.writeTo(entityDigester);
359 }
360 entityDigester.close();
361 } catch (final IOException ex) {
362 throw new AuthenticationException("I/O error reading entity content", ex);
363 }
364 a2 = buffer.append(method).append(":").append(uri)
365 .append(":").append(formatHex(entityDigester.getDigest())).toByteArray();
366 }
367 } else {
368 a2 = buffer.append(method).append(":").append(uri).toByteArray();
369 }
370
371 final String hasha2 = formatHex(digester.digest(a2));
372 buffer.reset();
373
374
375
376 final byte[] digestInput;
377 if (qop == QualityOfProtection.MISSING) {
378 buffer.append(hasha1).append(":").append(nonce).append(":").append(hasha2);
379 } else {
380 buffer.append(hasha1).append(":").append(nonce).append(":").append(nc).append(":")
381 .append(cnonce).append(":").append(qop == QualityOfProtection.AUTH_INT ? "auth-int" : "auth")
382 .append(":").append(hasha2);
383 }
384 digestInput = buffer.toByteArray();
385 buffer.reset();
386
387 final String digest = formatHex(digester.digest(digestInput));
388
389 final CharArrayBuffer buffer = new CharArrayBuffer(128);
390 buffer.append(StandardAuthScheme.DIGEST + " ");
391
392 final List<BasicNameValuePair> params = new ArrayList<>(20);
393 params.add(new BasicNameValuePair("username", username));
394 params.add(new BasicNameValuePair("realm", realm));
395 params.add(new BasicNameValuePair("nonce", nonce));
396 params.add(new BasicNameValuePair("uri", uri));
397 params.add(new BasicNameValuePair("response", digest));
398
399 if (qop != QualityOfProtection.MISSING) {
400 params.add(new BasicNameValuePair("qop", qop == QualityOfProtection.AUTH_INT ? "auth-int" : "auth"));
401 params.add(new BasicNameValuePair("nc", nc));
402 params.add(new BasicNameValuePair("cnonce", cnonce));
403 }
404
405 params.add(new BasicNameValuePair("algorithm", algorithm));
406 if (opaque != null) {
407 params.add(new BasicNameValuePair("opaque", opaque));
408 }
409
410 for (int i = 0; i < params.size(); i++) {
411 final BasicNameValuePair param = params.get(i);
412 if (i > 0) {
413 buffer.append(", ");
414 }
415 final String name = param.getName();
416 final boolean noQuotes = ("nc".equals(name) || "qop".equals(name)
417 || "algorithm".equals(name));
418 BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes);
419 }
420 return buffer.toString();
421 }
422
423 @Internal
424 public String getNonce() {
425 return lastNonce;
426 }
427
428 @Internal
429 public long getNounceCount() {
430 return nounceCount;
431 }
432
433 @Internal
434 public String getCnonce() {
435 return cnonce;
436 }
437
438 String getA1() {
439 return a1 != null ? new String(a1, StandardCharsets.US_ASCII) : null;
440 }
441
442 String getA2() {
443 return a2 != null ? new String(a2, StandardCharsets.US_ASCII) : null;
444 }
445
446
447
448
449
450
451
452
453 static String formatHex(final byte[] binaryData) {
454 final int n = binaryData.length;
455 final char[] buffer = new char[n * 2];
456 for (int i = 0; i < n; i++) {
457 final int low = (binaryData[i] & 0x0f);
458 final int high = ((binaryData[i] & 0xf0) >> 4);
459 buffer[i * 2] = HEXADECIMAL[high];
460 buffer[(i * 2) + 1] = HEXADECIMAL[low];
461 }
462
463 return new String(buffer);
464 }
465
466
467
468
469
470
471 static byte[] createCnonce() {
472 final SecureRandom rnd = new SecureRandom();
473 final byte[] tmp = new byte[8];
474 rnd.nextBytes(tmp);
475 return tmp;
476 }
477
478 private void writeObject(final ObjectOutputStream out) throws IOException {
479 out.defaultWriteObject();
480 out.writeUTF(defaultCharset.name());
481 }
482
483 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
484 in.defaultReadObject();
485 this.defaultCharset = Charset.forName(in.readUTF());
486 }
487
488 @Override
489 public String toString() {
490 return getName() + this.paramMap;
491 }
492
493 }