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