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 java.util.Objects;
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                String items[] = qop.split(",");
150                for (String item : items) {
151                    if ("auth".equalsIgnoreCase(item.trim())) {
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 (nonce == null || !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, String authenticatorKey) {
198        super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
199              AuthScheme.DIGEST,
200              url,
201              realm,
202              Objects.requireNonNull(authenticatorKey));
203        this.authMethod = authMethod;
204        this.pw = pw;
205        this.params = params;
206    }
207
208    public DigestAuthentication(boolean isProxy, String host, int port, String realm,
209                                String authMethod, PasswordAuthentication pw,
210                                Parameters params, String authenticatorKey) {
211        super(isProxy ? PROXY_AUTHENTICATION : SERVER_AUTHENTICATION,
212              AuthScheme.DIGEST,
213              host,
214              port,
215              realm,
216              Objects.requireNonNull(authenticatorKey));
217        this.authMethod = authMethod;
218        this.pw = pw;
219        this.params = params;
220    }
221
222    /**
223     * @return true if this authentication supports preemptive authorization
224     */
225    @Override
226    public boolean supportsPreemptiveAuthorization() {
227        return true;
228    }
229
230    /**
231     * Recalculates the request-digest and returns it.
232     *
233     * <P> Used in the common case where the requestURI is simply the
234     * abs_path.
235     *
236     * @param  url
237     *         the URL
238     *
239     * @param  method
240     *         the HTTP method
241     *
242     * @return the value of the HTTP header this authentication wants set
243     */
244    @Override
245    public String getHeaderValue(URL url, String method) {
246        return getHeaderValueImpl(url.getFile(), method);
247    }
248
249    /**
250     * Recalculates the request-digest and returns it.
251     *
252     * <P> Used when the requestURI is not the abs_path. The exact
253     * requestURI can be passed as a String.
254     *
255     * @param  requestURI
256     *         the Request-URI from the HTTP request line
257     *
258     * @param  method
259     *         the HTTP method
260     *
261     * @return the value of the HTTP header this authentication wants set
262     */
263    String getHeaderValue(String requestURI, String method) {
264        return getHeaderValueImpl(requestURI, method);
265    }
266
267    /**
268     * Check if the header indicates that the current auth. parameters are stale.
269     * If so, then replace the relevant field with the new value
270     * and return true. Otherwise return false.
271     * returning true means the request can be retried with the same userid/password
272     * returning false means we have to go back to the user to ask for a new
273     * username password.
274     */
275    @Override
276    public boolean isAuthorizationStale (String header) {
277        HeaderParser p = new HeaderParser (header);
278        String s = p.findValue ("stale");
279        if (s == null || !s.equals("true"))
280            return false;
281        String newNonce = p.findValue ("nonce");
282        if (newNonce == null || "".equals(newNonce)) {
283            return false;
284        }
285        params.setNonce (newNonce);
286        return true;
287    }
288
289    /**
290     * Set header(s) on the given connection.
291     * @param conn The connection to apply the header(s) to
292     * @param p A source of header values for this connection, if needed.
293     * @param raw Raw header values for this connection, if needed.
294     * @return true if all goes well, false if no headers were set.
295     */
296    @Override
297    public boolean setHeaders(HttpURLConnection conn, HeaderParser p, String raw) {
298        params.setNonce (p.findValue("nonce"));
299        params.setOpaque (p.findValue("opaque"));
300        params.setQop (p.findValue("qop"));
301
302        String uri="";
303        String method;
304        if (type == PROXY_AUTHENTICATION &&
305                conn.tunnelState() == HttpURLConnection.TunnelState.SETUP) {
306            uri = HttpURLConnection.connectRequestURI(conn.getURL());
307            method = HTTP_CONNECT;
308        } else {
309            try {
310                uri = conn.getRequestURI();
311            } catch (IOException e) {}
312            method = conn.getMethod();
313        }
314
315        if (params.nonce == null || authMethod == null || pw == null || realm == null) {
316            return false;
317        }
318        if (authMethod.length() >= 1) {
319            // Method seems to get converted to all lower case elsewhere.
320            // It really does need to start with an upper case letter
321            // here.
322            authMethod = Character.toUpperCase(authMethod.charAt(0))
323                        + authMethod.substring(1).toLowerCase();
324        }
325        String algorithm = p.findValue("algorithm");
326        if (algorithm == null || "".equals(algorithm)) {
327            algorithm = "MD5";  // The default, accoriding to rfc2069
328        }
329        params.setAlgorithm (algorithm);
330
331        // If authQop is true, then the server is doing RFC2617 and
332        // has offered qop=auth. We do not support any other modes
333        // and if auth is not offered we fallback to the RFC2069 behavior
334
335        if (params.authQop()) {
336            params.setNewCnonce();
337        }
338
339        String value = getHeaderValueImpl (uri, method);
340        if (value != null) {
341            conn.setAuthenticationProperty(getHeaderName(), value);
342            return true;
343        } else {
344            return false;
345        }
346    }
347
348    /* Calculate the Authorization header field given the request URI
349     * and based on the authorization information in params
350     */
351    private String getHeaderValueImpl (String uri, String method) {
352        String response;
353        char[] passwd = pw.getPassword();
354        boolean qop = params.authQop();
355        String opaque = params.getOpaque();
356        String cnonce = params.getCnonce ();
357        String nonce = params.getNonce ();
358        String algorithm = params.getAlgorithm ();
359        params.incrementNC ();
360        int  nccount = params.getNCCount ();
361        String ncstring=null;
362
363        if (nccount != -1) {
364            ncstring = Integer.toHexString (nccount).toLowerCase();
365            int len = ncstring.length();
366            if (len < 8)
367                ncstring = zeroPad [len] + ncstring;
368        }
369
370        try {
371            response = computeDigest(true, pw.getUserName(),passwd,realm,
372                                        method, uri, nonce, cnonce, ncstring);
373        } catch (NoSuchAlgorithmException ex) {
374            return null;
375        }
376
377        String ncfield = "\"";
378        if (qop) {
379            ncfield = "\", nc=" + ncstring;
380        }
381
382        String algoS, qopS;
383
384        if (delimCompatFlag) {
385            // Put quotes around these String value parameters
386            algoS = ", algorithm=\"" + algorithm + "\"";
387            qopS = ", qop=\"auth\"";
388        } else {
389            // Don't put quotes around them, per the RFC
390            algoS = ", algorithm=" + algorithm;
391            qopS = ", qop=auth";
392        }
393
394        String value = authMethod
395                        + " username=\"" + pw.getUserName()
396                        + "\", realm=\"" + realm
397                        + "\", nonce=\"" + nonce
398                        + ncfield
399                        + ", uri=\"" + uri
400                        + "\", response=\"" + response + "\""
401                        + algoS;
402        if (opaque != null) {
403            value += ", opaque=\"" + opaque + "\"";
404        }
405        if (cnonce != null) {
406            value += ", cnonce=\"" + cnonce + "\"";
407        }
408        if (qop) {
409            value += qopS;
410        }
411        return value;
412    }
413
414    public void checkResponse (String header, String method, URL url)
415                                                        throws IOException {
416        checkResponse (header, method, url.getFile());
417    }
418
419    public void checkResponse (String header, String method, String uri)
420                                                        throws IOException {
421        char[] passwd = pw.getPassword();
422        String username = pw.getUserName();
423        boolean qop = params.authQop();
424        String opaque = params.getOpaque();
425        String cnonce = params.cnonce;
426        String nonce = params.getNonce ();
427        String algorithm = params.getAlgorithm ();
428        int  nccount = params.getNCCount ();
429        String ncstring=null;
430
431        if (header == null) {
432            throw new ProtocolException ("No authentication information in response");
433        }
434
435        if (nccount != -1) {
436            ncstring = Integer.toHexString (nccount).toUpperCase();
437            int len = ncstring.length();
438            if (len < 8)
439                ncstring = zeroPad [len] + ncstring;
440        }
441        try {
442            String expected = computeDigest(false, username,passwd,realm,
443                                        method, uri, nonce, cnonce, ncstring);
444            HeaderParser p = new HeaderParser (header);
445            String rspauth = p.findValue ("rspauth");
446            if (rspauth == null) {
447                throw new ProtocolException ("No digest in response");
448            }
449            if (!rspauth.equals (expected)) {
450                throw new ProtocolException ("Response digest invalid");
451            }
452            /* Check if there is a nextnonce field */
453            String nextnonce = p.findValue ("nextnonce");
454            if (nextnonce != null && ! "".equals(nextnonce)) {
455                params.setNonce (nextnonce);
456            }
457
458        } catch (NoSuchAlgorithmException ex) {
459            throw new ProtocolException ("Unsupported algorithm in response");
460        }
461    }
462
463    private String computeDigest(
464                        boolean isRequest, String userName, char[] password,
465                        String realm, String connMethod,
466                        String requestURI, String nonceString,
467                        String cnonce, String ncValue
468                    ) throws NoSuchAlgorithmException
469    {
470
471        String A1, HashA1;
472        String algorithm = params.getAlgorithm ();
473        boolean md5sess = algorithm.equalsIgnoreCase ("MD5-sess");
474
475        MessageDigest md = MessageDigest.getInstance(md5sess?"MD5":algorithm);
476
477        if (md5sess) {
478            if ((HashA1 = params.getCachedHA1 ()) == null) {
479                String s = userName + ":" + realm + ":";
480                String s1 = encode (s, password, md);
481                A1 = s1 + ":" + nonceString + ":" + cnonce;
482                HashA1 = encode(A1, null, md);
483                params.setCachedHA1 (HashA1);
484            }
485        } else {
486            A1 = userName + ":" + realm + ":";
487            HashA1 = encode(A1, password, md);
488        }
489
490        String A2;
491        if (isRequest) {
492            A2 = connMethod + ":" + requestURI;
493        } else {
494            A2 = ":" + requestURI;
495        }
496        String HashA2 = encode(A2, null, md);
497        String combo, finalHash;
498
499        if (params.authQop()) { /* RRC2617 when qop=auth */
500            combo = HashA1+ ":" + nonceString + ":" + ncValue + ":" +
501                        cnonce + ":auth:" +HashA2;
502
503        } else { /* for compatibility with RFC2069 */
504            combo = HashA1 + ":" +
505                       nonceString + ":" +
506                       HashA2;
507        }
508        finalHash = encode(combo, null, md);
509        return finalHash;
510    }
511
512    private static final char charArray[] = {
513        '0', '1', '2', '3', '4', '5', '6', '7',
514        '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
515    };
516
517    private static final String zeroPad[] = {
518        // 0         1          2         3        4       5      6     7
519        "00000000", "0000000", "000000", "00000", "0000", "000", "00", "0"
520    };
521
522    private String encode(String src, char[] passwd, MessageDigest md) {
523        try {
524            md.update(src.getBytes("ISO-8859-1"));
525        } catch (java.io.UnsupportedEncodingException uee) {
526            assert false;
527        }
528        if (passwd != null) {
529            byte[] passwdBytes = new byte[passwd.length];
530            for (int i=0; i<passwd.length; i++)
531                passwdBytes[i] = (byte)passwd[i];
532            md.update(passwdBytes);
533            Arrays.fill(passwdBytes, (byte)0x00);
534        }
535        byte[] digest = md.digest();
536
537        StringBuilder res = new StringBuilder(digest.length * 2);
538        for (int i = 0; i < digest.length; i++) {
539            int hashchar = ((digest[i] >>> 4) & 0xf);
540            res.append(charArray[hashchar]);
541            hashchar = (digest[i] & 0xf);
542            res.append(charArray[hashchar]);
543        }
544        return res.toString();
545    }
546}
547