DefaultHashService.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.shiro.crypto.hash;

import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.util.ByteSource;

/**
 * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name,
 * secure-random salt generation, multiple hash iterations and an optional internal
 * {@link #setPrivateSalt(ByteSource) privateSalt}.
 * <h2>Hash Algorithm</h2>
 * You may specify a hash algorithm via the {@link #setHashAlgorithmName(String)} property.  Any algorithm name
 * understood by the JDK
 * {@link java.security.MessageDigest#getInstance(String) MessageDigest.getInstance(String algorithmName)} method
 * will work.  The default is {@code SHA-512}.
 * <h2>Random Salts</h2>
 * When a salt is not specified in a request, this implementation generates secure random salts via its
 * {@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} property.
 * Random salts (and potentially combined with the internal {@link #getPrivateSalt() privateSalt}) is a very strong
 * salting strategy, as salts should ideally never be based on known/guessable data.  The default instance is a
 * {@link SecureRandomNumberGenerator}.
 * <h2>Hash Iterations</h2>
 * Secure hashing strategies often employ multiple hash iterations to slow down the hashing process.  This technique
 * is usually used for password hashing, since the longer it takes to compute a password hash, the longer it would
 * take for an attacker to compromise a password.  This
 * <a href="http://www.stormpath.com/blog/strong-password-hashing-apache-shiro">blog article</a>
 * explains in greater detail why this is useful, as well as information on how many iterations is 'enough'.
 * <p/>
 * You may set the number of hash iterations via the {@link #setHashIterations(int)} property.  The default is
 * {@code 1}, but should be increased significantly if the {@code HashService} is intended to be used for password
 * hashing. See the linked blog article for more info.
 * <h2>Private Salt</h2>
 * If using this implementation as part of a password hashing strategy, it might be desirable to configure a
 * {@link #setPrivateSalt(ByteSource) private salt}:
 * <p/>
 * A hash and the salt used to compute it are often stored together.  If an attacker is ever able to access
 * the hash (e.g. during password cracking) and it has the full salt value, the attacker has all of the input necessary
 * to try to brute-force crack the hash (source + complete salt).
 * <p/>
 * However, if part of the salt is not available to the attacker (because it is not stored with the hash), it is
 * <em>much</em> harder to crack the hash value since the attacker does not have the complete inputs necessary.
 * <p/>
 * The {@link #getPrivateSalt() privateSalt} property exists to satisfy this private-and-not-shared part of the salt.
 * If you configure this attribute, you can obtain this additional very important safety feature.
 * <p/>
 * <b>*</b>By default, the {@link #getPrivateSalt() privateSalt} is null, since a sensible default cannot be used that
 * isn't easily compromised (because Shiro is an open-source project and any default could be easily seen and used).
 *
 * @since 1.2
 */
public class DefaultHashService implements ConfigurableHashService {

    /**
     * The RandomNumberGenerator to use to randomly generate the public part of the hash salt.
     */
    private RandomNumberGenerator rng;

    /**
     * The MessageDigest name of the hash algorithm to use for computing hashes.
     */
    private String algorithmName;

    /**
     * The 'private' part of the hash salt.
     */
    private ByteSource privateSalt;

    /**
     * The number of hash iterations to perform when computing hashes.
     */
    private int iterations;

    /**
     * Whether or not to generate public salts if a request does not provide one.
     */
    private boolean generatePublicSalt;

    /**
     * Constructs a new {@code DefaultHashService} instance with the following defaults:
     * <ul>
     * <li>{@link #setHashAlgorithmName(String) hashAlgorithmName} = {@code SHA-512}</li>
     * <li>{@link #setHashIterations(int) hashIterations} = {@code 1}</li>
     * <li>{@link #setRandomNumberGenerator(org.apache.shiro.crypto.RandomNumberGenerator) randomNumberGenerator} =
     * new {@link SecureRandomNumberGenerator}()</li>
     * <li>{@link #setGeneratePublicSalt(boolean) generatePublicSalt} = {@code false}</li>
     * </ul>
     * <p/>
     * If this hashService will be used for password hashing it is recommended to set the
     * {@link #setPrivateSalt(ByteSource) privateSalt} and significantly increase the number of
     * {@link #setHashIterations(int) hashIterations}.  See the class-level JavaDoc for more information.
     */
    public DefaultHashService() {
        this.algorithmName = "SHA-512";
        this.iterations = 1;
        this.generatePublicSalt = false;
        this.rng = new SecureRandomNumberGenerator();
    }

