• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
Parameters()84         Parameters () {
85             serverQop = false;
86             opaque = null;
87             algorithm = null;
88             cachedHA1 = null;
89             nonce = null;
90             setNewCnonce();
91         }
92 
authQop()93         boolean authQop () {
94             return serverQop;
95         }
incrementNC()96         synchronized void incrementNC() {
97             NCcount ++;
98         }
getNCCount()99         synchronized int getNCCount () {
100             return NCcount;
101         }
102 
103         int cnonce_count = 0;
104 
105         /* each call increments the counter */
getCnonce()106         synchronized String getCnonce () {
107             if (cnonce_count >= cnonceRepeat) {
108                 setNewCnonce();
109             }
110             cnonce_count++;
111             return cnonce;
112         }
setNewCnonce()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 
setQop(String qop)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 
getOpaque()140         synchronized String getOpaque () { return opaque;}
setOpaque(String s)141         synchronized void setOpaque (String s) { opaque=s;}
142 
getNonce()143         synchronized String getNonce () { return nonce;}
144 
setNonce(String s)145         synchronized void setNonce (String s) {
146             if (!s.equals(nonce)) {
147                 nonce=s;
148                 NCcount = 0;
149                 redoCachedHA1 = true;
150             }
151         }
152 
getCachedHA1()153         synchronized String getCachedHA1 () {
154             if (redoCachedHA1) {
155                 return null;
156             } else {
157                 return cachedHA1;
158             }
159         }
160 
setCachedHA1(String s)161         synchronized void setCachedHA1 (String s) {
162             cachedHA1=s;
163             redoCachedHA1=false;
164         }
165 
getAlgorithm()166         synchronized String getAlgorithm () { return algorithm;}
setAlgorithm(String s)167         synchronized void setAlgorithm (String s) { algorithm=s;}
168     }
169 
170     Parameters params;
171 
172     /**
173      * Create a DigestAuthentication
174      */
DigestAuthentication(boolean isProxy, URL url, String realm, String authMethod, PasswordAuthentication pw, Parameters params)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 
DigestAuthentication(boolean isProxy, String host, int port, String realm, String authMethod, PasswordAuthentication pw, Parameters params)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
supportsPreemptiveAuthorization()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
getHeaderValue(URL url, String method)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      */
getHeaderValue(String requestURI, String method)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
isAuthorizationStale(String header)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
setHeaders(HttpURLConnection conn, HeaderParser p, String raw)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      */
getHeaderValueImpl(String uri, String method)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 
checkResponse(String header, String method, URL url)381     public void checkResponse (String header, String method, URL url)
382                                                         throws IOException {
383         checkResponse (header, method, url.getFile());
384     }
385 
checkResponse(String header, String method, String uri)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 
computeDigest( boolean isRequest, String userName, char[] password, String realm, String connMethod, String requestURI, String nonceString, String cnonce, String ncValue )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 
encode(String src, char[] passwd, MessageDigest md)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