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}