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-2016 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.forgerock.opendj.config.server.ConfigurationChangeListener; 037import org.forgerock.opendj.server.config.server.DictionaryPasswordValidatorCfg; 038import org.forgerock.opendj.server.config.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 /** Creates a new instance of this dictionary password validator. */ 059 public DictionaryPasswordValidator() 060 { 061 super(); 062 063 // No implementation is required here. All initialization should be 064 // performed in the initializePasswordValidator() method. 065 } 066 067 @Override 068 public void initializePasswordValidator( 069 DictionaryPasswordValidatorCfg configuration) 070 throws ConfigException, InitializationException 071 { 072 configuration.addDictionaryChangeListener(this); 073 currentConfig = configuration; 074 075 dictionary = loadDictionary(configuration); 076 } 077 078 @Override 079 public void finalizePasswordValidator() 080 { 081 currentConfig.removeDictionaryChangeListener(this); 082 } 083 084 @Override 085 public boolean passwordIsAcceptable(ByteString newPassword, 086 Set<ByteString> currentPasswords, 087 Operation operation, Entry userEntry, 088 LocalizableMessageBuilder invalidReason) 089 { 090 // Get a handle to the current configuration. 091 DictionaryPasswordValidatorCfg config = currentConfig; 092 093 // Check to see if the provided password is in the dictionary in the order 094 // that it was provided. 095 String password = newPassword.toString(); 096 if (! config.isCaseSensitiveValidation()) 097 { 098 password = toLowerCase(password); 099 } 100 101 // Check to see if we should verify the whole password or the substrings. 102 // Either way, we initialise the minSubstringLength to the length of 103 // the password which is the default behaviour ('check-substrings: false') 104 int minSubstringLength = password.length(); 105 106 if (config.isCheckSubstrings() 107 // We apply the minimal substring length only if the provided value 108 // is smaller then the actual password length 109 && config.getMinSubstringLength() < password.length()) 110 { 111 minSubstringLength = config.getMinSubstringLength(); 112 } 113 114 // Verify if the dictionary contains the word(s) in the password 115 if (isDictionaryBased(password, minSubstringLength)) 116 { 117 invalidReason.append( 118 ERR_DICTIONARY_VALIDATOR_PASSWORD_IN_DICTIONARY.get()); 119 return false; 120 } 121 122 // If the reverse password checking is enabled, then verify if the 123 // reverse value of the password is in the dictionary. 124 if (config.isTestReversedPassword() 125 && isDictionaryBased( 126 new StringBuilder(password).reverse().toString(), minSubstringLength)) 127 { 128 invalidReason.append(ERR_DICTIONARY_VALIDATOR_PASSWORD_IN_DICTIONARY.get()); 129 return false; 130 } 131 132 // If we've gotten here, then the password is acceptable. 133 return true; 134 } 135 136 /** 137 * Loads the configured dictionary and returns it as a hash set. 138 * 139 * @param configuration the configuration for this password validator. 140 * 141 * @return The hash set containing the loaded dictionary data. 142 * 143 * @throws ConfigException If the configured dictionary file does not exist. 144 * 145 * @throws InitializationException If a problem occurs while attempting to 146 * read from the dictionary file. 147 */ 148 private HashSet<String> loadDictionary( 149 DictionaryPasswordValidatorCfg configuration) 150 throws ConfigException, InitializationException 151 { 152 // Get the path to the dictionary file and make sure it exists. 153 File dictionaryFile = getFileForPath(configuration.getDictionaryFile()); 154 if (! dictionaryFile.exists()) 155 { 156 LocalizableMessage message = ERR_DICTIONARY_VALIDATOR_NO_SUCH_FILE.get( 157 configuration.getDictionaryFile()); 158 throw new ConfigException(message); 159 } 160 161 // Read the contents of file into the dictionary as per the configuration. 162 BufferedReader reader = null; 163 HashSet<String> dictionary = new HashSet<>(); 164 try 165 { 166 reader = new BufferedReader(new FileReader(dictionaryFile)); 167 String line = reader.readLine(); 168 while (line != null) 169 { 170 if (! configuration.isCaseSensitiveValidation()) 171 { 172 line = line.toLowerCase(); 173 } 174 175 dictionary.add(line); 176 line = reader.readLine(); 177 } 178 } 179 catch (Exception e) 180 { 181 logger.traceException(e); 182 183 LocalizableMessage message = ERR_DICTIONARY_VALIDATOR_CANNOT_READ_FILE.get(configuration.getDictionaryFile(), e); 184 throw new InitializationException(message); 185 } 186 finally 187 { 188 close(reader); 189 } 190 191 return dictionary; 192 } 193 194 @Override 195 public boolean isConfigurationAcceptable(PasswordValidatorCfg configuration, 196 List<LocalizableMessage> unacceptableReasons) 197 { 198 DictionaryPasswordValidatorCfg config = 199 (DictionaryPasswordValidatorCfg) configuration; 200 return isConfigurationChangeAcceptable(config, unacceptableReasons); 201 } 202 203 @Override 204 public boolean isConfigurationChangeAcceptable( 205 DictionaryPasswordValidatorCfg configuration, 206 List<LocalizableMessage> unacceptableReasons) 207 { 208 // Make sure that we can load the dictionary. If so, then we'll accept the 209 // new configuration. 210 try 211 { 212 loadDictionary(configuration); 213 } 214 catch (ConfigException | InitializationException e) 215 { 216 unacceptableReasons.add(e.getMessageObject()); 217 return false; 218 } 219 catch (Exception e) 220 { 221 unacceptableReasons.add(getExceptionMessage(e)); 222 return false; 223 } 224 225 return true; 226 } 227 228 @Override 229 public ConfigChangeResult applyConfigurationChange( 230 DictionaryPasswordValidatorCfg configuration) 231 { 232 // Make sure we can load the dictionary. If we can, then activate the new 233 // configuration. 234 final ConfigChangeResult ccr = new ConfigChangeResult(); 235 try 236 { 237 dictionary = loadDictionary(configuration); 238 currentConfig = configuration; 239 } 240 catch (Exception e) 241 { 242 ccr.setResultCode(DirectoryConfig.getServerErrorResultCode()); 243 ccr.addMessage(getExceptionMessage(e)); 244 } 245 return ccr; 246 } 247 248 private boolean isDictionaryBased(String password, 249 int minSubstringLength) 250 { 251 HashSet<String> dictionary = this.dictionary; 252 final int passwordLength = password.length(); 253 254 for (int i = 0; i < passwordLength; i++) 255 { 256 for (int j = i + minSubstringLength; j <= passwordLength; j++) 257 { 258 String substring = password.substring(i, j); 259 if (dictionary.contains(substring)) 260 { 261 return true; 262 } 263 } 264 } 265 266 return false; 267 } 268}