1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 package nginx.unit.websocket;
18 
19 import java.nio.charset.StandardCharsets;
20 import java.security.MessageDigest;
21 import java.security.NoSuchAlgorithmException;
22 import java.security.SecureRandom;
23 import java.util.Map;
24 
25 import org.apache.tomcat.util.security.MD5Encoder;
26 
27 /**
28  * Authenticator supporting the DIGEST auth method.
29  */
30 public class DigestAuthenticator extends Authenticator {
31 
32     public static final String schemeName = "digest";
33     private SecureRandom cnonceGenerator;
34     private int nonceCount = 0;
35     private long cNonce;
36 
37     @Override
getAuthorization(String requestUri, String WWWAuthenticate, Map<String, Object> userProperties)38     public String getAuthorization(String requestUri, String WWWAuthenticate,
39             Map<String, Object> userProperties) throws AuthenticationException {
40 
41         String userName = (String) userProperties.get(Constants.WS_AUTHENTICATION_USER_NAME);
42         String password = (String) userProperties.get(Constants.WS_AUTHENTICATION_PASSWORD);
43 
44         if (userName == null || password == null) {
45             throw new AuthenticationException(
46                     "Failed to perform Digest authentication due to  missing user/password");
47         }
48 
49         Map<String, String> wwwAuthenticate = parseWWWAuthenticateHeader(WWWAuthenticate);
50 
51         String realm = wwwAuthenticate.get("realm");
52         String nonce = wwwAuthenticate.get("nonce");
53         String messageQop = wwwAuthenticate.get("qop");
54         String algorithm = wwwAuthenticate.get("algorithm") == null ? "MD5"
55                 : wwwAuthenticate.get("algorithm");
56         String opaque = wwwAuthenticate.get("opaque");
57 
58         StringBuilder challenge = new StringBuilder();
59 
60         if (!messageQop.isEmpty()) {
61             if (cnonceGenerator == null) {
62                 cnonceGenerator = new SecureRandom();
63             }
64 
65             cNonce = cnonceGenerator.nextLong();
66             nonceCount++;
67         }
68 
69         challenge.append("Digest ");
70         challenge.append("username =\"" + userName + "\",");
71         challenge.append("realm=\"" + realm + "\",");
72         challenge.append("nonce=\"" + nonce + "\",");
73         challenge.append("uri=\"" + requestUri + "\",");
74 
75         try {
76             challenge.append("response=\"" + calculateRequestDigest(requestUri, userName, password,
77                     realm, nonce, messageQop, algorithm) + "\",");
78         }
79 
80         catch (NoSuchAlgorithmException e) {
81             throw new AuthenticationException(
82                     "Unable to generate request digest " + e.getMessage());
83         }
84 
85         challenge.append("algorithm=" + algorithm + ",");
86         challenge.append("opaque=\"" + opaque + "\",");
87 
88         if (!messageQop.isEmpty()) {
89             challenge.append("qop=\"" + messageQop + "\"");
90             challenge.append(",cnonce=\"" + cNonce + "\",");
91             challenge.append("nc=" + String.format("%08X", Integer.valueOf(nonceCount)));
92         }
93 
94         return challenge.toString();
95 
96     }
97 
calculateRequestDigest(String requestUri, String userName, String password, String realm, String nonce, String qop, String algorithm)98     private String calculateRequestDigest(String requestUri, String userName, String password,
99             String realm, String nonce, String qop, String algorithm)
100             throws NoSuchAlgorithmException {
101 
102         StringBuilder preDigest = new StringBuilder();
103         String A1;
104 
105         if (algorithm.equalsIgnoreCase("MD5"))
106             A1 = userName + ":" + realm + ":" + password;
107 
108         else
109             A1 = encodeMD5(userName + ":" + realm + ":" + password) + ":" + nonce + ":" + cNonce;
110 
111         /*
112          * If the "qop" value is "auth-int", then A2 is: A2 = Method ":"
113          * digest-uri-value ":" H(entity-body) since we do not have an entity-body, A2 =
114          * Method ":" digest-uri-value for auth and auth_int
115          */
116         String A2 = "GET:" + requestUri;
117 
118         preDigest.append(encodeMD5(A1));
119         preDigest.append(":");
120         preDigest.append(nonce);
121 
122         if (qop.toLowerCase().contains("auth")) {
123             preDigest.append(":");
124             preDigest.append(String.format("%08X", Integer.valueOf(nonceCount)));
125             preDigest.append(":");
126             preDigest.append(String.valueOf(cNonce));
127             preDigest.append(":");
128             preDigest.append(qop);
129         }
130 
131         preDigest.append(":");
132         preDigest.append(encodeMD5(A2));
133 
134         return encodeMD5(preDigest.toString());
135 
136     }
137 
encodeMD5(String value)138     private String encodeMD5(String value) throws NoSuchAlgorithmException {
139         byte[] bytesOfMessage = value.getBytes(StandardCharsets.ISO_8859_1);
140         MessageDigest md = MessageDigest.getInstance("MD5");
141         byte[] thedigest = md.digest(bytesOfMessage);
142 
143         return MD5Encoder.encode(thedigest);
144     }
145 
146     @Override
getSchemeName()147     public String getSchemeName() {
148         return schemeName;
149     }
150 }
151