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