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