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 }