001: /*
002: * @(#)DigestAuthentication.java 1.16 06/10/10
003: *
004: * Copyright 1990-2006 Sun Microsystems, Inc. All Rights Reserved.
005: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License version
009: * 2 only, as published by the Free Software Foundation.
010: *
011: * This program is distributed in the hope that it will be useful, but
012: * WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * General Public License version 2 for more details (a copy is
015: * included at /legal/license.txt).
016: *
017: * You should have received a copy of the GNU General Public License
018: * version 2 along with this work; if not, write to the Free Software
019: * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA
021: *
022: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
023: * Clara, CA 95054 or visit www.sun.com if you need additional
024: * information or have any questions.
025: *
026: */
027:
028: package sun.net.www.protocol.http;
029:
030: import java.io.IOException;
031: import java.net.URL;
032: import java.net.ProtocolException;
033: import java.net.PasswordAuthentication;
034: import java.util.Arrays;
035: import java.util.StringTokenizer;
036: import java.util.Random;
037:
038: import sun.net.www.HeaderParser;
039: import java.security.MessageDigest;
040: import java.security.NoSuchAlgorithmException;
041:
042: /**
043: * DigestAuthentication: Encapsulate an http server authentication using
044: * the "Digest" scheme, as described in RFC2069 and updated in RFC2617
045: *
046: */
047:
048: // NOTE: This is a simple implementation of the basics of digest
049: // authentication. It's not as complete as it could be. Here
050: // are some features that it would be nice to add:
051: // * Support for the opaque field (see /home/internet/rfc/rfc2069)
052: // * Support for the nextnonce field, which would enable preemptive
053: // authorization
054: class DigestAuthentication extends AuthenticationInfo {
055:
056: static final char DIGEST_AUTH = 'D';
057:
058: PasswordAuthentication pw;
059:
060: private String authMethod;
061:
062: // Authentication parameters defined in RFC2617.
063: // One instance of these may be shared among several DigestAuthentication
064: // instances as a result of a single authorization (for multiple domains)
065:
066: static class Parameters {
067: private boolean serverQop; // server proposed qop=auth
068: private String opaque;
069: private String cnonce;
070: private String nonce;
071: private String algorithm;
072: private int NCcount;
073:
074: // The H(A1) string used for MD5-sess
075: private String cachedHA1;
076:
077: // Force the HA1 value to be recalculated because the nonce has changed
078: private boolean redoCachedHA1 = true;
079:
080: // max. number of times to reuse a client nonce value
081: // This is most useful when using MD5-sess and it effectively determines
082: // the duration of the session. Using MD5-sess reduces the computation
083: // overhead on both client and server.
084: private static int defaultCnonceRepeat = 5;
085: private static int cnonceRepeat;
086: private static final int cnoncelen = 40; /* number of characters in cnonce */
087:
088: private static Random random;
089:
090: static {
091: cnonceRepeat = ((Integer) java.security.AccessController
092: .doPrivileged(new sun.security.action.GetIntegerAction(
093: "http.auth.digest.cnonceRepeat",
094: defaultCnonceRepeat))).intValue();
095:
096: random = new Random();
097: }
098:
099: Parameters() {
100: serverQop = false;
101: opaque = null;
102: algorithm = null;
103: cachedHA1 = null;
104: nonce = null;
105: setNewCnonce();
106: }
107:
108: boolean authQop() {
109: return serverQop;
110: }
111:
112: synchronized int getNCCount() {
113: return NCcount;
114: }
115:
116: /* each call increments the counter */
117: synchronized String getCnonce() {
118: if (NCcount >= cnonceRepeat) {
119: setNewCnonce();
120: }
121: NCcount++;
122: return cnonce;
123: }
124:
125: synchronized void setNewCnonce() {
126: byte bb[] = new byte[cnoncelen / 2];
127: char cc[] = new char[cnoncelen];
128: random.nextBytes(bb);
129: for (int i = 0; i < (cnoncelen / 2); i++) {
130: int x = bb[i] + 128;
131: cc[i * 2] = (char) ('A' + x / 16);
132: cc[i * 2 + 1] = (char) ('A' + x % 16);
133: }
134: cnonce = new String(cc, 0, cnoncelen);
135: NCcount = 0;
136: redoCachedHA1 = true;
137: }
138:
139: synchronized void setQop(String qop) {
140: if (qop != null) {
141: StringTokenizer st = new StringTokenizer(qop, " ");
142: while (st.hasMoreTokens()) {
143: if (st.nextToken().equalsIgnoreCase("auth")) {
144: serverQop = true;
145: return;
146: }
147: }
148: }
149: serverQop = false;
150: }
151:
152: synchronized String getOpaque() {
153: return opaque;
154: }
155:
156: synchronized void setOpaque(String s) {
157: opaque = s;
158: }
159:
160: synchronized String getNonce() {
161: return nonce;
162: }
163:
164: synchronized void setNonce(String s) {
165: nonce = s;
166: redoCachedHA1 = true;
167: }
168:
169: synchronized String getCachedHA1() {
170: if (redoCachedHA1) {
171: return null;
172: } else {
173: return cachedHA1;
174: }
175: }
176:
177: synchronized void setCachedHA1(String s) {
178: cachedHA1 = s;
179: redoCachedHA1 = false;
180: }
181:
182: synchronized String getAlgorithm() {
183: return algorithm;
184: }
185:
186: synchronized void setAlgorithm(String s) {
187: algorithm = s;
188: }
189: }
190:
191: Parameters params;
192:
193: /**
194: * Create a DigestAuthentication
195: */
196: public DigestAuthentication(boolean isProxy, URL url, String realm,
197: String authMethod, PasswordAuthentication pw,
198: Parameters params) {
199: super (isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
200: DIGEST_AUTH, url, realm);
201: this .authMethod = authMethod;
202: this .pw = pw;
203: this .params = params;
204: }
205:
206: public DigestAuthentication(boolean isProxy, String host, int port,
207: String realm, String authMethod, PasswordAuthentication pw,
208: Parameters params) {
209: super (isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
210: DIGEST_AUTH, host, port, realm);
211: this .authMethod = authMethod;
212: this .pw = pw;
213: this .params = params;
214: }
215:
216: /**
217: * @return true if this authentication supports preemptive authorization
218: */
219: boolean supportsPreemptiveAuthorization() {
220: return true;
221: }
222:
223: /**
224: * @return the name of the HTTP header this authentication wants set
225: */
226: String getHeaderName() {
227: if (type == SERVER_AUTHENTICATION) {
228: return "Authorization";
229: } else {
230: return "Proxy-Authorization";
231: }
232: }
233:
234: /**
235: * Reclaculates the request-digest and returns it.
236: * @return the value of the HTTP header this authentication wants set
237: */
238: String getHeaderValue(URL url, String method) {
239: return getHeaderValueImpl(url.getFile(), method);
240: }
241:
242: /**
243: * Check if the header indicates that the current auth. parameters are stale.
244: * If so, then replace the relevant field with the new value
245: * and return true. Otherwise return false.
246: * returning true means the request can be retried with the same userid/password
247: * returning false means we have to go back to the user to ask for a new
248: * username password.
249: */
250: boolean isAuthorizationStale(String header) {
251: HeaderParser p = new HeaderParser(header);
252: String s = p.findValue("stale");
253: if (s == null || !s.equals("true"))
254: return false;
255: String newNonce = p.findValue("nonce");
256: if (newNonce == null || "".equals(newNonce)) {
257: return false;
258: }
259: params.setNonce(newNonce);
260: return true;
261: }
262:
263: /**
264: * Set header(s) on the given connection.
265: * @param conn The connection to apply the header(s) to
266: * @param p A source of header values for this connection, if needed.
267: * @param raw Raw header values for this connection, if needed.
268: * @return true if all goes well, false if no headers were set.
269: */
270: boolean setHeaders(HttpURLConnection conn, HeaderParser p,
271: String raw) {
272: params.setNonce(p.findValue("nonce"));
273: params.setOpaque(p.findValue("opaque"));
274: params.setQop(p.findValue("qop"));
275:
276: String uri = conn.getURL().getFile();
277:
278: if (params.nonce == null || authMethod == null || pw == null
279: || realm == null) {
280: return false;
281: }
282: if (authMethod.length() >= 1) {
283: // Method seems to get converted to all lower case elsewhere.
284: // It really does need to start with an upper case letter
285: // here.
286: authMethod = Character.toUpperCase(authMethod.charAt(0))
287: + authMethod.substring(1).toLowerCase();
288: }
289: String algorithm = p.findValue("algorithm");
290: if (algorithm == null || "".equals(algorithm)) {
291: algorithm = "MD5"; // The default, accoriding to rfc2069
292: }
293: params.setAlgorithm(algorithm);
294:
295: // If authQop is true, then the server is doing RFC2617 and
296: // has offered qop=auth. We do not support any other modes
297: // and if auth is not offered we fallback to the RFC2069 behavior
298:
299: if (params.authQop()) {
300: params.setNewCnonce();
301: }
302:
303: String value = getHeaderValueImpl(uri, conn.getMethod());
304: if (value != null) {
305: conn.setAuthenticationProperty(getHeaderName(), value);
306: return true;
307: } else {
308: return false;
309: }
310: }
311:
312: /* Calculate the Authorization header field given the request URI
313: * and based on the authorization information in params
314: */
315: private String getHeaderValueImpl(String uri, String method) {
316: String response;
317: char[] passwd = pw.getPassword();
318: boolean qop = params.authQop();
319: String opaque = params.getOpaque();
320: String cnonce = params.getCnonce();
321: String nonce = params.getNonce();
322: String algorithm = params.getAlgorithm();
323: int cncount = params.getNCCount();
324: String cnstring = null;
325:
326: if (cncount != -1) {
327: cnstring = Integer.toHexString(cncount).toLowerCase();
328: int len = cnstring.length();
329: if (len < 8)
330: cnstring = zeroPad[len] + cnstring;
331: }
332:
333: try {
334: response = computeDigest(true, pw.getUserName(), passwd,
335: realm, method, uri, nonce, cnonce, cnstring);
336: } catch (NoSuchAlgorithmException ex) {
337: return null;
338: }
339:
340: String value = authMethod + " username=\"" + pw.getUserName()
341: + "\", realm=\"" + realm + "\", nonce=\"" + nonce
342: + "\", uri=\"" + uri + "\", response=\"" + response
343: + "\", algorithm=\"" + algorithm;
344: if (opaque != null) {
345: value = value + "\", opaque=\"" + opaque;
346: }
347: if (cnonce != null) {
348: value = value + "\", nc=" + cnstring;
349: value = value + ", cnonce=\"" + cnonce;
350: }
351: if (qop) {
352: value = value + "\", qop=\"auth";
353: }
354: value = value + "\"";
355: return value;
356: }
357:
358: public void checkResponse(String header, String method, URL url)
359: throws IOException {
360: String uri = url.getFile();
361: char[] passwd = pw.getPassword();
362: String username = pw.getUserName();
363: boolean qop = params.authQop();
364: String opaque = params.getOpaque();
365: String cnonce = params.cnonce;
366: String nonce = params.getNonce();
367: String algorithm = params.getAlgorithm();
368: int cncount = params.getNCCount();
369: String cnstring = null;
370:
371: if (header == null) {
372: throw new ProtocolException(
373: "No authentication information in response");
374: }
375:
376: if (cncount != -1) {
377: cnstring = Integer.toHexString(cncount).toUpperCase();
378: int len = cnstring.length();
379: if (len < 8)
380: cnstring = zeroPad[len] + cnstring;
381: }
382: try {
383: String expected = computeDigest(false, username, passwd,
384: realm, method, uri, nonce, cnonce, cnstring);
385: HeaderParser p = new HeaderParser(header);
386: String rspauth = p.findValue("rspauth");
387: if (rspauth == null) {
388: throw new ProtocolException("No digest in response");
389: }
390: if (!rspauth.equals(expected)) {
391: throw new ProtocolException("Response digest invalid");
392: }
393: /* Check if there is a nextnonce field */
394: String nextnonce = p.findValue("nextnonce");
395: if (nextnonce != null && !"".equals(nextnonce)) {
396: params.setNonce(nextnonce);
397: }
398:
399: } catch (NoSuchAlgorithmException ex) {
400: throw new ProtocolException(
401: "Unsupported algorithm in response");
402: }
403: }
404:
405: private String computeDigest(boolean isRequest, String userName,
406: char[] password, String realm, String connMethod,
407: String requestURI, String nonceString, String cnonce,
408: String ncValue) throws NoSuchAlgorithmException {
409:
410: String A1, HashA1;
411: String algorithm = params.getAlgorithm();
412: boolean md5sess = algorithm.equalsIgnoreCase("MD5-sess");
413:
414: MessageDigest md = MessageDigest.getInstance(md5sess ? "MD5"
415: : algorithm);
416:
417: if (md5sess) {
418: if ((HashA1 = params.getCachedHA1()) == null) {
419: String s = userName + ":" + realm + ":";
420: String s1 = encode(s, password, md);
421: A1 = s1 + ":" + nonceString + ":" + cnonce;
422: HashA1 = encode(A1, null, md);
423: params.setCachedHA1(HashA1);
424: }
425: } else {
426: A1 = userName + ":" + realm + ":";
427: HashA1 = encode(A1, password, md);
428: }
429:
430: String A2;
431: if (isRequest) {
432: A2 = connMethod + ":" + requestURI;
433: } else {
434: A2 = ":" + requestURI;
435: }
436: String HashA2 = encode(A2, null, md);
437: String combo, finalHash;
438:
439: if (params.authQop()) { /* RRC2617 when qop=auth */
440: combo = HashA1 + ":" + nonceString + ":" + ncValue + ":"
441: + cnonce + ":auth:" + HashA2;
442:
443: } else { /* for compatibility with RFC2069 */
444: combo = HashA1 + ":" + nonceString + ":" + HashA2;
445: }
446: finalHash = encode(combo, null, md);
447: return finalHash;
448: }
449:
450: private final static char charArray[] = { '0', '1', '2', '3', '4',
451: '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
452:
453: private final static String zeroPad[] = {
454: // 0 1 2 3 4 5 6 7
455: "00000000", "0000000", "000000", "00000", "0000", "000",
456: "00", "0" };
457:
458: private String encode(String src, char[] passwd, MessageDigest md) {
459: md.update(src.getBytes());
460: if (passwd != null) {
461: byte[] passwdBytes = new byte[passwd.length];
462: for (int i = 0; i < passwd.length; i++)
463: passwdBytes[i] = (byte) passwd[i];
464: md.update(passwdBytes);
465: Arrays.fill(passwdBytes, (byte) 0x00);
466: }
467: byte[] digest = md.digest();
468:
469: StringBuffer res = new StringBuffer(digest.length * 2);
470: for (int i = 0; i < digest.length; i++) {
471: int hashchar = ((digest[i] >>> 4) & 0xf);
472: res.append(charArray[hashchar]);
473: hashchar = (digest[i] & 0xf);
474: res.append(charArray[hashchar]);
475: }
476: return res.toString();
477: }
478: }
|