DigestAuthentication.java revision 12745:f068a4ffddd2
1/* 2 * Copyright (c) 1997, 2013, 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.StringTokenizer; 34import java.util.Random; 35 36import sun.net.www.HeaderParser; 37import sun.net.NetProperties; 38import java.security.MessageDigest; 39import java.security.NoSuchAlgorithmException; 40import java.security.PrivilegedAction; 41import java.security.AccessController; 42import 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 51class 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 StringTokenizer st = new StringTokenizer (qop, " "); 150 while (st.hasMoreTokens()) { 151 if (st.nextToken().equalsIgnoreCase ("auth")) { 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 (!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) { 198 super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION, 199 AuthScheme.DIGEST, 200 url, 201 realm); 202 this.authMethod = authMethod; 203 this.pw = pw; 204 this.params = params; 205 } 206 207 public DigestAuthentication(boolean isProxy, String host, int port, String realm, 208 String authMethod, PasswordAuthentication pw, 209 Parameters params) { 210 super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION, 211 AuthScheme.DIGEST, 212 host, 213 port, 214 realm); 215 this.authMethod = authMethod; 216 this.pw = pw; 217 this.params = params; 218 } 219 220 /** 221 * @return true if this authentication supports preemptive authorization 222 */ 223 @Override 224 public boolean supportsPreemptiveAuthorization() { 225 return true; 226 } 227 228 /** 229 * Recalculates the request-digest and returns it. 230 * 231 * <P> Used in the common case where the requestURI is simply the 232 * abs_path. 233 * 234 * @param url 235 * the URL 236 * 237 * @param method 238 * the HTTP method 239 * 240 * @return the value of the HTTP header this authentication wants set 241 */ 242 @Override 243 public String getHeaderValue(URL url, String method) { 244 return getHeaderValueImpl(url.getFile(), method); 245 } 246 247 /** 248 * Recalculates the request-digest and returns it. 249 * 250 * <P> Used when the requestURI is not the abs_path. The exact 251 * requestURI can be passed as a String. 252 * 253 * @param requestURI 254 * the Request-URI from the HTTP request line 255 * 256 * @param method 257 * the HTTP method 258 * 259 * @return the value of the HTTP header this authentication wants set 260 */ 261 String getHeaderValue(String requestURI, String method) { 262 return getHeaderValueImpl(requestURI, method); 263 } 264 265 /** 266 * Check if the header indicates that the current auth. parameters are stale. 267 * If so, then replace the relevant field with the new value 268 * and return true. Otherwise return false. 269 * returning true means the request can be retried with the same userid/password 270 * returning false means we have to go back to the user to ask for a new 271 * username password. 272 */ 273 @Override 274 public boolean isAuthorizationStale (String header) { 275 HeaderParser p = new HeaderParser (header); 276 String s = p.findValue ("stale"); 277 if (s == null || !s.equals("true")) 278 return false; 279 String newNonce = p.findValue ("nonce"); 280 if (newNonce == null || "".equals(newNonce)) { 281 return false; 282 } 283 params.setNonce (newNonce); 284 return true; 285 } 286 287 /** 288 * Set header(s) on the given connection. 289 * @param conn The connection to apply the header(s) to 290 * @param p A source of header values for this connection, if needed. 291 * @param raw Raw header values for this connection, if needed. 292 * @return true if all goes well, false if no headers were set. 293 */ 294 @Override 295 public boolean setHeaders(HttpURLConnection conn, HeaderParser p, String raw) { 296 params.setNonce (p.findValue("nonce")); 297 params.setOpaque (p.findValue("opaque")); 298 params.setQop (p.findValue("qop")); 299 300 String uri=""; 301 String method; 302 if (type == PROXY_AUTHENTICATION && 303 conn.tunnelState() == HttpURLConnection.TunnelState.SETUP) { 304 uri = HttpURLConnection.connectRequestURI(conn.getURL()); 305 method = HTTP_CONNECT; 306 } else { 307 try { 308 uri = conn.getRequestURI(); 309 } catch (IOException e) {} 310 method = conn.getMethod(); 311 } 312 313 if (params.nonce == null || authMethod == null || pw == null || realm == null) { 314 return false; 315 } 316 if (authMethod.length() >= 1) { 317 // Method seems to get converted to all lower case elsewhere. 318 // It really does need to start with an upper case letter 319 // here. 320 authMethod = Character.toUpperCase(authMethod.charAt(0)) 321 + authMethod.substring(1).toLowerCase(); 322 } 323 String algorithm = p.findValue("algorithm"); 324 if (algorithm == null || "".equals(algorithm)) { 325 algorithm = "MD5"; // The default, accoriding to rfc2069 326 } 327 params.setAlgorithm (algorithm); 328 329 // If authQop is true, then the server is doing RFC2617 and 330 // has offered qop=auth. We do not support any other modes 331 // and if auth is not offered we fallback to the RFC2069 behavior 332 333 if (params.authQop()) { 334 params.setNewCnonce(); 335 } 336 337 String value = getHeaderValueImpl (uri, method); 338 if (value != null) { 339 conn.setAuthenticationProperty(getHeaderName(), value); 340 return true; 341 } else { 342 return false; 343 } 344 } 345 346 /* Calculate the Authorization header field given the request URI 347 * and based on the authorization information in params 348 */ 349 private String getHeaderValueImpl (String uri, String method) { 350 String response; 351 char[] passwd = pw.getPassword(); 352 boolean qop = params.authQop(); 353 String opaque = params.getOpaque(); 354 String cnonce = params.getCnonce (); 355 String nonce = params.getNonce (); 356 String algorithm = params.getAlgorithm (); 357 params.incrementNC (); 358 int nccount = params.getNCCount (); 359 String ncstring=null; 360 361 if (nccount != -1) { 362 ncstring = Integer.toHexString (nccount).toLowerCase(); 363 int len = ncstring.length(); 364 if (len < 8) 365 ncstring = zeroPad [len] + ncstring; 366 } 367 368 try { 369 response = computeDigest(true, pw.getUserName(),passwd,realm, 370 method, uri, nonce, cnonce, ncstring); 371 } catch (NoSuchAlgorithmException ex) { 372 return null; 373 } 374 375 String ncfield = "\""; 376 if (qop) { 377 ncfield = "\", nc=" + ncstring; 378 } 379 380 String algoS, qopS; 381 382 if (delimCompatFlag) { 383 // Put quotes around these String value parameters 384 algoS = ", algorithm=\"" + algorithm + "\""; 385 qopS = ", qop=\"auth\""; 386 } else { 387 // Don't put quotes around them, per the RFC 388 algoS = ", algorithm=" + algorithm; 389 qopS = ", qop=auth"; 390 } 391 392 String value = authMethod 393 + " username=\"" + pw.getUserName() 394 + "\", realm=\"" + realm 395 + "\", nonce=\"" + nonce 396 + ncfield 397 + ", uri=\"" + uri 398 + "\", response=\"" + response + "\"" 399 + algoS; 400 if (opaque != null) { 401 value += ", opaque=\"" + opaque + "\""; 402 } 403 if (cnonce != null) { 404 value += ", cnonce=\"" + cnonce + "\""; 405 } 406 if (qop) { 407 value += qopS; 408 } 409 return value; 410 } 411 412 public void checkResponse (String header, String method, URL url) 413 throws IOException { 414 checkResponse (header, method, url.getFile()); 415 } 416 417 public void checkResponse (String header, String method, String uri) 418 throws IOException { 419 char[] passwd = pw.getPassword(); 420 String username = pw.getUserName(); 421 boolean qop = params.authQop(); 422 String opaque = params.getOpaque(); 423 String cnonce = params.cnonce; 424 String nonce = params.getNonce (); 425 String algorithm = params.getAlgorithm (); 426 int nccount = params.getNCCount (); 427 String ncstring=null; 428 429 if (header == null) { 430 throw new ProtocolException ("No authentication information in response"); 431 } 432 433 if (nccount != -1) { 434 ncstring = Integer.toHexString (nccount).toUpperCase(); 435 int len = ncstring.length(); 436 if (len < 8) 437 ncstring = zeroPad [len] + ncstring; 438 } 439 try { 440 String expected = computeDigest(false, username,passwd,realm, 441 method, uri, nonce, cnonce, ncstring); 442 HeaderParser p = new HeaderParser (header); 443 String rspauth = p.findValue ("rspauth"); 444 if (rspauth == null) { 445 throw new ProtocolException ("No digest in response"); 446 } 447 if (!rspauth.equals (expected)) { 448 throw new ProtocolException ("Response digest invalid"); 449 } 450 /* Check if there is a nextnonce field */ 451 String nextnonce = p.findValue ("nextnonce"); 452 if (nextnonce != null && ! "".equals(nextnonce)) { 453 params.setNonce (nextnonce); 454 } 455 456 } catch (NoSuchAlgorithmException ex) { 457 throw new ProtocolException ("Unsupported algorithm in response"); 458 } 459 } 460 461 private String computeDigest( 462 boolean isRequest, String userName, char[] password, 463 String realm, String connMethod, 464 String requestURI, String nonceString, 465 String cnonce, String ncValue 466 ) throws NoSuchAlgorithmException 467 { 468 469 String A1, HashA1; 470 String algorithm = params.getAlgorithm (); 471 boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess"); 472 473 MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm); 474 475 if (md5sess) { 476 if ((HashA1 = params.getCachedHA1 ()) == null) { 477 String s = userName + ":" + realm + ":"; 478 String s1 = encode (s, password, md); 479 A1 = s1 + ":" + nonceString + ":" + cnonce; 480 HashA1 = encode(A1, null, md); 481 params.setCachedHA1 (HashA1); 482 } 483 } else { 484 A1 = userName + ":" + realm + ":"; 485 HashA1 = encode(A1, password, md); 486 } 487 488 String A2; 489 if (isRequest) { 490 A2 = connMethod + ":" + requestURI; 491 } else { 492 A2 = ":" + requestURI; 493 } 494 String HashA2 = encode(A2, null, md); 495 String combo, finalHash; 496 497 if (params.authQop()) { /* RRC2617 when qop=auth */ 498 combo = HashA1+ ":" + nonceString + ":" + ncValue + ":" + 499 cnonce + ":auth:" +HashA2; 500 501 } else { /* for compatibility with RFC2069 */ 502 combo = HashA1 + ":" + 503 nonceString + ":" + 504 HashA2; 505 } 506 finalHash = encode(combo, null, md); 507 return finalHash; 508 } 509 510 private static final char charArray[] = { 511 '0', '1', '2', '3', '4', '5', '6', '7', 512 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' 513 }; 514 515 private static final String zeroPad[] = { 516 // 0 1 2 3 4 5 6 7 517 "00000000", "0000000", "000000", "00000", "0000", "000", "00", "0" 518 }; 519 520 private String encode(String src, char[] passwd, MessageDigest md) { 521 try { 522 md.update(src.getBytes("ISO-8859-1")); 523 } catch (java.io.UnsupportedEncodingException uee) { 524 assert false; 525 } 526 if (passwd != null) { 527 byte[] passwdBytes = new byte[passwd.length]; 528 for (int i=0; i<passwd.length; i++) 529 passwdBytes[i] = (byte)passwd[i]; 530 md.update(passwdBytes); 531 Arrays.fill(passwdBytes, (byte)0x00); 532 } 533 byte[] digest = md.digest(); 534 535 StringBuilder res = new StringBuilder(digest.length * 2); 536 for (int i = 0; i < digest.length; i++) { 537 int hashchar = ((digest[i] >>> 4) & 0xf); 538 res.append(charArray[hashchar]); 539 hashchar = (digest[i] & 0xf); 540 res.append(charArray[hashchar]); 541 } 542 return res.toString(); 543 } 544} 545