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