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 2013-2015 ForgeRock AS.
025 */
026package org.opends.server.extensions;
027
028import java.security.NoSuchAlgorithmException;
029import java.security.SecureRandom;
030import java.security.spec.KeySpec;
031import java.util.Arrays;
032import java.util.List;
033
034import javax.crypto.SecretKeyFactory;
035import javax.crypto.spec.PBEKeySpec;
036
037import org.forgerock.i18n.LocalizableMessage;
038import org.forgerock.i18n.slf4j.LocalizedLogger;
039import org.forgerock.opendj.config.server.ConfigException;
040import org.forgerock.opendj.ldap.ByteSequence;
041import org.forgerock.opendj.ldap.ByteString;
042import org.forgerock.opendj.ldap.ResultCode;
043import org.opends.server.admin.server.ConfigurationChangeListener;
044import org.opends.server.admin.std.server.PBKDF2PasswordStorageSchemeCfg;
045import org.opends.server.api.PasswordStorageScheme;
046import org.opends.server.core.DirectoryServer;
047import org.forgerock.opendj.config.server.ConfigChangeResult;
048import org.opends.server.types.DirectoryException;
049import org.opends.server.types.InitializationException;
050import org.opends.server.util.Base64;
051
052import static org.opends.messages.ExtensionMessages.*;
053import static org.opends.server.extensions.ExtensionsConstants.*;
054import static org.opends.server.util.StaticUtils.*;
055
056/**
057 * This class defines a Directory Server password storage scheme based on the
058 * PBKDF2 algorithm defined in RFC 2898.  This is a one-way digest algorithm
059 * so there is no way to retrieve the original clear-text version of the
060 * password from the hashed value (although this means that it is not suitable
061 * for things that need the clear-text password like DIGEST-MD5).  This
062 * implementation uses a configurable number of iterations.
063 */
064public class PBKDF2PasswordStorageScheme
065    extends PasswordStorageScheme<PBKDF2PasswordStorageSchemeCfg>
066    implements ConfigurationChangeListener<PBKDF2PasswordStorageSchemeCfg>
067{
068  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
069
070  /** The fully-qualified name of this class. */
071  private static final String CLASS_NAME = "org.opends.server.extensions.PBKDF2PasswordStorageScheme";
072
073  /** The number of bytes of random data to use as the salt when generating the hashes. */
074  private static final int NUM_SALT_BYTES = 8;
075
076  /** The number of bytes the SHA-1 algorithm produces. */
077  private static final int SHA1_LENGTH = 20;
078
079  /** The secure random number generator to use to generate the salt values. */
080  private SecureRandom random;
081
082  /** The current configuration for this storage scheme. */
083  private volatile PBKDF2PasswordStorageSchemeCfg config;
084
085  /**
086   * Creates a new instance of this password storage scheme.  Note that no
087   * initialization should be performed here, as all initialization should be
088   * done in the <CODE>initializePasswordStorageScheme</CODE> method.
089   */
090  public PBKDF2PasswordStorageScheme()
091  {
092    super();
093  }
094
095  /** {@inheritDoc} */
096  @Override
097  public void initializePasswordStorageScheme(PBKDF2PasswordStorageSchemeCfg configuration)
098      throws ConfigException, InitializationException
099  {
100    try
101    {
102      random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
103      // Just try to verify if the algorithm is supported.
104      SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
105    }
106    catch (NoSuchAlgorithmException e)
107    {
108      throw new InitializationException(null);
109    }
110
111    this.config = configuration;
112    config.addPBKDF2ChangeListener(this);
113  }
114
115  /** {@inheritDoc} */
116  @Override
117  public boolean isConfigurationChangeAcceptable(PBKDF2PasswordStorageSchemeCfg configuration,
118                                                 List<LocalizableMessage> unacceptableReasons)
119  {
120    return true;
121  }
122
123  /** {@inheritDoc} */
124  @Override
125  public ConfigChangeResult applyConfigurationChange(PBKDF2PasswordStorageSchemeCfg configuration)
126  {
127    this.config = configuration;
128    return new ConfigChangeResult();
129  }
130
131  /** {@inheritDoc} */
132  @Override
133  public String getStorageSchemeName()
134  {
135    return STORAGE_SCHEME_NAME_PBKDF2;
136  }
137
138  /** {@inheritDoc} */
139  @Override
140  public ByteString encodePassword(ByteSequence plaintext)
141      throws DirectoryException
142  {
143    byte[] saltBytes      = new byte[NUM_SALT_BYTES];
144    int    iterations     = config.getPBKDF2Iterations();
145
146    byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations,random);
147    byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes);
148
149    return ByteString.valueOfUtf8(iterations + ":" + Base64.encode(hashPlusSalt));
150  }
151
152  /** {@inheritDoc} */
153  @Override
154  public ByteString encodePasswordWithScheme(ByteSequence plaintext)
155      throws DirectoryException
156  {
157    return ByteString.valueOfUtf8('{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + encodePassword(plaintext));
158  }
159
160  /** {@inheritDoc} */
161  @Override
162  public boolean passwordMatches(ByteSequence plaintextPassword, ByteSequence storedPassword) {
163    // Split the iterations from the stored value (separated by a ':')
164    // Base64-decode the remaining value and take the last 8 bytes as the salt.
165    try
166    {
167      final String stored = storedPassword.toString();
168      final int pos = stored.indexOf(':');
169      if (pos == -1)
170      {
171        throw new Exception();
172      }
173
174      final int iterations = Integer.parseInt(stored.substring(0, pos));
175      byte[] decodedBytes = Base64.decode(stored.substring(pos + 1));
176
177      final int saltLength = decodedBytes.length - SHA1_LENGTH;
178      if (saltLength <= 0)
179      {
180        logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword);
181        return false;
182      }
183
184      final byte[] digestBytes = new byte[SHA1_LENGTH];
185      final byte[] saltBytes = new byte[saltLength];
186      System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA1_LENGTH);
187      System.arraycopy(decodedBytes, SHA1_LENGTH, saltBytes, 0, saltLength);
188      return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
189    }
190    catch (Exception e)
191    {
192      logger.traceException(e);
193      logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e);
194      return false;
195    }
196  }
197
198  /** {@inheritDoc} */
199  @Override
200  public boolean supportsAuthPasswordSyntax()
201  {
202    return true;
203  }
204
205  /** {@inheritDoc} */
206  @Override
207  public String getAuthPasswordSchemeName()
208  {
209    return AUTH_PASSWORD_SCHEME_NAME_PBKDF2;
210  }
211
212  /** {@inheritDoc} */
213  @Override
214  public ByteString encodeAuthPassword(ByteSequence plaintext)
215      throws DirectoryException
216  {
217    byte[] saltBytes      = new byte[NUM_SALT_BYTES];
218    int    iterations     = config.getPBKDF2Iterations();
219    byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations,random);
220
221    // Encode and return the value.
222    return ByteString.valueOfUtf8(AUTH_PASSWORD_SCHEME_NAME_PBKDF2 + '$'
223        + iterations + ':' + Base64.encode(saltBytes) + '$' + Base64.encode(digestBytes));
224  }
225
226  /** {@inheritDoc} */
227  @Override
228  public boolean authPasswordMatches(ByteSequence plaintextPassword, String authInfo, String authValue)
229  {
230    try
231    {
232      int pos = authInfo.indexOf(':');
233      if (pos == -1)
234      {
235        throw new Exception();
236      }
237      int iterations = Integer.parseInt(authInfo.substring(0, pos));
238      byte[] saltBytes   = Base64.decode(authInfo.substring(pos + 1));
239      byte[] digestBytes = Base64.decode(authValue);
240      return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
241    }
242    catch (Exception e)
243    {
244      logger.traceException(e);
245      return false;
246    }
247  }
248
249  /** {@inheritDoc} */
250  @Override
251  public boolean isReversible()
252  {
253    return false;
254  }
255
256  /** {@inheritDoc} */
257  @Override
258  public ByteString getPlaintextValue(ByteSequence storedPassword)
259      throws DirectoryException
260  {
261    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_PBKDF2);
262    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
263  }
264
265  /** {@inheritDoc} */
266  @Override
267  public ByteString getAuthPasswordPlaintextValue(String authInfo, String authValue)
268      throws DirectoryException
269  {
270    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_PBKDF2);
271    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
272  }
273
274  /** {@inheritDoc} */
275  @Override
276  public boolean isStorageSchemeSecure()
277  {
278    return true;
279  }
280
281
282  /**
283   * Generates an encoded password string from the given clear-text password.
284   * This method is primarily intended for use when it is necessary to generate a password with the server
285   * offline (e.g., when setting the initial root user password).
286   *
287   * @param  passwordBytes  The bytes that make up the clear-text password.
288   * @return  The encoded password string, including the scheme name in curly braces.
289   * @throws  DirectoryException  If a problem occurs during processing.
290   */
291  public static String encodeOffline(byte[] passwordBytes)
292      throws DirectoryException
293  {
294    byte[] saltBytes      = new byte[NUM_SALT_BYTES];
295    int    iterations     = 10000;
296
297    final ByteString password = ByteString.wrap(passwordBytes);
298    byte[] digestBytes = encodeWithRandomSalt(password, saltBytes, iterations);
299    byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes);
300
301    return '{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + iterations + ':' + Base64.encode(hashPlusSalt);
302  }
303
304  private static byte[] encodeWithRandomSalt(ByteString plaintext, byte[] saltBytes, int iterations)
305      throws DirectoryException
306  {
307    try
308    {
309      final SecureRandom random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
310      return encodeWithRandomSalt(plaintext, saltBytes, iterations, random);
311    }
312    catch (DirectoryException e)
313    {
314      throw e;
315    }
316    catch (Exception e)
317    {
318      throw cannotEncodePassword(e);
319    }
320  }
321
322  private static byte[] encodeWithSalt(ByteSequence plaintext, byte[] saltBytes, int iterations)
323      throws DirectoryException
324  {
325    final char[] plaintextChars = plaintext.toString().toCharArray();
326    try
327    {
328      final SecretKeyFactory factory = SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
329      KeySpec spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, SHA1_LENGTH * 8);
330      return factory.generateSecret(spec).getEncoded();
331    }
332    catch (Exception e)
333    {
334      throw cannotEncodePassword(e);
335    }
336    finally
337    {
338      Arrays.fill(plaintextChars, '0');
339    }
340  }
341
342  private boolean encodeAndMatch(ByteSequence plaintext, byte[] saltBytes, byte[] digestBytes, int iterations)
343  {
344    try
345    {
346      final byte[] userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations);
347      return Arrays.equals(digestBytes, userDigestBytes);
348    }
349    catch (Exception e)
350    {
351      return false;
352    }
353  }
354
355  private static byte[] encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes,
356                                             int iterations, SecureRandom random)
357      throws DirectoryException
358  {
359    random.nextBytes(saltBytes);
360    return encodeWithSalt(plaintext, saltBytes, iterations);
361  }
362
363  private static DirectoryException cannotEncodePassword(Exception e)
364  {
365    logger.traceException(e);
366
367    LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(CLASS_NAME, getExceptionMessage(e));
368    return new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
369  }
370
371  private static byte[] concatenateHashPlusSalt(byte[] saltBytes, byte[] digestBytes) {
372    final byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
373    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
374    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, NUM_SALT_BYTES);
375    return hashPlusSalt;
376  }
377
378}