1/*
2 * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/impl/auth/DigestScheme.java $
3 * $Revision: 659595 $
4 * $Date: 2008-05-23 09:47:14 -0700 (Fri, 23 May 2008) $
5 *
6 * ====================================================================
7 *
8 *  Licensed to the Apache Software Foundation (ASF) under one or more
9 *  contributor license agreements.  See the NOTICE file distributed with
10 *  this work for additional information regarding copyright ownership.
11 *  The ASF licenses this file to You under the Apache License, Version 2.0
12 *  (the "License"); you may not use this file except in compliance with
13 *  the License.  You may obtain a copy of the License at
14 *
15 *      http://www.apache.org/licenses/LICENSE-2.0
16 *
17 *  Unless required by applicable law or agreed to in writing, software
18 *  distributed under the License is distributed on an "AS IS" BASIS,
19 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 *  See the License for the specific language governing permissions and
21 *  limitations under the License.
22 * ====================================================================
23 *
24 * This software consists of voluntary contributions made by many
25 * individuals on behalf of the Apache Software Foundation.  For more
26 * information on the Apache Software Foundation, please see
27 * <http://www.apache.org/>.
28 *
29 */
30
31package org.apache.http.impl.auth;
32
33import java.security.MessageDigest;
34import java.util.ArrayList;
35import java.util.List;
36import java.util.StringTokenizer;
37
38import org.apache.http.Header;
39import org.apache.http.HttpRequest;
40import org.apache.http.auth.AuthenticationException;
41import org.apache.http.auth.Credentials;
42import org.apache.http.auth.AUTH;
43import org.apache.http.auth.MalformedChallengeException;
44import org.apache.http.auth.params.AuthParams;
45import org.apache.http.message.BasicNameValuePair;
46import org.apache.http.message.BasicHeaderValueFormatter;
47import org.apache.http.message.BufferedHeader;
48import org.apache.http.util.CharArrayBuffer;
49import org.apache.http.util.EncodingUtils;
50
51/**
52 * <p>
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 * <p>
60 * Credential charset is configured via the
61 * {@link org.apache.http.auth.params.AuthPNames#CREDENTIAL_CHARSET
62 *        credential charset} parameter.
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 org.apache.http.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET
67 *        http element charset}.
68 * </p>
69 *
70 * @author <a href="mailto:remm@apache.org">Remy Maucherat</a>
71 * @author Rodney Waldhoff
72 * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
73 * @author Ortwin Glueck
74 * @author Sean C. Sullivan
75 * @author <a href="mailto:adrian@ephox.com">Adrian Sutton</a>
76 * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
77 * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a>
78 *
79 * @since 4.0
80 */
81
82public class DigestScheme extends RFC2617Scheme {
83
84    /**
85     * Hexa values used when creating 32 character long digest in HTTP DigestScheme
86     * in case of authentication.
87     *
88     * @see #encode(byte[])
89     */
90    private static final char[] HEXADECIMAL = {
91        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
92        'e', 'f'
93    };
94
95    /** Whether the digest authentication process is complete */
96    private boolean complete;
97
98    //TODO: supply a real nonce-count, currently a server will interprete a repeated request as a replay
99    private static final String NC = "00000001"; //nonce-count is always 1
100    private static final int QOP_MISSING = 0;
101    private static final int QOP_AUTH_INT = 1;
102    private static final int QOP_AUTH = 2;
103
104    private int qopVariant = QOP_MISSING;
105    private String cnonce;
106
107    /**
108     * Default constructor for the digest authetication scheme.
109     */
110    public DigestScheme() {
111        super();
112        this.complete = false;
113    }
114
115    /**
116     * Processes the Digest challenge.
117     *
118     * @param header the challenge header
119     *
120     * @throws MalformedChallengeException is thrown if the authentication challenge
121     * is malformed
122     */
123    @Override
124    public void processChallenge(
125            final Header header) throws MalformedChallengeException {
126        super.processChallenge(header);
127
128        if (getParameter("realm") == null) {
129            throw new MalformedChallengeException("missing realm in challange");
130        }
131        if (getParameter("nonce") == null) {
132            throw new MalformedChallengeException("missing nonce in challange");
133        }
134
135        boolean unsupportedQop = false;
136        // qop parsing
137        String qop = getParameter("qop");
138        if (qop != null) {
139            StringTokenizer tok = new StringTokenizer(qop,",");
140            while (tok.hasMoreTokens()) {
141                String variant = tok.nextToken().trim();
142                if (variant.equals("auth")) {
143                    qopVariant = QOP_AUTH;
144                    break; //that's our favourite, because auth-int is unsupported
145                } else if (variant.equals("auth-int")) {
146                    qopVariant = QOP_AUTH_INT;
147                } else {
148                    unsupportedQop = true;
149                }
150            }
151        }
152
153        if (unsupportedQop && (qopVariant == QOP_MISSING)) {
154            throw new MalformedChallengeException("None of the qop methods is supported");
155        }
156        // Reset cnonce
157        this.cnonce = null;
158        this.complete = true;
159    }
160
161    /**
162     * Tests if the Digest authentication process has been completed.
163     *
164     * @return <tt>true</tt> if Digest authorization has been processed,
165     *   <tt>false</tt> otherwise.
166     */
167    public boolean isComplete() {
168        String s = getParameter("stale");
169        if ("true".equalsIgnoreCase(s)) {
170            return false;
171        } else {
172            return this.complete;
173        }
174    }
175
176    /**
177     * Returns textual designation of the digest authentication scheme.
178     *
179     * @return <code>digest</code>
180     */
181    public String getSchemeName() {
182        return "digest";
183    }
184
185    /**
186     * Returns <tt>false</tt>. Digest authentication scheme is request based.
187     *
188     * @return <tt>false</tt>.
189     */
190    public boolean isConnectionBased() {
191        return false;
192    }
193
194    public void overrideParamter(final String name, final String value) {
195        getParameters().put(name, value);
196    }
197
198    private String getCnonce() {
199        if (this.cnonce == null) {
200            this.cnonce = createCnonce();
201        }
202        return this.cnonce;
203    }
204
205    /**
206     * Produces a digest authorization string for the given set of
207     * {@link Credentials}, method name and URI.
208     *
209     * @param credentials A set of credentials to be used for athentication
210     * @param request    The request being authenticated
211     *
212     * @throws org.apache.http.auth.InvalidCredentialsException if authentication credentials
213     *         are not valid or not applicable for this authentication scheme
214     * @throws AuthenticationException if authorization string cannot
215     *   be generated due to an authentication failure
216     *
217     * @return a digest authorization string
218     */
219    public Header authenticate(
220            final Credentials credentials,
221            final HttpRequest request) throws AuthenticationException {
222
223        if (credentials == null) {
224            throw new IllegalArgumentException("Credentials may not be null");
225        }
226        if (request == null) {
227            throw new IllegalArgumentException("HTTP request may not be null");
228        }
229
230        // Add method name and request-URI to the parameter map
231        getParameters().put("methodname", request.getRequestLine().getMethod());
232        getParameters().put("uri", request.getRequestLine().getUri());
233        String charset = getParameter("charset");
234        if (charset == null) {
235            charset = AuthParams.getCredentialCharset(request.getParams());
236            getParameters().put("charset", charset);
237        }
238        String digest = createDigest(credentials);
239        return createDigestHeader(credentials, digest);
240    }
241
242    private static MessageDigest createMessageDigest(
243            final String digAlg) throws UnsupportedDigestAlgorithmException {
244        try {
245            return MessageDigest.getInstance(digAlg);
246        } catch (Exception e) {
247            throw new UnsupportedDigestAlgorithmException(
248              "Unsupported algorithm in HTTP Digest authentication: "
249               + digAlg);
250        }
251    }
252
253    /**
254     * Creates an MD5 response digest.
255     *
256     * @return The created digest as string. This will be the response tag's
257     *         value in the Authentication HTTP header.
258     * @throws AuthenticationException when MD5 is an unsupported algorithm
259     */
260    private String createDigest(final Credentials credentials) throws AuthenticationException {
261        // Collecting required tokens
262        String uri = getParameter("uri");
263        String realm = getParameter("realm");
264        String nonce = getParameter("nonce");
265        String method = getParameter("methodname");
266        String algorithm = getParameter("algorithm");
267        if (uri == null) {
268            throw new IllegalStateException("URI may not be null");
269        }
270        if (realm == null) {
271            throw new IllegalStateException("Realm may not be null");
272        }
273        if (nonce == null) {
274            throw new IllegalStateException("Nonce may not be null");
275        }
276        // If an algorithm is not specified, default to MD5.
277        if (algorithm == null) {
278            algorithm = "MD5";
279        }
280        // If an charset is not specified, default to ISO-8859-1.
281        String charset = getParameter("charset");
282        if (charset == null) {
283            charset = "ISO-8859-1";
284        }
285
286        if (qopVariant == QOP_AUTH_INT) {
287            throw new AuthenticationException(
288                "Unsupported qop in HTTP Digest authentication");
289        }
290
291        MessageDigest md5Helper = createMessageDigest("MD5");
292
293        String uname = credentials.getUserPrincipal().getName();
294        String pwd = credentials.getPassword();
295
296        // 3.2.2.2: Calculating digest
297        StringBuilder tmp = new StringBuilder(uname.length() + realm.length() + pwd.length() + 2);
298        tmp.append(uname);
299        tmp.append(':');
300        tmp.append(realm);
301        tmp.append(':');
302        tmp.append(pwd);
303        // unq(username-value) ":" unq(realm-value) ":" passwd
304        String a1 = tmp.toString();
305
306        //a1 is suitable for MD5 algorithm
307        if(algorithm.equals("MD5-sess")) {
308            // H( unq(username-value) ":" unq(realm-value) ":" passwd )
309            //      ":" unq(nonce-value)
310            //      ":" unq(cnonce-value)
311
312            String cnonce = getCnonce();
313
314            String tmp2=encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset)));
315            StringBuilder tmp3 = new StringBuilder(tmp2.length() + nonce.length() + cnonce.length() + 2);
316            tmp3.append(tmp2);
317            tmp3.append(':');
318            tmp3.append(nonce);
319            tmp3.append(':');
320            tmp3.append(cnonce);
321            a1 = tmp3.toString();
322        } else if (!algorithm.equals("MD5")) {
323            throw new AuthenticationException("Unhandled algorithm " + algorithm + " requested");
324        }
325        String md5a1 = encode(md5Helper.digest(EncodingUtils.getBytes(a1, charset)));
326
327        String a2 = null;
328        if (qopVariant == QOP_AUTH_INT) {
329            // Unhandled qop auth-int
330            //we do not have access to the entity-body or its hash
331            //TODO: add Method ":" digest-uri-value ":" H(entity-body)
332        } else {
333            a2 = method + ':' + uri;
334        }
335        String md5a2 = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(a2)));
336
337        // 3.2.2.1
338        String serverDigestValue;
339        if (qopVariant == QOP_MISSING) {
340            StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length() + md5a2.length());
341            tmp2.append(md5a1);
342            tmp2.append(':');
343            tmp2.append(nonce);
344            tmp2.append(':');
345            tmp2.append(md5a2);
346            serverDigestValue = tmp2.toString();
347        } else {
348            String qopOption = getQopVariantString();
349            String cnonce = getCnonce();
350
351            StringBuilder tmp2 = new StringBuilder(md5a1.length() + nonce.length()
352                + NC.length() + cnonce.length() + qopOption.length() + md5a2.length() + 5);
353            tmp2.append(md5a1);
354            tmp2.append(':');
355            tmp2.append(nonce);
356            tmp2.append(':');
357            tmp2.append(NC);
358            tmp2.append(':');
359            tmp2.append(cnonce);
360            tmp2.append(':');
361            tmp2.append(qopOption);
362            tmp2.append(':');
363            tmp2.append(md5a2);
364            serverDigestValue = tmp2.toString();
365        }
366
367        String serverDigest =
368            encode(md5Helper.digest(EncodingUtils.getAsciiBytes(serverDigestValue)));
369
370        return serverDigest;
371    }
372
373    /**
374     * Creates digest-response header as defined in RFC2617.
375     *
376     * @param credentials User credentials
377     * @param digest The response tag's value as String.
378     *
379     * @return The digest-response as String.
380     */
381    private Header createDigestHeader(
382            final Credentials credentials,
383            final String digest) throws AuthenticationException {
384
385        CharArrayBuffer buffer = new CharArrayBuffer(128);
386        if (isProxy()) {
387            buffer.append(AUTH.PROXY_AUTH_RESP);
388        } else {
389            buffer.append(AUTH.WWW_AUTH_RESP);
390        }
391        buffer.append(": Digest ");
392
393        String uri = getParameter("uri");
394        String realm = getParameter("realm");
395        String nonce = getParameter("nonce");
396        String opaque = getParameter("opaque");
397        String response = digest;
398        String algorithm = getParameter("algorithm");
399
400        String uname = credentials.getUserPrincipal().getName();
401
402        List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20);
403        params.add(new BasicNameValuePair("username", uname));
404        params.add(new BasicNameValuePair("realm", realm));
405        params.add(new BasicNameValuePair("nonce", nonce));
406        params.add(new BasicNameValuePair("uri", uri));
407        params.add(new BasicNameValuePair("response", response));
408
409        if (qopVariant != QOP_MISSING) {
410            params.add(new BasicNameValuePair("qop", getQopVariantString()));
411            params.add(new BasicNameValuePair("nc", NC));
412            params.add(new BasicNameValuePair("cnonce", getCnonce()));
413        }
414        if (algorithm != null) {
415            params.add(new BasicNameValuePair("algorithm", algorithm));
416        }
417        if (opaque != null) {
418            params.add(new BasicNameValuePair("opaque", opaque));
419        }
420
421        for (int i = 0; i < params.size(); i++) {
422            BasicNameValuePair param = params.get(i);
423            if (i > 0) {
424                buffer.append(", ");
425            }
426            boolean noQuotes = "nc".equals(param.getName()) ||
427                               "qop".equals(param.getName());
428            BasicHeaderValueFormatter.DEFAULT
429                .formatNameValuePair(buffer, param, !noQuotes);
430        }
431        return new BufferedHeader(buffer);
432    }
433
434    private String getQopVariantString() {
435        String qopOption;
436        if (qopVariant == QOP_AUTH_INT) {
437            qopOption = "auth-int";
438        } else {
439            qopOption = "auth";
440        }
441        return qopOption;
442    }
443
444    /**
445     * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long
446     * <CODE>String</CODE> according to RFC 2617.
447     *
448     * @param binaryData array containing the digest
449     * @return encoded MD5, or <CODE>null</CODE> if encoding failed
450     */
451    private static String encode(byte[] binaryData) {
452        if (binaryData.length != 16) {
453            return null;
454        }
455
456        char[] buffer = new char[32];
457        for (int i = 0; i < 16; i++) {
458            int low = (binaryData[i] & 0x0f);
459            int high = ((binaryData[i] & 0xf0) >> 4);
460            buffer[i * 2] = HEXADECIMAL[high];
461            buffer[(i * 2) + 1] = HEXADECIMAL[low];
462        }
463
464        return new String(buffer);
465    }
466
467
468    /**
469     * Creates a random cnonce value based on the current time.
470     *
471     * @return The cnonce value as String.
472     * @throws UnsupportedDigestAlgorithmException if MD5 algorithm is not supported.
473     */
474    public static String createCnonce() {
475        String cnonce;
476
477        MessageDigest md5Helper = createMessageDigest("MD5");
478
479        cnonce = Long.toString(System.currentTimeMillis());
480        cnonce = encode(md5Helper.digest(EncodingUtils.getAsciiBytes(cnonce)));
481
482        return cnonce;
483    }
484}
485