/* * 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 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.codec.binary.Base64; 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 character salt followed by the salted * SHA1 digest Base64 encoded</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.warn(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 storedCredentials.equals(inputCredentials); } 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 serverDigest = storedCredentials.substring(5); String userDigest = Base64.encodeBase64String(ConcurrentMessageDigest.digest( getAlgorithm(), inputCredentials.getBytes(StandardCharsets.ISO_8859_1))); return userDigest.equals(serverDigest); } else if (storedCredentials.startsWith("{SSHA}")) { // Server is storing digested passwords with a prefix indicating // the digest type and the salt used when creating that digest String serverDigestPlusSalt = storedCredentials.substring(6); // Need to convert the salt to bytes to apply it to the user's // digested password. byte[] serverDigestPlusSaltBytes = Base64.decodeBase64(serverDigestPlusSalt); final int saltPos = 20; byte[] serverDigestBytes = new byte[saltPos]; System.arraycopy(serverDigestPlusSaltBytes, 0, serverDigestBytes, 0, saltPos); final int saltLength = serverDigestPlusSaltBytes.length - saltPos; byte[] serverSaltBytes = new byte[saltLength]; System.arraycopy(serverDigestPlusSaltBytes, saltPos, 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[] userDigest; if (salt == null) { userDigest = ConcurrentMessageDigest.digest(algorithm, iterations, inputCredentials.getBytes(encoding)); } else { userDigest = ConcurrentMessageDigest.digest(algorithm, iterations, salt, inputCredentials.getBytes(encoding)); } return HexUtils.toHexString(userDigest); } } @Override protected int getDefaultIterations() { return DEFAULT_ITERATIONS; } @Override protected Log getLog() { return log; } }