    /**
     * Computes and responds with a hash based on the specified request.
     * <p/>
     * This implementation functions as follows:
     * <ul>
     * <li>If the request's {@link org.apache.shiro.crypto.hash.HashRequest#getSalt() salt} is null:
     * <p/>
     * A salt will be generated and used to compute the hash.  The salt is generated as follows:
     * <ol>
     * <li>Use the {@link #getRandomNumberGenerator() randomNumberGenerator} to generate a new random number.</li>
     * <li>{@link #combine(ByteSource, ByteSource) combine} this random salt with any configured
     * {@link #getPrivateSalt() privateSalt}
     * </li>
     * <li>Use the combined value as the salt used during hash computation</li>
     * </ol>
     * </li>
     * <li>
     * If the request salt is not null:
     * <p/>
     * This indicates that the hash computation is for comparison purposes (of a
     * previously computed hash).  The request salt will be {@link #combine(ByteSource, ByteSource) combined} with any
     * configured {@link #getPrivateSalt() privateSalt} and used as the complete salt during hash computation.
     * </li>
     * </ul>
     * <p/>
     * The returned {@code Hash}'s {@link Hash#getSalt() salt} property
     * will contain <em>only</em> the 'public' part of the salt and <em>NOT</em> the privateSalt.  See the class-level
     * JavaDoc explanation for more info.
     *
     * @param request the request to process
     * @return the response containing the result of the hash computation, as well as any hash salt used that should be
     *         exposed to the caller.
     */
    public Hash computeHash(HashRequest request) {
        if (request == null || request.getSource() == null || request.getSource().isEmpty()) {
            return null;
        }

        String algorithmName = getAlgorithmName(request);
        ByteSource source = request.getSource();
        int iterations = getIterations(request);

        ByteSource publicSalt = getPublicSalt(request);
        ByteSource privateSalt = getPrivateSalt();
        ByteSource salt = combine(privateSalt, publicSalt);

        Hash computed = new SimpleHash(algorithmName, source, salt, iterations);

        SimpleHash result = new SimpleHash(algorithmName);
        result.setBytes(computed.getBytes());
        result.setIterations(iterations);
        //Only expose the public salt - not the real/combined salt that might have been used:
        result.setSalt(publicSalt);

        return result;
    }

    protected String getAlgorithmName(HashRequest request) {
        String name = request.getAlgorithmName();
        if (name == null) {
            name = getHashAlgorithmName();
        }
        return name;
    }

    protected int getIterations(HashRequest request) {
        int iterations = Math.max(0, request.getIterations());
        if (iterations < 1) {
            iterations = Math.max(1, getHashIterations());
        }
        return iterations;
    }

    /**
     * Returns the public salt that should be used to compute a hash based on the specified request or
     * {@code null} if no public salt should be used.
     * <p/>
     * This implementation functions as follows:
     * <ol>
     * <li>If the request salt is not null and non-empty, this will be used, return it.</li>
     * <li>If the request salt is null or empty:
     * <ol>
     * <li>If a private salt has been set <em>OR</em> {@link #isGeneratePublicSalt()} is {@code true},
     * auto generate a random public salt via the configured
     * {@link #getRandomNumberGenerator() randomNumberGenerator}.</li>
     * <li>If a private salt has not been configured and {@link #isGeneratePublicSalt()} is {@code false},
     * do nothing - return {@code null} to indicate a salt should not be used during hash computation.</li>
     * </ol>
     * </li>
     * </ol>
     *
     * @param request request the request to process
     * @return the public salt that should be used to compute a hash based on the specified request or
     *         {@code null} if no public salt should be used.
     */
    protected ByteSource getPublicSalt(HashRequest request) {

        ByteSource publicSalt = request.getSalt();

        if (publicSalt != null && !publicSalt.isEmpty()) {
            //a public salt was explicitly requested to be used - go ahead and use it:
            return publicSalt;
        }

        publicSalt = null;

        //check to see if we need to generate one:
        ByteSource privateSalt = getPrivateSalt();
        boolean privateSaltExists = privateSalt != null && !privateSalt.isEmpty();

        //If a private salt exists, we must generate a public salt to protect the integrity of the private salt.
        //Or generate it if the instance is explicitly configured to do so:
        if (privateSaltExists || isGeneratePublicSalt()) {
            publicSalt = getRandomNumberGenerator().nextBytes();
        }

        return publicSalt;
    }

