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 module hunt.shiro.authc.credential.DefaultPasswordService;
20 
21 import hunt.shiro.authc.credential.HashingPasswordService;
22 
23 import hunt.shiro.crypto.hash;
24 import hunt.shiro.util.ByteSource;
25 import hunt.shiro.util.SimpleByteSource;
26 import hunt.logging.ConsoleLogger;
27 
28 import std.array;
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  */
40 class DefaultPasswordService : HashingPasswordService {
41 
42     enum string DEFAULT_HASH_ALGORITHM = "SHA-256";
43     enum int DEFAULT_HASH_ITERATIONS = 500000; //500,000
44 
45 
46 
47     private HashService hashService;
48     private HashFormat hashFormat;
49     private HashFormatFactory hashFormatFactory;
50 
51     private bool hashFormatWarned; //used to avoid excessive log noise
52 
53     this() {
54         this.hashFormatWarned = false;
55 
56         DefaultHashService hashService = new DefaultHashService();
57         hashService.setHashAlgorithmName(DEFAULT_HASH_ALGORITHM);
58         hashService.setHashIterations(DEFAULT_HASH_ITERATIONS);
59         hashService.setGeneratePublicSalt(true); //always want generated salts for user passwords to be most secure
60         this.hashService = hashService;
61 
62         this.hashFormat = new Shiro1CryptFormat();
63         this.hashFormatFactory = new DefaultHashFormatFactory();
64     }
65 
66     string encryptPassword(Object plaintext) {
67         Hash hash = hashPassword(plaintext);
68         checkHashFormatDurability();
69         return this.hashFormat.format(hash);
70     }
71 
72     Hash hashPassword(Object plaintext) {
73         ByteSource plaintextBytes = createByteSource(plaintext);
74         if (plaintextBytes  is null || plaintextBytes.isEmpty()) {
75             return null;
76         }
77         HashRequest request = createHashRequest(plaintextBytes);
78         return hashService.computeHash(request);
79     }
80 
81     bool passwordsMatch(Object plaintext, Hash saved) {
82         ByteSource plaintextBytes = createByteSource(plaintext);
83 
84         if (saved  is null || saved.isEmpty()) {
85             return plaintextBytes  is null || plaintextBytes.isEmpty();
86         } else {
87             if (plaintextBytes  is null || plaintextBytes.isEmpty()) {
88                 return false;
89             }
90         }
91 
92         HashRequest request = buildHashRequest(plaintextBytes, saved);
93 
94         Hash computed = this.hashService.computeHash(request);
95 
96         return saved == computed;
97     }
98 
99     protected void checkHashFormatDurability() {
100 
101         if (!this.hashFormatWarned) {
102 
103             version(HUNT_DEBUG) {
104                 HashFormat format = this.hashFormat;
105                 ParsableHashFormat formatCast = cast(ParsableHashFormat)format;
106                 if (!(formatCast !is null)) {
107                     string msg = "The configured hashFormat instance [" ~ 
108                             typeid(cast(Object)format).name ~ "] is not a " ~
109                             typeid(ParsableHashFormat).toString() ~ " implementation.  This is " ~
110                             "required if you wish to support backwards compatibility " ~ 
111                             "for saved password checking (almost " ~
112                             "always desirable).  Without a " ~ 
113                             typeid(ParsableHashFormat).toString() ~ " instance, " ~
114                             "any hashService configuration changes will break previously hashed/saved passwords.";
115                     warning(msg);
116                     this.hashFormatWarned = true;
117                 }
118             }
119         }
120     }
121 
122     protected HashRequest createHashRequest(ByteSource plaintext) {
123         return new HashRequest.Builder().setSource(plaintext).build();
124     }
125 
126     protected ByteSource createByteSource(Object o) {
127         return ByteSourceUtil.bytes(o);
128     }
129 
130      bool passwordsMatch(Object submittedPlaintext, string saved) {
131         ByteSource plaintextBytes = createByteSource(submittedPlaintext);
132 
133         if (saved.empty()) {
134             return plaintextBytes  is null || plaintextBytes.isEmpty();
135         } else {
136             if (plaintextBytes  is null || plaintextBytes.isEmpty()) {
137                 return false;
138             }
139         }
140 
141         //First check to see if we can reconstitute the original hash - this allows us to
142         //perform password hash comparisons even for previously saved passwords that don't
143         //match the current HashService configuration values.  This is a very nice feature
144         //for password comparisons because it ensures backwards compatibility even after
145         //configuration changes.
146         HashFormat discoveredFormat = this.hashFormatFactory.getInstance(saved);
147         auto discoveredFormatCast = cast(ParsableHashFormat)discoveredFormat;
148         if (discoveredFormat !is null && discoveredFormatCast !is null) {
149 
150             ParsableHashFormat parsableHashFormat = discoveredFormatCast;
151             Hash savedHash = parsableHashFormat.parse(saved);
152 
153             return passwordsMatch(submittedPlaintext, savedHash);
154         }
155 
156         //If we're at this point in the method's execution, We couldn't reconstitute the original hash.
157         //So, we need to hash the submittedPlaintext using current HashService configuration and then
158         //compare the formatted output with the saved string.  This will correctly compare passwords,
159         //but does not allow changing the HashService configuration without breaking previously saved
160         //passwords:
161 
162         //The saved text value can't be reconstituted into a Hash instance.  We need to format the
163         //submittedPlaintext and then compare this formatted value with the saved value:
164         HashRequest request = createHashRequest(plaintextBytes);
165         Hash computed = this.hashService.computeHash(request);
166         string formatted = this.hashFormat.format(computed);
167 
168         return saved== formatted;
169     }
170 
171     protected HashRequest buildHashRequest(ByteSource plaintext, Hash saved) {
172         //keep everything from the saved hash except for the source:
173         return new HashRequest.Builder().setSource(plaintext)
174                 //now use the existing saved data:
175                 .setAlgorithmName(saved.getAlgorithmName())
176                 .setSalt(saved.getSalt())
177                 .setIterations(saved.getIterations())
178                 .build();
179     }
180 
181      HashService getHashService() {
182         return hashService;
183     }
184 
185      void setHashService(HashService hashService) {
186         this.hashService = hashService;
187     }
188 
189      HashFormat getHashFormat() {
190         return hashFormat;
191     }
192 
193      void setHashFormat(HashFormat hashFormat) {
194         this.hashFormat = hashFormat;
195     }
196 
197      HashFormatFactory getHashFormatFactory() {
198         return hashFormatFactory;
199     }
200 
201      void setHashFormatFactory(HashFormatFactory hashFormatFactory) {
202         this.hashFormatFactory = hashFormatFactory;
203     }
204 }