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 }