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