001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2008 Sun Microsystems, Inc. 015 * Portions Copyright 2011 profiq, s.r.o. 016 * Portions Copyright 2014-2015 ForgeRock AS. 017 */ 018package org.opends.server.extensions; 019 020import static org.opends.messages.ExtensionMessages.*; 021import static org.opends.server.util.StaticUtils.*; 022 023import java.io.BufferedReader; 024import java.io.File; 025import java.io.FileReader; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Set; 029 030import org.forgerock.i18n.LocalizableMessage; 031import org.forgerock.i18n.LocalizableMessageBuilder; 032import org.forgerock.i18n.slf4j.LocalizedLogger; 033import org.forgerock.opendj.config.server.ConfigChangeResult; 034import org.forgerock.opendj.config.server.ConfigException; 035import org.forgerock.opendj.ldap.ByteString; 036import org.opends.server.admin.server.ConfigurationChangeListener; 037import org.opends.server.admin.std.server.DictionaryPasswordValidatorCfg; 038import org.opends.server.admin.std.server.PasswordValidatorCfg; 039import org.opends.server.api.PasswordValidator; 040import org.opends.server.types.*; 041 042/** 043 * This class provides an OpenDS password validator that may be used to ensure 044 * that proposed passwords are not contained in a specified dictionary. 045 */ 046public class DictionaryPasswordValidator 047 extends PasswordValidator<DictionaryPasswordValidatorCfg> 048 implements ConfigurationChangeListener<DictionaryPasswordValidatorCfg> 049{ 050 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 051 052 /** The current configuration for this password validator. */ 053 private DictionaryPasswordValidatorCfg currentConfig; 054 055 /** The current dictionary that we should use when performing the validation. */ 056 private HashSet<String> dictionary; 057 058 059 060 /** 061 * Creates a new instance of this dictionary password validator. 062 */ 063 public DictionaryPasswordValidator() 064 { 065 super(); 066 067 // No implementation is required here. All initialization should be 068 // performed in the initializePasswordValidator() method. 069 } 070 071 072 073 /** {@inheritDoc} */ 074 @Override 075 public void initializePasswordValidator( 076 DictionaryPasswordValidatorCfg configuration) 077 throws ConfigException, InitializationException 078 { 079 configuration.addDictionaryChangeListener(this); 080 currentConfig = configuration; 081 082 dictionary = loadDictionary(configuration); 083 } 084 085 086 087 /** {@inheritDoc} */ 088 @Override 089 public void finalizePasswordValidator() 090 { 091 currentConfig.removeDictionaryChangeListener(this); 092 } 093 094 095 096 /** {@inheritDoc} */ 097 @Override 098 public boolean passwordIsAcceptable(ByteString newPassword, 099 Set<ByteString> currentPasswords, 100 Operation operation, Entry userEntry, 101 LocalizableMessageBuilder invalidReason) 102 { 103 // Get a handle to the current configuration. 104 DictionaryPasswordValidatorCfg config = currentConfig; 105 106 // Check to see if the provided password is in the dictionary in the order 107 // that it was provided. 108 String password = newPassword.toString(); 109 if (! config.isCaseSensitiveValidation()) 110 { 111 password = toLowerCase(password); 112 } 113 114 // Check to see if we should verify the whole password or the substrings. 115 // Either way, we initialise the minSubstringLength to the length of 116 // the password which is the default behaviour ('check-substrings: false') 117 int minSubstringLength = password.length(); 118 119 if (config.isCheckSubstrings() 120 // We apply the minimal substring length only if the provided value 121 // is smaller then the actual password length 122 && config.getMinSubstringLength() < password.length()) 123 { 124 minSubstringLength = config.getMinSubstringLength(); 125 } 126 127 // Verify if the dictionary contains the word(s) in the password 128 if (isDictionaryBased(password, minSubstringLength)) 129 { 130 invalidReason.append( 131 ERR_DICTIONARY_VALIDATOR_PASSWORD_IN_DICTIONARY.get()); 132 return false; 133 } 134 135 // If the reverse password checking is enabled, then verify if the 136 // reverse value of the password is in the dictionary. 137 if (config.isTestReversedPassword() 138 && isDictionaryBased( 139 new StringBuilder(password).reverse().toString(), minSubstringLength)) 140 { 141 invalidReason.append(ERR_DICTIONARY_VALIDATOR_PASSWORD_IN_DICTIONARY.get()); 142 return false; 143 } 144 145 146 // If we've gotten here, then the password is acceptable. 147 return true; 148 } 149 150 151 152 /** 153 * Loads the configured dictionary and returns it as a hash set. 154 * 155 * @param configuration the configuration for this password validator. 156 * 157 * @return The hash set containing the loaded dictionary data. 158 * 159 * @throws ConfigException If the configured dictionary file does not exist. 160 * 161 * @throws InitializationException If a problem occurs while attempting to 162 * read from the dictionary file. 163 */ 164 private HashSet<String> loadDictionary( 165 DictionaryPasswordValidatorCfg configuration) 166 throws ConfigException, InitializationException 167 { 168 // Get the path to the dictionary file and make sure it exists. 169 File dictionaryFile = getFileForPath(configuration.getDictionaryFile()); 170 if (! dictionaryFile.exists()) 171 { 172 LocalizableMessage message = ERR_DICTIONARY_VALIDATOR_NO_SUCH_FILE.get( 173 configuration.getDictionaryFile()); 174 throw new ConfigException(message); 175 } 176 177 178 // Read the contents of file into the dictionary as per the configuration. 179 BufferedReader reader = null; 180 HashSet<String> dictionary = new HashSet<>(); 181 try 182 { 183 reader = new BufferedReader(new FileReader(dictionaryFile)); 184 String line = reader.readLine(); 185 while (line != null) 186 { 187 if (! configuration.isCaseSensitiveValidation()) 188 { 189 line = line.toLowerCase(); 190 } 191 192 dictionary.add(line); 193 line = reader.readLine(); 194 } 195 } 196 catch (Exception e) 197 { 198 logger.traceException(e); 199 200 LocalizableMessage message = ERR_DICTIONARY_VALIDATOR_CANNOT_READ_FILE.get(configuration.getDictionaryFile(), e); 201 throw new InitializationException(message); 202 } 203 finally 204 { 205 close(reader); 206 } 207 208 return dictionary; 209 } 210 211 212 213 /** {@inheritDoc} */ 214 @Override 215 public boolean isConfigurationAcceptable(PasswordValidatorCfg configuration, 216 List<LocalizableMessage> unacceptableReasons) 217 { 218 DictionaryPasswordValidatorCfg config = 219 (DictionaryPasswordValidatorCfg) configuration; 220 return isConfigurationChangeAcceptable(config, unacceptableReasons); 221 } 222 223 224 225 /** {@inheritDoc} */ 226 @Override 227 public boolean isConfigurationChangeAcceptable( 228 DictionaryPasswordValidatorCfg configuration, 229 List<LocalizableMessage> unacceptableReasons) 230 { 231 // Make sure that we can load the dictionary. If so, then we'll accept the 232 // new configuration. 233 try 234 { 235 loadDictionary(configuration); 236 } 237 catch (ConfigException | InitializationException e) 238 { 239 unacceptableReasons.add(e.getMessageObject()); 240 return false; 241 } 242 catch (Exception e) 243 { 244 unacceptableReasons.add(getExceptionMessage(e)); 245 return false; 246 } 247 248 return true; 249 } 250 251 252 253 /** {@inheritDoc} */ 254 @Override 255 public ConfigChangeResult applyConfigurationChange( 256 DictionaryPasswordValidatorCfg configuration) 257 { 258 // Make sure we can load the dictionary. If we can, then activate the new 259 // configuration. 260 final ConfigChangeResult ccr = new ConfigChangeResult(); 261 try 262 { 263 dictionary = loadDictionary(configuration); 264 currentConfig = configuration; 265 } 266 catch (Exception e) 267 { 268 ccr.setResultCode(DirectoryConfig.getServerErrorResultCode()); 269 ccr.addMessage(getExceptionMessage(e)); 270 } 271 return ccr; 272 } 273 274 private boolean isDictionaryBased(String password, 275 int minSubstringLength) 276 { 277 HashSet<String> dictionary = this.dictionary; 278 final int passwordLength = password.length(); 279 280 for (int i = 0; i < passwordLength; i++) 281 { 282 for (int j = i + minSubstringLength; j <= passwordLength; j++) 283 { 284 String substring = password.substring(i, j); 285 if (dictionary.contains(substring)) 286 { 287 return true; 288 } 289 } 290 } 291 292 return false; 293 } 294}