001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027package org.apache.http.impl.auth;
028
029import java.io.IOException;
030import java.nio.charset.Charset;
031import java.security.MessageDigest;
032import java.security.SecureRandom;
033import java.util.ArrayList;
034import java.util.Formatter;
035import java.util.HashSet;
036import java.util.List;
037import java.util.Locale;
038import java.util.Set;
039import java.util.StringTokenizer;
040
041import org.apache.http.Consts;
042import org.apache.http.Header;
043import org.apache.http.HttpEntity;
044import org.apache.http.HttpEntityEnclosingRequest;
045import org.apache.http.HttpRequest;
046import org.apache.http.auth.AUTH;
047import org.apache.http.auth.AuthenticationException;
048import org.apache.http.auth.ChallengeState;
049import org.apache.http.auth.Credentials;
050import org.apache.http.auth.MalformedChallengeException;
051import org.apache.http.message.BasicHeaderValueFormatter;
052import org.apache.http.message.BasicNameValuePair;
053import org.apache.http.message.BufferedHeader;
054import org.apache.http.protocol.BasicHttpContext;
055import org.apache.http.protocol.HttpContext;
056import org.apache.http.util.Args;
057import org.apache.http.util.CharArrayBuffer;
058import org.apache.http.util.EncodingUtils;
059
060/**
061 * Digest authentication scheme as defined in RFC 2617.
062 * Both MD5 (default) and MD5-sess are supported.
063 * Currently only qop=auth or no qop is supported. qop=auth-int
064 * is unsupported. If auth and auth-int are provided, auth is
065 * used.
066 * <p>
067 * Since the digest username is included as clear text in the generated
068 * Authentication header, the charset of the username must be compatible
069 * with the HTTP element charset used by the connection.
070 * </p>
071 *
072 * @since 4.0
073 */
074public class DigestScheme extends RFC2617Scheme {
075
076    private static final long serialVersionUID = 3883908186234566916L;
077
078    /**
079     * Hexa values used when creating 32 character long digest in HTTP DigestScheme
080     * in case of authentication.
081     *
082     * @see #encode(byte[])
083     */
084    private static final char[] HEXADECIMAL = {
085        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
086        'e', 'f'
087    };
088
089    /** Whether the digest authentication process is complete */
090    private boolean complete;
091
092    private static final int QOP_UNKNOWN = -1;
093    private static final int QOP_MISSING = 0;
094    private static final int QOP_AUTH_INT = 1;
095    private static final int QOP_AUTH = 2;
096
097    private String lastNonce;
098    private long nounceCount;
099    private String cnonce;
100    private String a1;
101    private String a2;
102
103    /**
104     * @since 4.3
105     */
106    public DigestScheme(final Charset credentialsCharset) {
107        super(credentialsCharset);
108        this.complete = false;
109    }
110
111    /**
112     * Creates an instance of {@code DigestScheme} with the given challenge
113     * state.
114     *
115     * @since 4.2
116     *
117     * @deprecated (4.3) do not use.
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     * Processes the Digest challenge.
130     *
131     * @param header the challenge header
132     *
133     * @throws MalformedChallengeException is thrown if the authentication challenge
134     * is malformed
135     */
136    @Override
137    public void processChallenge(
138            final Header header) throws MalformedChallengeException {
139        super.processChallenge(header);
140        this.complete = true;
141        if (getParameters().isEmpty()) {
142            throw new MalformedChallengeException("Authentication challenge is empty");
143        }
144    }
145
146    /**
147     * Tests if the Digest authentication process has been completed.
148     *
149     * @return {@code true} if Digest authorization has been processed,
150     *   {@code false} otherwise.
151     */
152    @Override
153    public boolean isComplete() {
154        final String s = getParameter("stale");
155        if ("true".equalsIgnoreCase(s)) {
156            return false;
157        } else {
158            return this.complete;
159        }
160    }
161
162    /**
163     * Returns textual designation of the digest authentication scheme.
164     *
165     * @return {@code digest}
166     */
167    @Override
168    public String getSchemeName() {
169        return "digest";
170    }
171
172    /**
173     * Returns {@code false}. Digest authentication scheme is request based.
174     *
175     * @return {@code false}.
176     */
177    @Override
178    public boolean isConnectionBased() {
179        return false;
180    }
181
182    public void overrideParamter(final String name, final String value) {
183        getParameters().put(name, value);
184    }
185
186    /**
187     * @deprecated (4.2) Use {@link org.apache.http.auth.ContextAwareAuthScheme#authenticate(
188     *   Credentials, HttpRequest, org.apache.http.protocol.HttpContext)}
189     */
190    @Override
191    @Deprecated
192    public Header authenticate(
193            final Credentials credentials, final HttpRequest request) throws AuthenticationException {
194        return authenticate(credentials, request, new BasicHttpContext());
195    }
196
197    /**
198     * Produces a digest authorization string for the given set of
199     * {@link Credentials}, method name and URI.
200     *
201     * @param credentials A set of credentials to be used for athentication
202     * @param request    The request being authenticated
203     *
204     * @throws org.apache.http.auth.InvalidCredentialsException if authentication credentials
205     *         are not valid or not applicable for this authentication scheme
206     * @throws AuthenticationException if authorization string cannot
207     *   be generated due to an authentication failure
208     *
209     * @return a digest authorization string
210     */
211    @Override
212    public Header authenticate(
213            final Credentials credentials,
214            final HttpRequest request,
215            final HttpContext context) throws AuthenticationException {
216
217        Args.notNull(credentials, "Credentials");
218        Args.notNull(request, "HTTP request");
219        if (getParameter("realm") == null) {
220            throw new AuthenticationException("missing realm in challenge");
221        }
222        if (getParameter("nonce") == null) {
223            throw new AuthenticationException("missing nonce in challenge");
224        }
225        // Add method name and request-URI to the parameter map
226        getParameters().put("methodname", request.getRequestLine().getMethod());
227        getParameters().put("uri", request.getRequestLine().getUri());
228        final String charset = getParameter("charset");
229        if (charset == null) {
230            getParameters().put("charset", getCredentialsCharset(request));
231        }
232        return createDigestHeader(credentials, request);
233    }
234
235    private static MessageDigest createMessageDigest(
236            final String digAlg) throws UnsupportedDigestAlgorithmException {
237        try {
238            return MessageDigest.getInstance(digAlg);
239        } catch (final Exception e) {
240            throw new UnsupportedDigestAlgorithmException(
241              "Unsupported algorithm in HTTP Digest authentication: "
242               + digAlg);
243        }
244    }
245
246    /**
247     * Creates digest-response header as defined in RFC2617.
248     *
249     * @param credentials User credentials
250     *
251     * @return The digest-response as String.
252     */
253    private Header createDigestHeader(
254            final Credentials credentials,
255            final HttpRequest request) throws AuthenticationException {
256        final String uri = getParameter("uri");
257        final String realm = getParameter("realm");
258        final String nonce = getParameter("nonce");
259        final String opaque = getParameter("opaque");
260        final String method = getParameter("methodname");
261        String algorithm = getParameter("algorithm");
262        // If an algorithm is not specified, default to MD5.
263        if (algorithm == null) {
264            algorithm = "MD5";
265        }
266
267        final Set<String> qopset = new HashSet<String>(8);
268        int qop = QOP_UNKNOWN;
269        final String qoplist = getParameter("qop");
270        if (qoplist != null) {
271            final StringTokenizer tok = new StringTokenizer(qoplist, ",");
272            while (tok.hasMoreTokens()) {
273                final String variant = tok.nextToken().trim();
274                qopset.add(variant.toLowerCase(Locale.ROOT));
275            }
276            if (request instanceof HttpEntityEnclosingRequest && qopset.contains("auth-int")) {
277                qop = QOP_AUTH_INT;
278            } else if (qopset.contains("auth")) {
279                qop = QOP_AUTH;
280            }
281        } else {
282            qop = QOP_MISSING;
283        }
284
285        if (qop == QOP_UNKNOWN) {
286            throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
287        }
288
289        String charset = getParameter("charset");
290        if (charset == null) {
291            charset = "ISO-8859-1";
292        }
293
294        String digAlg = algorithm;
295        if (digAlg.equalsIgnoreCase("MD5-sess")) {
296            digAlg = "MD5";
297        }
298
299        final MessageDigest digester;
300        try {
301            digester = createMessageDigest(digAlg);
302        } catch (final UnsupportedDigestAlgorithmException ex) {
303            throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg);
304        }
305
306        final String uname = credentials.getUserPrincipal().getName();
307        final String pwd = credentials.getPassword();
308
309        if (nonce.equals(this.lastNonce)) {
310            nounceCount++;
311        } else {
312            nounceCount = 1;
313            cnonce = null;
314            lastNonce = nonce;
315        }
316        final StringBuilder sb = new StringBuilder(256);
317        final Formatter formatter = new Formatter(sb, Locale.US);
318        formatter.format("%08x", Long.valueOf(nounceCount));
319        formatter.close();
320        final String nc = sb.toString();
321
322        if (cnonce == null) {
323            cnonce = createCnonce();
324        }
325
326        a1 = null;
327        a2 = null;
328        // 3.2.2.2: Calculating digest
329        if (algorithm.equalsIgnoreCase("MD5-sess")) {
330            // H( unq(username-value) ":" unq(realm-value) ":" passwd )
331            //      ":" unq(nonce-value)
332            //      ":" unq(cnonce-value)
333
334            // calculated one per session
335            sb.setLength(0);
336            sb.append(uname).append(':').append(realm).append(':').append(pwd);
337            final String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset)));
338            sb.setLength(0);
339            sb.append(checksum).append(':').append(nonce).append(':').append(cnonce);
340            a1 = sb.toString();
341        } else {
342            // unq(username-value) ":" unq(realm-value) ":" passwd
343            sb.setLength(0);
344            sb.append(uname).append(':').append(realm).append(':').append(pwd);
345            a1 = sb.toString();
346        }
347
348        final String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset)));
349
350        if (qop == QOP_AUTH) {
351            // Method ":" digest-uri-value
352            a2 = method + ':' + uri;
353        } else if (qop == QOP_AUTH_INT) {
354            // Method ":" digest-uri-value ":" H(entity-body)
355            HttpEntity entity = null;
356            if (request instanceof HttpEntityEnclosingRequest) {
357                entity = ((HttpEntityEnclosingRequest) request).getEntity();
358            }
359            if (entity != null && !entity.isRepeatable()) {
360                // If the entity is not repeatable, try falling back onto QOP_AUTH
361                if (qopset.contains("auth")) {
362                    qop = QOP_AUTH;
363                    a2 = method + ':' + uri;
364                } else {
365                    throw new AuthenticationException("Qop auth-int cannot be used with " +
366                            "a non-repeatable entity");
367                }
368            } else {
369                final HttpEntityDigester entityDigester = new HttpEntityDigester(digester);
370                try {
371                    if (entity != null) {
372                        entity.writeTo(entityDigester);
373                    }
374                    entityDigester.close();
375                } catch (final IOException ex) {
376                    throw new AuthenticationException("I/O error reading entity content", ex);
377                }
378                a2 = method + ':' + uri + ':' + encode(entityDigester.getDigest());
379            }
380        } else {
381            a2 = method + ':' + uri;
382        }
383
384        final String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset)));
385
386        // 3.2.2.1
387
388        final String digestValue;
389        if (qop == QOP_MISSING) {
390            sb.setLength(0);
391            sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2);
392            digestValue = sb.toString();
393        } else {
394            sb.setLength(0);
395            sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':')
396                .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth")
397                .append(':').append(hasha2);
398            digestValue = sb.toString();
399        }
400
401        final String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue)));
402
403        final CharArrayBuffer buffer = new CharArrayBuffer(128);
404        if (isProxy()) {
405            buffer.append(AUTH.PROXY_AUTH_RESP);
406        } else {
407            buffer.append(AUTH.WWW_AUTH_RESP);
408        }
409        buffer.append(": Digest ");
410
411        final List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20);
412        params.add(new BasicNameValuePair("username", uname));
413        params.add(new BasicNameValuePair("realm", realm));
414        params.add(new BasicNameValuePair("nonce", nonce));
415        params.add(new BasicNameValuePair("uri", uri));
416        params.add(new BasicNameValuePair("response", digest));
417
418        if (qop != QOP_MISSING) {
419            params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth"));
420            params.add(new BasicNameValuePair("nc", nc));
421            params.add(new BasicNameValuePair("cnonce", cnonce));
422        }
423        // algorithm cannot be null here
424        params.add(new BasicNameValuePair("algorithm", algorithm));
425        if (opaque != null) {
426            params.add(new BasicNameValuePair("opaque", opaque));
427        }
428
429        for (int i = 0; i < params.size(); i++) {
430            final BasicNameValuePair param = params.get(i);
431            if (i > 0) {
432                buffer.append(", ");
433            }
434            final String name = param.getName();
435            final boolean noQuotes = ("nc".equals(name) || "qop".equals(name)
436                    || "algorithm".equals(name));
437            BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(buffer, param, !noQuotes);
438        }
439        return new BufferedHeader(buffer);
440    }
441
442    String getCnonce() {
443        return cnonce;
444    }
445
446    String getA1() {
447        return a1;
448    }
449
450    String getA2() {
451        return a2;
452    }
453
454    /**
455     * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long
456     * <CODE>String</CODE> according to RFC 2617.
457     *
458     * @param binaryData array containing the digest
459     * @return encoded MD5, or <CODE>null</CODE> if encoding failed
460     */
461    static String encode(final byte[] binaryData) {
462        final int n = binaryData.length;
463        final char[] buffer = new char[n * 2];
464        for (int i = 0; i < n; i++) {
465            final int low = (binaryData[i] & 0x0f);
466            final int high = ((binaryData[i] & 0xf0) >> 4);
467            buffer[i * 2] = HEXADECIMAL[high];
468            buffer[(i * 2) + 1] = HEXADECIMAL[low];
469        }
470
471        return new String(buffer);
472    }
473
474
475    /**
476     * Creates a random cnonce value based on the current time.
477     *
478     * @return The cnonce value as String.
479     */
480    public static String createCnonce() {
481        final SecureRandom rnd = new SecureRandom();
482        final byte[] tmp = new byte[8];
483        rnd.nextBytes(tmp);
484        return encode(tmp);
485    }
486
487    @Override
488    public String toString() {
489        final StringBuilder builder = new StringBuilder();
490        builder.append("DIGEST [complete=").append(complete)
491                .append(", nonce=").append(lastNonce)
492                .append(", nc=").append(nounceCount)
493                .append("]");
494        return builder.toString();
495    }
496
497}