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 */
019package org.apache.shiro.authc.credential;
020
021import org.apache.shiro.crypto.hash.DefaultHashService;
022import org.apache.shiro.crypto.hash.Hash;
023import org.apache.shiro.crypto.hash.HashRequest;
024import org.apache.shiro.crypto.hash.HashService;
025import org.apache.shiro.crypto.hash.format.DefaultHashFormatFactory;
026import org.apache.shiro.crypto.hash.format.HashFormat;
027import org.apache.shiro.crypto.hash.format.HashFormatFactory;
028import org.apache.shiro.crypto.hash.format.ParsableHashFormat;
029import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat;
030import org.apache.shiro.lang.util.ByteSource;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import java.security.MessageDigest;
035
036import static java.util.Objects.requireNonNull;
037
038/**
039 * Default implementation of the {@link PasswordService} interface that relies on an internal
040 * {@link HashService}, {@link HashFormat}, and {@link HashFormatFactory} to function:
041 * <h2>Hashing Passwords</h2>
042 *
043 * <h2>Comparing Passwords</h2>
044 * All hashing operations are performed by the internal {@link #getHashService() hashService}.
045 *
046 * @since 1.2
047 */
048public class DefaultPasswordService implements HashingPasswordService {
049
050    /**
051     * default hash algorithm.
052     */
053    public static final String DEFAULT_HASH_ALGORITHM = "argon2id";
054
055    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPasswordService.class);
056
057    private HashService hashService;
058    private HashFormat hashFormat;
059    private HashFormatFactory hashFormatFactory;
060
061    /**
062     * used to avoid excessive log noise
063     */
064    private volatile boolean hashFormatWarned;
065
066    /**
067     * Constructs a new PasswordService with a default hash service and the default
068     * algorithm name {@value #DEFAULT_HASH_ALGORITHM}, a default hash format (shiro2) and
069     * a default hash format factory.
070     *
071     * <p>The default algorithm can change between minor versions and does not introduce
072     * API incompatibility by design.</p>
073     */
074    public DefaultPasswordService() {
075        this.hashFormatWarned = false;
076
077        DefaultHashService hashService = new DefaultHashService();
078        hashService.setDefaultAlgorithmName(DEFAULT_HASH_ALGORITHM);
079        this.hashService = hashService;
080
081        this.hashFormat = new Shiro2CryptFormat();
082        this.hashFormatFactory = new DefaultHashFormatFactory();
083    }
084
085    @Override
086    public String encryptPassword(Object plaintext) {
087        Hash hash = hashPassword(requireNonNull(plaintext));
088        checkHashFormatDurability();
089        return this.hashFormat.format(hash);
090    }
091
092    @Override
093    public Hash hashPassword(Object plaintext) {
094        ByteSource plaintextBytes = createByteSource(plaintext);
095        if (plaintextBytes == null || plaintextBytes.isEmpty()) {
096            return null;
097        }
098        HashRequest request = createHashRequest(plaintextBytes);
099        return hashService.computeHash(request);
100    }
101
102    @Override
103    public boolean passwordsMatch(Object plaintext, Hash saved) {
104        ByteSource plaintextBytes = createByteSource(plaintext);
105
106        if (saved == null || saved.isEmpty()) {
107            return plaintextBytes == null || plaintextBytes.isEmpty();
108        } else {
109            if (plaintextBytes == null || plaintextBytes.isEmpty()) {
110                return false;
111            }
112        }
113
114        return saved.matchesPassword(plaintextBytes);
115    }
116
117    private boolean constantEquals(String savedHash, String computedHash) {
118
119        byte[] savedHashByteArray = savedHash.getBytes();
120        byte[] computedHashByteArray = computedHash.getBytes();
121
122        return MessageDigest.isEqual(savedHashByteArray, computedHashByteArray);
123    }
124
125    protected void checkHashFormatDurability() {
126
127        if (!this.hashFormatWarned) {
128
129            HashFormat format = this.hashFormat;
130
131            if (!(format instanceof ParsableHashFormat) && LOGGER.isWarnEnabled()) {
132                String msg = "The configured hashFormat instance [" + format.getClass().getName() + "] is not a "
133                        + ParsableHashFormat.class.getName() + " implementation.  This is "
134                        + "required if you wish to support backwards compatibility for saved password checking (almost "
135                        + "always desirable).  Without a " + ParsableHashFormat.class.getSimpleName() + " instance, "
136                        + "any hashService configuration changes will break previously hashed/saved passwords.";
137                LOGGER.warn(msg);
138                this.hashFormatWarned = true;
139            }
140        }
141    }
142
143    protected HashRequest createHashRequest(ByteSource plaintext) {
144        return new HashRequest.Builder().setSource(plaintext)
145                .setAlgorithmName(getHashService().getDefaultAlgorithmName())
146                .build();
147    }
148
149    protected ByteSource createByteSource(Object o) {
150        return ByteSource.Util.bytes(o);
151    }
152
153    @Override
154    public boolean passwordsMatch(Object submittedPlaintext, String saved) {
155        ByteSource plaintextBytes = createByteSource(submittedPlaintext);
156
157        if (saved == null || saved.length() == 0) {
158            return plaintextBytes == null || plaintextBytes.isEmpty();
159        } else {
160            if (plaintextBytes == null || plaintextBytes.isEmpty()) {
161                return false;
162            }
163        }
164
165        //First check to see if we can reconstitute the original hash - this allows us to
166        //perform password hash comparisons even for previously saved passwords that don't
167        //match the current HashService configuration values.  This is a very nice feature
168        //for password comparisons because it ensures backwards compatibility even after
169        //configuration changes.
170        HashFormat discoveredFormat = this.hashFormatFactory.getInstance(saved);
171
172        if (discoveredFormat instanceof ParsableHashFormat) {
173
174            ParsableHashFormat parsableHashFormat = (ParsableHashFormat) discoveredFormat;
175            Hash savedHash = parsableHashFormat.parse(saved);
176
177            return passwordsMatch(submittedPlaintext, savedHash);
178        }
179
180        //If we're at this point in the method's execution, We couldn't reconstitute the original hash.
181        //So, we need to hash the submittedPlaintext using current HashService configuration and then
182        //compare the formatted output with the saved string.  This will correctly compare passwords,
183        //but does not allow changing the HashService configuration without breaking previously saved
184        //passwords:
185
186        //The saved text value can't be reconstituted into a Hash instance.  We need to format the
187        //submittedPlaintext and then compare this formatted value with the saved value:
188        HashRequest request = createHashRequest(plaintextBytes);
189        Hash computed = this.hashService.computeHash(request);
190        String formatted = this.hashFormat.format(computed);
191
192        return constantEquals(saved, formatted);
193    }
194
195    public HashService getHashService() {
196        return hashService;
197    }
198
199    public void setHashService(HashService hashService) {
200        this.hashService = hashService;
201    }
202
203    public HashFormat getHashFormat() {
204        return hashFormat;
205    }
206
207    public void setHashFormat(HashFormat hashFormat) {
208        this.hashFormat = hashFormat;
209    }
210
211    public HashFormatFactory getHashFormatFactory() {
212        return hashFormatFactory;
213    }
214
215    public void setHashFormatFactory(HashFormatFactory hashFormatFactory) {
216        this.hashFormatFactory = hashFormatFactory;
217    }
218}