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 }