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.Optional;
023import java.util.Random;
024
025/**
026 * Default implementation of the {@link HashService} interface, supporting a customizable hash algorithm name.
027 * <h2>Hash Algorithm</h2>
028 * You may specify a hash algorithm via the {@link #setDefaultAlgorithmName(String)} property. Any algorithm name
029 * understood by the JDK
030 * {@link java.security.MessageDigest#getInstance(String) MessageDigest.getInstance(String algorithmName)} method
031 * will work, or any Hash algorithm implemented by any loadable {@link HashSpi}. The default is {@code argon2}.
032 * </p>
033 * A hash and the salt used to compute it are often stored together.  If an attacker is ever able to access
034 * the hash (e.g. during password cracking) and it has the full salt value, the attacker has all of the input necessary
035 * to try to brute-force crack the hash (source + complete salt).
036 * <p/>
037 * However, if part of the salt is not available to the attacker (because it is not stored with the hash), it is
038 * <em>much</em> harder to crack the hash value since the attacker does not have the complete inputs necessary.
039 * <p/>
040 *
041 * @since 1.2
042 */
043public class DefaultHashService implements ConfigurableHashService {
044
045    private final Random random;
046
047    /**
048     * The MessageDigest name of the hash algorithm to use for computing hashes.
049     */
050    private String defaultAlgorithmName;
051
052
053    /**
054     * Constructs a new {@code DefaultHashService} instance with the following defaults:
055     * <ul>
056     * <li>{@link #setDefaultAlgorithmName(String) hashAlgorithmName} = {@code SHA-512}</li>
057     * </ul>
058     */
059    public DefaultHashService() {
060        this.random = new SecureRandom();
061        this.defaultAlgorithmName = "argon2";
062    }
063
064    /**
065     * Computes and responds with a hash based on the specified request.
066     * <p/>
067     * This implementation functions as follows:
068     * <ul>
069     * <li>If the request's {@link org.apache.shiro.crypto.hash.HashRequest#getSalt() salt} is null:
070     * <p/>
071     * A salt will be generated and used to compute the hash.  The salt is generated as follows:
072     * <ol>
073     * <li>Use the combined value as the salt used during hash computation</li>
074     * </ol>
075     * </li>
076     * <li>
077     *
078     * @param request the request to process
079     * @return the response containing the result of the hash computation, as well as any hash salt used that should be
080     * exposed to the caller.
081     */
082    @Override
083    public Hash computeHash(HashRequest request) {
084        if (request == null || request.getSource() == null || request.getSource().isEmpty()) {
085            return null;
086        }
087
088        String algorithmName = getAlgorithmName(request);
089
090        Optional<HashSpi> kdfHash = HashProvider.getByAlgorithmName(algorithmName);
091        if (kdfHash.isPresent()) {
092            HashSpi hashSpi = kdfHash.get();
093
094            return hashSpi.newHashFactory(random).generate(request);
095        }
096
097        throw new UnsupportedOperationException("Cannot create a hash with the given algorithm: " + algorithmName);
098    }
099
100
101    protected String getAlgorithmName(HashRequest request) {
102        return request.getAlgorithmName().orElseGet(this::getDefaultAlgorithmName);
103    }
104
105    @Override
106    public void setDefaultAlgorithmName(String name) {
107        this.defaultAlgorithmName = name;
108    }
109
110    @Override
111    public String getDefaultAlgorithmName() {
112        return this.defaultAlgorithmName;
113    }
114
115}