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.Logger; 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 }