Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
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 */
27 package ch.boye.httpclientandroidlib.impl.auth;
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;
37 import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
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;
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 {
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 };
91 /** Whether the digest authentication process is complete */
92 private boolean complete;
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;
99 private String lastNonce;
100 private long nounceCount;
101 private String cnonce;
102 private String a1;
103 private String a2;
105 /**
106 * Default constructor for the digest authetication scheme.
107 */
108 public DigestScheme() {
109 super();
110 this.complete = false;
111 }
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);
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 }
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 }
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 }
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 }
168 public void overrideParamter(final String name, final String value) {
169 getParameters().put(name, value);
170 }
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 {
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 }
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 }
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 }
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 }
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 }
260 if (qop == QOP_UNKNOWN) {
261 throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
262 }
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 }
274 String digAlg = algorithm;
275 if (digAlg.equalsIgnoreCase("MD5-sess")) {
276 digAlg = "MD5";
277 }
279 MessageDigest digester;
280 try {
281 digester = createMessageDigest(digAlg);
282 } catch (UnsupportedDigestAlgorithmException ex) {
283 throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg);
284 }
286 String uname = credentials.getUserPrincipal().getName();
287 String pwd = credentials.getPassword();
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();
301 if (cnonce == null) {
302 cnonce = createCnonce();
303 }
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)
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 }
327 String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset)));
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 }
340 String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset)));
342 // 3.2.2.1
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 }
357 String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue)));
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 ");
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));
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 }
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 }
397 String getCnonce() {
398 return cnonce;
399 }
401 String getA1() {
402 return a1;
403 }
405 String getA2() {
406 return a2;
407 }
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 }
426 return new String(buffer);
427 }
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 }
442 }