/* * 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.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Random; import org.apache.catalina.CredentialHandler; import org.apache.juli.logging.Log; import org.apache.tomcat.util.buf.HexUtils; import org.apache.tomcat.util.res.StringManager; /** * Base implementation for the Tomcat provided {@link CredentialHandler}s. */ public abstract class DigestCredentialHandlerBase implements CredentialHandler { protected static final StringManager sm = StringManager.getManager(DigestCredentialHandlerBase.class); public static final int DEFAULT_SALT_LENGTH = 32; private int iterations = getDefaultIterations(); private int saltLength = getDefaultSaltLength(); private final Object randomLock = new Object(); private volatile Random random = null; private boolean logInvalidStoredCredentials = false; /** * @return the number of iterations of the associated algorithm that will be * used when creating a new stored credential for a given input credential. */ public int getIterations() { return iterations; } /** * Set the number of iterations of the associated algorithm that will be * used when creating a new stored credential for a given input credential. * @param iterations the iterations count */ public void setIterations(int iterations) { this.iterations = iterations; } /** * @return the salt length that will be used when creating a new stored * credential for a given input credential. */ public int getSaltLength() { return saltLength; } /** * Set the salt length that will be used when creating a new stored * credential for a given input credential. * @param saltLength the salt length */ public void setSaltLength(int saltLength) { this.saltLength = saltLength; } /** * When checking input credentials against stored credentials will a warning * message be logged if invalid stored credentials are discovered? * @return <code>true</code> if logging will occur */ public boolean getLogInvalidStoredCredentials() { return logInvalidStoredCredentials; } /** * Set whether a warning message will be logged if invalid stored * credentials are discovered while checking input credentials against * stored credentials? * @param logInvalidStoredCredentials <code>true</code> to log, the * default value is <code>false</code> */ public void setLogInvalidStoredCredentials(boolean logInvalidStoredCredentials) { this.logInvalidStoredCredentials = logInvalidStoredCredentials; } @Override public String mutate(String userCredential) { byte[] salt = null; int iterations = getIterations(); int saltLength = getSaltLength(); if (saltLength == 0) { salt = new byte[0]; } else if (saltLength > 0) { // Double checked locking. OK since random is volatile. if (random == null) { synchronized (randomLock) { if (random == null) { random = new SecureRandom(); } } } salt = new byte[saltLength]; // Concurrent use of this random is unlikely to be a performance // issue as it is only used during stored password generation. random.nextBytes(salt); } String serverCredential = mutate(userCredential, salt, iterations); // Failed to generate server credential from user credential. Points to // a configuration issue. The root cause should have been logged in the // mutate() method. if (serverCredential == null) { return null; } if (saltLength == 0 && iterations == 1) { // Output the simple/old format for backwards compatibility return serverCredential; } else { StringBuilder result = new StringBuilder((saltLength << 1) + 10 + serverCredential.length() + 2); result.append(HexUtils.toHexString(salt)); result.append('$'); result.append(iterations); result.append('$'); result.append(serverCredential); return result.toString(); } } /** * Checks whether the provided credential matches the stored credential when * the stored credential is in the form salt$iteration-count$credential * * @param inputCredentials The input credential * @param storedCredentials The stored credential * * @return <code>true</code> if they match, otherwise <code>false</code> */ protected boolean matchesSaltIterationsEncoded(String inputCredentials, String storedCredentials) { if (storedCredentials == null) { // Stored credentials are invalid // This may be expected if nested credential handlers are being used logInvalidStoredCredentials(storedCredentials); return false; } int sep1 = storedCredentials.indexOf('$'); int sep2 = storedCredentials.indexOf('$', sep1 + 1); if (sep1 < 0 || sep2 < 0) { // Stored credentials are invalid // This may be expected if nested credential handlers are being used logInvalidStoredCredentials(storedCredentials); return false; } String hexSalt = storedCredentials.substring(0, sep1); int iterations = Integer.parseInt(storedCredentials.substring(sep1 + 1, sep2)); String storedHexEncoded = storedCredentials.substring(sep2 + 1); byte[] salt; try { salt = HexUtils.fromHexString(hexSalt); } catch (IllegalArgumentException iae) { logInvalidStoredCredentials(storedCredentials); return false; } String inputHexEncoded = mutate(inputCredentials, salt, iterations, HexUtils.fromHexString(storedHexEncoded).length * Byte.SIZE); if (inputHexEncoded == null) { // Failed to mutate user credentials. Automatic fail. // Root cause should be logged by mutate() return false; } return storedHexEncoded.equalsIgnoreCase(inputHexEncoded); } private void logInvalidStoredCredentials(String storedCredentials) { if (logInvalidStoredCredentials) { // Logging credentials could be a security concern but they are // invalid and that is probably a bigger problem getLog().warn(sm.getString("credentialHandler.invalidStoredCredential", storedCredentials)); } } /** * @return the default salt length used by the {@link CredentialHandler}. */ protected int getDefaultSaltLength() { return DEFAULT_SALT_LENGTH; } /** * Generates the equivalent stored credentials for the given input * credentials, salt and iterations. If the algorithm requires a key length, * the default will be used. * * @param inputCredentials User provided credentials * @param salt Salt, if any * @param iterations Number of iterations of the algorithm associated * with this CredentialHandler applied to the * inputCredentials to generate the equivalent * stored credentials * * @return The equivalent stored credentials for the given input * credentials or <code>null</code> if the generation fails */ protected abstract String mutate(String inputCredentials, byte[] salt, int iterations); /** * Generates the equivalent stored credentials for the given input * credentials, salt, iterations and key length. The default implementation * calls ignores the key length and calls * {@link #mutate(String, byte[], int)}. Sub-classes that use the key length * should override this method. * * @param inputCredentials User provided credentials * @param salt Salt, if any * @param iterations Number of iterations of the algorithm associated * with this CredentialHandler applied to the * inputCredentials to generate the equivalent * stored credentials * @param keyLength Length of the produced digest in bits for * implementations where it's applicable * * @return The equivalent stored credentials for the given input * credentials or <code>null</code> if the generation fails */ protected String mutate(String inputCredentials, byte[] salt, int iterations, int keyLength) { return mutate(inputCredentials, salt, iterations); } /** * Set the algorithm used to convert input credentials to stored * credentials. * @param algorithm the algorithm * @throws NoSuchAlgorithmException if the specified algorithm * is not supported */ public abstract void setAlgorithm(String algorithm) throws NoSuchAlgorithmException; /** * @return the algorithm used to convert input credentials to stored * credentials. */ public abstract String getAlgorithm(); /** * @return the default number of iterations used by the * {@link CredentialHandler}. */ protected abstract int getDefaultIterations(); /** * @return the logger for the CredentialHandler instance. */ protected abstract Log getLog(); }