|
1 /* |
|
2 * ==================================================================== |
|
3 * |
|
4 * Licensed to the Apache Software Foundation (ASF) under one or more |
|
5 * contributor license agreements. See the NOTICE file distributed with |
|
6 * this work for additional information regarding copyright ownership. |
|
7 * The ASF licenses this file to You under the Apache License, Version 2.0 |
|
8 * (the "License"); you may not use this file except in compliance with |
|
9 * the License. You may obtain a copy of the License at |
|
10 * |
|
11 * http://www.apache.org/licenses/LICENSE-2.0 |
|
12 * |
|
13 * Unless required by applicable law or agreed to in writing, software |
|
14 * distributed under the License is distributed on an "AS IS" BASIS, |
|
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
16 * See the License for the specific language governing permissions and |
|
17 * limitations under the License. |
|
18 * ==================================================================== |
|
19 * |
|
20 * This software consists of voluntary contributions made by many |
|
21 * individuals on behalf of the Apache Software Foundation. For more |
|
22 * information on the Apache Software Foundation, please see |
|
23 * <http://www.apache.org/>. |
|
24 * |
|
25 */ |
|
26 |
|
27 package ch.boye.httpclientandroidlib.impl.auth; |
|
28 |
|
29 import java.security.MessageDigest; |
|
30 import java.security.SecureRandom; |
|
31 import java.util.ArrayList; |
|
32 import java.util.Formatter; |
|
33 import java.util.List; |
|
34 import java.util.Locale; |
|
35 import java.util.StringTokenizer; |
|
36 |
|
37 import ch.boye.httpclientandroidlib.annotation.NotThreadSafe; |
|
38 |
|
39 import ch.boye.httpclientandroidlib.Header; |
|
40 import ch.boye.httpclientandroidlib.HttpRequest; |
|
41 import ch.boye.httpclientandroidlib.auth.AuthenticationException; |
|
42 import ch.boye.httpclientandroidlib.auth.Credentials; |
|
43 import ch.boye.httpclientandroidlib.auth.AUTH; |
|
44 import ch.boye.httpclientandroidlib.auth.MalformedChallengeException; |
|
45 import ch.boye.httpclientandroidlib.auth.params.AuthParams; |
|
46 import ch.boye.httpclientandroidlib.message.BasicNameValuePair; |
|
47 import ch.boye.httpclientandroidlib.message.BasicHeaderValueFormatter; |
|
48 import ch.boye.httpclientandroidlib.message.BufferedHeader; |
|
49 import ch.boye.httpclientandroidlib.util.CharArrayBuffer; |
|
50 import ch.boye.httpclientandroidlib.util.EncodingUtils; |
|
51 |
|
52 /** |
|
53 * Digest authentication scheme as defined in RFC 2617. |
|
54 * Both MD5 (default) and MD5-sess are supported. |
|
55 * Currently only qop=auth or no qop is supported. qop=auth-int |
|
56 * is unsupported. If auth and auth-int are provided, auth is |
|
57 * used. |
|
58 * <p> |
|
59 * Credential charset is configured via the |
|
60 * {@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET} |
|
61 * parameter of the HTTP request. |
|
62 * <p> |
|
63 * Since the digest username is included as clear text in the generated |
|
64 * Authentication header, the charset of the username must be compatible |
|
65 * with the |
|
66 * {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET |
|
67 * http element charset}. |
|
68 * <p> |
|
69 * The following parameters can be used to customize the behavior of this |
|
70 * class: |
|
71 * <ul> |
|
72 * <li>{@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET}</li> |
|
73 * </ul> |
|
74 * |
|
75 * @since 4.0 |
|
76 */ |
|
77 @NotThreadSafe |
|
78 public class DigestScheme extends RFC2617Scheme { |
|
79 |
|
80 /** |
|
81 * Hexa values used when creating 32 character long digest in HTTP DigestScheme |
|
82 * in case of authentication. |
|
83 * |
|
84 * @see #encode(byte[]) |
|
85 */ |
|
86 private static final char[] HEXADECIMAL = { |
|
87 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', |
|
88 'e', 'f' |
|
89 }; |
|
90 |
|
91 /** Whether the digest authentication process is complete */ |
|
92 private boolean complete; |
|
93 |
|
94 private static final int QOP_UNKNOWN = -1; |
|
95 private static final int QOP_MISSING = 0; |
|
96 private static final int QOP_AUTH_INT = 1; |
|
97 private static final int QOP_AUTH = 2; |
|
98 |
|
99 private String lastNonce; |
|
100 private long nounceCount; |
|
101 private String cnonce; |
|
102 private String a1; |
|
103 private String a2; |
|
104 |
|
105 /** |
|
106 * Default constructor for the digest authetication scheme. |
|
107 */ |
|
108 public DigestScheme() { |
|
109 super(); |
|
110 this.complete = false; |
|
111 } |
|
112 |
|
113 /** |
|
114 * Processes the Digest challenge. |
|
115 * |
|
116 * @param header the challenge header |
|
117 * |
|
118 * @throws MalformedChallengeException is thrown if the authentication challenge |
|
119 * is malformed |
|
120 */ |
|
121 @Override |
|
122 public void processChallenge( |
|
123 final Header header) throws MalformedChallengeException { |
|
124 super.processChallenge(header); |
|
125 |
|
126 if (getParameter("realm") == null) { |
|
127 throw new MalformedChallengeException("missing realm in challenge"); |
|
128 } |
|
129 if (getParameter("nonce") == null) { |
|
130 throw new MalformedChallengeException("missing nonce in challenge"); |
|
131 } |
|
132 this.complete = true; |
|
133 } |
|
134 |
|
135 /** |
|
136 * Tests if the Digest authentication process has been completed. |
|
137 * |
|
138 * @return <tt>true</tt> if Digest authorization has been processed, |
|
139 * <tt>false</tt> otherwise. |
|
140 */ |
|
141 public boolean isComplete() { |
|
142 String s = getParameter("stale"); |
|
143 if ("true".equalsIgnoreCase(s)) { |
|
144 return false; |
|
145 } else { |
|
146 return this.complete; |
|
147 } |
|
148 } |
|
149 |
|
150 /** |
|
151 * Returns textual designation of the digest authentication scheme. |
|
152 * |
|
153 * @return <code>digest</code> |
|
154 */ |
|
155 public String getSchemeName() { |
|
156 return "digest"; |
|
157 } |
|
158 |
|
159 /** |
|
160 * Returns <tt>false</tt>. Digest authentication scheme is request based. |
|
161 * |
|
162 * @return <tt>false</tt>. |
|
163 */ |
|
164 public boolean isConnectionBased() { |
|
165 return false; |
|
166 } |
|
167 |
|
168 public void overrideParamter(final String name, final String value) { |
|
169 getParameters().put(name, value); |
|
170 } |
|
171 |
|
172 /** |
|
173 * Produces a digest authorization string for the given set of |
|
174 * {@link Credentials}, method name and URI. |
|
175 * |
|
176 * @param credentials A set of credentials to be used for athentication |
|
177 * @param request The request being authenticated |
|
178 * |
|
179 * @throws ch.boye.httpclientandroidlib.auth.InvalidCredentialsException if authentication credentials |
|
180 * are not valid or not applicable for this authentication scheme |
|
181 * @throws AuthenticationException if authorization string cannot |
|
182 * be generated due to an authentication failure |
|
183 * |
|
184 * @return a digest authorization string |
|
185 */ |
|
186 public Header authenticate( |
|
187 final Credentials credentials, |
|
188 final HttpRequest request) throws AuthenticationException { |
|
189 |
|
190 if (credentials == null) { |
|
191 throw new IllegalArgumentException("Credentials may not be null"); |
|
192 } |
|
193 if (request == null) { |
|
194 throw new IllegalArgumentException("HTTP request may not be null"); |
|
195 } |
|
196 |
|
197 // Add method name and request-URI to the parameter map |
|
198 getParameters().put("methodname", request.getRequestLine().getMethod()); |
|
199 getParameters().put("uri", request.getRequestLine().getUri()); |
|
200 String charset = getParameter("charset"); |
|
201 if (charset == null) { |
|
202 charset = AuthParams.getCredentialCharset(request.getParams()); |
|
203 getParameters().put("charset", charset); |
|
204 } |
|
205 return createDigestHeader(credentials); |
|
206 } |
|
207 |
|
208 private static MessageDigest createMessageDigest( |
|
209 final String digAlg) throws UnsupportedDigestAlgorithmException { |
|
210 try { |
|
211 return MessageDigest.getInstance(digAlg); |
|
212 } catch (Exception e) { |
|
213 throw new UnsupportedDigestAlgorithmException( |
|
214 "Unsupported algorithm in HTTP Digest authentication: " |
|
215 + digAlg); |
|
216 } |
|
217 } |
|
218 |
|
219 /** |
|
220 * Creates digest-response header as defined in RFC2617. |
|
221 * |
|
222 * @param credentials User credentials |
|
223 * |
|
224 * @return The digest-response as String. |
|
225 */ |
|
226 private Header createDigestHeader( |
|
227 final Credentials credentials) throws AuthenticationException { |
|
228 String uri = getParameter("uri"); |
|
229 String realm = getParameter("realm"); |
|
230 String nonce = getParameter("nonce"); |
|
231 String opaque = getParameter("opaque"); |
|
232 String method = getParameter("methodname"); |
|
233 String algorithm = getParameter("algorithm"); |
|
234 if (uri == null) { |
|
235 throw new IllegalStateException("URI may not be null"); |
|
236 } |
|
237 if (realm == null) { |
|
238 throw new IllegalStateException("Realm may not be null"); |
|
239 } |
|
240 if (nonce == null) { |
|
241 throw new IllegalStateException("Nonce may not be null"); |
|
242 } |
|
243 |
|
244 //TODO: add support for QOP_INT |
|
245 int qop = QOP_UNKNOWN; |
|
246 String qoplist = getParameter("qop"); |
|
247 if (qoplist != null) { |
|
248 StringTokenizer tok = new StringTokenizer(qoplist, ","); |
|
249 while (tok.hasMoreTokens()) { |
|
250 String variant = tok.nextToken().trim(); |
|
251 if (variant.equals("auth")) { |
|
252 qop = QOP_AUTH; |
|
253 break; |
|
254 } |
|
255 } |
|
256 } else { |
|
257 qop = QOP_MISSING; |
|
258 } |
|
259 |
|
260 if (qop == QOP_UNKNOWN) { |
|
261 throw new AuthenticationException("None of the qop methods is supported: " + qoplist); |
|
262 } |
|
263 |
|
264 // If an algorithm is not specified, default to MD5. |
|
265 if (algorithm == null) { |
|
266 algorithm = "MD5"; |
|
267 } |
|
268 // If an charset is not specified, default to ISO-8859-1. |
|
269 String charset = getParameter("charset"); |
|
270 if (charset == null) { |
|
271 charset = "ISO-8859-1"; |
|
272 } |
|
273 |
|
274 String digAlg = algorithm; |
|
275 if (digAlg.equalsIgnoreCase("MD5-sess")) { |
|
276 digAlg = "MD5"; |
|
277 } |
|
278 |
|
279 MessageDigest digester; |
|
280 try { |
|
281 digester = createMessageDigest(digAlg); |
|
282 } catch (UnsupportedDigestAlgorithmException ex) { |
|
283 throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg); |
|
284 } |
|
285 |
|
286 String uname = credentials.getUserPrincipal().getName(); |
|
287 String pwd = credentials.getPassword(); |
|
288 |
|
289 if (nonce.equals(this.lastNonce)) { |
|
290 nounceCount++; |
|
291 } else { |
|
292 nounceCount = 1; |
|
293 cnonce = null; |
|
294 lastNonce = nonce; |
|
295 } |
|
296 StringBuilder sb = new StringBuilder(256); |
|
297 Formatter formatter = new Formatter(sb, Locale.US); |
|
298 formatter.format("%08x", nounceCount); |
|
299 String nc = sb.toString(); |
|
300 |
|
301 if (cnonce == null) { |
|
302 cnonce = createCnonce(); |
|
303 } |
|
304 |
|
305 a1 = null; |
|
306 a2 = null; |
|
307 // 3.2.2.2: Calculating digest |
|
308 if (algorithm.equalsIgnoreCase("MD5-sess")) { |
|
309 // H( unq(username-value) ":" unq(realm-value) ":" passwd ) |
|
310 // ":" unq(nonce-value) |
|
311 // ":" unq(cnonce-value) |
|
312 |
|
313 // calculated one per session |
|
314 sb.setLength(0); |
|
315 sb.append(uname).append(':').append(realm).append(':').append(pwd); |
|
316 String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset))); |
|
317 sb.setLength(0); |
|
318 sb.append(checksum).append(':').append(nonce).append(':').append(cnonce); |
|
319 a1 = sb.toString(); |
|
320 } else { |
|
321 // unq(username-value) ":" unq(realm-value) ":" passwd |
|
322 sb.setLength(0); |
|
323 sb.append(uname).append(':').append(realm).append(':').append(pwd); |
|
324 a1 = sb.toString(); |
|
325 } |
|
326 |
|
327 String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset))); |
|
328 |
|
329 if (qop == QOP_AUTH) { |
|
330 // Method ":" digest-uri-value |
|
331 a2 = method + ':' + uri; |
|
332 } else if (qop == QOP_AUTH_INT) { |
|
333 // Method ":" digest-uri-value ":" H(entity-body) |
|
334 //TODO: calculate entity hash if entity is repeatable |
|
335 throw new AuthenticationException("qop-int method is not suppported"); |
|
336 } else { |
|
337 a2 = method + ':' + uri; |
|
338 } |
|
339 |
|
340 String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset))); |
|
341 |
|
342 // 3.2.2.1 |
|
343 |
|
344 String digestValue; |
|
345 if (qop == QOP_MISSING) { |
|
346 sb.setLength(0); |
|
347 sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2); |
|
348 digestValue = sb.toString(); |
|
349 } else { |
|
350 sb.setLength(0); |
|
351 sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':') |
|
352 .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth") |
|
353 .append(':').append(hasha2); |
|
354 digestValue = sb.toString(); |
|
355 } |
|
356 |
|
357 String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue))); |
|
358 |
|
359 CharArrayBuffer buffer = new CharArrayBuffer(128); |
|
360 if (isProxy()) { |
|
361 buffer.append(AUTH.PROXY_AUTH_RESP); |
|
362 } else { |
|
363 buffer.append(AUTH.WWW_AUTH_RESP); |
|
364 } |
|
365 buffer.append(": Digest "); |
|
366 |
|
367 List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20); |
|
368 params.add(new BasicNameValuePair("username", uname)); |
|
369 params.add(new BasicNameValuePair("realm", realm)); |
|
370 params.add(new BasicNameValuePair("nonce", nonce)); |
|
371 params.add(new BasicNameValuePair("uri", uri)); |
|
372 params.add(new BasicNameValuePair("response", digest)); |
|
373 |
|
374 if (qop != QOP_MISSING) { |
|
375 params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth")); |
|
376 params.add(new BasicNameValuePair("nc", nc)); |
|
377 params.add(new BasicNameValuePair("cnonce", cnonce)); |
|
378 } |
|
379 if (algorithm != null) { |
|
380 params.add(new BasicNameValuePair("algorithm", algorithm)); |
|
381 } |
|
382 if (opaque != null) { |
|
383 params.add(new BasicNameValuePair("opaque", opaque)); |
|
384 } |
|
385 |
|
386 for (int i = 0; i < params.size(); i++) { |
|
387 BasicNameValuePair param = params.get(i); |
|
388 if (i > 0) { |
|
389 buffer.append(", "); |
|
390 } |
|
391 boolean noQuotes = "nc".equals(param.getName()) || "qop".equals(param.getName()); |
|
392 BasicHeaderValueFormatter.DEFAULT.formatNameValuePair(buffer, param, !noQuotes); |
|
393 } |
|
394 return new BufferedHeader(buffer); |
|
395 } |
|
396 |
|
397 String getCnonce() { |
|
398 return cnonce; |
|
399 } |
|
400 |
|
401 String getA1() { |
|
402 return a1; |
|
403 } |
|
404 |
|
405 String getA2() { |
|
406 return a2; |
|
407 } |
|
408 |
|
409 /** |
|
410 * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long |
|
411 * <CODE>String</CODE> according to RFC 2617. |
|
412 * |
|
413 * @param binaryData array containing the digest |
|
414 * @return encoded MD5, or <CODE>null</CODE> if encoding failed |
|
415 */ |
|
416 private static String encode(byte[] binaryData) { |
|
417 int n = binaryData.length; |
|
418 char[] buffer = new char[n * 2]; |
|
419 for (int i = 0; i < n; i++) { |
|
420 int low = (binaryData[i] & 0x0f); |
|
421 int high = ((binaryData[i] & 0xf0) >> 4); |
|
422 buffer[i * 2] = HEXADECIMAL[high]; |
|
423 buffer[(i * 2) + 1] = HEXADECIMAL[low]; |
|
424 } |
|
425 |
|
426 return new String(buffer); |
|
427 } |
|
428 |
|
429 |
|
430 /** |
|
431 * Creates a random cnonce value based on the current time. |
|
432 * |
|
433 * @return The cnonce value as String. |
|
434 */ |
|
435 public static String createCnonce() { |
|
436 SecureRandom rnd = new SecureRandom(); |
|
437 byte[] tmp = new byte[8]; |
|
438 rnd.nextBytes(tmp); |
|
439 return encode(tmp); |
|
440 } |
|
441 |
|
442 } |