MessageDigestCredentialHandler.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.catalina.realm;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.B2CConverter;
import org.apache.tomcat.util.buf.HexUtils;
import org.apache.tomcat.util.security.ConcurrentMessageDigest;
/**
* This credential handler supports the following forms of stored passwords:
* <ul>
* <li><b>encodedCredential</b> - a hex encoded digest of the password digested using the configured digest</li>
* <li><b>{MD5}encodedCredential</b> - a Base64 encoded MD5 digest of the password</li>
* <li><b>{SHA}encodedCredential</b> - a Base64 encoded SHA1 digest of the password</li>
* <li><b>{SSHA}encodedCredential</b> - 20 byte Base64 encoded SHA1 digest followed by variable length salt.
*
* <pre>
* {SSHA}<sha-1 digest:20><salt:n>
* </pre>
*
* </li>
* <li><b>salt$iterationCount$encodedCredential</b> - a hex encoded salt, iteration code and a hex encoded credential,
* each separated by $</li>
* </ul>
* <p>
* If the stored password form does not include an iteration count then an iteration count of 1 is used.
* <p>
* If the stored password form does not include salt then no salt is used.
*/
public class MessageDigestCredentialHandler extends DigestCredentialHandlerBase {
private static final Log log = LogFactory.getLog(MessageDigestCredentialHandler.class);
public static final int DEFAULT_ITERATIONS = 1;
private Charset encoding = StandardCharsets.UTF_8;
private String algorithm = null;
public String getEncoding() {
return encoding.name();
}
public void setEncoding(String encodingName) {
if (encodingName == null) {
encoding = StandardCharsets.UTF_8;
} else {
try {
this.encoding = B2CConverter.getCharset(encodingName);
} catch (UnsupportedEncodingException e) {
log.error(sm.getString("mdCredentialHandler.unknownEncoding", encodingName, encoding.name()));
}
}
}
@Override
public String getAlgorithm() {
return algorithm;
}
@Override
public void setAlgorithm(String algorithm) throws NoSuchAlgorithmException {
ConcurrentMessageDigest.init(algorithm);
this.algorithm = algorithm;
}
@Override
public boolean matches(String inputCredentials, String storedCredentials) {
if (inputCredentials == null || storedCredentials == null) {
return false;
}
if (getAlgorithm() == null) {
// No digests, compare directly
return DigestCredentialHandlerBase.equals(inputCredentials, storedCredentials, false);
} else {
// Some directories and databases prefix the password with the hash
// type. The string is in a format compatible with Base64.encode not
// the normal hex encoding of the digest
if (storedCredentials.startsWith("{MD5}") || storedCredentials.startsWith("{SHA}")) {
// Server is storing digested passwords with a prefix indicating
// the digest type
String base64ServerDigest = storedCredentials.substring(5);
byte[] userDigest = ConcurrentMessageDigest.digest(getAlgorithm(),
inputCredentials.getBytes(StandardCharsets.ISO_8859_1));
String base64UserDigest = Base64.getEncoder().encodeToString(userDigest);
return DigestCredentialHandlerBase.equals(base64UserDigest, base64ServerDigest, false);
} else if (storedCredentials.startsWith("{SSHA}")) {
// "{SSHA}<sha-1 digest:20><salt:n>"
// Need to convert the salt to bytes to apply it to the user's
// digested password.
String serverDigestPlusSalt = storedCredentials.substring(6);
byte[] serverDigestPlusSaltBytes = Base64.getDecoder().decode(serverDigestPlusSalt);
// Extract the first 20 bytes containing the SHA-1 digest
final int digestLength = 20;
byte[] serverDigestBytes = new byte[digestLength];
System.arraycopy(serverDigestPlusSaltBytes, 0, serverDigestBytes, 0, digestLength);
// the remaining bytes are the salt
final int saltLength = serverDigestPlusSaltBytes.length - digestLength;
byte[] serverSaltBytes = new byte[saltLength];
System.arraycopy(serverDigestPlusSaltBytes, digestLength, serverSaltBytes, 0, saltLength);
// Generate the digested form of the user provided password
// using the salt
byte[] userDigestBytes = ConcurrentMessageDigest.digest(getAlgorithm(),
inputCredentials.getBytes(StandardCharsets.ISO_8859_1), serverSaltBytes);
return Arrays.equals(userDigestBytes, serverDigestBytes);
} else if (storedCredentials.indexOf('$') > -1) {
return matchesSaltIterationsEncoded(inputCredentials, storedCredentials);
} else {
// Hex hashes should be compared case-insensitively
String userDigest = mutate(inputCredentials, null, 1);
if (userDigest == null) {
// Failed to mutate user credentials. Automatic fail.
// Root cause should be logged by mutate()
return false;
}
return storedCredentials.equalsIgnoreCase(userDigest);
}
}
}
@Override
protected String mutate(String inputCredentials, byte[] salt, int iterations) {
if (algorithm == null) {
return inputCredentials;
} else {
byte[] inputCredentialbytes = inputCredentials.getBytes(encoding);
byte[] userDigest;
if (salt == null) {
userDigest = ConcurrentMessageDigest.digest(algorithm, iterations, inputCredentialbytes);
} else {
userDigest = ConcurrentMessageDigest.digest(algorithm, iterations, salt, inputCredentialbytes);
}
return HexUtils.toHexString(userDigest);
}
}
@Override
protected int getDefaultIterations() {
return DEFAULT_ITERATIONS;
}
@Override
protected Log getLog() {
return log;
}
}