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