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