mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestScheme.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/impl/auth/DigestScheme.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,442 @@
     1.4 +/*
     1.5 + * ====================================================================
     1.6 + *
     1.7 + *  Licensed to the Apache Software Foundation (ASF) under one or more
     1.8 + *  contributor license agreements.  See the NOTICE file distributed with
     1.9 + *  this work for additional information regarding copyright ownership.
    1.10 + *  The ASF licenses this file to You under the Apache License, Version 2.0
    1.11 + *  (the "License"); you may not use this file except in compliance with
    1.12 + *  the License.  You may obtain a copy of the License at
    1.13 + *
    1.14 + *      http://www.apache.org/licenses/LICENSE-2.0
    1.15 + *
    1.16 + *  Unless required by applicable law or agreed to in writing, software
    1.17 + *  distributed under the License is distributed on an "AS IS" BASIS,
    1.18 + *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    1.19 + *  See the License for the specific language governing permissions and
    1.20 + *  limitations under the License.
    1.21 + * ====================================================================
    1.22 + *
    1.23 + * This software consists of voluntary contributions made by many
    1.24 + * individuals on behalf of the Apache Software Foundation.  For more
    1.25 + * information on the Apache Software Foundation, please see
    1.26 + * <http://www.apache.org/>.
    1.27 + *
    1.28 + */
    1.29 +
    1.30 +package ch.boye.httpclientandroidlib.impl.auth;
    1.31 +
    1.32 +import java.security.MessageDigest;
    1.33 +import java.security.SecureRandom;
    1.34 +import java.util.ArrayList;
    1.35 +import java.util.Formatter;
    1.36 +import java.util.List;
    1.37 +import java.util.Locale;
    1.38 +import java.util.StringTokenizer;
    1.39 +
    1.40 +import ch.boye.httpclientandroidlib.annotation.NotThreadSafe;
    1.41 +
    1.42 +import ch.boye.httpclientandroidlib.Header;
    1.43 +import ch.boye.httpclientandroidlib.HttpRequest;
    1.44 +import ch.boye.httpclientandroidlib.auth.AuthenticationException;
    1.45 +import ch.boye.httpclientandroidlib.auth.Credentials;
    1.46 +import ch.boye.httpclientandroidlib.auth.AUTH;
    1.47 +import ch.boye.httpclientandroidlib.auth.MalformedChallengeException;
    1.48 +import ch.boye.httpclientandroidlib.auth.params.AuthParams;
    1.49 +import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
    1.50 +import ch.boye.httpclientandroidlib.message.BasicHeaderValueFormatter;
    1.51 +import ch.boye.httpclientandroidlib.message.BufferedHeader;
    1.52 +import ch.boye.httpclientandroidlib.util.CharArrayBuffer;
    1.53 +import ch.boye.httpclientandroidlib.util.EncodingUtils;
    1.54 +
    1.55 +/**
    1.56 + * Digest authentication scheme as defined in RFC 2617.
    1.57 + * Both MD5 (default) and MD5-sess are supported.
    1.58 + * Currently only qop=auth or no qop is supported. qop=auth-int
    1.59 + * is unsupported. If auth and auth-int are provided, auth is
    1.60 + * used.
    1.61 + * <p>
    1.62 + * Credential charset is configured via the
    1.63 + * {@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET}
    1.64 + * parameter of the HTTP request.
    1.65 + * <p>
    1.66 + * Since the digest username is included as clear text in the generated
    1.67 + * Authentication header, the charset of the username must be compatible
    1.68 + * with the
    1.69 + * {@link ch.boye.httpclientandroidlib.params.CoreProtocolPNames#HTTP_ELEMENT_CHARSET
    1.70 + *        http element charset}.
    1.71 + * <p>
    1.72 + * The following parameters can be used to customize the behavior of this
    1.73 + * class:
    1.74 + * <ul>
    1.75 + *  <li>{@link ch.boye.httpclientandroidlib.auth.params.AuthPNames#CREDENTIAL_CHARSET}</li>
    1.76 + * </ul>
    1.77 + *
    1.78 + * @since 4.0
    1.79 + */
    1.80 +@NotThreadSafe
    1.81 +public class DigestScheme extends RFC2617Scheme {
    1.82 +
    1.83 +    /**
    1.84 +     * Hexa values used when creating 32 character long digest in HTTP DigestScheme
    1.85 +     * in case of authentication.
    1.86 +     *
    1.87 +     * @see #encode(byte[])
    1.88 +     */
    1.89 +    private static final char[] HEXADECIMAL = {
    1.90 +        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
    1.91 +        'e', 'f'
    1.92 +    };
    1.93 +
    1.94 +    /** Whether the digest authentication process is complete */
    1.95 +    private boolean complete;
    1.96 +
    1.97 +    private static final int QOP_UNKNOWN = -1;
    1.98 +    private static final int QOP_MISSING = 0;
    1.99 +    private static final int QOP_AUTH_INT = 1;
   1.100 +    private static final int QOP_AUTH = 2;
   1.101 +
   1.102 +    private String lastNonce;
   1.103 +    private long nounceCount;
   1.104 +    private String cnonce;
   1.105 +    private String a1;
   1.106 +    private String a2;
   1.107 +
   1.108 +    /**
   1.109 +     * Default constructor for the digest authetication scheme.
   1.110 +     */
   1.111 +    public DigestScheme() {
   1.112 +        super();
   1.113 +        this.complete = false;
   1.114 +    }
   1.115 +
   1.116 +    /**
   1.117 +     * Processes the Digest challenge.
   1.118 +     *
   1.119 +     * @param header the challenge header
   1.120 +     *
   1.121 +     * @throws MalformedChallengeException is thrown if the authentication challenge
   1.122 +     * is malformed
   1.123 +     */
   1.124 +    @Override
   1.125 +    public void processChallenge(
   1.126 +            final Header header) throws MalformedChallengeException {
   1.127 +        super.processChallenge(header);
   1.128 +
   1.129 +        if (getParameter("realm") == null) {
   1.130 +            throw new MalformedChallengeException("missing realm in challenge");
   1.131 +        }
   1.132 +        if (getParameter("nonce") == null) {
   1.133 +            throw new MalformedChallengeException("missing nonce in challenge");
   1.134 +        }
   1.135 +        this.complete = true;
   1.136 +    }
   1.137 +
   1.138 +    /**
   1.139 +     * Tests if the Digest authentication process has been completed.
   1.140 +     *
   1.141 +     * @return <tt>true</tt> if Digest authorization has been processed,
   1.142 +     *   <tt>false</tt> otherwise.
   1.143 +     */
   1.144 +    public boolean isComplete() {
   1.145 +        String s = getParameter("stale");
   1.146 +        if ("true".equalsIgnoreCase(s)) {
   1.147 +            return false;
   1.148 +        } else {
   1.149 +            return this.complete;
   1.150 +        }
   1.151 +    }
   1.152 +
   1.153 +    /**
   1.154 +     * Returns textual designation of the digest authentication scheme.
   1.155 +     *
   1.156 +     * @return <code>digest</code>
   1.157 +     */
   1.158 +    public String getSchemeName() {
   1.159 +        return "digest";
   1.160 +    }
   1.161 +
   1.162 +    /**
   1.163 +     * Returns <tt>false</tt>. Digest authentication scheme is request based.
   1.164 +     *
   1.165 +     * @return <tt>false</tt>.
   1.166 +     */
   1.167 +    public boolean isConnectionBased() {
   1.168 +        return false;
   1.169 +    }
   1.170 +
   1.171 +    public void overrideParamter(final String name, final String value) {
   1.172 +        getParameters().put(name, value);
   1.173 +    }
   1.174 +
   1.175 +    /**
   1.176 +     * Produces a digest authorization string for the given set of
   1.177 +     * {@link Credentials}, method name and URI.
   1.178 +     *
   1.179 +     * @param credentials A set of credentials to be used for athentication
   1.180 +     * @param request    The request being authenticated
   1.181 +     *
   1.182 +     * @throws ch.boye.httpclientandroidlib.auth.InvalidCredentialsException if authentication credentials
   1.183 +     *         are not valid or not applicable for this authentication scheme
   1.184 +     * @throws AuthenticationException if authorization string cannot
   1.185 +     *   be generated due to an authentication failure
   1.186 +     *
   1.187 +     * @return a digest authorization string
   1.188 +     */
   1.189 +    public Header authenticate(
   1.190 +            final Credentials credentials,
   1.191 +            final HttpRequest request) throws AuthenticationException {
   1.192 +
   1.193 +        if (credentials == null) {
   1.194 +            throw new IllegalArgumentException("Credentials may not be null");
   1.195 +        }
   1.196 +        if (request == null) {
   1.197 +            throw new IllegalArgumentException("HTTP request may not be null");
   1.198 +        }
   1.199 +
   1.200 +        // Add method name and request-URI to the parameter map
   1.201 +        getParameters().put("methodname", request.getRequestLine().getMethod());
   1.202 +        getParameters().put("uri", request.getRequestLine().getUri());
   1.203 +        String charset = getParameter("charset");
   1.204 +        if (charset == null) {
   1.205 +            charset = AuthParams.getCredentialCharset(request.getParams());
   1.206 +            getParameters().put("charset", charset);
   1.207 +        }
   1.208 +        return createDigestHeader(credentials);
   1.209 +    }
   1.210 +
   1.211 +    private static MessageDigest createMessageDigest(
   1.212 +            final String digAlg) throws UnsupportedDigestAlgorithmException {
   1.213 +        try {
   1.214 +            return MessageDigest.getInstance(digAlg);
   1.215 +        } catch (Exception e) {
   1.216 +            throw new UnsupportedDigestAlgorithmException(
   1.217 +              "Unsupported algorithm in HTTP Digest authentication: "
   1.218 +               + digAlg);
   1.219 +        }
   1.220 +    }
   1.221 +
   1.222 +    /**
   1.223 +     * Creates digest-response header as defined in RFC2617.
   1.224 +     *
   1.225 +     * @param credentials User credentials
   1.226 +     *
   1.227 +     * @return The digest-response as String.
   1.228 +     */
   1.229 +    private Header createDigestHeader(
   1.230 +            final Credentials credentials) throws AuthenticationException {
   1.231 +        String uri = getParameter("uri");
   1.232 +        String realm = getParameter("realm");
   1.233 +        String nonce = getParameter("nonce");
   1.234 +        String opaque = getParameter("opaque");
   1.235 +        String method = getParameter("methodname");
   1.236 +        String algorithm = getParameter("algorithm");
   1.237 +        if (uri == null) {
   1.238 +            throw new IllegalStateException("URI may not be null");
   1.239 +        }
   1.240 +        if (realm == null) {
   1.241 +            throw new IllegalStateException("Realm may not be null");
   1.242 +        }
   1.243 +        if (nonce == null) {
   1.244 +            throw new IllegalStateException("Nonce may not be null");
   1.245 +        }
   1.246 +
   1.247 +        //TODO: add support for QOP_INT
   1.248 +        int qop = QOP_UNKNOWN;
   1.249 +        String qoplist = getParameter("qop");
   1.250 +        if (qoplist != null) {
   1.251 +            StringTokenizer tok = new StringTokenizer(qoplist, ",");
   1.252 +            while (tok.hasMoreTokens()) {
   1.253 +                String variant = tok.nextToken().trim();
   1.254 +                if (variant.equals("auth")) {
   1.255 +                    qop = QOP_AUTH;
   1.256 +                    break;
   1.257 +                }
   1.258 +            }
   1.259 +        } else {
   1.260 +            qop = QOP_MISSING;
   1.261 +        }
   1.262 +
   1.263 +        if (qop == QOP_UNKNOWN) {
   1.264 +            throw new AuthenticationException("None of the qop methods is supported: " + qoplist);
   1.265 +        }
   1.266 +
   1.267 +        // If an algorithm is not specified, default to MD5.
   1.268 +        if (algorithm == null) {
   1.269 +            algorithm = "MD5";
   1.270 +        }
   1.271 +        // If an charset is not specified, default to ISO-8859-1.
   1.272 +        String charset = getParameter("charset");
   1.273 +        if (charset == null) {
   1.274 +            charset = "ISO-8859-1";
   1.275 +        }
   1.276 +
   1.277 +        String digAlg = algorithm;
   1.278 +        if (digAlg.equalsIgnoreCase("MD5-sess")) {
   1.279 +            digAlg = "MD5";
   1.280 +        }
   1.281 +
   1.282 +        MessageDigest digester;
   1.283 +        try {
   1.284 +            digester = createMessageDigest(digAlg);
   1.285 +        } catch (UnsupportedDigestAlgorithmException ex) {
   1.286 +            throw new AuthenticationException("Unsuppported digest algorithm: " + digAlg);
   1.287 +        }
   1.288 +
   1.289 +        String uname = credentials.getUserPrincipal().getName();
   1.290 +        String pwd = credentials.getPassword();
   1.291 +
   1.292 +        if (nonce.equals(this.lastNonce)) {
   1.293 +            nounceCount++;
   1.294 +        } else {
   1.295 +            nounceCount = 1;
   1.296 +            cnonce = null;
   1.297 +            lastNonce = nonce;
   1.298 +        }
   1.299 +        StringBuilder sb = new StringBuilder(256);
   1.300 +        Formatter formatter = new Formatter(sb, Locale.US);
   1.301 +        formatter.format("%08x", nounceCount);
   1.302 +        String nc = sb.toString();
   1.303 +
   1.304 +        if (cnonce == null) {
   1.305 +            cnonce = createCnonce();
   1.306 +        }
   1.307 +
   1.308 +        a1 = null;
   1.309 +        a2 = null;
   1.310 +        // 3.2.2.2: Calculating digest
   1.311 +        if (algorithm.equalsIgnoreCase("MD5-sess")) {
   1.312 +            // H( unq(username-value) ":" unq(realm-value) ":" passwd )
   1.313 +            //      ":" unq(nonce-value)
   1.314 +            //      ":" unq(cnonce-value)
   1.315 +
   1.316 +            // calculated one per session
   1.317 +            sb.setLength(0);
   1.318 +            sb.append(uname).append(':').append(realm).append(':').append(pwd);
   1.319 +            String checksum = encode(digester.digest(EncodingUtils.getBytes(sb.toString(), charset)));
   1.320 +            sb.setLength(0);
   1.321 +            sb.append(checksum).append(':').append(nonce).append(':').append(cnonce);
   1.322 +            a1 = sb.toString();
   1.323 +        } else {
   1.324 +            // unq(username-value) ":" unq(realm-value) ":" passwd
   1.325 +            sb.setLength(0);
   1.326 +            sb.append(uname).append(':').append(realm).append(':').append(pwd);
   1.327 +            a1 = sb.toString();
   1.328 +        }
   1.329 +
   1.330 +        String hasha1 = encode(digester.digest(EncodingUtils.getBytes(a1, charset)));
   1.331 +
   1.332 +        if (qop == QOP_AUTH) {
   1.333 +            // Method ":" digest-uri-value
   1.334 +            a2 = method + ':' + uri;
   1.335 +        } else if (qop == QOP_AUTH_INT) {
   1.336 +            // Method ":" digest-uri-value ":" H(entity-body)
   1.337 +            //TODO: calculate entity hash if entity is repeatable
   1.338 +            throw new AuthenticationException("qop-int method is not suppported");
   1.339 +        } else {
   1.340 +            a2 = method + ':' + uri;
   1.341 +        }
   1.342 +
   1.343 +        String hasha2 = encode(digester.digest(EncodingUtils.getBytes(a2, charset)));
   1.344 +
   1.345 +        // 3.2.2.1
   1.346 +
   1.347 +        String digestValue;
   1.348 +        if (qop == QOP_MISSING) {
   1.349 +            sb.setLength(0);
   1.350 +            sb.append(hasha1).append(':').append(nonce).append(':').append(hasha2);
   1.351 +            digestValue = sb.toString();
   1.352 +        } else {
   1.353 +            sb.setLength(0);
   1.354 +            sb.append(hasha1).append(':').append(nonce).append(':').append(nc).append(':')
   1.355 +                .append(cnonce).append(':').append(qop == QOP_AUTH_INT ? "auth-int" : "auth")
   1.356 +                .append(':').append(hasha2);
   1.357 +            digestValue = sb.toString();
   1.358 +        }
   1.359 +
   1.360 +        String digest = encode(digester.digest(EncodingUtils.getAsciiBytes(digestValue)));
   1.361 +
   1.362 +        CharArrayBuffer buffer = new CharArrayBuffer(128);
   1.363 +        if (isProxy()) {
   1.364 +            buffer.append(AUTH.PROXY_AUTH_RESP);
   1.365 +        } else {
   1.366 +            buffer.append(AUTH.WWW_AUTH_RESP);
   1.367 +        }
   1.368 +        buffer.append(": Digest ");
   1.369 +
   1.370 +        List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>(20);
   1.371 +        params.add(new BasicNameValuePair("username", uname));
   1.372 +        params.add(new BasicNameValuePair("realm", realm));
   1.373 +        params.add(new BasicNameValuePair("nonce", nonce));
   1.374 +        params.add(new BasicNameValuePair("uri", uri));
   1.375 +        params.add(new BasicNameValuePair("response", digest));
   1.376 +
   1.377 +        if (qop != QOP_MISSING) {
   1.378 +            params.add(new BasicNameValuePair("qop", qop == QOP_AUTH_INT ? "auth-int" : "auth"));
   1.379 +            params.add(new BasicNameValuePair("nc", nc));
   1.380 +            params.add(new BasicNameValuePair("cnonce", cnonce));
   1.381 +        }
   1.382 +        if (algorithm != null) {
   1.383 +            params.add(new BasicNameValuePair("algorithm", algorithm));
   1.384 +        }
   1.385 +        if (opaque != null) {
   1.386 +            params.add(new BasicNameValuePair("opaque", opaque));
   1.387 +        }
   1.388 +
   1.389 +        for (int i = 0; i < params.size(); i++) {
   1.390 +            BasicNameValuePair param = params.get(i);
   1.391 +            if (i > 0) {
   1.392 +                buffer.append(", ");
   1.393 +            }
   1.394 +            boolean noQuotes = "nc".equals(param.getName()) || "qop".equals(param.getName());
   1.395 +            BasicHeaderValueFormatter.DEFAULT.formatNameValuePair(buffer, param, !noQuotes);
   1.396 +        }
   1.397 +        return new BufferedHeader(buffer);
   1.398 +    }
   1.399 +
   1.400 +    String getCnonce() {
   1.401 +        return cnonce;
   1.402 +    }
   1.403 +
   1.404 +    String getA1() {
   1.405 +        return a1;
   1.406 +    }
   1.407 +
   1.408 +    String getA2() {
   1.409 +        return a2;
   1.410 +    }
   1.411 +
   1.412 +    /**
   1.413 +     * Encodes the 128 bit (16 bytes) MD5 digest into a 32 characters long
   1.414 +     * <CODE>String</CODE> according to RFC 2617.
   1.415 +     *
   1.416 +     * @param binaryData array containing the digest
   1.417 +     * @return encoded MD5, or <CODE>null</CODE> if encoding failed
   1.418 +     */
   1.419 +    private static String encode(byte[] binaryData) {
   1.420 +        int n = binaryData.length;
   1.421 +        char[] buffer = new char[n * 2];
   1.422 +        for (int i = 0; i < n; i++) {
   1.423 +            int low = (binaryData[i] & 0x0f);
   1.424 +            int high = ((binaryData[i] & 0xf0) >> 4);
   1.425 +            buffer[i * 2] = HEXADECIMAL[high];
   1.426 +            buffer[(i * 2) + 1] = HEXADECIMAL[low];
   1.427 +        }
   1.428 +
   1.429 +        return new String(buffer);
   1.430 +    }
   1.431 +
   1.432 +
   1.433 +    /**
   1.434 +     * Creates a random cnonce value based on the current time.
   1.435 +     *
   1.436 +     * @return The cnonce value as String.
   1.437 +     */
   1.438 +    public static String createCnonce() {
   1.439 +        SecureRandom rnd = new SecureRandom();
   1.440 +        byte[] tmp = new byte[8];
   1.441 +        rnd.nextBytes(tmp);
   1.442 +        return encode(tmp);
   1.443 +    }
   1.444 +
   1.445 +}

mercurial