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 @Override 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 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 */ 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 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 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 uri = conn.getRequestURI(); 288 method = conn.getMethod(); 289 } 290 291 if (params.nonce == null || authMethod == null || pw == null || realm == null) { 292 return false; 293 } 294 if (authMethod.length() >= 1) { 295 // Method seems to get converted to all lower case elsewhere. 296 // It really does need to start with an upper case letter 297 // here. 298 authMethod = Character.toUpperCase(authMethod.charAt(0)) 299 + authMethod.substring(1).toLowerCase(); 300 } 301 String algorithm = p.findValue("algorithm"); 302 if (algorithm == null || "".equals(algorithm)) { 303 algorithm = "MD5"; // The default, accoriding to rfc2069 304 } 305 params.setAlgorithm (algorithm); 306 307 // If authQop is true, then the server is doing RFC2617 and 308 // has offered qop=auth. We do not support any other modes 309 // and if auth is not offered we fallback to the RFC2069 behavior 310 311 if (params.authQop()) { 312 params.setNewCnonce(); 313 } 314 315 String value = getHeaderValueImpl (uri, method); 316 if (value != null) { 317 conn.setAuthenticationProperty(getHeaderName(), value); 318 return true; 319 } else { 320 return false; 321 } 322 } 323 324 /* Calculate the Authorization header field given the request URI 325 * and based on the authorization information in params 326 */ 327 private String getHeaderValueImpl (String uri, String method) { 328 String response; 329 char[] passwd = pw.getPassword(); 330 boolean qop = params.authQop(); 331 String opaque = params.getOpaque(); 332 String cnonce = params.getCnonce (); 333 String nonce = params.getNonce (); 334 String algorithm = params.getAlgorithm (); 335 params.incrementNC (); 336 int nccount = params.getNCCount (); 337 String ncstring=null; 338 339 if (nccount != -1) { 340 ncstring = Integer.toHexString (nccount).toLowerCase(); 341 int len = ncstring.length(); 342 if (len < 8) 343 ncstring = zeroPad [len] + ncstring; 344 } 345 346 try { 347 response = computeDigest(true, pw.getUserName(),passwd,realm, 348 method, uri, nonce, cnonce, ncstring); 349 } catch (NoSuchAlgorithmException ex) { 350 return null; 351 } 352 353 String ncfield = "\""; 354 if (qop) { 355 ncfield = "\", nc=" + ncstring; 356 } 357 358 String value = authMethod 359 + " username=\"" + pw.getUserName() 360 + "\", realm=\"" + realm 361 + "\", nonce=\"" + nonce 362 + ncfield 363 + ", uri=\"" + uri 364 + "\", response=\"" + response 365 + "\", algorithm=\"" + algorithm; 366 if (opaque != null) { 367 value = value + "\", opaque=\"" + opaque; 368 } 369 if (cnonce != null) { 370 value = value + "\", cnonce=\"" + cnonce; 371 } 372 if (qop) { 373 value = value + "\", qop=\"auth"; 374 } 375 value = value + "\""; 376 return value; 377 } 378 379 public void checkResponse (String header, String method, URL url) 380 throws IOException { 381 checkResponse (header, method, url.getFile()); 382 } 383 384 public void checkResponse (String header, String method, String uri) 385 throws IOException { 386 char[] passwd = pw.getPassword(); 387 String username = pw.getUserName(); 388 boolean qop = params.authQop(); 389 String opaque = params.getOpaque(); 390 String cnonce = params.cnonce; 391 String nonce = params.getNonce (); 392 String algorithm = params.getAlgorithm (); 393 int nccount = params.getNCCount (); 394 String ncstring=null; 395 396 if (header == null) { 397 throw new ProtocolException ("No authentication information in response"); 398 } 399 400 if (nccount != -1) { 401 ncstring = Integer.toHexString (nccount).toUpperCase(); 402 int len = ncstring.length(); 403 if (len < 8) 404 ncstring = zeroPad [len] + ncstring; 405 } 406 try { 407 String expected = computeDigest(false, username,passwd,realm, 408 method, uri, nonce, cnonce, ncstring); 409 HeaderParser p = new HeaderParser (header); 410 String rspauth = p.findValue ("rspauth"); 411 if (rspauth == null) { 412 throw new ProtocolException ("No digest in response"); 413 } 414 if (!rspauth.equals (expected)) { 415 throw new ProtocolException ("Response digest invalid"); 416 } 417 /* Check if there is a nextnonce field */ 418 String nextnonce = p.findValue ("nextnonce"); 419 if (nextnonce != null && ! "".equals(nextnonce)) { 420 params.setNonce (nextnonce); 421 } 422 423 } catch (NoSuchAlgorithmException ex) { 424 throw new ProtocolException ("Unsupported algorithm in response"); 425 } 426 } 427 428 private String computeDigest( 429 boolean isRequest, String userName, char[] password, 430 String realm, String connMethod, 431 String requestURI, String nonceString, 432 String cnonce, String ncValue 433 ) throws NoSuchAlgorithmException 434 { 435 436 String A1, HashA1; 437 String algorithm = params.getAlgorithm (); 438 boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess"); 439 440 MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm); 441 442 if (md5sess) { 443 if ((HashA1 = params.getCachedHA1 ()) == null) { 444 String s = userName + ":" + realm + ":"; 445 String s1 = encode (s, password, md); 446 A1 = s1 + ":" + nonceString + ":" + cnonce; 447 HashA1 = encode(A1, null, md); 448 params.setCachedHA1 (HashA1); 449 } 450 } else { 451 A1 = userName + ":" + realm + ":"; 452 HashA1 = encode(A1, password, md); 453 } 454 455 String A2; 456 if (isRequest) { 457 A2 = connMethod + ":" + requestURI; 458 } else { 459 A2 = ":" + requestURI; 460 } 461 String HashA2 = encode(A2, null, md); 462 String combo, finalHash; 463 464 if (params.authQop()) { /* RRC2617 when qop=auth */ 465 combo = HashA1+ ":" + nonceString + ":" + ncValue + ":" + 466 cnonce + ":auth:" +HashA2; 467 468 } else { /* for compatibility with RFC2069 */ 469 combo = HashA1 + ":" + 470 nonceString + ":" + 471 HashA2; 472 } 473 finalHash = encode(combo, null, md); 474 return finalHash; 475 } 476 477 private final static char charArray[] = { 478 '0', '1', '2', '3', '4', '5', '6', '7', 479 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' 480 }; 481 482 private final static String zeroPad[] = { 483 // 0 1 2 3 4 5 6 7 484 "00000000", "0000000", "000000", "00000", "0000", "000", "00", "0" 485 }; 486 487 private String encode(String src, char[] passwd, MessageDigest md) { 488 try { 489 md.update(src.getBytes("ISO-8859-1")); 490 } catch (java.io.UnsupportedEncodingException uee) { 491 assert false; 492 } 493 if (passwd != null) { 494 byte[] passwdBytes = new byte[passwd.length]; 495 for (int i=0; i<passwd.length; i++) 496 passwdBytes[i] = (byte)passwd[i]; 497 md.update(passwdBytes); 498 Arrays.fill(passwdBytes, (byte)0x00); 499 } 500 byte[] digest = md.digest(); 501 502 StringBuffer res = new StringBuffer(digest.length * 2); 503 for (int i = 0; i < digest.length; i++) { 504 int hashchar = ((digest[i] >>> 4) & 0xf); 505 res.append(charArray[hashchar]); 506 hashchar = (digest[i] & 0xf); 507 res.append(charArray[hashchar]); 508 } 509 return res.toString(); 510 } 511 } --- EOF ---