DigestAuthentication.java revision 13536:d354886acd3f
1/*
2 * Copyright (c) 1997, 2016, 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.Random;
34
35import sun.net.www.HeaderParser;
36import sun.net.NetProperties;
37import java.security.MessageDigest;
38import java.security.NoSuchAlgorithmException;
39import java.security.PrivilegedAction;
40import java.security.AccessController;
41import static sun.net.www.protocol.http.HttpURLConnection.HTTP_CONNECT;
42
43/**
44 * DigestAuthentication: Encapsulate an http server authentication using
45 * the "Digest" scheme, as described in RFC2069 and updated in RFC2617
46 *
47 * @author Bill Foote
48 */
49
50class DigestAuthentication extends AuthenticationInfo {
51
52    private static final long serialVersionUID = 100L;
53
54    private String authMethod;
55
56    private static final String compatPropName = "http.auth.digest." +
57        "quoteParameters";
58
59    // true if http.auth.digest.quoteParameters Net property is true
60    private static final boolean delimCompatFlag;
61
62    static {
63        Boolean b = AccessController.doPrivileged(
64            new PrivilegedAction<>() {
65                public Boolean run() {
66                    return NetProperties.getBoolean(compatPropName);
67                }
68            }
69        );
70        delimCompatFlag = (b == null) ? false : b.booleanValue();
71    }
72
73    // Authentication parameters defined in RFC2617.
74    // One instance of these may be shared among several DigestAuthentication
75    // instances as a result of a single authorization (for multiple domains)
76
77    static class Parameters implements java.io.Serializable {
78        private static final long serialVersionUID = -3584543755194526252L;
79
80        private boolean serverQop; // server proposed qop=auth
81        private String opaque;
82        private String cnonce;
83        private String nonce;
84        private String algorithm;
85        private int NCcount=0;
86
87        // The H(A1) string used for MD5-sess
88        private String  cachedHA1;
89
90        // Force the HA1 value to be recalculated because the nonce has changed
91        private boolean redoCachedHA1 = true;
92
93        private static final int cnonceRepeat = 5;
94
95        private static final int cnoncelen = 40; /* number of characters in cnonce */
96
97        private static Random   random;
98
99        static {
100            random = new Random();
101        }
102
103        Parameters () {
104            serverQop = false;
105            opaque = null;
106            algorithm = null;
107            cachedHA1 = null;
108            nonce = null;
109            setNewCnonce();
110        }
111
112        boolean authQop () {
113            return serverQop;
114        }
115        synchronized void incrementNC() {
116            NCcount ++;
117        }
118        synchronized int getNCCount () {
119            return NCcount;
120        }
121
122        int cnonce_count = 0;
123
124        /* each call increments the counter */
125        synchronized String getCnonce () {
126            if (cnonce_count >= cnonceRepeat) {
127                setNewCnonce();
128            }
129            cnonce_count++;
130            return cnonce;
131        }
132        synchronized void setNewCnonce () {
133            byte bb[] = new byte [cnoncelen/2];
134            char cc[] = new char [cnoncelen];
135            random.nextBytes (bb);
136            for (int  i=0; i<(cnoncelen/2); i++) {
137                int x = bb[i] + 128;
138                cc[i*2]= (char) ('A'+ x/16);
139                cc[i*2+1]= (char) ('A'+ x%16);
140            }
141            cnonce = new String (cc, 0, cnoncelen);
142            cnonce_count = 0;
143            redoCachedHA1 = true;
144        }
145
146        synchronized void setQop (String qop) {
147            if (qop != null) {
148                String items[] = qop.split(",");
149                for (String item : items) {
150                    if ("auth".equalsIgnoreCase(item.trim())) {
151                        serverQop = true;
152                        return;
153                    }
154                }
155            }
156            serverQop = false;
157        }
158
159        synchronized String getOpaque () { return opaque;}
160        synchronized void setOpaque (String s) { opaque=s;}
161
162        synchronized String getNonce () { return nonce;}
163
164        synchronized void setNonce (String s) {
165            if (nonce == null || !s.equals(nonce)) {
166                nonce=s;
167                NCcount = 0;
168                redoCachedHA1 = true;
169            }
170        }
171
172        synchronized String getCachedHA1 () {
173            if (redoCachedHA1) {
174                return null;
175            } else {
176                return cachedHA1;
177            }
178        }
179
180        synchronized void setCachedHA1 (String s) {
181            cachedHA1=s;
182            redoCachedHA1=false;
183        }
184
185        synchronized String getAlgorithm () { return algorithm;}
186        synchronized void setAlgorithm (String s) { algorithm=s;}
187    }
188
189    Parameters params;
190
191    /**
192     * Create a DigestAuthentication
193     */
194    public DigestAuthentication(boolean isProxy, URL url, String realm,
195                                String authMethod, PasswordAuthentication pw,
196                                Parameters params) {
197        super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
198              AuthScheme.DIGEST,
199              url,
200              realm);
201        this.authMethod = authMethod;
202        this.pw = pw;
203        this.params = params;
204    }
205
206    public DigestAuthentication(boolean isProxy, String host, int port, String realm,
207                                String authMethod, PasswordAuthentication pw,
208                                Parameters params) {
209        super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
210              AuthScheme.DIGEST,
211              host,
212              port,
213              realm);
214        this.authMethod = authMethod;
215        this.pw = pw;
216        this.params = params;
217    }
218
219    /**
220     * @return true if this authentication supports preemptive authorization
221     */
222    @Override
223    public boolean supportsPreemptiveAuthorization() {
224        return true;
225    }
226
227    /**
228     * Recalculates the request-digest and returns it.
229     *
230     * <P> Used in the common case where the requestURI is simply the
231     * abs_path.
232     *
233     * @param  url
234     *         the URL
235     *
236     * @param  method
237     *         the HTTP method
238     *
239     * @return the value of the HTTP header this authentication wants set
240     */
241    @Override
242    public String getHeaderValue(URL url, String method) {
243        return getHeaderValueImpl(url.getFile(), method);
244    }
245
246    /**
247     * Recalculates the request-digest and returns it.
248     *
249     * <P> Used when the requestURI is not the abs_path. The exact
250     * requestURI can be passed as a String.
251     *
252     * @param  requestURI
253     *         the Request-URI from the HTTP request line
254     *
255     * @param  method
256     *         the HTTP method
257     *
258     * @return the value of the HTTP header this authentication wants set
259     */
260    String getHeaderValue(String requestURI, String method) {
261        return getHeaderValueImpl(requestURI, method);
262    }
263
264    /**
265     * Check if the header indicates that the current auth. parameters are stale.
266     * If so, then replace the relevant field with the new value
267     * and return true. Otherwise return false.
268     * returning true means the request can be retried with the same userid/password
269     * returning false means we have to go back to the user to ask for a new
270     * username password.
271     */
272    @Override
273    public boolean isAuthorizationStale (String header) {
274        HeaderParser p = new HeaderParser (header);
275        String s = p.findValue ("stale");
276        if (s == null || !s.equals("true"))
277            return false;
278        String newNonce = p.findValue ("nonce");
279        if (newNonce == null || "".equals(newNonce)) {
280            return false;
281        }
282        params.setNonce (newNonce);
283        return true;
284    }
285
286    /**
287     * Set header(s) on the given connection.
288     * @param conn The connection to apply the header(s) to
289     * @param p A source of header values for this connection, if needed.
290     * @param raw Raw header values for this connection, if needed.
291     * @return true if all goes well, false if no headers were set.
292     */
293    @Override
294    public boolean setHeaders(HttpURLConnection conn, HeaderParser p, String raw) {
295        params.setNonce (p.findValue("nonce"));
296        params.setOpaque (p.findValue("opaque"));
297        params.setQop (p.findValue("qop"));
298
299        String uri="";
300        String method;
301        if (type == PROXY_AUTHENTICATION &&
302                conn.tunnelState() == HttpURLConnection.TunnelState.SETUP) {
303            uri = HttpURLConnection.connectRequestURI(conn.getURL());
304            method = HTTP_CONNECT;
305        } else {
306            try {
307                uri = conn.getRequestURI();
308            } catch (IOException e) {}
309            method = conn.getMethod();
310        }
311
312        if (params.nonce == null || authMethod == null || pw == null || realm == null) {
313            return false;
314        }
315        if (authMethod.length() >= 1) {
316            // Method seems to get converted to all lower case elsewhere.
317            // It really does need to start with an upper case letter
318            // here.
319            authMethod = Character.toUpperCase(authMethod.charAt(0))
320                        + authMethod.substring(1).toLowerCase();
321        }
322        String algorithm = p.findValue("algorithm");
323        if (algorithm == null || "".equals(algorithm)) {
324            algorithm = "MD5";  // The default, accoriding to rfc2069
325        }
326        params.setAlgorithm (algorithm);
327
328        // If authQop is true, then the server is doing RFC2617 and
329        // has offered qop=auth. We do not support any other modes
330        // and if auth is not offered we fallback to the RFC2069 behavior
331
332        if (params.authQop()) {
333            params.setNewCnonce();
334        }
335
336        String value = getHeaderValueImpl (uri, method);
337        if (value != null) {
338            conn.setAuthenticationProperty(getHeaderName(), value);
339            return true;
340        } else {
341            return false;
342        }
343    }
344
345    /* Calculate the Authorization header field given the request URI
346     * and based on the authorization information in params
347     */
348    private String getHeaderValueImpl (String uri, String method) {
349        String response;
350        char[] passwd = pw.getPassword();
351        boolean qop = params.authQop();
352        String opaque = params.getOpaque();
353        String cnonce = params.getCnonce ();
354        String nonce = params.getNonce ();
355        String algorithm = params.getAlgorithm ();
356        params.incrementNC ();
357        int  nccount = params.getNCCount ();
358        String ncstring=null;
359
360        if (nccount != -1) {
361            ncstring = Integer.toHexString (nccount).toLowerCase();
362            int len = ncstring.length();
363            if (len < 8)
364                ncstring = zeroPad [len] + ncstring;
365        }
366
367        try {
368            response = computeDigest(true, pw.getUserName(),passwd,realm,
369                                        method, uri, nonce, cnonce, ncstring);
370        } catch (NoSuchAlgorithmException ex) {
371            return null;
372        }
373
374        String ncfield = "\"";
375        if (qop) {
376            ncfield = "\", nc=" + ncstring;
377        }
378
379        String algoS, qopS;
380
381        if (delimCompatFlag) {
382            // Put quotes around these String value parameters
383            algoS = ", algorithm=\"" + algorithm + "\"";
384            qopS = ", qop=\"auth\"";
385        } else {
386            // Don't put quotes around them, per the RFC
387            algoS = ", algorithm=" + algorithm;
388            qopS = ", qop=auth";
389        }
390
391        String value = authMethod
392                        + " username=\"" + pw.getUserName()
393                        + "\", realm=\"" + realm
394                        + "\", nonce=\"" + nonce
395                        + ncfield
396                        + ", uri=\"" + uri
397                        + "\", response=\"" + response + "\""
398                        + algoS;
399        if (opaque != null) {
400            value += ", opaque=\"" + opaque + "\"";
401        }
402        if (cnonce != null) {
403            value += ", cnonce=\"" + cnonce + "\"";
404        }
405        if (qop) {
406            value += qopS;
407        }
408        return value;
409    }
410
411    public void checkResponse (String header, String method, URL url)
412                                                        throws IOException {
413        checkResponse (header, method, url.getFile());
414    }
415
416    public void checkResponse (String header, String method, String uri)
417                                                        throws IOException {
418        char[] passwd = pw.getPassword();
419        String username = pw.getUserName();
420        boolean qop = params.authQop();
421        String opaque = params.getOpaque();
422        String cnonce = params.cnonce;
423        String nonce = params.getNonce ();
424        String algorithm = params.getAlgorithm ();
425        int  nccount = params.getNCCount ();
426        String ncstring=null;
427
428        if (header == null) {
429            throw new ProtocolException ("No authentication information in response");
430        }
431
432        if (nccount != -1) {
433            ncstring = Integer.toHexString (nccount).toUpperCase();
434            int len = ncstring.length();
435            if (len < 8)
436                ncstring = zeroPad [len] + ncstring;
437        }
438        try {
439            String expected = computeDigest(false, username,passwd,realm,
440                                        method, uri, nonce, cnonce, ncstring);
441            HeaderParser p = new HeaderParser (header);
442            String rspauth = p.findValue ("rspauth");
443            if (rspauth == null) {
444                throw new ProtocolException ("No digest in response");
445            }
446            if (!rspauth.equals (expected)) {
447                throw new ProtocolException ("Response digest invalid");
448            }
449            /* Check if there is a nextnonce field */
450            String nextnonce = p.findValue ("nextnonce");
451            if (nextnonce != null && ! "".equals(nextnonce)) {
452                params.setNonce (nextnonce);
453            }
454
455        } catch (NoSuchAlgorithmException ex) {
456            throw new ProtocolException ("Unsupported algorithm in response");
457        }
458    }
459
460    private String computeDigest(
461                        boolean isRequest, String userName, char[] password,
462                        String realm, String connMethod,
463                        String requestURI, String nonceString,
464                        String cnonce, String ncValue
465                    ) throws NoSuchAlgorithmException
466    {
467
468        String A1, HashA1;
469        String algorithm = params.getAlgorithm ();
470        boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");
471
472        MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);
473
474        if (md5sess) {
475            if ((HashA1 = params.getCachedHA1 ()) == null) {
476                String s = userName + ":" + realm + ":";
477                String s1 = encode (s, password, md);
478                A1 = s1 + ":" + nonceString + ":" + cnonce;
479                HashA1 = encode(A1, null, md);
480                params.setCachedHA1 (HashA1);
481            }
482        } else {
483            A1 = userName + ":" + realm + ":";
484            HashA1 = encode(A1, password, md);
485        }
486
487        String A2;
488        if (isRequest) {
489            A2 = connMethod + ":" + requestURI;
490        } else {
491            A2 = ":" + requestURI;
492        }
493        String HashA2 = encode(A2, null, md);
494        String combo, finalHash;
495
496        if (params.authQop()) { /* RRC2617 when qop=auth */
497            combo = HashA1+ ":" + nonceString + ":" + ncValue + ":" +
498                        cnonce + ":auth:" +HashA2;
499
500        } else { /* for compatibility with RFC2069 */
501            combo = HashA1 + ":" +
502                       nonceString + ":" +
503                       HashA2;
504        }
505        finalHash = encode(combo, null, md);
506        return finalHash;
507    }
508
509    private static final char charArray[] = {
510        '0', '1', '2', '3', '4', '5', '6', '7',
511        '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
512    };
513
514    private static final String zeroPad[] = {
515        // 0         1          2         3        4       5      6     7
516        "00000000", "0000000", "000000", "00000", "0000", "000", "00", "0"
517    };
518
519    private String encode(String src, char[] passwd, MessageDigest md) {
520        try {
521            md.update(src.getBytes("ISO-8859-1"));
522        } catch (java.io.UnsupportedEncodingException uee) {
523            assert false;
524        }
525        if (passwd != null) {
526            byte[] passwdBytes = new byte[passwd.length];
527            for (int i=0; i<passwd.length; i++)
528                passwdBytes[i] = (byte)passwd[i];
529            md.update(passwdBytes);
530            Arrays.fill(passwdBytes, (byte)0x00);
531        }
532        byte[] digest = md.digest();
533
534        StringBuilder res = new StringBuilder(digest.length * 2);
535        for (int i = 0; i < digest.length; i++) {
536            int hashchar = ((digest[i] >>> 4) & 0xf);
537            res.append(charArray[hashchar]);
538            hashchar = (digest[i] & 0xf);
539            res.append(charArray[hashchar]);
540        }
541        return res.toString();
542    }
543}
544