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 org.apache.catalina.realm;
18
19 import java.io.UnsupportedEncodingException;
20 import java.nio.charset.Charset;
21 import java.nio.charset.StandardCharsets;
22 import java.security.NoSuchAlgorithmException;
23 import java.util.Arrays;
24
25 import org.apache.juli.logging.Log;
26 import org.apache.juli.logging.LogFactory;
27 import org.apache.tomcat.util.buf.B2CConverter;
28 import org.apache.tomcat.util.buf.HexUtils;
29 import org.apache.tomcat.util.codec.binary.Base64;
30 import org.apache.tomcat.util.security.ConcurrentMessageDigest;
31
32 /**
33 * This credential handler supports the following forms of stored passwords:
34 * <ul>
35 * <li><b>encodedCredential</b> - a hex encoded digest of the password digested
36 * using the configured digest</li>
37 * <li><b>{MD5}encodedCredential</b> - a Base64 encoded MD5 digest of the
38 * password</li>
39 * <li><b>{SHA}encodedCredential</b> - a Base64 encoded SHA1 digest of the
40 * password</li>
41 * <li><b>{SSHA}encodedCredential</b> - 20 character salt followed by the salted
42 * SHA1 digest Base64 encoded</li>
43 * <li><b>salt$iterationCount$encodedCredential</b> - a hex encoded salt,
44 * iteration code and a hex encoded credential, each separated by $</li>
45 * </ul>
46 *
47 * <p>
48 * If the stored password form does not include an iteration count then an
49 * iteration count of 1 is used.
50 * <p>
51 * If the stored password form does not include salt then no salt is used.
52 */
53 public class MessageDigestCredentialHandler extends DigestCredentialHandlerBase {
54
55 private static final Log log = LogFactory.getLog(MessageDigestCredentialHandler.class);
56
57 public static final int DEFAULT_ITERATIONS = 1;
58
59 private Charset encoding = StandardCharsets.UTF_8;
60 private String algorithm = null;
61
62
63 public String getEncoding() {
64 return encoding.name();
65 }
66
67
68 public void setEncoding(String encodingName) {
69 if (encodingName == null) {
70 encoding = StandardCharsets.UTF_8;
71 } else {
72 try {
73 this.encoding = B2CConverter.getCharset(encodingName);
74 } catch (UnsupportedEncodingException e) {
75 log.error(sm.getString("mdCredentialHandler.unknownEncoding",
76 encodingName, encoding.name()));
77 }
78 }
79 }
80
81
82 @Override
83 public String getAlgorithm() {
84 return algorithm;
85 }
86
87
88 @Override
89 public void setAlgorithm(String algorithm) throws NoSuchAlgorithmException {
90 ConcurrentMessageDigest.init(algorithm);
91 this.algorithm = algorithm;
92 }
93
94
95 @Override
96 public boolean matches(String inputCredentials, String storedCredentials) {
97
98 if (inputCredentials == null || storedCredentials == null) {
99 return false;
100 }
101
102 if (getAlgorithm() == null) {
103 // No digests, compare directly
104 return storedCredentials.equals(inputCredentials);
105 } else {
106 // Some directories and databases prefix the password with the hash
107 // type. The string is in a format compatible with Base64.encode not
108 // the normal hex encoding of the digest
109 if (storedCredentials.startsWith("{MD5}") ||
110 storedCredentials.startsWith("{SHA}")) {
111 // Server is storing digested passwords with a prefix indicating
112 // the digest type
113 String serverDigest = storedCredentials.substring(5);
114 String userDigest = Base64.encodeBase64String(ConcurrentMessageDigest.digest(
115 getAlgorithm(), inputCredentials.getBytes(StandardCharsets.ISO_8859_1)));
116 return userDigest.equals(serverDigest);
117
118 } else if (storedCredentials.startsWith("{SSHA}")) {
119 // Server is storing digested passwords with a prefix indicating
120 // the digest type and the salt used when creating that digest
121
122 String serverDigestPlusSalt = storedCredentials.substring(6);
123
124 // Need to convert the salt to bytes to apply it to the user's
125 // digested password.
126 byte[] serverDigestPlusSaltBytes =
127 Base64.decodeBase64(serverDigestPlusSalt);
128 final int saltPos = 20;
129 byte[] serverDigestBytes = new byte[saltPos];
130 System.arraycopy(serverDigestPlusSaltBytes, 0,
131 serverDigestBytes, 0, saltPos);
132 final int saltLength = serverDigestPlusSaltBytes.length - saltPos;
133 byte[] serverSaltBytes = new byte[saltLength];
134 System.arraycopy(serverDigestPlusSaltBytes, saltPos,
135 serverSaltBytes, 0, saltLength);
136
137 // Generate the digested form of the user provided password
138 // using the salt
139 byte[] userDigestBytes = ConcurrentMessageDigest.digest(getAlgorithm(),
140 inputCredentials.getBytes(StandardCharsets.ISO_8859_1),
141 serverSaltBytes);
142
143 return Arrays.equals(userDigestBytes, serverDigestBytes);
144
145 } else if (storedCredentials.indexOf('$') > -1) {
146 return matchesSaltIterationsEncoded(inputCredentials, storedCredentials);
147
148 } else {
149 // Hex hashes should be compared case-insensitively
150 String userDigest = mutate(inputCredentials, null, 1);
151 if (userDigest == null) {
152 // Failed to mutate user credentials. Automatic fail.
153 // Root cause should be logged by mutate()
154 return false;
155 }
156 return storedCredentials.equalsIgnoreCase(userDigest);
157 }
158 }
159 }
160
161
162 @Override
163 protected String mutate(String inputCredentials, byte[] salt, int iterations) {
164 if (algorithm == null) {
165 return inputCredentials;
166 } else {
167 byte[] userDigest;
168 if (salt == null) {
169 userDigest = ConcurrentMessageDigest.digest(algorithm, iterations,
170 inputCredentials.getBytes(encoding));
171 } else {
172 userDigest = ConcurrentMessageDigest.digest(algorithm, iterations,
173 salt, inputCredentials.getBytes(encoding));
174 }
175 return HexUtils.toHexString(userDigest);
176 }
177 }
178
179
180 @Override
181 protected int getDefaultIterations() {
182 return DEFAULT_ITERATIONS;
183 }
184
185
186 @Override
187 protected Log getLog() {
188 return log;
189 }
190 }
191