    /**
     * Combines the specified 'private' salt bytes with the specified additional extra bytes to use as the
     * total salt during hash computation.  {@code privateSaltBytes} will be {@code null} }if no private salt has been
     * configured.
     *
     * @param privateSalt the (possibly {@code null}) 'private' salt to combine with the specified extra bytes
     * @param publicSalt  the extra bytes to use in addition to the given private salt.
     * @return a combination of the specified private salt bytes and extra bytes that will be used as the total
     *         salt during hash computation.
     */
    protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) {

        byte[] privateSaltBytes = privateSalt != null ? privateSalt.getBytes() : null;
        int privateSaltLength = privateSaltBytes != null ? privateSaltBytes.length : 0;

        byte[] publicSaltBytes = publicSalt != null ? publicSalt.getBytes() : null;
        int extraBytesLength = publicSaltBytes != null ? publicSaltBytes.length : 0;

        int length = privateSaltLength + extraBytesLength;

        if (length <= 0) {
            return null;
        }

        byte[] combined = new byte[length];

        int i = 0;
        for (int j = 0; j < privateSaltLength; j++) {
            assert privateSaltBytes != null;
            combined[i++] = privateSaltBytes[j];
        }
        for (int j = 0; j < extraBytesLength; j++) {
            assert publicSaltBytes != null;
            combined[i++] = publicSaltBytes[j];
        }

        return ByteSource.Util.bytes(combined);
    }

    public void setHashAlgorithmName(String name) {
        this.algorithmName = name;
    }

    public String getHashAlgorithmName() {
        return this.algorithmName;
    }

    public void setPrivateSalt(ByteSource privateSalt) {
        this.privateSalt = privateSalt;
    }

    public ByteSource getPrivateSalt() {
        return this.privateSalt;
    }

    public void setHashIterations(int count) {
        this.iterations = count;
    }

    public int getHashIterations() {
        return this.iterations;
    }

    public void setRandomNumberGenerator(RandomNumberGenerator rng) {
        this.rng = rng;
    }

    public RandomNumberGenerator getRandomNumberGenerator() {
        return this.rng;
    }

    /**
     * Returns {@code true} if a public salt should be randomly generated and used to compute a hash if a
     * {@link HashRequest} does not specify a salt, {@code false} otherwise.
     * <p/>
     * The default value is {@code false} but should definitely be set to {@code true} if the
     * {@code HashService} instance is being used for password hashing.
     * <p/>
     * <b>NOTE:</b> this property only has an effect if a {@link #getPrivateSalt() privateSalt} is NOT configured.  If a
     * private salt has been configured and a request does not provide a salt, a random salt will always be generated
     * to protect the integrity of the private salt (without a public salt, the private salt would be exposed as-is,
     * which is undesirable).
     *
     * @return {@code true} if a public salt should be randomly generated and used to compute a hash if a
     *         {@link HashRequest} does not specify a salt, {@code false} otherwise.
     */
    public boolean isGeneratePublicSalt() {
        return generatePublicSalt;
    }

    /**
     * Sets whether or not a public salt should be randomly generated and used to compute a hash if a
     * {@link HashRequest} does not specify a salt.
     * <p/>
     * The default value is {@code false} but should definitely be set to {@code true} if the
     * {@code HashService} instance is being used for password hashing.
     * <p/>
     * <b>NOTE:</b> this property only has an effect if a {@link #getPrivateSalt() privateSalt} is NOT configured.  If a
     * private salt has been configured and a request does not provide a salt, a random salt will always be generated
     * to protect the integrity of the private salt (without a public salt, the private salt would be exposed as-is,
     * which is undesirable).
     *
     * @param generatePublicSalt whether or not a public salt should be randomly generated and used to compute a hash
     *                           if a {@link HashRequest} does not specify a salt.
     */
    public void setGeneratePublicSalt(boolean generatePublicSalt) {
        this.generatePublicSalt = generatePublicSalt;
    }
}