001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.crypto.hash;
020
021import java.security.SecureRandom;
022import java.util.NoSuchElementException;
023import java.util.Optional;
024import java.util.Random;
025
026/**
027 * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name.
028 * <h2>Hash Algorithm</h2>
029 * You may specify a hash algorithm via the {@link #setDefaultAlgorithmName(String)} property. Any algorithm name
030 * understood by the JDK
031 * {@link java.security.MessageDigest#getInstance(String) MessageDigest.getInstance(String algorithmName)} method
032 * will work, or any Hash algorithm implemented by any loadable {@link HashSpi}. The default is {@code argon2}.
033 * </p>
034 * A hash and the salt used to compute it are often stored together.  If an attacker is ever able to access
035 * the hash (e.g. during password cracking) and it has the full salt value, the attacker has all of the input necessary
036 * to try to brute-force crack the hash (source + complete salt).
037 * <p/>
038 * However, if part of the salt is not available to the attacker (because it is not stored with the hash), it is
039 * <em>much</em> harder to crack the hash value since the attacker does not have the complete inputs necessary.
040 * <p/>
041 *
042 * @since 1.2
043 */
044public class DefaultHashService implements ConfigurableHashService {
045
046    private final Random random;
047
048    /**
049     * The MessageDigest name of the hash algorithm to use for computing hashes.
050     */
051    private String defaultAlgorithmName;
052
053
054    /**
055     * Constructs a new {@code DefaultHashService} instance with the following defaults:
056     * <ul>
057     * <li>{@link #setDefaultAlgorithmName(String) hashAlgorithmName} = {@code SHA-512}</li>
058     * </ul>
059     */
060    public DefaultHashService() {
061        this.random = new SecureRandom();
062        this.defaultAlgorithmName = "argon2";
063    }
064
065    /**
066     * Computes and responds with a hash based on the specified request.
067     * <p/>
068     * This implementation functions as follows:
069     * <ul>
070     * <li>If the request's {@link org.apache.shiro.crypto.hash.HashRequest#getSalt() salt} is null:
071     * <p/>
072     * A salt will be generated and used to compute the hash.  The salt is generated as follows:
073     * <ol>
074     * <li>Use the combined value as the salt used during hash computation</li>
075     * </ol>
076     * </li>
077     * <li>
078     *
079     * @param request the request to process
080     * @return the response containing the result of the hash computation, as well as any hash salt used that should be
081     * exposed to the caller.
082     */
083    @Override
084    public Hash computeHash(HashRequest request) {
085        if (request == null || request.getSource() == null || request.getSource().isEmpty()) {
086            return null;
087        }
088
089        String algorithmName = getAlgorithmName(request);
090
091        Optional<HashSpi> kdfHash = HashProvider.getByAlgorithmName(algorithmName);
092        if (kdfHash.isPresent()) {
093            HashSpi hashSpi = kdfHash.orElseThrow(NoSuchElementException::new);
094
095            return hashSpi.newHashFactory(random).generate(request);
096        }
097
098        throw new UnsupportedOperationException("Cannot create a hash with the given algorithm: " + algorithmName);
099    }
100
101
102    protected String getAlgorithmName(HashRequest request) {
103        return request.getAlgorithmName().orElseGet(this::getDefaultAlgorithmName);
104    }
105
106    @Override
107    public void setDefaultAlgorithmName(String name) {
108        this.defaultAlgorithmName = name;
109    }
110
111    @Override
112    public String getDefaultAlgorithmName() {
113        return this.defaultAlgorithmName;
114    }
115
116}