Home | History | Annotate | Download | only in http
      1 /*
      2  * Copyright (c) 1997, 2008, Oracle and/or its affiliates. All rights reserved.
      3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      4  *
      5  * This code is free software; you can redistribute it and/or modify it
      6  * under the terms of the GNU General Public License version 2 only, as
      7  * published by the Free Software Foundation.  Oracle designates this
      8  * particular file as subject to the "Classpath" exception as provided
      9  * by Oracle in the LICENSE file that accompanied this code.
     10  *
     11  * This code is distributed in the hope that it will be useful, but WITHOUT
     12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
     13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
     14  * version 2 for more details (a copy is included in the LICENSE file that
     15  * accompanied this code).
     16  *
     17  * You should have received a copy of the GNU General Public License version
     18  * 2 along with this work; if not, write to the Free Software Foundation,
     19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
     20  *
     21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
     22  * or visit www.oracle.com if you need additional information or have any
     23  * questions.
     24  */
     25 
     26 package sun.net.www.protocol.http;
     27 
     28 import java.io.*;
     29 import java.net.URL;
     30 import java.net.ProtocolException;
     31 import java.net.PasswordAuthentication;
     32 import java.util.Arrays;
     33 import java.util.StringTokenizer;
     34 import java.util.Random;
     35 
     36 import sun.net.www.HeaderParser;
     37 import java.security.MessageDigest;
     38 import java.security.NoSuchAlgorithmException;
     39 import static sun.net.www.protocol.http.HttpURLConnection.HTTP_CONNECT;
     40 
     41 /**
     42  * DigestAuthentication: Encapsulate an http server authentication using
     43  * the "Digest" scheme, as described in RFC2069 and updated in RFC2617
     44  *
     45  * @author Bill Foote
     46  */
     47 
     48 class DigestAuthentication extends AuthenticationInfo {
     49 
     50     private static final long serialVersionUID = 100L;
     51 
     52     private String authMethod;
     53 
     54     // Authentication parameters defined in RFC2617.
     55     // One instance of these may be shared among several DigestAuthentication
     56     // instances as a result of a single authorization (for multiple domains)
     57 
     58     static class Parameters implements java.io.Serializable {
     59         private static final long serialVersionUID = -3584543755194526252L;
     60 
     61         private boolean serverQop; // server proposed qop=auth
     62         private String opaque;
     63         private String cnonce;
     64         private String nonce;
     65         private String algorithm;
     66         private int NCcount=0;
     67 
     68         // The H(A1) string used for MD5-sess
     69         private String  cachedHA1;
     70 
     71         // Force the HA1 value to be recalculated because the nonce has changed
     72         private boolean redoCachedHA1 = true;
     73 
     74         private static final int cnonceRepeat = 5;
     75 
     76         private static final int cnoncelen = 40; /* number of characters in cnonce */
     77 
     78         private static Random   random;
     79 
     80         static {
     81             random = new Random();
     82         }
     83 
     84         Parameters () {
     85             serverQop = false;
     86             opaque = null;
     87             algorithm = null;
     88             cachedHA1 = null;
     89             nonce = null;
     90             setNewCnonce();
     91         }
     92 
     93         boolean authQop () {
     94             return serverQop;
     95         }
     96         synchronized void incrementNC() {
     97             NCcount ++;
     98         }
     99         synchronized int getNCCount () {
    100             return NCcount;
    101         }
    102 
    103         int cnonce_count = 0;
    104 
    105         /* each call increments the counter */
    106         synchronized String getCnonce () {
    107             if (cnonce_count >= cnonceRepeat) {
    108                 setNewCnonce();
    109             }
    110             cnonce_count++;
    111             return cnonce;
    112         }
    113         synchronized void setNewCnonce () {
    114             byte bb[] = new byte [cnoncelen/2];
    115             char cc[] = new char [cnoncelen];
    116             random.nextBytes (bb);
    117             for (int  i=0; i<(cnoncelen/2); i++) {
    118                 int x = bb[i] + 128;
    119                 cc[i*2]= (char) ('A'+ x/16);
    120                 cc[i*2+1]= (char) ('A'+ x%16);
    121             }
    122             cnonce = new String (cc, 0, cnoncelen);
    123             cnonce_count = 0;
    124             redoCachedHA1 = true;
    125         }
    126 
    127         synchronized void setQop (String qop) {
    128             if (qop != null) {
    129                 StringTokenizer st = new StringTokenizer (qop, " ");
    130                 while (st.hasMoreTokens()) {
    131                     if (st.nextToken().equalsIgnoreCase ("auth")) {
    132                         serverQop = true;
    133                         return;
    134                     }
    135                 }
    136             }
    137             serverQop = false;
    138         }
    139 
    140         synchronized String getOpaque () { return opaque;}
    141         synchronized void setOpaque (String s) { opaque=s;}
    142 
    143         synchronized String getNonce () { return nonce;}
    144 
    145         synchronized void setNonce (String s) {
    146             if (!s.equals(nonce)) {
    147                 nonce=s;
    148                 NCcount = 0;
    149                 redoCachedHA1 = true;
    150             }
    151         }
    152 
    153         synchronized String getCachedHA1 () {
    154             if (redoCachedHA1) {
    155                 return null;
    156             } else {
    157                 return cachedHA1;
    158             }
    159         }
    160 
    161         synchronized void setCachedHA1 (String s) {
    162             cachedHA1=s;
    163             redoCachedHA1=false;
    164         }
    165 
    166         synchronized String getAlgorithm () { return algorithm;}
    167         synchronized void setAlgorithm (String s) { algorithm=s;}
    168     }
    169 
    170     Parameters params;
    171 
    172     /**
    173      * Create a DigestAuthentication
    174      */
    175     public DigestAuthentication(boolean isProxy, URL url, String realm,
    176                                 String authMethod, PasswordAuthentication pw,
    177                                 Parameters params) {
    178         super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
    179               AuthScheme.DIGEST,
    180               url,
    181               realm);
    182         this.authMethod = authMethod;
    183         this.pw = pw;
    184         this.params = params;
    185     }
    186 
    187     public DigestAuthentication(boolean isProxy, String host, int port, String realm,
    188                                 String authMethod, PasswordAuthentication pw,
    189                                 Parameters params) {
    190         super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
    191               AuthScheme.DIGEST,
    192               host,
    193               port,
    194               realm);
    195         this.authMethod = authMethod;
    196         this.pw = pw;
    197         this.params = params;
    198     }
    199 
    200     /**
    201      * @return true if this authentication supports preemptive authorization
    202      */
    203     @Override
    204     public boolean supportsPreemptiveAuthorization() {
    205         return true;
    206     }
    207 
    208     /**
    209      * Reclaculates the request-digest and returns it.
    210      *
    211      * <P> Used in the common case where the requestURI is simply the
    212      * abs_path.
    213      *
    214      * @param  url
    215      *         the URL
    216      *
    217      * @param  method
    218      *         the HTTP method
    219      *
    220      * @return the value of the HTTP header this authentication wants set
    221      */
    222     @Override
    223     public String getHeaderValue(URL url, String method) {
    224         return getHeaderValueImpl(url.getFile(), method);
    225     }
    226 
    227     /**
    228      * Reclaculates the request-digest and returns it.
    229      *
    230      * <P> Used when the requestURI is not the abs_path. The exact
    231      * requestURI can be passed as a String.
    232      *
    233      * @param  requestURI
    234      *         the Request-URI from the HTTP request line
    235      *
    236      * @param  method
    237      *         the HTTP method
    238      *
    239      * @return the value of the HTTP header this authentication wants set
    240      */
    241     String getHeaderValue(String requestURI, String method) {
    242         return getHeaderValueImpl(requestURI, method);
    243     }
    244 
    245     /**
    246      * Check if the header indicates that the current auth. parameters are stale.
    247      * If so, then replace the relevant field with the new value
    248      * and return true. Otherwise return false.
    249      * returning true means the request can be retried with the same userid/password
    250      * returning false means we have to go back to the user to ask for a new
    251      * username password.
    252      */
    253     @Override
    254     public boolean isAuthorizationStale (String header) {
    255         HeaderParser p = new HeaderParser (header);
    256         String s = p.findValue ("stale");
    257         if (s == null || !s.equals("true"))
    258             return false;
    259         String newNonce = p.findValue ("nonce");
    260         if (newNonce == null || "".equals(newNonce)) {
    261             return false;
    262         }
    263         params.setNonce (newNonce);
    264         return true;
    265     }
    266 
    267     /**
    268      * Set header(s) on the given connection.
    269      * @param conn The connection to apply the header(s) to
    270      * @param p A source of header values for this connection, if needed.
    271      * @param raw Raw header values for this connection, if needed.
    272      * @return true if all goes well, false if no headers were set.
    273      */
    274     @Override
    275     public boolean setHeaders(HttpURLConnection conn, HeaderParser p, String raw) {
    276         params.setNonce (p.findValue("nonce"));
    277         params.setOpaque (p.findValue("opaque"));
    278         params.setQop (p.findValue("qop"));
    279 
    280         String uri="";
    281         String method;
    282         if (type == PROXY_AUTHENTICATION &&
    283                 conn.tunnelState() == HttpURLConnection.TunnelState.SETUP) {
    284             uri = HttpURLConnection.connectRequestURI(conn.getURL());
    285             method = HTTP_CONNECT;
    286         } else {
    287             try {
    288                 uri = conn.getRequestURI();
    289             } catch (IOException e) {}
    290             method = conn.getMethod();
    291         }
    292 
    293         if (params.nonce == null || authMethod == null || pw == null || realm == null) {
    294             return false;
    295         }
    296         if (authMethod.length() >= 1) {
    297             // Method seems to get converted to all lower case elsewhere.
    298             // It really does need to start with an upper case letter
    299             // here.
    300             authMethod = Character.toUpperCase(authMethod.charAt(0))
    301                         + authMethod.substring(1).toLowerCase();
    302         }
    303         String algorithm = p.findValue("algorithm");
    304         if (algorithm == null || "".equals(algorithm)) {
    305             algorithm = "MD5";  // The default, accoriding to rfc2069
    306         }
    307         params.setAlgorithm (algorithm);
    308 
    309         // If authQop is true, then the server is doing RFC2617 and
    310         // has offered qop=auth. We do not support any other modes
    311         // and if auth is not offered we fallback to the RFC2069 behavior
    312 
    313         if (params.authQop()) {
    314             params.setNewCnonce();
    315         }
    316 
    317         String value = getHeaderValueImpl (uri, method);
    318         if (value != null) {
    319             conn.setAuthenticationProperty(getHeaderName(), value);
    320             return true;
    321         } else {
    322             return false;
    323         }
    324     }
    325 
    326     /* Calculate the Authorization header field given the request URI
    327      * and based on the authorization information in params
    328      */
    329     private String getHeaderValueImpl (String uri, String method) {
    330         String response;
    331         char[] passwd = pw.getPassword();
    332         boolean qop = params.authQop();
    333         String opaque = params.getOpaque();
    334         String cnonce = params.getCnonce ();
    335         String nonce = params.getNonce ();
    336         String algorithm = params.getAlgorithm ();
    337         params.incrementNC ();
    338         int  nccount = params.getNCCount ();
    339         String ncstring=null;
    340 
    341         if (nccount != -1) {
    342             ncstring = Integer.toHexString (nccount).toLowerCase();
    343             int len = ncstring.length();
    344             if (len < 8)
    345                 ncstring = zeroPad [len] + ncstring;
    346         }
    347 
    348         try {
    349             response = computeDigest(true, pw.getUserName(),passwd,realm,
    350                                         method, uri, nonce, cnonce, ncstring);
    351         } catch (NoSuchAlgorithmException ex) {
    352             return null;
    353         }
    354 
    355         String ncfield = "\"";
    356         if (qop) {
    357             ncfield = "\", nc=" + ncstring;
    358         }
    359 
    360         String value = authMethod
    361                         + " username=\"" + pw.getUserName()
    362                         + "\", realm=\"" + realm
    363                         + "\", nonce=\"" + nonce
    364                         + ncfield
    365                         + ", uri=\"" + uri
    366                         + "\", response=\"" + response
    367                         + "\", algorithm=\"" + algorithm;
    368         if (opaque != null) {
    369             value = value + "\", opaque=\"" + opaque;
    370         }
    371         if (cnonce != null) {
    372             value = value + "\", cnonce=\"" + cnonce;
    373         }
    374         if (qop) {
    375             value = value + "\", qop=\"auth";
    376         }
    377         value = value + "\"";
    378         return value;
    379     }
    380 
    381     public void checkResponse (String header, String method, URL url)
    382                                                         throws IOException {
    383         checkResponse (header, method, url.getFile());
    384     }
    385 
    386     public void checkResponse (String header, String method, String uri)
    387                                                         throws IOException {
    388         char[] passwd = pw.getPassword();
    389         String username = pw.getUserName();
    390         boolean qop = params.authQop();
    391         String opaque = params.getOpaque();
    392         String cnonce = params.cnonce;
    393         String nonce = params.getNonce ();
    394         String algorithm = params.getAlgorithm ();
    395         int  nccount = params.getNCCount ();
    396         String ncstring=null;
    397 
    398         if (header == null) {
    399             throw new ProtocolException ("No authentication information in response");
    400         }
    401 
    402         if (nccount != -1) {
    403             ncstring = Integer.toHexString (nccount).toUpperCase();
    404             int len = ncstring.length();
    405             if (len < 8)
    406                 ncstring = zeroPad [len] + ncstring;
    407         }
    408         try {
    409             String expected = computeDigest(false, username,passwd,realm,
    410                                         method, uri, nonce, cnonce, ncstring);
    411             HeaderParser p = new HeaderParser (header);
    412             String rspauth = p.findValue ("rspauth");
    413             if (rspauth == null) {
    414                 throw new ProtocolException ("No digest in response");
    415             }
    416             if (!rspauth.equals (expected)) {
    417                 throw new ProtocolException ("Response digest invalid");
    418             }
    419             /* Check if there is a nextnonce field */
    420             String nextnonce = p.findValue ("nextnonce");
    421             if (nextnonce != null && ! "".equals(nextnonce)) {
    422                 params.setNonce (nextnonce);
    423             }
    424 
    425         } catch (NoSuchAlgorithmException ex) {
    426             throw new ProtocolException ("Unsupported algorithm in response");
    427         }
    428     }
    429 
    430     private String computeDigest(
    431                         boolean isRequest, String userName, char[] password,
    432                         String realm, String connMethod,
    433                         String requestURI, String nonceString,
    434                         String cnonce, String ncValue
    435                     ) throws NoSuchAlgorithmException
    436     {
    437 
    438         String A1, HashA1;
    439         String algorithm = params.getAlgorithm ();
    440         boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");
    441 
    442         MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);
    443 
    444         if (md5sess) {
    445             if ((HashA1 = params.getCachedHA1 ()) == null) {
    446                 String s = userName + ":" + realm + ":";
    447                 String s1 = encode (s, password, md);
    448                 A1 = s1 + ":" + nonceString + ":" + cnonce;
    449                 HashA1 = encode(A1, null, md);
    450                 params.setCachedHA1 (HashA1);
    451             }
    452         } else {
    453             A1 = userName + ":" + realm + ":";
    454             HashA1 = encode(A1, password, md);
    455         }
    456 
    457         String A2;
    458         if (isRequest) {
    459             A2 = connMethod + ":" + requestURI;
    460         } else {
    461             A2 = ":" + requestURI;
    462         }
    463         String HashA2 = encode(A2, null, md);
    464         String combo, finalHash;
    465 
    466         if (params.authQop()) { /* RRC2617 when qop=auth */
    467             combo = HashA1+ ":" + nonceString + ":" + ncValue + ":" +
    468                         cnonce + ":auth:" +HashA2;
    469 
    470         } else { /* for compatibility with RFC2069 */
    471             combo = HashA1 + ":" +
    472                        nonceString + ":" +
    473                        HashA2;
    474         }
    475         finalHash = encode(combo, null, md);
    476         return finalHash;
    477     }
    478 
    479     private final static char charArray[] = {
    480         '0', '1', '2', '3', '4', '5', '6', '7',
    481         '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
    482     };
    483 
    484     private final static String zeroPad[] = {
    485         // 0         1          2         3        4       5      6     7
    486         "00000000", "0000000", "000000", "00000", "0000", "000", "00", "0"
    487     };
    488 
    489     private String encode(String src, char[] passwd, MessageDigest md) {
    490         try {
    491             md.update(src.getBytes("ISO-8859-1"));
    492         } catch (java.io.UnsupportedEncodingException uee) {
    493             assert false;
    494         }
    495         if (passwd != null) {
    496             byte[] passwdBytes = new byte[passwd.length];
    497             for (int i=0; i<passwd.length; i++)
    498                 passwdBytes[i] = (byte)passwd[i];
    499             md.update(passwdBytes);
    500             Arrays.fill(passwdBytes, (byte)0x00);
    501         }
    502         byte[] digest = md.digest();
    503 
    504         StringBuffer res = new StringBuffer(digest.length * 2);
    505         for (int i = 0; i < digest.length; i++) {
    506             int hashchar = ((digest[i] >>> 4) & 0xf);
    507             res.append(charArray[hashchar]);
    508             hashchar = (digest[i] & 0xf);
    509             res.append(charArray[hashchar]);
    510         }
    511         return res.toString();
    512     }
    513 }
    514