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.annotation.NotThreadSafe;
47 import org.apache.http.auth.AUTH;
48 import org.apache.http.auth.AuthenticationException;
49 import org.apache.http.auth.ChallengeState;
50 import org.apache.http.auth.ContextAwareAuthScheme;
51 import org.apache.http.auth.Credentials;
52 import org.apache.http.auth.MalformedChallengeException;
53 import org.apache.http.message.BasicHeaderValueFormatter;
54 import org.apache.http.message.BasicNameValuePair;
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.Args;
59 import org.apache.http.util.CharArrayBuffer;
60 import org.apache.http.util.EncodingUtils;
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75 @NotThreadSafe
76 public class DigestScheme extends RFC2617Scheme {
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 }
142
143
144
145
146
147
148
149 public boolean isComplete() {
150 final String s = getParameter("stale");
151 if ("true".equalsIgnoreCase(s)) {
152 return false;
153 } else {
154 return this.complete;
155 }
156 }
157
158
159
160
161
162
163 public String getSchemeName() {
164 return "digest";
165 }
166
167
168
169
170
171
172 public boolean isConnectionBased() {
173 return false;
174 }
175
176 public void overrideParamter(final String name, final String value) {
177 getParameters().put(name, value);
178 }
179
180
181
182
183 @Deprecated
184 public Header authenticate(
185 final Credentials credentials, final HttpRequest request) throws AuthenticationException {
186 return authenticate(credentials, request, new BasicHttpContext());
187 }
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203 @Override
204 public Header authenticate(
205 final Credentials credentials,
206 final HttpRequest request,
207 final HttpContext context) throws AuthenticationException {
208
209 Args.notNull(credentials, "Credentials");
210 Args.notNull(request, "HTTP request");
211 if (getParameter("realm") == null) {
212 throw new AuthenticationException("missing realm in challenge");
213 }
214 if (getParameter("nonce") == null) {
215 throw new AuthenticationException("missing nonce in challenge");
216 }
217
218 getParameters().put("methodname", request.getRequestLine().getMethod());
219 getParameters().put("uri", request.getRequestLine().getUri());
220 final String charset = getParameter("charset");
221 if (charset == null) {
222 getParameters().put("charset", getCredentialsCharset(request));
223 }
224 return createDigestHeader(credentials, request);
225 }
226
227 private static MessageDigest createMessageDigest(
228 final String digAlg) throws UnsupportedDigestAlgorithmException {
229 try {
230 return MessageDigest.getInstance(digAlg);
231 } catch (final Exception e) {
232 throw new UnsupportedDigestAlgorithmException(
233 "Unsupported algorithm in HTTP Digest authentication: "
234 + digAlg);
235 }
236 }
237
238
239
240
241
242
243
244
245 private Header createDigestHeader(
246 final Credentials credentials,
247 final HttpRequest request) throws AuthenticationException {
248 final String uri = getParameter("uri");
249 final String realm = getParameter("realm");
250 final String nonce = getParameter("nonce");
251 final String opaque = getParameter("opaque");
252 final String method = getParameter("methodname");
253 String algorithm = getParameter("algorithm");
254
255 if (algorithm == null) {
256 algorithm = "MD5";
257 }
258
259 final Set<String> qopset = new HashSet<String>(8);
260 int qop = QOP_UNKNOWN;
261 final String qoplist = getParameter("qop");
262 if (qoplist != null) {
263 final StringTokenizer tok = new StringTokenizer(qoplist, ",");
264 while (tok.hasMoreTokens()) {
265 final String variant = tok.nextToken().trim();
266 qopset.add(variant.toLowerCase(Locale.US));
267 }
268 if (request instanceof HttpEntityEnclosingRequest && qopset.contains("auth-int")) {
269 qop = QOP_AUTH_INT;
270 } else if (qopset.contains("auth")) {
271 qop = QOP_AUTH;
272 }
273 } else {
274 qop = QOP_MISSING;
275 }
276
277 if (qop == QOP_UNKNOWN) {
278 throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
279 }
280
281 String charset = getParameter("charset");
282 if (charset == null) {
283 charset = "ISO-8859-1";
284 }
285
286 String digAlg = algorithm;
287 if (digAlg.equalsIgnoreCase("MD5-sess")) {
288 digAlg = "MD5";
289 }
290
291 MessageDigest digester;
292 try {
293 digester = createMessageDigest(digAlg);
294 } catch (final UnsupportedDigestAlgorithmException ex) {
295 throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg);
296 }
297
298 final String uname = credentials.getUserPrincipal().getName();
299 final String pwd = credentials.getPassword();
300
301 if (nonce.equals(this.lastNonce)) {
302 nounceCount++;
303 } else {
304 nounceCount = 1;
305 cnonce = null;
306 lastNonce = nonce;
307 }
308 final StringBuilder sb = new StringBuilder(256);
309 final Formatter formatter = new Formatter(sb, Locale.US);
310 formatter.format("%08x", nounceCount);
311 formatter.close();
312 final String nc = sb.toString();
313
314 if (cnonce == null) {
315 cnonce = createCnonce();
316 }
317
318 a1 = null;
319 a2 = null;
320
321 if (algorithm.equalsIgnoreCase("MD5-sess")) {
322
323
324
325
326
327 sb.setLength(0);
328 sb.append(uname).append(':').append(realm).append(':').append(pwd);
329 final String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset)));
330 sb.setLength(0);
331 sb.append(checksum).append(':').append(nonce).append(':').append(cnonce);
332 a1 = sb.toString();
333 } else {
334
335 sb.setLength(0);
336 sb.append(uname).append(':').append(realm).append(':').append(pwd);
337 a1 = sb.toString();
338 }
339
340 final String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset)));
341
342 if (qop == QOP_AUTH) {
343
344 a2 = method + ':' + uri;
345 } else if (qop == QOP_AUTH_INT) {
346
347 HttpEntity entity = null;
348 if (request instanceof HttpEntityEnclosingRequest) {
349 entity = ((HttpEntityEnclosingRequest) request).getEntity();
350 }
351 if (entity != null && !entity.isRepeatable()) {
352
353 if (qopset.contains("auth")) {
354 qop = QOP_AUTH;
355 a2 = method + ':' + uri;
356 } else {
357 throw new AuthenticationException("Qop auth-int cannot be used with " +
358 "a non-repeatable entity");
359 }
360 } else {
361 final HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
362 try {
363 if (entity != null) {
364 entity.writeTo(entityDigester);
365 }
366 entityDigester.close();
367 } catch (final IOException ex) {
368 throw new AuthenticationException("I/O error reading entity content", ex);
369 }
370 a2 = method + ':' + uri + ':' + encode(entityDigester.getDigest());
371 }
372 } else {
373 a2 = method + ':' + uri;
374 }
375
376 final String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset)));
377
378
379
380 String digestValue;
381 if (qop == QOP_MISSING) {
382 sb.setLength(0);
383 sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2);
384 digestValue = sb.toString();
385 } else {
386 sb.setLength(0);
387 sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':')
388 .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth")
389 .append(':').append(hasha2);
390 digestValue = sb.toString();
391 }
392
393 final String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue)));
394
395 final CharArrayBuffer buffer = new CharArrayBuffer(128);
396 if (isProxy()) {
397 buffer.append(AUTH.PROXY_AUTH_RESP);
398 } else {
399 buffer.append(AUTH.WWW_AUTH_RESP);
400 }
401 buffer.append(": Digest ");
402
403 final List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20);
404 params.add(new BasicNameValuePair("username", uname));
405 params.add(new BasicNameValuePair("realm", realm));
406 params.add(new BasicNameValuePair("nonce", nonce));
407 params.add(new BasicNameValuePair("uri", uri));
408 params.add(new BasicNameValuePair("response", digest));
409
410 if (qop != QOP_MISSING) {
411 params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth"));
412 params.add(new BasicNameValuePair("nc", nc));
413 params.add(new BasicNameValuePair("cnonce", cnonce));
414 }
415
416 params.add(new BasicNameValuePair("algorithm", algorithm));
417 if (opaque != null) {
418 params.add(new BasicNameValuePair("opaque", opaque));
419 }
420
421 for (int i = 0; i < params.size(); i++) {
422 final BasicNameValuePair param = params.get(i);
423 if (i > 0) {
424 buffer.append(", ");
425 }
426 final boolean noQuotes = "nc".equals(param.getName()) || "qop".equals(param.getName());
427 BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes);
428 }
429 return new BufferedHeader(buffer);
430 }
431
432 String getCnonce() {
433 return cnonce;
434 }
435
436 String getA1() {
437 return a1;
438 }
439
440 String getA2() {
441 return a2;
442 }
443
444
445
446
447
448
449
450
451 static String encode(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
470 public static String createCnonce() {
471 final SecureRandom rnd = new SecureRandom();
472 final byte[] tmp = new byte[8];
473 rnd.nextBytes(tmp);
474 return encode(tmp);
475 }
476
477 }