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