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