blob: 26a852eb87b54068a8ccd637db6ba411a62a3ccd [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 1997-2005 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
26package sun.net.www.protocol.http;
27
28import java.io.*;
29import java.net.URL;
30import java.net.ProtocolException;
31import java.net.PasswordAuthentication;
32import java.util.Arrays;
33import java.util.StringTokenizer;
34import java.util.Random;
35
36import sun.net.www.HeaderParser;
37import java.security.MessageDigest;
38import java.security.NoSuchAlgorithmException;
39
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
48class DigestAuthentication extends AuthenticationInfo {
49
50 private static final long serialVersionUID = 100L;
51
52 static final char DIGEST_AUTH = 'D';
53
54 private String authMethod;
55
56 // Authentication parameters defined in RFC2617.
57 // One instance of these may be shared among several DigestAuthentication
58 // instances as a result of a single authorization (for multiple domains)
59
60 static class Parameters implements java.io.Serializable {
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, DIGEST_AUTH,url, realm);
179 this.authMethod = authMethod;
180 this.pw = pw;
181 this.params = params;
182 }
183
184 public DigestAuthentication(boolean isProxy, String host, int port, String realm,
185 String authMethod, PasswordAuthentication pw,
186 Parameters params) {
187 super(isProxy?PROXY_AUTHENTICATION:SERVER_AUTHENTICATION, DIGEST_AUTH,host, port, realm);
188 this.authMethod = authMethod;
189 this.pw = pw;
190 this.params = params;
191 }
192
193 /**
194 * @return true if this authentication supports preemptive authorization
195 */
196 boolean supportsPreemptiveAuthorization() {
197 return true;
198 }
199
200 /**
201 * @return the name of the HTTP header this authentication wants set
202 */
203 String getHeaderName() {
204 if (type == SERVER_AUTHENTICATION) {
205 return "Authorization";
206 } else {
207 return "Proxy-Authorization";
208 }
209 }
210
211 /**
212 * Reclaculates the request-digest and returns it.
213 * @return the value of the HTTP header this authentication wants set
214 */
215 String getHeaderValue(URL url, String method) {
216 return getHeaderValueImpl (url.getFile(), method);
217 }
218
219 /**
220 * Check if the header indicates that the current auth. parameters are stale.
221 * If so, then replace the relevant field with the new value
222 * and return true. Otherwise return false.
223 * returning true means the request can be retried with the same userid/password
224 * returning false means we have to go back to the user to ask for a new
225 * username password.
226 */
227 boolean isAuthorizationStale (String header) {
228 HeaderParser p = new HeaderParser (header);
229 String s = p.findValue ("stale");
230 if (s == null || !s.equals("true"))
231 return false;
232 String newNonce = p.findValue ("nonce");
233 if (newNonce == null || "".equals(newNonce)) {
234 return false;
235 }
236 params.setNonce (newNonce);
237 return true;
238 }
239
240 /**
241 * Set header(s) on the given connection.
242 * @param conn The connection to apply the header(s) to
243 * @param p A source of header values for this connection, if needed.
244 * @param raw Raw header values for this connection, if needed.
245 * @return true if all goes well, false if no headers were set.
246 */
247 boolean setHeaders(HttpURLConnection conn, HeaderParser p, String raw) {
248 params.setNonce (p.findValue("nonce"));
249 params.setOpaque (p.findValue("opaque"));
250 params.setQop (p.findValue("qop"));
251
252 String uri = conn.getURL().getFile();
253
254 if (params.nonce == null || authMethod == null || pw == null || realm == null) {
255 return false;
256 }
257 if (authMethod.length() >= 1) {
258 // Method seems to get converted to all lower case elsewhere.
259 // It really does need to start with an upper case letter
260 // here.
261 authMethod = Character.toUpperCase(authMethod.charAt(0))
262 + authMethod.substring(1).toLowerCase();
263 }
264 String algorithm = p.findValue("algorithm");
265 if (algorithm == null || "".equals(algorithm)) {
266 algorithm = "MD5"; // The default, accoriding to rfc2069
267 }
268 params.setAlgorithm (algorithm);
269
270 // If authQop is true, then the server is doing RFC2617 and
271 // has offered qop=auth. We do not support any other modes
272 // and if auth is not offered we fallback to the RFC2069 behavior
273
274 if (params.authQop()) {
275 params.setNewCnonce();
276 }
277
278 String value = getHeaderValueImpl (uri, conn.getMethod());
279 if (value != null) {
280 conn.setAuthenticationProperty(getHeaderName(), value);
281 return true;
282 } else {
283 return false;
284 }
285 }
286
287 /* Calculate the Authorization header field given the request URI
288 * and based on the authorization information in params
289 */
290 private String getHeaderValueImpl (String uri, String method) {
291 String response;
292 char[] passwd = pw.getPassword();
293 boolean qop = params.authQop();
294 String opaque = params.getOpaque();
295 String cnonce = params.getCnonce ();
296 String nonce = params.getNonce ();
297 String algorithm = params.getAlgorithm ();
298 params.incrementNC ();
299 int nccount = params.getNCCount ();
300 String ncstring=null;
301
302 if (nccount != -1) {
303 ncstring = Integer.toHexString (nccount).toLowerCase();
304 int len = ncstring.length();
305 if (len < 8)
306 ncstring = zeroPad [len] + ncstring;
307 }
308
309 try {
310 response = computeDigest(true, pw.getUserName(),passwd,realm,
311 method, uri, nonce, cnonce, ncstring);
312 } catch (NoSuchAlgorithmException ex) {
313 return null;
314 }
315
316 String ncfield = "\"";
317 if (qop) {
318 ncfield = "\", nc=" + ncstring;
319 }
320
321 String value = authMethod
322 + " username=\"" + pw.getUserName()
323 + "\", realm=\"" + realm
324 + "\", nonce=\"" + nonce
325 + ncfield
326 + ", uri=\"" + uri
327 + "\", response=\"" + response
328 + "\", algorithm=\"" + algorithm;
329 if (opaque != null) {
330 value = value + "\", opaque=\"" + opaque;
331 }
332 if (cnonce != null) {
333 value = value + "\", cnonce=\"" + cnonce;
334 }
335 if (qop) {
336 value = value + "\", qop=\"auth";
337 }
338 value = value + "\"";
339 return value;
340 }
341
342 public void checkResponse (String header, String method, URL url)
343 throws IOException {
344 String uri = url.getFile();
345 char[] passwd = pw.getPassword();
346 String username = pw.getUserName();
347 boolean qop = params.authQop();
348 String opaque = params.getOpaque();
349 String cnonce = params.cnonce;
350 String nonce = params.getNonce ();
351 String algorithm = params.getAlgorithm ();
352 int nccount = params.getNCCount ();
353 String ncstring=null;
354
355 if (header == null) {
356 throw new ProtocolException ("No authentication information in response");
357 }
358
359 if (nccount != -1) {
360 ncstring = Integer.toHexString (nccount).toUpperCase();
361 int len = ncstring.length();
362 if (len < 8)
363 ncstring = zeroPad [len] + ncstring;
364 }
365 try {
366 String expected = computeDigest(false, username,passwd,realm,
367 method, uri, nonce, cnonce, ncstring);
368 HeaderParser p = new HeaderParser (header);
369 String rspauth = p.findValue ("rspauth");
370 if (rspauth == null) {
371 throw new ProtocolException ("No digest in response");
372 }
373 if (!rspauth.equals (expected)) {
374 throw new ProtocolException ("Response digest invalid");
375 }
376 /* Check if there is a nextnonce field */
377 String nextnonce = p.findValue ("nextnonce");
378 if (nextnonce != null && ! "".equals(nextnonce)) {
379 params.setNonce (nextnonce);
380 }
381
382 } catch (NoSuchAlgorithmException ex) {
383 throw new ProtocolException ("Unsupported algorithm in response");
384 }
385 }
386
387 private String computeDigest(
388 boolean isRequest, String userName, char[] password,
389 String realm, String connMethod,
390 String requestURI, String nonceString,
391 String cnonce, String ncValue
392 ) throws NoSuchAlgorithmException
393 {
394
395 String A1, HashA1;
396 String algorithm = params.getAlgorithm ();
397 boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");
398
399 MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);
400
401 if (md5sess) {
402 if ((HashA1 = params.getCachedHA1 ()) == null) {
403 String s = userName + ":" + realm + ":";
404 String s1 = encode (s, password, md);
405 A1 = s1 + ":" + nonceString + ":" + cnonce;
406 HashA1 = encode(A1, null, md);
407 params.setCachedHA1 (HashA1);
408 }
409 } else {
410 A1 = userName + ":" + realm + ":";
411 HashA1 = encode(A1, password, md);
412 }
413
414 String A2;
415 if (isRequest) {
416 A2 = connMethod + ":" + requestURI;
417 } else {
418 A2 = ":" + requestURI;
419 }
420 String HashA2 = encode(A2, null, md);
421 String combo, finalHash;
422
423 if (params.authQop()) { /* RRC2617 when qop=auth */
424 combo = HashA1+ ":" + nonceString + ":" + ncValue + ":" +
425 cnonce + ":auth:" +HashA2;
426
427 } else { /* for compatibility with RFC2069 */
428 combo = HashA1 + ":" +
429 nonceString + ":" +
430 HashA2;
431 }
432 finalHash = encode(combo, null, md);
433 return finalHash;
434 }
435
436 private final static char charArray[] = {
437 '0', '1', '2', '3', '4', '5', '6', '7',
438 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
439 };
440
441 private final static String zeroPad[] = {
442 // 0 1 2 3 4 5 6 7
443 "00000000", "0000000", "000000", "00000", "0000", "000", "00", "0"
444 };
445
446 private String encode(String src, char[] passwd, MessageDigest md) {
447 try {
448 md.update(src.getBytes("ISO-8859-1"));
449 } catch (java.io.UnsupportedEncodingException uee) {
450 assert false;
451 }
452 if (passwd != null) {
453 byte[] passwdBytes = new byte[passwd.length];
454 for (int i=0; i<passwd.length; i++)
455 passwdBytes[i] = (byte)passwd[i];
456 md.update(passwdBytes);
457 Arrays.fill(passwdBytes, (byte)0x00);
458 }
459 byte[] digest = md.digest();
460
461 StringBuffer res = new StringBuffer(digest.length * 2);
462 for (int i = 0; i < digest.length; i++) {
463 int hashchar = ((digest[i] >>> 4) & 0xf);
464 res.append(charArray[hashchar]);
465 hashchar = (digest[i] & 0xf);
466 res.append(charArray[hashchar]);
467 }
468 return res.toString();
469 }
470}