1 /*
2  * ====================================================================
3  * Licensed to the Apache Software Foundation (ASF) under one
4  * or more contributor license agreements.  See the NOTICE file
5  * distributed with this work for additional information
6  * regarding copyright ownership.  The ASF licenses this file
7  * to you under the Apache License, Version 2.0 (the
8  * "License"); you may not use this file except in compliance
9  * with 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,
14  * software distributed under the License is distributed on an
15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  * KIND, either express or implied.  See the License for the
17  * specific language governing permissions and limitations
18  * under the License.
19  * ====================================================================
20  *
21  * This software consists of voluntary contributions made by many
22  * individuals on behalf of the Apache Software Foundation.  For more
23  * information on the Apache Software Foundation, please see
24  * <http://www.apache.org/>.
25  *
26  */
27 package ch.boye.httpclientandroidlib.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 ch.boye.httpclientandroidlib.Consts;
42 import ch.boye.httpclientandroidlib.Header;
43 import ch.boye.httpclientandroidlib.HttpEntity;
44 import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
45 import ch.boye.httpclientandroidlib.HttpRequest;
46 import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
47 import ch.boye.httpclientandroidlib.auth.AUTH;
48 import ch.boye.httpclientandroidlib.auth.AuthenticationException;
49 import ch.boye.httpclientandroidlib.auth.ChallengeState;
50 import ch.boye.httpclientandroidlib.auth.Credentials;
51 import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
52 import ch.boye.httpclientandroidlib.message.BasicHeaderValueFormatter;
53 import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
54 import ch.boye.httpclientandroidlib.message.BufferedHeader;
55 import ch.boye.httpclientandroidlib.protocol.BasicHttpContext;
56 import ch.boye.httpclientandroidlib.protocol.HttpContext;
57 import ch.boye.httpclientandroidlib.util.Args;
58 import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
59 import ch.boye.httpclientandroidlib.util.EncodingUtils;
60 
61 /**
62  * Digest authentication scheme as defined in RFC 2617.
63  * Both MD5 (default) and MD5-sess are supported.
64  * Currently only qop=auth or no qop is supported. qop=auth-int
65  * is unsupported. If auth and auth-int are provided, auth is
66  * used.
67  * <p/>
68  * Since the digest username is included as clear text in the generated
69  * Authentication header, the charset of the username must be compatible
70  * with the HTTP element charset used by the connection.
71  *
72  * @since 4.0
73  */
74 @NotThreadSafe
75 public class DigestScheme extends RFC2617Scheme {
76 
77     /**
78      * Hexa values used when creating 32 character long digest in HTTP DigestScheme
79      * in case of authentication.
80      *
81      * @see #encode(byte[])
82      */
83     private static final char[] HEXADECIMAL = {
84         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
85         'e', 'f'
86     };
87 
88     /** Whether the digest authentication process is complete */
89     private boolean complete;
90 
91     private static final int QOP_UNKNOWN = -1;
92     private static final int QOP_MISSING = 0;
93     private static final int QOP_AUTH_INT = 1;
94     private static final int QOP_AUTH = 2;
95 
96     private String lastNonce;
97     private long nounceCount;
98     private String cnonce;
99     private String a1;
100     private String a2;
101 
102     /**
103      * @since 4.3
104      */
DigestScheme(final Charset credentialsCharset)105     public DigestScheme(final Charset credentialsCharset) {
106         super(credentialsCharset);
107         this.complete = false;
108     }
109 
110     /**
111      * Creates an instance of <tt>DigestScheme</tt> with the given challenge
112      * state.
113      *
114      * @since 4.2
115      *
116      * @deprecated (4.3) do not use.
117      */
118     @Deprecated
DigestScheme(final ChallengeState challengeState)119     public DigestScheme(final ChallengeState challengeState) {
120         super(challengeState);
121     }
122 
DigestScheme()123     public DigestScheme() {
124         this(Consts.ASCII);
125     }
126 
127     /**
128      * Processes the Digest challenge.
129      *
130      * @param header the challenge header
131      *
132      * @throws MalformedChallengeException is thrown if the authentication challenge
133      * is malformed
134      */
135     @Override
processChallenge( final Header header)136     public void processChallenge(
137             final Header header) throws MalformedChallengeException {
138         super.processChallenge(header);
139         this.complete = true;
140     }
141 
142     /**
143      * Tests if the Digest authentication process has been completed.
144      *
145      * @return <tt>true</tt> if Digest authorization has been processed,
146      *   <tt>false</tt> otherwise.
147      */
isComplete()148     public boolean isComplete() {
149         final String s = getParameter("stale");
150         if ("true".equalsIgnoreCase(s)) {
151             return false;
152         } else {
153             return this.complete;
154         }
155     }
156 
157     /**
158      * Returns textual designation of the digest authentication scheme.
159      *
160      * @return <code>digest</code>
161      */
getSchemeName()162     public String getSchemeName() {
163         return "digest";
164     }
165 
166     /**
167      * Returns <tt>false</tt>. Digest authentication scheme is request based.
168      *
169      * @return <tt>false</tt>.
170      */
isConnectionBased()171     public boolean isConnectionBased() {
172         return false;
173     }
174 
overrideParamter(final String name, final String value)175     public void overrideParamter(final String name, final String value) {
176         getParameters().put(name, value);
177     }
178 
179     /**
180      * @deprecated (4.2) Use {@link ch.boye.httpclientandroidlib.auth.ContextAwareAuthScheme#authenticate(
181      *   Credentials, HttpRequest, ch.boye.httpclientandroidlib.protocol.HttpContext)}
182      */
183     @Deprecated
authenticate( final Credentials credentials, final HttpRequest request)184     public Header authenticate(
185             final Credentials credentials, final HttpRequest request) throws AuthenticationException {
186         return authenticate(credentials, request, new BasicHttpContext());
187     }
188 
189     /**
190      * Produces a digest authorization string for the given set of
191      * {@link Credentials}, method name and URI.
192      *
193      * @param credentials A set of credentials to be used for athentication
194      * @param request    The request being authenticated
195      *
196      * @throws ch.boye.httpclientandroidlib.auth.InvalidCredentialsException if authentication credentials
197      *         are not valid or not applicable for this authentication scheme
198      * @throws AuthenticationException if authorization string cannot
199      *   be generated due to an authentication failure
200      *
201      * @return a digest authorization string
202      */
203     @Override
authenticate( final Credentials credentials, final HttpRequest request, final HttpContext context)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         // Add method name and request-URI to the parameter map
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 
createMessageDigest( final String digAlg)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      * Creates digest-response header as defined in RFC2617.
240      *
241      * @param credentials User credentials
242      *
243      * @return The digest-response as String.
244      */
createDigestHeader( final Credentials credentials, final HttpRequest request)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         // If an algorithm is not specified, default to MD5.
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.ENGLISH));
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         final 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         // 3.2.2.2: Calculating digest
321         if (algorithm.equalsIgnoreCase("MD5-sess")) {
322             // H( unq(username-value) ":" unq(realm-value) ":" passwd )
323             //      ":" unq(nonce-value)
324             //      ":" unq(cnonce-value)
325 
326             // calculated one per session
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             // unq(username-value) ":" unq(realm-value) ":" passwd
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             // Method ":" digest-uri-value
344             a2 = method + ':' + uri;
345         } else if (qop == QOP_AUTH_INT) {
346             // Method ":" digest-uri-value ":" H(entity-body)
347             HttpEntity entity = null;
348             if (request instanceof HttpEntityEnclosingRequest) {
349                 entity = ((HttpEntityEnclosingRequest) request).getEntity();
350             }
351             if (entity != null && !entity.isRepeatable()) {
352                 // If the entity is not repeatable, try falling back onto QOP_AUTH
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         // 3.2.2.1
379 
380         final 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         // algorithm cannot be null here
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 String name = param.getName();
427             final boolean noQuotes = ("nc".equals(name) || "qop".equals(name)
428                     || "algorithm".equals(name));
429             BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes);
430         }
431         return new BufferedHeader(buffer);
432     }
433 
getCnonce()434     String getCnonce() {
435         return cnonce;
436     }
437 
getA1()438     String getA1() {
439         return a1;
440     }
441 
getA2()442     String getA2() {
443         return a2;
444     }
445 
446     /**
447      * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long
448      * <CODE>String</CODE> according to RFC 2617.
449      *
450      * @param binaryData array containing the digest
451      * @return encoded MD5, or <CODE>null</CODE> if encoding failed
452      */
encode(final byte[] binaryData)453     static String encode(final byte[] binaryData) {
454         final int n = binaryData.length;
455         final char[] buffer = new char[n * 2];
456         for (int i = 0; i < n; i++) {
457             final int low = (binaryData[i] & 0x0f);
458             final int high = ((binaryData[i] & 0xf0) >> 4);
459             buffer[i * 2] = HEXADECIMAL[high];
460             buffer[(i * 2) + 1] = HEXADECIMAL[low];
461         }
462 
463         return new String(buffer);
464     }
465 
466 
467     /**
468      * Creates a random cnonce value based on the current time.
469      *
470      * @return The cnonce value as String.
471      */
createCnonce()472     public static String createCnonce() {
473         final SecureRandom rnd = new SecureRandom();
474         final byte[] tmp = new byte[8];
475         rnd.nextBytes(tmp);
476         return encode(tmp);
477     }
478 
479     @Override
toString()480     public String toString() {
481         final StringBuilder builder = new StringBuilder();
482         builder.append("DIGEST [complete=").append(complete)
483                 .append(", nonce=").append(lastNonce)
484                 .append(", nc=").append(nounceCount)
485                 .append("]");
486         return builder.toString();
487     }
488 
489 }
490