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.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
105
106
107 private enum QualityOfProtection {
108 UNKNOWN, MISSING, AUTH_INT, AUTH
109 }
110
111 private transient Charset defaultCharset;
112 private final Map<String, String> paramMap;
113 private boolean complete;
114 private transient ByteArrayBuilder buffer;
115
116 private String lastNonce;
117 private long nounceCount;
118 private String cnonce;
119 private byte[] a1;
120 private byte[] a2;
121
122 private UsernamePasswordCredentials credentials;
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 Args.check(credentials instanceof UsernamePasswordCredentials,
137 "Unsupported credential type: " + credentials.getClass());
138 this.credentials = (UsernamePasswordCredentials) credentials;
139 this.paramMap.put("cnonce", cnonce);
140 this.paramMap.put("realm", realm);
141 }
142
143 @Override
144 public String getName() {
145 return StandardAuthScheme.DIGEST;
146 }
147
148 @Override
149 public boolean isConnectionBased() {
150 return false;
151 }
152
153 @Override
154 public String getRealm() {
155 return this.paramMap.get("realm");
156 }
157
158 @Override
159 public void processChallenge(
160 final AuthChallenge authChallenge,
161 final HttpContext context) throws MalformedChallengeException {
162 Args.notNull(authChallenge, "AuthChallenge");
163 this.paramMap.clear();
164 final List<NameValuePair> params = authChallenge.getParams();
165 if (params != null) {
166 for (final NameValuePair param: params) {
167 this.paramMap.put(param.getName().toLowerCase(Locale.ROOT), param.getValue());
168 }
169 }
170 if (this.paramMap.isEmpty()) {
171 throw new MalformedChallengeException("Missing digest auth parameters");
172 }
173 this.complete = true;
174 }
175
176 @Override
177 public boolean isChallengeComplete() {
178 final String s = this.paramMap.get("stale");
179 return !"true".equalsIgnoreCase(s) && this.complete;
180 }
181
182 @Override
183 public boolean isResponseReady(
184 final HttpHost host,
185 final CredentialsProvider credentialsProvider,
186 final HttpContext context) throws AuthenticationException {
187
188 Args.notNull(host, "Auth host");
189 Args.notNull(credentialsProvider, "CredentialsProvider");
190
191 final AuthScope authScope = new AuthScope(host, getRealm(), getName());
192 final Credentials credentials = credentialsProvider.getCredentials(
193 authScope, context);
194 if (credentials instanceof UsernamePasswordCredentials) {
195 this.credentials = (UsernamePasswordCredentials) credentials;
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.credentials = 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 if (credentials == null) {
242 throw new AuthenticationException("User credentials have not been provided");
243 }
244 final String uri = request.getRequestUri();
245 final String method = request.getMethod();
246 final String realm = this.paramMap.get("realm");
247 final String nonce = this.paramMap.get("nonce");
248 final String opaque = this.paramMap.get("opaque");
249 final String algorithm = this.paramMap.get("algorithm");
250
251 final Set<String> qopset = new HashSet<>(8);
252 QualityOfProtection qop = QualityOfProtection.UNKNOWN;
253 final String qoplist = this.paramMap.get("qop");
254 if (qoplist != null) {
255 final StringTokenizer tok = new StringTokenizer(qoplist, ",");
256 while (tok.hasMoreTokens()) {
257 final String variant = tok.nextToken().trim();
258 qopset.add(variant.toLowerCase(Locale.ROOT));
259 }
260 final HttpEntity entity = request instanceof ClassicHttpRequest ? ((ClassicHttpRequest) request).getEntity() : null;
261 if (entity != null && qopset.contains("auth-int")) {
262 qop = QualityOfProtection.AUTH_INT;
263 } else if (qopset.contains("auth")) {
264 qop = QualityOfProtection.AUTH;
265 } else if (qopset.contains("auth-int")) {
266 qop = QualityOfProtection.AUTH_INT;
267 }
268 } else {
269 qop = QualityOfProtection.MISSING;
270 }
271
272 if (qop == QualityOfProtection.UNKNOWN) {
273 throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
274 }
275
276 final Charset charset = AuthSchemeSupport.parseCharset(paramMap.get("charset"), defaultCharset);
277 String digAlg = algorithm;
278
279 if (digAlg == null || digAlg.equalsIgnoreCase("MD5-sess")) {
280 digAlg = "MD5";
281 }
282
283 final MessageDigest digester;
284 try {
285 digester = createMessageDigest(digAlg);
286 } catch (final UnsupportedDigestAlgorithmException ex) {
287 throw new AuthenticationException("Unsupported digest algorithm: " + digAlg);
288 }
289
290 if (nonce.equals(this.lastNonce)) {
291 nounceCount++;
292 } else {
293 nounceCount = 1;
294 cnonce = null;
295 lastNonce = nonce;
296 }
297
298 final StringBuilder sb = new StringBuilder(8);
299 try (final Formatter formatter = new Formatter(sb, Locale.ROOT)) {
300 formatter.format("%08x", nounceCount);
301 }
302 final String nc = sb.toString();
303
304 if (cnonce == null) {
305 cnonce = formatHex(createCnonce());
306 }
307
308 if (buffer == null) {
309 buffer = new ByteArrayBuilder(128);
310 } else {
311 buffer.reset();
312 }
313 buffer.charset(charset);
314
315 a1 = null;
316 a2 = null;
317
318 if ("MD5-sess".equalsIgnoreCase(algorithm)) {
319
320
321
322
323
324 buffer.append(credentials.getUserName()).append(":").append(realm).append(":").append(credentials.getUserPassword());
325 final String checksum = formatHex(digester.digest(this.buffer.toByteArray()));
326 buffer.reset();
327 buffer.append(checksum).append(":").append(nonce).append(":").append(cnonce);
328 } else {
329
330 buffer.append(credentials.getUserName()).append(":").append(realm).append(":").append(credentials.getUserPassword());
331 }
332 a1 = buffer.toByteArray();
333
334 final String hasha1 = formatHex(digester.digest(a1));
335 buffer.reset();
336
337 if (qop == QualityOfProtection.AUTH) {
338
339 a2 = buffer.append(method).append(":").append(uri).toByteArray();
340 } else if (qop == QualityOfProtection.AUTH_INT) {
341
342 final HttpEntity entity = request instanceof ClassicHttpRequest ? ((ClassicHttpRequest) request).getEntity() : null;
343 if (entity != null && !entity.isRepeatable()) {
344
345 if (qopset.contains("auth")) {
346 qop = QualityOfProtection.AUTH;
347 a2 = buffer.append(method).append(":").append(uri).toByteArray();
348 } else {
349 throw new AuthenticationException("Qop auth-int cannot be used with " +
350 "a non-repeatable entity");
351 }
352 } else {
353 final HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
354 try {
355 if (entity != null) {
356 entity.writeTo(entityDigester);
357 }
358 entityDigester.close();
359 } catch (final IOException ex) {
360 throw new AuthenticationException("I/O error reading entity content", ex);
361 }
362 a2 = buffer.append(method).append(":").append(uri)
363 .append(":").append(formatHex(entityDigester.getDigest())).toByteArray();
364 }
365 } else {
366 a2 = buffer.append(method).append(":").append(uri).toByteArray();
367 }
368
369 final String hasha2 = formatHex(digester.digest(a2));
370 buffer.reset();
371
372
373
374 final byte[] digestInput;
375 if (qop == QualityOfProtection.MISSING) {
376 buffer.append(hasha1).append(":").append(nonce).append(":").append(hasha2);
377 } else {
378 buffer.append(hasha1).append(":").append(nonce).append(":").append(nc).append(":")
379 .append(cnonce).append(":").append(qop == QualityOfProtection.AUTH_INT ? "auth-int" : "auth")
380 .append(":").append(hasha2);
381 }
382 digestInput = buffer.toByteArray();
383 buffer.reset();
384
385 final String digest = formatHex(digester.digest(digestInput));
386
387 final CharArrayBuffer buffer = new CharArrayBuffer(128);
388 buffer.append(StandardAuthScheme.DIGEST + " ");
389
390 final List<BasicNameValuePair> params = new ArrayList<>(20);
391 params.add(new BasicNameValuePair("username", credentials.getUserName()));
392 params.add(new BasicNameValuePair("realm", realm));
393 params.add(new BasicNameValuePair("nonce", nonce));
394 params.add(new BasicNameValuePair("uri", uri));
395 params.add(new BasicNameValuePair("response", digest));
396
397 if (qop != QualityOfProtection.MISSING) {
398 params.add(new BasicNameValuePair("qop", qop == QualityOfProtection.AUTH_INT ? "auth-int" : "auth"));
399 params.add(new BasicNameValuePair("nc", nc));
400 params.add(new BasicNameValuePair("cnonce", cnonce));
401 }
402 if (algorithm != null) {
403 params.add(new BasicNameValuePair("algorithm", algorithm));
404 }
405 if (opaque != null) {
406 params.add(new BasicNameValuePair("opaque", opaque));
407 }
408
409 for (int i = 0; i < params.size(); i++) {
410 final BasicNameValuePair param = params.get(i);
411 if (i > 0) {
412 buffer.append(", ");
413 }
414 final String name = param.getName();
415 final boolean noQuotes = ("nc".equals(name) || "qop".equals(name)
416 || "algorithm".equals(name));
417 BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes);
418 }
419 return buffer.toString();
420 }
421
422 @Internal
423 public String getNonce() {
424 return lastNonce;
425 }
426
427 @Internal
428 public long getNounceCount() {
429 return nounceCount;
430 }
431
432 @Internal
433 public String getCnonce() {
434 return cnonce;
435 }
436
437 String getA1() {
438 return a1 != null ? new String(a1, StandardCharsets.US_ASCII) : null;
439 }
440
441 String getA2() {
442 return a2 != null ? new String(a2, StandardCharsets.US_ASCII) : null;
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 }