1 /* 2 * Copyright 1997-2008 Sun Microsystems, Inc. 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. Sun designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, 22 * CA 95054 USA or visit www.sun.com if you need additional information or 23 * have any 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 84 Parameters () { 85 serverQop = false; 86 opaque = null; 87 algorithm = null; 88 cachedHA1 = null; 89 nonce = null; 90 setNewCnonce(); 91 } 92 93 boolean authQop () { 94 return serverQop; 95 } 96 synchronized void incrementNC() { 97 NCcount ++; 98 } 99 synchronized int getNCCount () { 100 return NCcount; 101 } 102 103 int cnonce_count = 0; 104 105 /* each call increments the counter */ 106 synchronized String getCnonce () { 107 if (cnonce_count >= cnonceRepeat) { 108 setNewCnonce(); 109 } 110 cnonce_count++; 111 return cnonce; 112 } 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 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 140 synchronized String getOpaque () { return opaque;} 141 synchronized void setOpaque (String s) { opaque=s;} 142 143 synchronized String getNonce () { return nonce;} 144 145 synchronized void setNonce (String s) { 146 if (!s.equals(nonce)) { 147 nonce=s; 148 NCcount = 0; 149 redoCachedHA1 = true; 150 } 151 } 152 153 synchronized String getCachedHA1 () { 154 if (redoCachedHA1) { 155 return null; 156 } else { 157 return cachedHA1; 158 } 159 } 160 161 synchronized void setCachedHA1 (String s) { 162 cachedHA1=s; 163 redoCachedHA1=false; 164 } 165 166 synchronized String getAlgorithm () { return algorithm;} 167 synchronized void setAlgorithm (String s) { algorithm=s;} 168 } 169 170 Parameters params; 171 172 /** 173 * Create a DigestAuthentication 174 */ 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 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 boolean supportsPreemptiveAuthorization() { 204 return true; 205 } 206 207 /** 208 * @return the name of the HTTP header this authentication wants set 209 */ 210 String getHeaderName() { 211 if (type == SERVER_AUTHENTICATION) { 212 return "Authorization"; 213 } else { 214 return "Proxy-Authorization"; 215 } 216 } 217 218 /** 219 * Reclaculates the request-digest and returns it. 220 * 221 * <P> Used in the common case where the requestURI is simply the 222 * abs_path. 223 * 224 * @param url 225 * the URL 226 * 227 * @param method 228 * the HTTP method 229 * 230 * @return the value of the HTTP header this authentication wants set 231 */ 232 String getHeaderValue(URL url, String method) { 233 return getHeaderValueImpl(url.getFile(), method); 234 } 235 236 /** 237 * Reclaculates the request-digest and returns it. 238 * 239 * <P> Used when the requestURI is not the abs_path. The exact 240 * requestURI can be passed as a String. 241 * 242 * @param requestURI 243 * the Request-URI from the HTTP request line 244 * 245 * @param method 246 * the HTTP method 247 * 248 * @return the value of the HTTP header this authentication wants set 249 */ 250 String getHeaderValue(String requestURI, String method) { 251 return getHeaderValueImpl(requestURI, method); 252 } 253 254 /** 255 * Check if the header indicates that the current auth. parameters are stale. 256 * If so, then replace the relevant field with the new value 257 * and return true. Otherwise return false. 258 * returning true means the request can be retried with the same userid/password 259 * returning false means we have to go back to the user to ask for a new 260 * username password. 261 */ 262 boolean isAuthorizationStale (String header) { 263 HeaderParser p = new HeaderParser (header); 264 String s = p.findValue ("stale"); 265 if (s == null || !s.equals("true")) 266 return false; 267 String newNonce = p.findValue ("nonce"); 268 if (newNonce == null || "".equals(newNonce)) { 269 return false; 270 } 271 params.setNonce (newNonce); 272 return true; 273 } 274 275 /** 276 * Set header(s) on the given connection. 277 * @param conn The connection to apply the header(s) to 278 * @param p A source of header values for this connection, if needed. 279 * @param raw Raw header values for this connection, if needed. 280 * @return true if all goes well, false if no headers were set. 281 */ 282 boolean setHeaders(HttpURLConnection conn, HeaderParser p, String raw) { 283 params.setNonce (p.findValue("nonce")); 284 params.setOpaque (p.findValue("opaque")); 285 params.setQop (p.findValue("qop")); 286 287 String uri; 288 String method; 289 if (type == PROXY_AUTHENTICATION && 290 conn.tunnelState() == HttpURLConnection.TunnelState.SETUP) { 291 uri = HttpURLConnection.connectRequestURI(conn.getURL()); 292 method = HTTP_CONNECT; 293 } else { 294 uri = conn.getRequestURI(); 295 method = conn.getMethod(); 296 } 297 298 if (params.nonce == null || authMethod == null || pw == null || realm == null) { 299 return false; 300 } 301 if (authMethod.length() >= 1) { 302 // Method seems to get converted to all lower case elsewhere. 303 // It really does need to start with an upper case letter 304 // here. 305 authMethod = Character.toUpperCase(authMethod.charAt(0)) 306 + authMethod.substring(1).toLowerCase(); 307 } 308 String algorithm = p.findValue("algorithm"); 309 if (algorithm == null || "".equals(algorithm)) { 310 algorithm = "MD5"; // The default, accoriding to rfc2069 311 } 312 params.setAlgorithm (algorithm); 313 314 // If authQop is true, then the server is doing RFC2617 and 315 // has offered qop=auth. We do not support any other modes 316 // and if auth is not offered we fallback to the RFC2069 behavior 317 318 if (params.authQop()) { 319 params.setNewCnonce(); 320 } 321 322 String value = getHeaderValueImpl (uri, method); 323 if (value != null) { 324 conn.setAuthenticationProperty(getHeaderName(), value); 325 return true; 326 } else { 327 return false; 328 } 329 } 330 331 /* Calculate the Authorization header field given the request URI 332 * and based on the authorization information in params 333 */ 334 private String getHeaderValueImpl (String uri, String method) { 335 String response; 336 char[] passwd = pw.getPassword(); 337 boolean qop = params.authQop(); 338 String opaque = params.getOpaque(); 339 String cnonce = params.getCnonce (); 340 String nonce = params.getNonce (); 341 String algorithm = params.getAlgorithm (); 342 params.incrementNC (); 343 int nccount = params.getNCCount (); 344 String ncstring=null; 345 346 if (nccount != -1) { 347 ncstring = Integer.toHexString (nccount).toLowerCase(); 348 int len = ncstring.length(); 349 if (len < 8) 350 ncstring = zeroPad [len] + ncstring; 351 } 352 353 try { 354 response = computeDigest(true, pw.getUserName(),passwd,realm, 355 method, uri, nonce, cnonce, ncstring); 356 } catch (NoSuchAlgorithmException ex) { 357 return null; 358 } 359 360 String ncfield = "\""; 361 if (qop) { 362 ncfield = "\", nc=" + ncstring; 363 } 364 365 String value = authMethod 366 + " username=\"" + pw.getUserName() 367 + "\", realm=\"" + realm 368 + "\", nonce=\"" + nonce 369 + ncfield 370 + ", uri=\"" + uri 371 + "\", response=\"" + response 372 + "\", algorithm=\"" + algorithm; 373 if (opaque != null) { 374 value = value + "\", opaque=\"" + opaque; 375 } 376 if (cnonce != null) { 377 value = value + "\", cnonce=\"" + cnonce; 378 } 379 if (qop) { 380 value = value + "\", qop=\"auth"; 381 } 382 value = value + "\""; 383 return value; 384 } 385 386 public void checkResponse (String header, String method, URL url) 387 throws IOException { 388 checkResponse (header, method, url.getFile()); 389 } 390 391 public void checkResponse (String header, String method, String uri) 392 throws IOException { 393 char[] passwd = pw.getPassword(); 394 String username = pw.getUserName(); 395 boolean qop = params.authQop(); 396 String opaque = params.getOpaque(); 397 String cnonce = params.cnonce; 398 String nonce = params.getNonce (); 399 String algorithm = params.getAlgorithm (); 400 int nccount = params.getNCCount (); 401 String ncstring=null; 402 403 if (header == null) { 404 throw new ProtocolException ("No authentication information in response"); 405 } 406 407 if (nccount != -1) { 408 ncstring = Integer.toHexString (nccount).toUpperCase(); 409 int len = ncstring.length(); 410 if (len < 8) 411 ncstring = zeroPad [len] + ncstring; 412 } 413 try { 414 String expected = computeDigest(false, username,passwd,realm, 415 method, uri, nonce, cnonce, ncstring); 416 HeaderParser p = new HeaderParser (header); 417 String rspauth = p.findValue ("rspauth"); 418 if (rspauth == null) { 419 throw new ProtocolException ("No digest in response"); 420 } 421 if (!rspauth.equals (expected)) { 422 throw new ProtocolException ("Response digest invalid"); 423 } 424 /* Check if there is a nextnonce field */ 425 String nextnonce = p.findValue ("nextnonce"); 426 if (nextnonce != null && ! "".equals(nextnonce)) { 427 params.setNonce (nextnonce); 428 } 429 430 } catch (NoSuchAlgorithmException ex) { 431 throw new ProtocolException ("Unsupported algorithm in response"); 432 } 433 } 434 435 private String computeDigest( 436 boolean isRequest, String userName, char[] password, 437 String realm, String connMethod, 438 String requestURI, String nonceString, 439 String cnonce, String ncValue 440 ) throws NoSuchAlgorithmException 441 { 442 443 String A1, HashA1; 444 String algorithm = params.getAlgorithm (); 445 boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess"); 446 447 MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm); 448 449 if (md5sess) { 450 if ((HashA1 = params.getCachedHA1 ()) == null) { 451 String s = userName + ":" + realm + ":"; 452 String s1 = encode (s, password, md); 453 A1 = s1 + ":" + nonceString + ":" + cnonce; 454 HashA1 = encode(A1, null, md); 455 params.setCachedHA1 (HashA1); 456 } 457 } else { 458 A1 = userName + ":" + realm + ":"; 459 HashA1 = encode(A1, password, md); 460 } 461 462 String A2; 463 if (isRequest) { 464 A2 = connMethod + ":" + requestURI; 465 } else { 466 A2 = ":" + requestURI; 467 } 468 String HashA2 = encode(A2, null, md); 469 String combo, finalHash; 470 471 if (params.authQop()) { /* RRC2617 when qop=auth */ 472 combo = HashA1+ ":" + nonceString + ":" + ncValue + ":" + 473 cnonce + ":auth:" +HashA2; 474 475 } else { /* for compatibility with RFC2069 */ 476 combo = HashA1 + ":" + 477 nonceString + ":" + 478 HashA2; 479 } 480 finalHash = encode(combo, null, md); 481 return finalHash; 482 } 483 484 private final static char charArray[] = { 485 '0', '1', '2', '3', '4', '5', '6', '7', 486 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' 487 }; 488 489 private final static String zeroPad[] = { 490 // 0 1 2 3 4 5 6 7 491 "00000000", "0000000", "000000", "00000", "0000", "000", "00", "0" 492 }; 493 494 private String encode(String src, char[] passwd, MessageDigest md) { 495 try { 496 md.update(src.getBytes("ISO-8859-1")); 497 } catch (java.io.UnsupportedEncodingException uee) { 498 assert false; 499 } 500 if (passwd != null) { 501 byte[] passwdBytes = new byte[passwd.length]; 502 for (int i=0; i<passwd.length; i++) 503 passwdBytes[i] = (byte)passwd[i]; 504 md.update(passwdBytes); 505 Arrays.fill(passwdBytes, (byte)0x00); 506 } 507 byte[] digest = md.digest(); 508 509 StringBuffer res = new StringBuffer(digest.length * 2); 510 for (int i = 0; i < digest.length; i++) { 511 int hashchar = ((digest[i] >>> 4) & 0xf); 512 res.append(charArray[hashchar]); 513 hashchar = (digest[i] & 0xf); 514 res.append(charArray[hashchar]); 515 } 516 return res.toString(); 517 } 518 }