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