View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.shiro.crypto.hash;
21  
22  import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat;
23  import org.apache.shiro.lang.util.ByteSource;
24  import org.apache.shiro.lang.util.SimpleByteSource;
25  
26  import java.util.Arrays;
27  import java.util.Base64;
28  import java.util.NoSuchElementException;
29  import java.util.Optional;
30  import java.util.Random;
31  import java.util.Set;
32  
33  import static java.util.Collections.unmodifiableSet;
34  import static java.util.stream.Collectors.toSet;
35  
36  /**
37   * Creates a hash provider for salt (+pepper) and Hash-based KDFs, i.e. where the algorithm name
38   * is a SHA algorithm or similar.
39   *
40   * @since 2.0
41   */
42  public class SimpleHashProvider implements HashSpi {
43  
44      private static final Set<String> IMPLEMENTED_ALGORITHMS = Arrays.stream(new String[] {
45                      Sha256Hash.ALGORITHM_NAME,
46                      Sha384Hash.ALGORITHM_NAME,
47                      Sha512Hash.ALGORITHM_NAME
48              })
49              .collect(toSet());
50  
51      @Override
52      public Set<String> getImplementedAlgorithms() {
53          return unmodifiableSet(IMPLEMENTED_ALGORITHMS);
54      }
55  
56      @Override
57      public SimpleHash fromString(String format) {
58          Hash hash = new Shiro1CryptFormat().parse(format);
59  
60          if (!(hash instanceof SimpleHash)) {
61              throw new IllegalArgumentException("formatted string was not a simple hash: " + format);
62          }
63  
64          return (SimpleHash) hash;
65      }
66  
67      @Override
68      public HashFactory newHashFactory(Random random) {
69          return new SimpleHashFactory(random);
70      }
71  
72      static class SimpleHashFactory implements HashSpi.HashFactory {
73  
74          private final Random random;
75  
76          SimpleHashFactory(Random random) {
77              this.random = random;
78          }
79  
80          @Override
81          public SimpleHash generate(HashRequest hashRequest) {
82              String algorithmName = hashRequest.getAlgorithmName().orElse(Parameters.DEFAULT_ALGORITHM);
83              ByteSource source = hashRequest.getSource();
84              final int iterations = getIterations(hashRequest);
85  
86              final ByteSource publicSalt = getPublicSalt(hashRequest);
87              final /*nullable*/ ByteSource secretSalt = getSecretSalt(hashRequest);
88              final ByteSource salt = combine(secretSalt, publicSalt);
89  
90              return createSimpleHash(algorithmName, source, iterations, publicSalt, salt);
91          }
92  
93          /**
94           * Returns the public salt that should be used to compute a hash based on the specified request.
95           * <p/>
96           * This implementation functions as follows:
97           * <ol>
98           *   <li>If the request salt is not null and non-empty, this will be used, return it.</li>
99           *   <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 }