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 org.apache.shiro.crypto.hash.DefaultHashService;
22  import org.apache.shiro.crypto.hash.Hash;
23  import org.apache.shiro.crypto.hash.HashRequest;
24  import org.apache.shiro.crypto.hash.HashService;
25  import org.apache.shiro.crypto.hash.format.*;
26  import org.apache.shiro.util.ByteSource;
27  import org.slf4j.Logger;
28  import org.slf4j.LoggerFactory;
29  
30  /**
31   * Default implementation of the {@link PasswordService} interface that relies on an internal
32   * {@link HashService}, {@link HashFormat}, and {@link HashFormatFactory} to function:
33   * <h2>Hashing Passwords</h2>
34   *
35   * <h2>Comparing Passwords</h2>
36   * All hashing operations are performed by the internal {@link #getHashService() hashService}.  After the hash
37   * is computed, it is formatted into a String value via the internal {@link #getHashFormat() hashFormat}.
38   *
39   * @since 1.2
40   */
41  public class DefaultPasswordService implements HashingPasswordService {
42  
43      public static final String DEFAULT_HASH_ALGORITHM = "SHA-256";
44      public static final int DEFAULT_HASH_ITERATIONS = 500000; //500,000
45  
46      private static final Logger log = LoggerFactory.getLogger(DefaultPasswordService.class);
47  
48      private HashService hashService;
49      private HashFormat hashFormat;
50      private HashFormatFactory hashFormatFactory;
51  
52      private volatile boolean hashFormatWarned; //used to avoid excessive log noise
53  
54      public DefaultPasswordService() {
55          this.hashFormatWarned = false;
56  
57          DefaultHashService hashService = new DefaultHashService();
58          hashService.setHashAlgorithmName(DEFAULT_HASH_ALGORITHM);
59          hashService.setHashIterations(DEFAULT_HASH_ITERATIONS);
60          hashService.setGeneratePublicSalt(true); //always want generated salts for user passwords to be most secure
61          this.hashService = hashService;
62  
63          this.hashFormat = new Shiro1CryptFormat();
64          this.hashFormatFactory = new DefaultHashFormatFactory();
65      }
66  
67      public String encryptPassword(Object plaintext) {
68          Hash hash = hashPassword(plaintext);
69          checkHashFormatDurability();
70          return this.hashFormat.format(hash);
71      }
72  
73      public Hash hashPassword(Object plaintext) {
74          ByteSource plaintextBytes = createByteSource(plaintext);
75          if (plaintextBytes == null || plaintextBytes.isEmpty()) {
76              return null;
77          }
78          HashRequest request = createHashRequest(plaintextBytes);
79          return hashService.computeHash(request);
80      }
81  
82      public boolean passwordsMatch(Object plaintext, Hash saved) {
83          ByteSource plaintextBytes = createByteSource(plaintext);
84  
85          if (saved == null || saved.isEmpty()) {
86              return plaintextBytes == null || plaintextBytes.isEmpty();
87          } else {
88              if (plaintextBytes == null || plaintextBytes.isEmpty()) {
89                  return false;
90              }
91          }
92  
93          HashRequest request = buildHashRequest(plaintextBytes, saved);
94  
95          Hash computed = this.hashService.computeHash(request);
96  
97          return saved.equals(computed);
98      }
99  
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 }