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}