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