1 /*
   2  * Copyright (c) 1997, 2016, 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.Random;
  34 
  35 import sun.net.www.HeaderParser;
  36 import sun.net.NetProperties;
  37 import java.security.MessageDigest;
  38 import java.security.NoSuchAlgorithmException;
  39 import java.security.PrivilegedAction;
  40 import java.security.AccessController;
  41 import java.util.Objects;
  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 static final 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<>() {
  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                 String items[] = qop.split(",");
 150                 for (String item : items) {
 151                     if ("auth".equalsIgnoreCase(item.trim())) {
 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 (nonce == null || !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, String authenticatorKey) {
 198         super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
 199               AuthScheme.DIGEST,
 200               url,
 201               realm,
 202               Objects.requireNonNull(authenticatorKey));
 203         this.authMethod = authMethod;
 204         this.pw = pw;
 205         this.params = params;
 206     }
 207 
 208     public DigestAuthentication(boolean isProxy, String host, int port, String realm,
 209                                 String authMethod, PasswordAuthentication pw,
 210                                 Parameters params, String authenticatorKey) {
 211         super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
 212               AuthScheme.DIGEST,
 213               host,
 214               port,
 215               realm,
 216               Objects.requireNonNull(authenticatorKey));
 217         this.authMethod = authMethod;
 218         this.pw = pw;
 219         this.params = params;
 220     }
 221 
 222     /**
 223      * @return true if this authentication supports preemptive authorization
 224      */
 225     @Override
 226     public boolean supportsPreemptiveAuthorization() {
 227         return true;
 228     }
 229 
 230     /**
 231      * Recalculates the request-digest and returns it.
 232      *
 233      * <P> Used in the common case where the requestURI is simply the
 234      * abs_path.
 235      *
 236      * @param  url
 237      *         the URL
 238      *
 239      * @param  method
 240      *         the HTTP method
 241      *
 242      * @return the value of the HTTP header this authentication wants set
 243      */
 244     @Override
 245     public String getHeaderValue(URL url, String method) {
 246         return getHeaderValueImpl(url.getFile(), method);
 247     }
 248 
 249     /**
 250      * Recalculates the request-digest and returns it.
 251      *
 252      * <P> Used when the requestURI is not the abs_path. The exact
 253      * requestURI can be passed as a String.
 254      *
 255      * @param  requestURI
 256      *         the Request-URI from the HTTP request line
 257      *
 258      * @param  method
 259      *         the HTTP method
 260      *
 261      * @return the value of the HTTP header this authentication wants set
 262      */
 263     String getHeaderValue(String requestURI, String method) {
 264         return getHeaderValueImpl(requestURI, method);
 265     }
 266 
 267     /**
 268      * Check if the header indicates that the current auth. parameters are stale.
 269      * If so, then replace the relevant field with the new value
 270      * and return true. Otherwise return false.
 271      * returning true means the request can be retried with the same userid/password
 272      * returning false means we have to go back to the user to ask for a new
 273      * username password.
 274      */
 275     @Override
 276     public boolean isAuthorizationStale (String header) {
 277         HeaderParser p = new HeaderParser (header);
 278         String s = p.findValue ("stale");
 279         if (s == null || !s.equals("true"))
 280             return false;
 281         String newNonce = p.findValue ("nonce");
 282         if (newNonce == null || "".equals(newNonce)) {
 283             return false;
 284         }
 285         params.setNonce (newNonce);
 286         return true;
 287     }
 288 
 289     /**
 290      * Set header(s) on the given connection.
 291      * @param conn The connection to apply the header(s) to
 292      * @param p A source of header values for this connection, if needed.
 293      * @param raw Raw header values for this connection, if needed.
 294      * @return true if all goes well, false if no headers were set.
 295      */
 296     @Override
 297     public boolean setHeaders(HttpURLConnection conn, HeaderParser p, String raw) {
 298         params.setNonce (p.findValue("nonce"));
 299         params.setOpaque (p.findValue("opaque"));
 300         params.setQop (p.findValue("qop"));
 301 
 302         String uri="";
 303         String method;
 304         if (type == PROXY_AUTHENTICATION &&
 305                 conn.tunnelState() == HttpURLConnection.TunnelState.SETUP) {
 306             uri = HttpURLConnection.connectRequestURI(conn.getURL());
 307             method = HTTP_CONNECT;
 308         } else {
 309             try {
 310                 uri = conn.getRequestURI();
 311             } catch (IOException e) {}
 312             method = conn.getMethod();
 313         }
 314 
 315         if (params.nonce == null || authMethod == null || pw == null || realm == null) {
 316             return false;
 317         }
 318         if (authMethod.length() >= 1) {
 319             // Method seems to get converted to all lower case elsewhere.
 320             // It really does need to start with an upper case letter
 321             // here.
 322             authMethod = Character.toUpperCase(authMethod.charAt(0))
 323                         + authMethod.substring(1).toLowerCase();
 324         }
 325         String algorithm = p.findValue("algorithm");
 326         if (algorithm == null || "".equals(algorithm)) {
 327             algorithm = "MD5";  // The default, accoriding to rfc2069
 328         }
 329         params.setAlgorithm (algorithm);
 330 
 331         // If authQop is true, then the server is doing RFC2617 and
 332         // has offered qop=auth. We do not support any other modes
 333         // and if auth is not offered we fallback to the RFC2069 behavior
 334 
 335         if (params.authQop()) {
 336             params.setNewCnonce();
 337         }
 338 
 339         String value = getHeaderValueImpl (uri, method);
 340         if (value != null) {
 341             conn.setAuthenticationProperty(getHeaderName(), value);
 342             return true;
 343         } else {
 344             return false;
 345         }
 346     }
 347 
 348     /* Calculate the Authorization header field given the request URI
 349      * and based on the authorization information in params
 350      */
 351     private String getHeaderValueImpl (String uri, String method) {
 352         String response;
 353         char[] passwd = pw.getPassword();
 354         boolean qop = params.authQop();
 355         String opaque = params.getOpaque();
 356         String cnonce = params.getCnonce ();
 357         String nonce = params.getNonce ();
 358         String algorithm = params.getAlgorithm ();
 359         params.incrementNC ();
 360         int  nccount = params.getNCCount ();
 361         String ncstring=null;
 362 
 363         if (nccount != -1) {
 364             ncstring = Integer.toHexString (nccount).toLowerCase();
 365             int len = ncstring.length();
 366             if (len < 8)
 367                 ncstring = zeroPad [len] + ncstring;
 368         }
 369 
 370         try {
 371             response = computeDigest(true, pw.getUserName(),passwd,realm,
 372                                         method, uri, nonce, cnonce, ncstring);
 373         } catch (NoSuchAlgorithmException ex) {
 374             return null;
 375         }
 376 
 377         String ncfield = "\"";
 378         if (qop) {
 379             ncfield = "\", nc=" + ncstring;
 380         }
 381 
 382         String algoS, qopS;
 383 
 384         if (delimCompatFlag) {
 385             // Put quotes around these String value parameters
 386             algoS = ", algorithm=\"" + algorithm + "\"";
 387             qopS = ", qop=\"auth\"";
 388         } else {
 389             // Don't put quotes around them, per the RFC
 390             algoS = ", algorithm=" + algorithm;
 391             qopS = ", qop=auth";
 392         }
 393 
 394         String value = authMethod
 395                         + " username=\"" + pw.getUserName()
 396                         + "\", realm=\"" + realm
 397                         + "\", nonce=\"" + nonce
 398                         + ncfield
 399                         + ", uri=\"" + uri
 400                         + "\", response=\"" + response + "\""
 401                         + algoS;
 402         if (opaque != null) {
 403             value += ", opaque=\"" + opaque + "\"";
 404         }
 405         if (cnonce != null) {
 406             value += ", cnonce=\"" + cnonce + "\"";
 407         }
 408         if (qop) {
 409             value += qopS;
 410         }
 411         return value;
 412     }
 413 
 414     public void checkResponse (String header, String method, URL url)
 415                                                         throws IOException {
 416         checkResponse (header, method, url.getFile());
 417     }
 418 
 419     public void checkResponse (String header, String method, String uri)
 420                                                         throws IOException {
 421         char[] passwd = pw.getPassword();
 422         String username = pw.getUserName();
 423         boolean qop = params.authQop();
 424         String opaque = params.getOpaque();
 425         String cnonce = params.cnonce;
 426         String nonce = params.getNonce ();
 427         String algorithm = params.getAlgorithm ();
 428         int  nccount = params.getNCCount ();
 429         String ncstring=null;
 430 
 431         if (header == null) {
 432             throw new ProtocolException ("No authentication information in response");
 433         }
 434 
 435         if (nccount != -1) {
 436             ncstring = Integer.toHexString (nccount).toUpperCase();
 437             int len = ncstring.length();
 438             if (len < 8)
 439                 ncstring = zeroPad [len] + ncstring;
 440         }
 441         try {
 442             String expected = computeDigest(false, username,passwd,realm,
 443                                         method, uri, nonce, cnonce, ncstring);
 444             HeaderParser p = new HeaderParser (header);
 445             String rspauth = p.findValue ("rspauth");
 446             if (rspauth == null) {
 447                 throw new ProtocolException ("No digest in response");
 448             }
 449             if (!rspauth.equals (expected)) {
 450                 throw new ProtocolException ("Response digest invalid");
 451             }
 452             /* Check if there is a nextnonce field */
 453             String nextnonce = p.findValue ("nextnonce");
 454             if (nextnonce != null && ! "".equals(nextnonce)) {
 455                 params.setNonce (nextnonce);
 456             }
 457 
 458         } catch (NoSuchAlgorithmException ex) {
 459             throw new ProtocolException ("Unsupported algorithm in response");
 460         }
 461     }
 462 
 463     private String computeDigest(
 464                         boolean isRequest, String userName, char[] password,
 465                         String realm, String connMethod,
 466                         String requestURI, String nonceString,
 467                         String cnonce, String ncValue
 468                     ) throws NoSuchAlgorithmException
 469     {
 470 
 471         String A1, HashA1;
 472         String algorithm = params.getAlgorithm ();
 473         boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");
 474 
 475         MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);
 476 
 477         if (md5sess) {
 478             if ((HashA1 = params.getCachedHA1 ()) == null) {
 479                 String s = userName + ":" + realm + ":";
 480                 String s1 = encode (s, password, md);
 481                 A1 = s1 + ":" + nonceString + ":" + cnonce;
 482                 HashA1 = encode(A1, null, md);
 483                 params.setCachedHA1 (HashA1);
 484             }
 485         } else {
 486             A1 = userName + ":" + realm + ":";
 487             HashA1 = encode(A1, password, md);
 488         }
 489 
 490         String A2;
 491         if (isRequest) {
 492             A2 = connMethod + ":" + requestURI;
 493         } else {
 494             A2 = ":" + requestURI;
 495         }
 496         String HashA2 = encode(A2, null, md);
 497         String combo, finalHash;
 498 
 499         if (params.authQop()) { /* RRC2617 when qop=auth */
 500             combo = HashA1+ ":" + nonceString + ":" + ncValue + ":" +
 501                         cnonce + ":auth:" +HashA2;
 502 
 503         } else { /* for compatibility with RFC2069 */
 504             combo = HashA1 + ":" +
 505                        nonceString + ":" +
 506                        HashA2;
 507         }
 508         finalHash = encode(combo, null, md);
 509         return finalHash;
 510     }
 511 
 512     private static final char charArray[] = {
 513         '0', '1', '2', '3', '4', '5', '6', '7',
 514         '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
 515     };
 516 
 517     private static final String zeroPad[] = {
 518         // 0         1          2         3        4       5      6     7
 519         "00000000", "0000000", "000000", "00000", "0000", "000", "00", "0"
 520     };
 521 
 522     private String encode(String src, char[] passwd, MessageDigest md) {
 523         try {
 524             md.update(src.getBytes("ISO-8859-1"));
 525         } catch (java.io.UnsupportedEncodingException uee) {
 526             assert false;
 527         }
 528         if (passwd != null) {
 529             byte[] passwdBytes = new byte[passwd.length];
 530             for (int i=0; i<passwd.length; i++)
 531                 passwdBytes[i] = (byte)passwd[i];
 532             md.update(passwdBytes);
 533             Arrays.fill(passwdBytes, (byte)0x00);
 534         }
 535         byte[] digest = md.digest();
 536 
 537         StringBuilder res = new StringBuilder(digest.length * 2);
 538         for (int i = 0; i < digest.length; i++) {
 539             int hashchar = ((digest[i] >>> 4) & 0xf);
 540             res.append(charArray[hashchar]);
 541             hashchar = (digest[i] & 0xf);
 542             res.append(charArray[hashchar]);
 543         }
 544         return res.toString();
 545     }
 546 }