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.Map;
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    private Map<String, Object> parameters = Map.of();
054
055    /**
056     * Constructs a new {@code DefaultHashService} instance with the following defaults:
057     * <ul>
058     * <li>{@link #setDefaultAlgorithmName(String) hashAlgorithmName} = {@code SHA-512}</li>
059     * </ul>
060     */
061    public DefaultHashService() {
062        this.random = new SecureRandom();
063        this.defaultAlgorithmName = "argon2";
064    }
065
066    /**
067     * Computes and responds with a hash based on the specified request.
068     * <p/>
069     * This implementation functions as follows:
070     * <ul>
071     * <li>If the request's {@link org.apache.shiro.crypto.hash.HashRequest#getSalt() salt} is null:
072     * <p/>
073     * A salt will be generated and used to compute the hash.  The salt is generated as follows:
074     * <ol>
075     * <li>Use the combined value as the salt used during hash computation</li>
076     * </ol>
077     * </li>
078     * <li>
079     *
080     * @param request the request to process
081     * @return the response containing the result of the hash computation, as well as any hash salt used that should be
082     * exposed to the caller.
083     */
084    @Override
085    public Hash computeHash(HashRequest request) {
086        if (request == null || request.getSource() == null || request.getSource().isEmpty()) {
087            return null;
088        }
089
090        String algorithmName = getAlgorithmName(request);
091
092        Optional<HashSpi> kdfHash = HashProvider.getByAlgorithmName(algorithmName);
093        if (kdfHash.isPresent()) {
094            HashSpi hashSpi = kdfHash.get();
095
096            return hashSpi.newHashFactory(random).generate(request);
097        }
098
099        throw new UnsupportedOperationException("Cannot create a hash with the given algorithm: " + algorithmName);
100    }
101
102
103    protected String getAlgorithmName(HashRequest request) {
104        return request.getAlgorithmName().orElseGet(this::getDefaultAlgorithmName);
105    }
106
107    @Override
108    public void setDefaultAlgorithmName(String name) {
109        this.defaultAlgorithmName = name;
110    }
111
112    @Override
113    public String getDefaultAlgorithmName() {
114        return this.defaultAlgorithmName;
115    }
116
117    @Override
118    public Map<String, Object> getParameters() {
119        return Map.copyOf(this.parameters);
120    }
121
122    @Override
123    public void setParameters(Map<String, Object> parameters) {
124        this.parameters = Map.copyOf(parameters);
125    }
126}