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