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