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.buf.HexUtils; 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 HexUtils.toHexString(thedigest); 144 } 145 146 @Override getSchemeName()147 public String getSchemeName() { 148 return schemeName; 149 } 150 } 151