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 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 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 */ 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 381 public void checkResponse (String header, String method, URL url) 382 throws IOException { 383 checkResponse (header, method, url.getFile()); 384 } 385 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 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 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