1 /*
   2  * Copyright 1997-2008 Sun Microsystems, Inc.  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.  Sun designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
  22  * CA 95054 USA or visit www.sun.com if you need additional information or
  23  * have any 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             uri = conn.getRequestURI();
 288             method = conn.getMethod();
 289         }
 290 
 291         if (params.nonce == null || authMethod == null || pw == null || realm == null) {
 292             return false;
 293         }
 294         if (authMethod.length() >= 1) {
 295             // Method seems to get converted to all lower case elsewhere.
 296             // It really does need to start with an upper case letter
 297             // here.
 298             authMethod = Character.toUpperCase(authMethod.charAt(0))
 299                         + authMethod.substring(1).toLowerCase();
 300         }
 301         String algorithm = p.findValue("algorithm");
 302         if (algorithm == null || "".equals(algorithm)) {
 303             algorithm = "MD5";  // The default, accoriding to rfc2069
 304         }
 305         params.setAlgorithm (algorithm);
 306 
 307         // If authQop is true, then the server is doing RFC2617 and
 308         // has offered qop=auth. We do not support any other modes
 309         // and if auth is not offered we fallback to the RFC2069 behavior
 310 
 311         if (params.authQop()) {
 312             params.setNewCnonce();
 313         }
 314 
 315         String value = getHeaderValueImpl (uri, method);
 316         if (value != null) {
 317             conn.setAuthenticationProperty(getHeaderName(), value);
 318             return true;
 319         } else {
 320             return false;
 321         }
 322     }
 323 
 324     /* Calculate the Authorization header field given the request URI
 325      * and based on the authorization information in params
 326      */
 327     private String getHeaderValueImpl (String uri, String method) {
 328         String response;
 329         char[] passwd = pw.getPassword();
 330         boolean qop = params.authQop();
 331         String opaque = params.getOpaque();
 332         String cnonce = params.getCnonce ();
 333         String nonce = params.getNonce ();
 334         String algorithm = params.getAlgorithm ();
 335         params.incrementNC ();
 336         int  nccount = params.getNCCount ();
 337         String ncstring=null;
 338 
 339         if (nccount != -1) {
 340             ncstring = Integer.toHexString (nccount).toLowerCase();
 341             int len = ncstring.length();
 342             if (len < 8)
 343                 ncstring = zeroPad [len] + ncstring;
 344         }
 345 
 346         try {
 347             response = computeDigest(true, pw.getUserName(),passwd,realm,
 348                                         method, uri, nonce, cnonce, ncstring);
 349         } catch (NoSuchAlgorithmException ex) {
 350             return null;
 351         }
 352 
 353         String ncfield = "\"";
 354         if (qop) {
 355             ncfield = "\", nc=" + ncstring;
 356         }
 357 
 358         String value = authMethod
 359                         + " username=\"" + pw.getUserName()
 360                         + "\", realm=\"" + realm
 361                         + "\", nonce=\"" + nonce
 362                         + ncfield
 363                         + ", uri=\"" + uri
 364                         + "\", response=\"" + response
 365                         + "\", algorithm=\"" + algorithm;
 366         if (opaque != null) {
 367             value = value + "\", opaque=\"" + opaque;
 368         }
 369         if (cnonce != null) {
 370             value = value + "\", cnonce=\"" + cnonce;
 371         }
 372         if (qop) {
 373             value = value + "\", qop=\"auth";
 374         }
 375         value = value + "\"";
 376         return value;
 377     }
 378 
 379     public void checkResponse (String header, String method, URL url)
 380                                                         throws IOException {
 381         checkResponse (header, method, url.getFile());
 382     }
 383 
 384     public void checkResponse (String header, String method, String uri)
 385                                                         throws IOException {
 386         char[] passwd = pw.getPassword();
 387         String username = pw.getUserName();
 388         boolean qop = params.authQop();
 389         String opaque = params.getOpaque();
 390         String cnonce = params.cnonce;
 391         String nonce = params.getNonce ();
 392         String algorithm = params.getAlgorithm ();
 393         int  nccount = params.getNCCount ();
 394         String ncstring=null;
 395 
 396         if (header == null) {
 397             throw new ProtocolException ("No authentication information in response");
 398         }
 399 
 400         if (nccount != -1) {
 401             ncstring = Integer.toHexString (nccount).toUpperCase();
 402             int len = ncstring.length();
 403             if (len < 8)
 404                 ncstring = zeroPad [len] + ncstring;
 405         }
 406         try {
 407             String expected = computeDigest(false, username,passwd,realm,
 408                                         method, uri, nonce, cnonce, ncstring);
 409             HeaderParser p = new HeaderParser (header);
 410             String rspauth = p.findValue ("rspauth");
 411             if (rspauth == null) {
 412                 throw new ProtocolException ("No digest in response");
 413             }
 414             if (!rspauth.equals (expected)) {
 415                 throw new ProtocolException ("Response digest invalid");
 416             }
 417             /* Check if there is a nextnonce field */
 418             String nextnonce = p.findValue ("nextnonce");
 419             if (nextnonce != null && ! "".equals(nextnonce)) {
 420                 params.setNonce (nextnonce);
 421             }
 422 
 423         } catch (NoSuchAlgorithmException ex) {
 424             throw new ProtocolException ("Unsupported algorithm in response");
 425         }
 426     }
 427 
 428     private String computeDigest(
 429                         boolean isRequest, String userName, char[] password,
 430                         String realm, String connMethod,
 431                         String requestURI, String nonceString,
 432                         String cnonce, String ncValue
 433                     ) throws NoSuchAlgorithmException
 434     {
 435 
 436         String A1, HashA1;
 437         String algorithm = params.getAlgorithm ();
 438         boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");
 439 
 440         MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);
 441 
 442         if (md5sess) {
 443             if ((HashA1 = params.getCachedHA1 ()) == null) {
 444                 String s = userName + ":" + realm + ":";
 445                 String s1 = encode (s, password, md);
 446                 A1 = s1 + ":" + nonceString + ":" + cnonce;
 447                 HashA1 = encode(A1, null, md);
 448                 params.setCachedHA1 (HashA1);
 449             }
 450         } else {
 451             A1 = userName + ":" + realm + ":";
 452             HashA1 = encode(A1, password, md);
 453         }
 454 
 455         String A2;
 456         if (isRequest) {
 457             A2 = connMethod + ":" + requestURI;
 458         } else {
 459             A2 = ":" + requestURI;
 460         }
 461         String HashA2 = encode(A2, null, md);
 462         String combo, finalHash;
 463 
 464         if (params.authQop()) { /* RRC2617 when qop=auth */
 465             combo = HashA1+ ":" + nonceString + ":" + ncValue + ":" +
 466                         cnonce + ":auth:" +HashA2;
 467 
 468         } else { /* for compatibility with RFC2069 */
 469             combo = HashA1 + ":" +
 470                        nonceString + ":" +
 471                        HashA2;
 472         }
 473         finalHash = encode(combo, null, md);
 474         return finalHash;
 475     }
 476 
 477     private final static char charArray[] = {
 478         '0', '1', '2', '3', '4', '5', '6', '7',
 479         '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
 480     };
 481 
 482     private final static String zeroPad[] = {
 483         // 0         1          2         3        4       5      6     7
 484         "00000000", "0000000", "000000", "00000", "0000", "000", "00", "0"
 485     };
 486 
 487     private String encode(String src, char[] passwd, MessageDigest md) {
 488         try {
 489             md.update(src.getBytes("ISO-8859-1"));
 490         } catch (java.io.UnsupportedEncodingException uee) {
 491             assert false;
 492         }
 493         if (passwd != null) {
 494             byte[] passwdBytes = new byte[passwd.length];
 495             for (int i=0; i<passwd.length; i++)
 496                 passwdBytes[i] = (byte)passwd[i];
 497             md.update(passwdBytes);
 498             Arrays.fill(passwdBytes, (byte)0x00);
 499         }
 500         byte[] digest = md.digest();
 501 
 502         StringBuffer res = new StringBuffer(digest.length * 2);
 503         for (int i = 0; i < digest.length; i++) {
 504             int hashchar = ((digest[i] >>> 4) & 0xf);
 505             res.append(charArray[hashchar]);
 506             hashchar = (digest[i] & 0xf);
 507             res.append(charArray[hashchar]);
 508         }
 509         return res.toString();
 510     }
 511 }
--- EOF ---