|
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 |
|
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 sun.net.NetProperties; |
|
38 import java.security.MessageDigest; |
|
39 import java.security.NoSuchAlgorithmException; |
|
40 import java.security.PrivilegedAction; |
|
41 import java.security.AccessController; |
|
42 import 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 |
|
51 class DigestAuthentication extends AuthenticationInfo { |
|
52 |
|
53 private static final long serialVersionUID = 100L; |
|
54 |
|
55 private String authMethod; |
|
56 |
|
57 private final static 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<Boolean>() { |
|
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 final static char charArray[] = { |
|
511 '0', '1', '2', '3', '4', '5', '6', '7', |
|
512 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' |
|
513 }; |
|
514 |
|
515 private final static 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 } |