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.lang.codec.Base64;
23  import org.apache.shiro.lang.codec.Hex;
24  import org.apache.shiro.lang.util.ByteSource;
25  
26  import java.io.Serializable;
27  import java.nio.charset.StandardCharsets;
28  import java.util.Arrays;
29  import java.util.Locale;
30  import java.util.Objects;
31  import java.util.StringJoiner;
32  import java.util.regex.Pattern;
33  
34  import static java.util.Objects.requireNonNull;
35  
36  /**
37   * Abstract class for hashes following the posix crypt(3) format.
38   *
39   * <p>These implementations must contain a salt, a salt length, can format themselves to a valid String
40   * suitable for the {@code /etc/shadow} file.</p>
41   *
42   * <p>It also defines the hex and base64 output by wrapping the output of {@link #formatToCryptString()}.</p>
43   *
44   * <p>Implementation notice: Implementations should provide a static {@code fromString()} method.</p>
45   *
46   * @since 2.0
47   */
48  public abstract class AbstractCryptHash implements Hash, Serializable {
49  
50      protected static final Pattern DELIMITER = Pattern.compile("\\$");
51  
52      private static final long serialVersionUID = 2483214646921027859L;
53  
54      private final String algorithmName;
55      private final byte[] hashedData;
56      private final ByteSource salt;
57  
58      /**
59       * Cached value of the {@link #toHex() toHex()} call so multiple calls won't incur repeated overhead.
60       */
61      private String hexEncoded;
62      /**
63       * Cached value of the {@link #toBase64() toBase64()} call so multiple calls won't incur repeated overhead.
64       */
65      private String base64Encoded;
66  
67      /**
68       * Constructs an {@link AbstractCryptHash} using the algorithm name, hashed data and salt parameters.
69       *
70       * <p>Other required parameters must be stored by the implementation.</p>
71       *
72       * @param algorithmName internal algorithm name, e.g. {@code 2y} for bcrypt and {@code argon2id} for argon2.
73       * @param hashedData    the hashed data as a byte array. Does not include the salt or other parameters.
74       * @param salt          the salt which was used when generating the hash.
75       * @throws IllegalArgumentException if the salt is not the same size as {@link #getSaltLength()}.
76       */
77      public AbstractCryptHash(final String algorithmName, final byte[] hashedData, final ByteSource salt) {
78          this.algorithmName = algorithmName;
79          this.hashedData = Arrays.copyOf(hashedData, hashedData.length);
80          this.salt = requireNonNull(salt);
81          checkValid();
82      }
83  
84      protected final void checkValid() {
85          checkValidAlgorithm();
86  
87          checkValidSalt();
88      }
89  
90      /**
91       * Algorithm-specific checks of the algorithm’s parameters.
92       *
93       * <p>While the salt length will be checked by default, other checks will be useful.
94       * Examples are: Argon2 checking for the memory and parallelism parameters, bcrypt checking
95       * for the cost parameters being in a valid range.</p>
96       *
97       * @throws IllegalArgumentException if any of the parameters are invalid.
98       */
99      protected abstract void checkValidAlgorithm();
100 
101     /**
102      * Default check method for a valid salt. Can be overridden, because multiple salt lengths could be valid.
103      * <p>
104      * By default, this method checks if the number of bytes in the salt
105      * are equal to the int returned by {@link #getSaltLength()}.
106      *
107      * @throws IllegalArgumentException if the salt length does not match the returned value of {@link #getSaltLength()}.
108      */
109     protected void checkValidSalt() {
110         int length = salt.getBytes().length;
111         if (length != getSaltLength()) {
112             String message = String.format(
113                     Locale.ENGLISH,
114                     "Salt length is expected to be [%d] bytes, but was [%d] bytes.",
115                     getSaltLength(),
116                     length
117             );
118             throw new IllegalArgumentException(message);
119         }
120     }
121 
122     /**
123      * Implemented by subclasses, this specifies the KDF algorithm name
124      * to use when performing the hash.
125      *
126      * <p>When multiple algorithm names are acceptable, then this method should return the primary algorithm name.</p>
127      *
128      * <p>Example: Bcrypt hashed can be identified by {@code 2y} and {@code 2a}. The method will return {@code 2y}
129      * for newly generated hashes by default, unless otherwise overridden.</p>
130      *
131      * @return the KDF algorithm name to use when performing the hash.
132      */
133     @Override
134     public String getAlgorithmName() {
135         return this.algorithmName;
136     }
137 
138     /**
139      * The length in number of bytes of the salt which is needed for this algorithm.
140      *
141      * @return the expected length of the salt (in bytes).
142      */
143     public abstract int getSaltLength();
144 
145     @Override
146     public ByteSource getSalt() {
147         return this.salt;
148     }
149 
150     /**
151      * Returns only the hashed data. Those are of no value on their own. If you need to serialize
152      * the hash, please refer to {@link #formatToCryptString()}.
153      *
154      * @return A copy of the hashed data as bytes.
155      * @see #formatToCryptString()
156      */
157     @Override
158     public byte[] getBytes() {
159         return Arrays.copyOf(this.hashedData, this.hashedData.length);
160     }
161 
162     @Override
163     public boolean isEmpty() {
164         return false;
165     }
166 
167     /**
168      * Returns a hex-encoded string of the underlying {@link #formatToCryptString()} formatted output}.
169      * <p/>
170      * This implementation caches the resulting hex string so multiple calls to this method remain efficient.
171      *
172      * @return a hex-encoded string of the underlying {@link #formatToCryptString()} formatted output}.
173      */
174     @Override
175     public String toHex() {
176         if (this.hexEncoded == null) {
177             this.hexEncoded = Hex.encodeToString(this.formatToCryptString().getBytes(StandardCharsets.UTF_8));
178         }
179         return this.hexEncoded;
180     }
181 
182     /**
183      * Returns a Base64-encoded string of the underlying {@link #formatToCryptString()} formatted output}.
184      * <p/>
185      * This implementation caches the resulting Base64 string so multiple calls to this method remain efficient.
186      *
187      * @return a Base64-encoded string of the underlying {@link #formatToCryptString()} formatted output}.
188      */
189     @Override
190     public String toBase64() {
191         if (this.base64Encoded == null) {
192             //cache result in case this method is called multiple times.
193             this.base64Encoded = Base64.encodeToString(this.formatToCryptString().getBytes(StandardCharsets.UTF_8));
194         }
195         return this.base64Encoded;
196     }
197 
198     /**
199      * This method <strong>MUST</strong> return a single-lined string which would also be recognizable by
200      * a posix {@code /etc/passwd} file.
201      *
202      * @return a formatted string, e.g. {@code $2y$10$7rOjsAf2U/AKKqpMpCIn6e$tuOXyQ86tp2Tn9xv6FyXl2T0QYc3.G.} for bcrypt.
203      */
204     public abstract String formatToCryptString();
205 
206     /**
207      * Returns {@code true} if the specified object is an AbstractCryptHash and its
208      * {@link #formatToCryptString()} formatted output} is identical to
209      * this AbstractCryptHash's formatted output, {@code false} otherwise.
210      *
211      * @param other the object (AbstractCryptHash) to check for equality.
212      * @return {@code true} if the specified object is a AbstractCryptHash
213      * and its {@link #formatToCryptString()} formatted output} is identical to
214      * this AbstractCryptHash's formatted output, {@code false} otherwise.
215      */
216     @Override
217     public boolean equals(final Object other) {
218         if (other instanceof AbstractCryptHash) {
219             final AbstractCryptHash that = (AbstractCryptHash) other;
220             return this.formatToCryptString().equals(that.formatToCryptString());
221         }
222         return false;
223     }
224 
225     /**
226      * Hashes the formatted crypt string.
227      *
228      * <p>Implementations should not override this method, as different algorithms produce different output formats
229      * and require different parameters.</p>
230      *
231      * @return a hashcode from the {@link #formatToCryptString() formatted output}.
232      */
233     @Override
234     public int hashCode() {
235         return Objects.hash(this.formatToCryptString());
236     }
237 
238     /**
239      * Simple implementation that merely returns {@link #toHex() toHex()}.
240      *
241      * @return the {@link #toHex() toHex()} value.
242      */
243     @Override
244     public String toString() {
245         return new StringJoiner(", ", AbstractCryptHash.class.getSimpleName() + "[", "]")
246                 .add("super=" + super.toString())
247                 .add("algorithmName='" + algorithmName + "'")
248                 .add("hashedData=" + Arrays.toString(hashedData))
249                 .add("salt=" + salt)
250                 .add("hexEncoded='" + hexEncoded + "'")
251                 .add("base64Encoded='" + base64Encoded + "'")
252                 .toString();
253     }
254 }