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    package org.apache.shiro.authc.credential;
020    
021    import org.apache.shiro.crypto.hash.DefaultHashService;
022    import org.apache.shiro.crypto.hash.Hash;
023    import org.apache.shiro.crypto.hash.HashRequest;
024    import org.apache.shiro.crypto.hash.HashService;
025    import org.apache.shiro.crypto.hash.format.*;
026    import org.apache.shiro.util.ByteSource;
027    import org.slf4j.Logger;
028    import 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     */
041    public 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    }