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 */ 019 020package org.apache.shiro.crypto.hash; 021 022import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat; 023import org.apache.shiro.lang.util.ByteSource; 024import org.apache.shiro.lang.util.SimpleByteSource; 025 026import java.util.Arrays; 027import java.util.Base64; 028import java.util.NoSuchElementException; 029import java.util.Optional; 030import java.util.Random; 031import java.util.Set; 032 033import static java.util.Collections.unmodifiableSet; 034import static java.util.stream.Collectors.toSet; 035 036/** 037 * Creates a hash provider for salt (+pepper) and Hash-based KDFs, i.e. where the algorithm name 038 * is a SHA algorithm or similar. 039 * 040 * @since 2.0 041 */ 042public class SimpleHashProvider implements HashSpi { 043 044 private static final Set<String> IMPLEMENTED_ALGORITHMS = Arrays.stream(new String[] { 045 Sha256Hash.ALGORITHM_NAME, 046 Sha384Hash.ALGORITHM_NAME, 047 Sha512Hash.ALGORITHM_NAME 048 }) 049 .collect(toSet()); 050 051 @Override 052 public Set<String> getImplementedAlgorithms() { 053 return unmodifiableSet(IMPLEMENTED_ALGORITHMS); 054 } 055 056 @Override 057 public SimpleHash fromString(String format) { 058 Hash hash = new Shiro1CryptFormat().parse(format); 059 060 if (!(hash instanceof SimpleHash)) { 061 throw new IllegalArgumentException("formatted string was not a simple hash: " + format); 062 } 063 064 return (SimpleHash) hash; 065 } 066 067 @Override 068 public HashFactory newHashFactory(Random random) { 069 return new SimpleHashFactory(random); 070 } 071 072 static class SimpleHashFactory implements HashSpi.HashFactory { 073 074 private final Random random; 075 076 SimpleHashFactory(Random random) { 077 this.random = random; 078 } 079 080 @Override 081 public SimpleHash generate(HashRequest hashRequest) { 082 String algorithmName = hashRequest.getAlgorithmName().orElse(Parameters.DEFAULT_ALGORITHM); 083 ByteSource source = hashRequest.getSource(); 084 final int iterations = getIterations(hashRequest); 085 086 final ByteSource publicSalt = getPublicSalt(hashRequest); 087 final /*nullable*/ ByteSource secretSalt = getSecretSalt(hashRequest); 088 final ByteSource salt = combine(secretSalt, publicSalt); 089 090 return createSimpleHash(algorithmName, source, iterations, publicSalt, salt); 091 } 092 093 /** 094 * Returns the public salt that should be used to compute a hash based on the specified request. 095 * <p/> 096 * This implementation functions as follows: 097 * <ol> 098 * <li>If the request salt is not null and non-empty, this will be used, return it.</li> 099 * <li>If the request salt is null or empty: 100 * <ol><li>create a new 16-byte salt.</li></ol> 101 * </li> 102 * </ol> 103 * 104 * @param request request the request to process 105 * @return the public salt that should be used to compute a hash based on the specified request or 106 * {@code null} if no public salt should be used. 107 */ 108 protected ByteSource getPublicSalt(HashRequest request) { 109 Optional<ByteSource> publicSalt = request.getSalt(); 110 111 if (publicSalt.isPresent() && !publicSalt.orElseThrow(NoSuchElementException::new).isEmpty()) { 112 //a public salt was explicitly requested to be used - go ahead and use it: 113 return publicSalt.orElseThrow(NoSuchElementException::new); 114 } 115 116 // generate salt if absent from the request. 117 @SuppressWarnings("checkstyle:MagicNumber") 118 byte[] ps = new byte[16]; 119 random.nextBytes(ps); 120 121 return new SimpleByteSource(ps); 122 } 123 124 private ByteSource getSecretSalt(HashRequest request) { 125 Optional<Object> secretSalt = Optional.ofNullable(request.getParameters().get(Parameters.PARAMETER_SECRET_SALT)); 126 127 return secretSalt 128 .map(salt -> (String) salt) 129 .map(salt -> Base64.getDecoder().decode(salt)) 130 .map(SimpleByteSource::new) 131 .orElse(null); 132 } 133 134 private SimpleHash createSimpleHash(String algorithmName, ByteSource source, 135 int iterations, ByteSource publicSalt, ByteSource salt) { 136 Hash computed = new SimpleHash(algorithmName, source, salt, iterations); 137 138 SimpleHash result = new SimpleHash(algorithmName); 139 result.setBytes(computed.getBytes()); 140 result.setIterations(iterations); 141 //Only expose the public salt - not the real/combined salt that might have been used: 142 result.setSalt(publicSalt); 143 144 return result; 145 } 146 147 protected int getIterations(HashRequest request) { 148 Object parameterIterations = request.getParameters().getOrDefault(Parameters.PARAMETER_ITERATIONS, 0); 149 150 if (!(parameterIterations instanceof Integer)) { 151 return Parameters.DEFAULT_ITERATIONS; 152 } 153 154 final int iterations = Math.max(0, (Integer) parameterIterations); 155 156 if (iterations < 1) { 157 return Parameters.DEFAULT_ITERATIONS; 158 } 159 160 return iterations; 161 } 162 163 /** 164 * Combines the specified 'private' salt bytes with the specified additional extra bytes to use as the 165 * total salt during hash computation. {@code privateSaltBytes} will be {@code null} }if no private salt has been 166 * configured. 167 * 168 * @param privateSalt the (possibly {@code null}) 'private' salt to combine with the specified extra bytes 169 * @param publicSalt the extra bytes to use in addition to the given private salt. 170 * @return a combination of the specified private salt bytes and extra bytes that will be used as the total 171 * salt during hash computation. 172 */ 173 protected ByteSource combine(ByteSource privateSalt, ByteSource publicSalt) { 174 175 // optional 'pepper' 176 byte[] privateSaltBytes = privateSalt != null ? privateSalt.getBytes() : null; 177 int privateSaltLength = privateSaltBytes != null ? privateSaltBytes.length : 0; 178 179 // salt must always be present. 180 byte[] publicSaltBytes = publicSalt.getBytes(); 181 int extraBytesLength = publicSaltBytes.length; 182 183 int length = privateSaltLength + extraBytesLength; 184 185 if (length <= 0) { 186 return SimpleByteSource.empty(); 187 } 188 189 byte[] combined = new byte[length]; 190 191 int i = 0; 192 for (int j = 0; j < privateSaltLength; j++) { 193 combined[i++] = privateSaltBytes[j]; 194 } 195 for (int j = 0; j < extraBytesLength; j++) { 196 combined[i++] = publicSaltBytes[j]; 197 } 198 199 return ByteSource.Util.bytes(combined); 200 } 201 } 202 203 static final class Parameters { 204 public static final String PARAMETER_ITERATIONS = "SimpleHash.iterations"; 205 206 /** 207 * A secret part added to the salt. Sometimes also referred to as {@literal "Pepper"}. 208 * 209 * <p>For more information, see <a href="https://en.wikipedia.org/wiki/Pepper_(cryptography)"> 210 * Pepper (cryptography) on Wikipedia</a>.</p> 211 */ 212 public static final String PARAMETER_SECRET_SALT = "SimpleHash.secretSalt"; 213 214 public static final String DEFAULT_ALGORITHM = "SHA-512"; 215 216 public static final int DEFAULT_ITERATIONS = 50_000; 217 218 219 private Parameters() { 220 // util class 221 } 222 } 223}