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.lang.codec.Base64;
023import org.apache.shiro.lang.codec.Hex;
024import org.apache.shiro.lang.util.ByteSource;
025
026import java.io.Serializable;
027import java.nio.charset.StandardCharsets;
028import java.util.Arrays;
029import java.util.Locale;
030import java.util.Objects;
031import java.util.StringJoiner;
032import java.util.regex.Pattern;
033
034import static java.util.Objects.requireNonNull;
035
036/**
037 * Abstract class for hashes following the posix crypt(3) format.
038 *
039 * <p>These implementations must contain a salt, a salt length, can format themselves to a valid String
040 * suitable for the {@code /etc/shadow} file.</p>
041 *
042 * <p>It also defines the hex and base64 output by wrapping the output of {@link #formatToCryptString()}.</p>
043 *
044 * <p>Implementation notice: Implementations should provide a static {@code fromString()} method.</p>
045 *
046 * @since 2.0
047 */
048public abstract class AbstractCryptHash implements Hash, Serializable {
049
050    protected static final Pattern DELIMITER = Pattern.compile("\\$");
051
052    private static final long serialVersionUID = 2483214646921027859L;
053
054    private final String algorithmName;
055    private final byte[] hashedData;
056    private final ByteSource salt;
057
058    /**
059     * Cached value of the {@link #toHex() toHex()} call so multiple calls won't incur repeated overhead.
060     */
061    private String hexEncoded;
062    /**
063     * Cached value of the {@link #toBase64() toBase64()} call so multiple calls won't incur repeated overhead.
064     */
065    private String base64Encoded;
066
067    /**
068     * Constructs an {@link AbstractCryptHash} using the algorithm name, hashed data and salt parameters.
069     *
070     * <p>Other required parameters must be stored by the implementation.</p>
071     *
072     * @param algorithmName internal algorithm name, e.g. {@code 2y} for bcrypt and {@code argon2id} for argon2.
073     * @param hashedData    the hashed data as a byte array. Does not include the salt or other parameters.
074     * @param salt          the salt which was used when generating the hash.
075     * @throws IllegalArgumentException if the salt is not the same size as {@link #getSaltLength()}.
076     */
077    public AbstractCryptHash(final String algorithmName, final byte[] hashedData, final ByteSource salt) {
078        this.algorithmName = algorithmName;
079        this.hashedData = Arrays.copyOf(hashedData, hashedData.length);
080        this.salt = requireNonNull(salt);
081        checkValid();
082    }
083
084    protected final void checkValid() {
085        checkValidAlgorithm();
086
087        checkValidSalt();
088    }
089
090    /**
091     * Algorithm-specific checks of the algorithm’s parameters.
092     *
093     * <p>While the salt length will be checked by default, other checks will be useful.
094     * Examples are: Argon2 checking for the memory and parallelism parameters, bcrypt checking
095     * for the cost parameters being in a valid range.</p>
096     *
097     * @throws IllegalArgumentException if any of the parameters are invalid.
098     */
099    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}