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

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

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 }

mercurial