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