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