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 */ 019package org.apache.shiro.authc.credential; 020 021import java.security.MessageDigest; 022 023import org.apache.shiro.crypto.hash.DefaultHashService; 024import org.apache.shiro.crypto.hash.Hash; 025import org.apache.shiro.crypto.hash.HashRequest; 026import org.apache.shiro.crypto.hash.HashService; 027import org.apache.shiro.crypto.hash.format.*; 028import org.apache.shiro.util.ByteSource; 029import org.slf4j.Logger; 030import org.slf4j.LoggerFactory; 031 032/** 033 * Default implementation of the {@link PasswordService} interface that relies on an internal 034 * {@link HashService}, {@link HashFormat}, and {@link HashFormatFactory} to function: 035 * <h2>Hashing Passwords</h2> 036 * 037 * <h2>Comparing Passwords</h2> 038 * All hashing operations are performed by the internal {@link #getHashService() hashService}. After the hash 039 * is computed, it is formatted into a String value via the internal {@link #getHashFormat() hashFormat}. 040 * 041 * @since 1.2 042 */ 043public class DefaultPasswordService implements HashingPasswordService { 044 045 public static final String DEFAULT_HASH_ALGORITHM = "SHA-256"; 046 public static final int DEFAULT_HASH_ITERATIONS = 500000; //500,000 047 048 private static final Logger log = LoggerFactory.getLogger(DefaultPasswordService.class); 049 050 private HashService hashService; 051 private HashFormat hashFormat; 052 private HashFormatFactory hashFormatFactory; 053 054 private volatile boolean hashFormatWarned; //used to avoid excessive log noise 055 056 public DefaultPasswordService() { 057 this.hashFormatWarned = false; 058 059 DefaultHashService hashService = new DefaultHashService(); 060 hashService.setHashAlgorithmName(DEFAULT_HASH_ALGORITHM); 061 hashService.setHashIterations(DEFAULT_HASH_ITERATIONS); 062 hashService.setGeneratePublicSalt(true); //always want generated salts for user passwords to be most secure 063 this.hashService = hashService; 064 065 this.hashFormat = new Shiro1CryptFormat(); 066 this.hashFormatFactory = new DefaultHashFormatFactory(); 067 } 068 069 public String encryptPassword(Object plaintext) { 070 Hash hash = hashPassword(plaintext); 071 checkHashFormatDurability(); 072 return this.hashFormat.format(hash); 073 } 074 075 public Hash hashPassword(Object plaintext) { 076 ByteSource plaintextBytes = createByteSource(plaintext); 077 if (plaintextBytes == null || plaintextBytes.isEmpty()) { 078 return null; 079 } 080 HashRequest request = createHashRequest(plaintextBytes); 081 return hashService.computeHash(request); 082 } 083 084 public boolean passwordsMatch(Object plaintext, Hash saved) { 085 ByteSource plaintextBytes = createByteSource(plaintext); 086 087 if (saved == null || saved.isEmpty()) { 088 return plaintextBytes == null || plaintextBytes.isEmpty(); 089 } else { 090 if (plaintextBytes == null || plaintextBytes.isEmpty()) { 091 return false; 092 } 093 } 094 095 HashRequest request = buildHashRequest(plaintextBytes, saved); 096 097 Hash computed = this.hashService.computeHash(request); 098 099 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}