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.DefaultHashFormatFactory; 026import org.apache.shiro.crypto.hash.format.HashFormat; 027import org.apache.shiro.crypto.hash.format.HashFormatFactory; 028import org.apache.shiro.crypto.hash.format.ParsableHashFormat; 029import org.apache.shiro.crypto.hash.format.Shiro2CryptFormat; 030import org.apache.shiro.lang.util.ByteSource; 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034import java.security.MessageDigest; 035 036import static java.util.Objects.requireNonNull; 037 038/** 039 * Default implementation of the {@link PasswordService} interface that relies on an internal 040 * {@link HashService}, {@link HashFormat}, and {@link HashFormatFactory} to function: 041 * <h2>Hashing Passwords</h2> 042 * 043 * <h2>Comparing Passwords</h2> 044 * All hashing operations are performed by the internal {@link #getHashService() hashService}. 045 * 046 * @since 1.2 047 */ 048public class DefaultPasswordService implements HashingPasswordService { 049 050 /** 051 * default hash algorithm. 052 */ 053 public static final String DEFAULT_HASH_ALGORITHM = "argon2id"; 054 055 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPasswordService.class); 056 057 private HashService hashService; 058 private HashFormat hashFormat; 059 private HashFormatFactory hashFormatFactory; 060 061 /** 062 * used to avoid excessive log noise 063 */ 064 private volatile boolean hashFormatWarned; 065 066 /** 067 * Constructs a new PasswordService with a default hash service and the default 068 * algorithm name {@value #DEFAULT_HASH_ALGORITHM}, a default hash format (shiro2) and 069 * a default hash format factory. 070 * 071 * <p>The default algorithm can change between minor versions and does not introduce 072 * API incompatibility by design.</p> 073 */ 074 public DefaultPasswordService() { 075 this.hashFormatWarned = false; 076 077 DefaultHashService hashService = new DefaultHashService(); 078 hashService.setDefaultAlgorithmName(DEFAULT_HASH_ALGORITHM); 079 this.hashService = hashService; 080 081 this.hashFormat = new Shiro2CryptFormat(); 082 this.hashFormatFactory = new DefaultHashFormatFactory(); 083 } 084 085 @Override 086 public String encryptPassword(Object plaintext) { 087 Hash hash = hashPassword(requireNonNull(plaintext)); 088 checkHashFormatDurability(); 089 return this.hashFormat.format(hash); 090 } 091 092 @Override 093 public Hash hashPassword(Object plaintext) { 094 ByteSource plaintextBytes = createByteSource(plaintext); 095 if (plaintextBytes == null || plaintextBytes.isEmpty()) { 096 return null; 097 } 098 HashRequest request = createHashRequest(plaintextBytes); 099 return hashService.computeHash(request); 100 } 101 102 @Override 103 public boolean passwordsMatch(Object plaintext, Hash saved) { 104 ByteSource plaintextBytes = createByteSource(plaintext); 105 106 if (saved == null || saved.isEmpty()) { 107 return plaintextBytes == null || plaintextBytes.isEmpty(); 108 } else { 109 if (plaintextBytes == null || plaintextBytes.isEmpty()) { 110 return false; 111 } 112 } 113 114 return saved.matchesPassword(plaintextBytes); 115 } 116 117 private boolean constantEquals(String savedHash, String computedHash) { 118 119 byte[] savedHashByteArray = savedHash.getBytes(); 120 byte[] computedHashByteArray = computedHash.getBytes(); 121 122 return MessageDigest.isEqual(savedHashByteArray, computedHashByteArray); 123 } 124 125 protected void checkHashFormatDurability() { 126 127 if (!this.hashFormatWarned) { 128 129 HashFormat format = this.hashFormat; 130 131 if (!(format instanceof ParsableHashFormat) && LOGGER.isWarnEnabled()) { 132 String msg = "The configured hashFormat instance [" + format.getClass().getName() + "] is not a " 133 + ParsableHashFormat.class.getName() + " implementation. This is " 134 + "required if you wish to support backwards compatibility for saved password checking (almost " 135 + "always desirable). Without a " + ParsableHashFormat.class.getSimpleName() + " instance, " 136 + "any hashService configuration changes will break previously hashed/saved passwords."; 137 LOGGER.warn(msg); 138 this.hashFormatWarned = true; 139 } 140 } 141 } 142 143 protected HashRequest createHashRequest(ByteSource plaintext) { 144 return new HashRequest.Builder().setSource(plaintext) 145 .setAlgorithmName(getHashService().getDefaultAlgorithmName()) 146 .build(); 147 } 148 149 protected ByteSource createByteSource(Object o) { 150 return ByteSource.Util.bytes(o); 151 } 152 153 @Override 154 public boolean passwordsMatch(Object submittedPlaintext, String saved) { 155 ByteSource plaintextBytes = createByteSource(submittedPlaintext); 156 157 if (saved == null || saved.length() == 0) { 158 return plaintextBytes == null || plaintextBytes.isEmpty(); 159 } else { 160 if (plaintextBytes == null || plaintextBytes.isEmpty()) { 161 return false; 162 } 163 } 164 165 //First check to see if we can reconstitute the original hash - this allows us to 166 //perform password hash comparisons even for previously saved passwords that don't 167 //match the current HashService configuration values. This is a very nice feature 168 //for password comparisons because it ensures backwards compatibility even after 169 //configuration changes. 170 HashFormat discoveredFormat = this.hashFormatFactory.getInstance(saved); 171 172 if (discoveredFormat instanceof ParsableHashFormat) { 173 174 ParsableHashFormat parsableHashFormat = (ParsableHashFormat) discoveredFormat; 175 Hash savedHash = parsableHashFormat.parse(saved); 176 177 return passwordsMatch(submittedPlaintext, savedHash); 178 } 179 180 //If we're at this point in the method's execution, We couldn't reconstitute the original hash. 181 //So, we need to hash the submittedPlaintext using current HashService configuration and then 182 //compare the formatted output with the saved string. This will correctly compare passwords, 183 //but does not allow changing the HashService configuration without breaking previously saved 184 //passwords: 185 186 //The saved text value can't be reconstituted into a Hash instance. We need to format the 187 //submittedPlaintext and then compare this formatted value with the saved value: 188 HashRequest request = createHashRequest(plaintextBytes); 189 Hash computed = this.hashService.computeHash(request); 190 String formatted = this.hashFormat.format(computed); 191 192 return constantEquals(saved, formatted); 193 } 194 195 public HashService getHashService() { 196 return hashService; 197 } 198 199 public void setHashService(HashService hashService) { 200 this.hashService = hashService; 201 } 202 203 public HashFormat getHashFormat() { 204 return hashFormat; 205 } 206 207 public void setHashFormat(HashFormat hashFormat) { 208 this.hashFormat = hashFormat; 209 } 210 211 public HashFormatFactory getHashFormatFactory() { 212 return hashFormatFactory; 213 } 214 215 public void setHashFormatFactory(HashFormatFactory hashFormatFactory) { 216 this.hashFormatFactory = hashFormatFactory; 217 } 218}