View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.authc.credential;
20  
21  import java.security.MessageDigest;
22  
23  import org.apache.shiro.crypto.hash.DefaultHashService;
24  import org.apache.shiro.crypto.hash.Hash;
25  import org.apache.shiro.crypto.hash.HashRequest;
26  import org.apache.shiro.crypto.hash.HashService;
27  import org.apache.shiro.crypto.hash.format.*;
28  import org.apache.shiro.util.ByteSource;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  /**
33   * Default implementation of the {@link PasswordService} interface that relies on an internal
34   * {@link HashService}, {@link HashFormat}, and {@link HashFormatFactory} to function:
35   * <h2>Hashing Passwords</h2>
36   *
37   * <h2>Comparing Passwords</h2>
38   * All hashing operations are performed by the internal {@link #getHashService() hashService}.  After the hash
39   * is computed, it is formatted into a String value via the internal {@link #getHashFormat() hashFormat}.
40   *
41   * @since 1.2
42   */
43  public class DefaultPasswordService implements HashingPasswordService {
44  
45      public static final String DEFAULT_HASH_ALGORITHM = "SHA-256";
46      public static final int DEFAULT_HASH_ITERATIONS = 500000; //500,000
47  
48      private static final Logger log = LoggerFactory.getLogger(DefaultPasswordService.class);
49  
50      private HashService hashService;
51      private HashFormat hashFormat;
52      private HashFormatFactory hashFormatFactory;
53  
54      private volatile boolean hashFormatWarned; //used to avoid excessive log noise
55  
56      public DefaultPasswordService() {
57          this.hashFormatWarned = false;
58  
59          DefaultHashService hashService = new DefaultHashService();
60          hashService.setHashAlgorithmName(DEFAULT_HASH_ALGORITHM);
61          hashService.setHashIterations(DEFAULT_HASH_ITERATIONS);
62          hashService.setGeneratePublicSalt(true); //always want generated salts for user passwords to be most secure
63          this.hashService = hashService;
64  
65          this.hashFormat = new Shiro1CryptFormat();
66          this.hashFormatFactory = new DefaultHashFormatFactory();
67      }
68  
69      public String encryptPassword(Object plaintext) {
70          Hash hash = hashPassword(plaintext);
71          checkHashFormatDurability();
72          return this.hashFormat.format(hash);
73      }
74  
75      public Hash hashPassword(Object plaintext) {
76          ByteSource plaintextBytes = createByteSource(plaintext);
77          if (plaintextBytes == null || plaintextBytes.isEmpty()) {
78              return null;
79          }
80          HashRequest request = createHashRequest(plaintextBytes);
81          return hashService.computeHash(request);
82      }
83  
84      public boolean passwordsMatch(Object plaintext, Hash saved) {
85          ByteSource plaintextBytes = createByteSource(plaintext);
86  
87          if (saved == null || saved.isEmpty()) {
88              return plaintextBytes == null || plaintextBytes.isEmpty();
89          } else {
90              if (plaintextBytes == null || plaintextBytes.isEmpty()) {
91                  return false;
92              }
93          }
94  
95          HashRequest request = buildHashRequest(plaintextBytes, saved);
96  
97          Hash computed = this.hashService.computeHash(request);
98  
99          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 }