mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Chris Ridd
20.09.2013 e1b78d96d01a01bb9e537a5c2428198e6c994a64
Fix OPENDJ-510 Add support for PBKDF2 password storage scheme
3 files added
4 files modified
726 ■■■■■ changed files
opends/resource/admin/abbreviations.xsl 3 ●●●● patch | view | raw | blame | history
opends/resource/config/config.ldif 10 ●●●●● patch | view | raw | blame | history
opends/resource/schema/02-config.ldif 12 ●●●●● patch | view | raw | blame | history
opends/src/admin/defn/org/opends/server/admin/std/PBKDF2PasswordStorageSchemeConfiguration.xml 78 ●●●●● patch | view | raw | blame | history
opends/src/admin/messages/PBKDF2PasswordStorageSchemeCfgDefn.properties 7 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/ExtensionsConstants.java 34 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/PBKDF2PasswordStorageScheme.java 582 ●●●●● patch | view | raw | blame | history
opends/resource/admin/abbreviations.xsl
@@ -23,7 +23,7 @@
  !
  !
  !      Copyright 2008-2009 Sun Microsystems, Inc.
  !      Portions copyright 2011-2012 ForgeRock AS
  !      Portions copyright 2011-2013 ForgeRock AS
  ! -->
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
@@ -55,6 +55,7 @@
              or $value = 'des' or $value = 'aes' or $value = 'rc4'
              or $value = 'db' or $value = 'snmp' or $value = 'qos'
              or $value = 'ecl' or $value = 'ttl' or $value = 'jpeg'
              or $value = 'pbkdf2'
             "/>
  </xsl:template>
</xsl:stylesheet>
opends/resource/config/config.ldif
@@ -21,7 +21,7 @@
# CDDL HEADER END
#
#      Copyright 2006-2010 Sun Microsystems, Inc.
#      Portions Copyright 2010-2012 ForgeRock AS.
#      Portions Copyright 2010-2013 ForgeRock AS.
#      Portions Copyright 2012 Manuel Gaupp
#
#
@@ -1591,6 +1591,14 @@
ds-cfg-java-class: org.opends.server.extensions.SaltedSHA512PasswordStorageScheme
ds-cfg-enabled: true
dn: cn=PBKDF2,cn=Password Storage Schemes,cn=config
objectClass: top
objectClass: ds-cfg-password-storage-scheme
objectClass: ds-cfg-pbkdf2-password-storage-scheme
cn: PBKDF2
ds-cfg-java-class: org.opends.server.extensions.PBKDF2PasswordStorageScheme
ds-cfg-enabled: true
dn: cn=SHA-1,cn=Password Storage Schemes,cn=config
objectClass: top
objectClass: ds-cfg-password-storage-scheme
opends/resource/schema/02-config.ldif
@@ -3659,6 +3659,12 @@
  EQUALITY caseIgnoreMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.117
  NAME 'ds-cfg-pbkdf2-iterations'
  EQUALITY integerMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
  SINGLE-VALUE
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
  NAME 'ds-cfg-access-control-handler'
  SUP top
@@ -5526,3 +5532,9 @@
  ds-mon-extended-operations-total-count $
  ds-mon-resident-time-extended-operations-total-time )
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.14
  NAME 'ds-cfg-pbkdf2-password-storage-scheme'
  SUP ds-cfg-password-storage-scheme
  STRUCTURAL
  MAY ds-cfg-pbkdf2-iterations
  X-ORIGIN 'OpenDJ Directory Server' )
opends/src/admin/defn/org/opends/server/admin/std/PBKDF2PasswordStorageSchemeConfiguration.xml
New file
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
  ! CDDL HEADER START
  !
  ! The contents of this file are subject to the terms of the
  ! Common Development and Distribution License, Version 1.0 only
  ! (the "License").  You may not use this file except in compliance
  ! with the License.
  !
  ! You can obtain a copy of the license at
  ! trunk/opends/resource/legal-notices/OpenDS.LICENSE
  ! or https://OpenDS.dev.java.net/OpenDS.LICENSE.
  ! See the License for the specific language governing permissions
  ! and limitations under the License.
  !
  ! When distributing Covered Code, include this CDDL HEADER in each
  ! file and include the License file at
  ! trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
  ! add the following below this CDDL HEADER, with the fields enclosed
  ! by brackets "[]" replaced with your own identifying information:
  !      Portions Copyright [yyyy] [name of copyright owner]
  !
  ! CDDL HEADER END
  !
  !
  !      Copyright 2013 ForgeRock AS.
  ! -->
<adm:managed-object name="pbkdf2-password-storage-scheme"
  plural-name="pbkdf2-password-storage-schemes"
  package="org.opends.server.admin.std"
  extends="password-storage-scheme"
  xmlns:adm="http://www.opends.org/admin"
  xmlns:ldap="http://www.opends.org/admin-ldap">
  <adm:synopsis>
    The
    <adm:user-friendly-name />
    provides a mechanism for encoding user passwords using the
    PBKDF2 message digest algorithm.
  </adm:synopsis>
  <adm:description>
    This scheme contains an implementation for the user password syntax,
    with a storage scheme name of "PBKDF2".
  </adm:description>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>ds-cfg-pbkdf2-password-storage-scheme</ldap:name>
      <ldap:superior>ds-cfg-password-storage-scheme</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property-override name="java-class" advanced="true">
    <adm:default-behavior>
      <adm:defined>
        <adm:value>
          org.opends.server.extensions.PBKDF2PasswordStorageScheme
        </adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
  <adm:property name="pbkdf2-iterations" advanced="false">
    <adm:synopsis>
      The number of algorithm iterations to make. NIST recommends
      at least 1000.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>10000</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:integer lower-limit="1" />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-pbkdf2-iterations</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opends/src/admin/messages/PBKDF2PasswordStorageSchemeCfgDefn.properties
New file
@@ -0,0 +1,7 @@
user-friendly-name=PBKDF2 Password Storage Scheme
user-friendly-plural-name=PBKDF2 Password Storage Schemes
synopsis=The PBKDF2 Password Storage Scheme provides a mechanism for encoding user passwords using the PBKDF2 message digest algorithm.
description=This scheme contains an implementation for the user password syntax, with a storage scheme name of "PBKDF2".
property.enabled.synopsis=Indicates whether the PBKDF2 Password Storage Scheme is enabled for use.
property.java-class.synopsis=Specifies the fully-qualified name of the Java class that provides the PBKDF2 Password Storage Scheme implementation.
property.pbkdf2-iterations.synopsis=The number of algorithm iterations to make. NIST recommends at least 1000.
opends/src/server/org/opends/server/extensions/ExtensionsConstants.java
@@ -23,6 +23,7 @@
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Portions copyright 2013 ForgeRock AS.
 */
package org.opends.server.extensions;
@@ -78,6 +79,15 @@
  /**
   * The authentication password scheme name for use with passwords encoded in a
   * PBKDF2 representation.
   */
  public static final String AUTH_PASSWORD_SCHEME_NAME_PBKDF2 =
       "PBKDF2";
  /**
   * The name of the message digest algorithm that should be used to generate
   * MD5 hashes.
   */
@@ -118,6 +128,22 @@
  /**
   * The name of the message digest algorithm that should be used to generate
   * PBKDF2 hashes.
   */
  public static final String MESSAGE_DIGEST_ALGORITHM_PBKDF2 =
       "PBKDF2WithHmacSHA1";
  /**
   * The name of the pseudo-random number generator using SHA-1.
   */
  public static final String SECURE_PRNG_SHA1 = "SHA1PRNG";
  /**
   * The cipher transformation that should be used when performing 3DES
   * encryption/decription.
   */
@@ -295,6 +321,14 @@
  /**
   * The password storage scheme name that will be used for passwords stored in
   * a PBKDF2 representation.
   */
  public static final String STORAGE_SCHEME_NAME_PBKDF2 = "PBKDF2";
  /**
   * The password storage scheme name that will be used for passwords stored in
   * a UNIX crypt representation.
   */
  public static final String STORAGE_SCHEME_NAME_CRYPT = "CRYPT";
opends/src/server/org/opends/server/extensions/PBKDF2PasswordStorageScheme.java
New file
@@ -0,0 +1,582 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2013 ForgeRock AS.
 */
package org.opends.server.extensions;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.security.spec.KeySpec;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import org.opends.messages.Message;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.PBKDF2PasswordStorageSchemeCfg;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.loggers.ErrorLogger;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.types.*;
import org.opends.server.util.Base64;
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.extensions.ExtensionsConstants.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.util.StaticUtils.getExceptionMessage;
/**
 * This class defines a Directory Server password storage scheme based on the
 * PBKDF2 algorithm defined in RFC 2898.  This is a one-way digest algorithm
 * so there is no way to retrieve the original clear-text version of the
 * password from the hashed value (although this means that it is not suitable
 * for things that need the clear-text password like DIGEST-MD5).  This
 * implementation uses a configurable number of iterations.
 */
public class PBKDF2PasswordStorageScheme
  extends PasswordStorageScheme<PBKDF2PasswordStorageSchemeCfg>
  implements ConfigurationChangeListener<PBKDF2PasswordStorageSchemeCfg>
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();
  /**
   * The fully-qualified name of this class.
   */
  private static final String CLASS_NAME =
       "org.opends.server.extensions.PBKDF2PasswordStorageScheme";
  /**
   * The number of bytes of random data to use as the salt when generating the
   * hashes.
   */
  private static final int NUM_SALT_BYTES = 8;
  // The number of bytes the SHA-1 algorithm produces
  private static final int SHA1_LENGTH = 20;
  // The factory used to generate the PBKDF2 hashes.
  private SecretKeyFactory factory;
  // The lock used to provide threadsafe access to the message digest.
  private Object factoryLock;
  // The secure random number generator to use to generate the salt values.
  private SecureRandom random;
  // The current configuration for this storage scheme.
  private volatile PBKDF2PasswordStorageSchemeCfg config;
  /**
   * Creates a new instance of this password storage scheme.  Note that no
   * initialization should be performed here, as all initialization should be
   * done in the <CODE>initializePasswordStorageScheme</CODE> method.
   */
  public PBKDF2PasswordStorageScheme()
  {
    super();
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void initializePasswordStorageScheme(
                   PBKDF2PasswordStorageSchemeCfg configuration)
         throws ConfigException, InitializationException
  {
    factoryLock = new Object();
    try
    {
      random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
      factory = SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
    }
    catch (NoSuchAlgorithmException e)
    {
      throw new InitializationException(null);
    }
    this.config = configuration;
    config.addPBKDF2ChangeListener(this);
  }
  /**
   * {@inheritDoc}
   */
  public boolean isConfigurationChangeAcceptable(
                      PBKDF2PasswordStorageSchemeCfg configuration,
                      List<Message> unacceptableReasons)
  {
    // The configuration will always be acceptable.
    return true;
  }
  /**
   * {@inheritDoc}
   */
  public ConfigChangeResult applyConfigurationChange(
      PBKDF2PasswordStorageSchemeCfg configuration)
  {
    this.config = configuration;
    return new ConfigChangeResult(ResultCode.SUCCESS, false);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public String getStorageSchemeName()
  {
    return STORAGE_SCHEME_NAME_PBKDF2;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public ByteString encodePassword(ByteSequence plaintext)
         throws DirectoryException
  {
    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
    byte[] digestBytes;
    int    iterations    = config.getPBKDF2Iterations();
    synchronized(factoryLock)
    {
      try
      {
        random.nextBytes(saltBytes);
        KeySpec spec = new PBEKeySpec(plaintext.toString().toCharArray(),
            saltBytes, iterations, SHA1_LENGTH * 8);
        digestBytes = factory.generateSecret(spec).getEncoded();
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
            CLASS_NAME, getExceptionMessage(e));
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     message, e);
      }
    }
    // Append the salt to the hashed value and base64-the whole thing.
    byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
                     NUM_SALT_BYTES);
    StringBuilder sb = new StringBuilder();
    sb.append(Integer.toString(iterations));
    sb.append(':');
    sb.append(Base64.encode(hashPlusSalt));
    return ByteString.valueOf(sb.toString());
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public ByteString encodePasswordWithScheme(ByteSequence plaintext)
         throws DirectoryException
  {
    StringBuilder buffer = new StringBuilder();
    buffer.append('{');
    buffer.append(STORAGE_SCHEME_NAME_PBKDF2);
    buffer.append('}');
    buffer.append(encodePassword(plaintext));
    return ByteString.valueOf(buffer.toString());
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean passwordMatches(ByteSequence plaintextPassword,
                                 ByteSequence storedPassword)
  {
    // Split the iterations from the stored value (separated by a ":")
    // Base64-decode the remaining value and take the last 8 bytes as the salt.
    int iterations;
    byte[] saltBytes;
    byte[] digestBytes = new byte[SHA1_LENGTH];
    int saltLength = 0;
    try
    {
      String stored = storedPassword.toString();
      int stored_length = stored.length();
      int pos = 0;
      while (pos < stored_length && stored.charAt(pos) != ':')
      {
        pos++;
      }
      if (pos >= (stored_length - 1) || pos == 0)
        throw new Exception();
      iterations = Integer.parseInt(stored.substring(0, pos));
      byte[] decodedBytes = Base64.decode(stored.substring(pos + 1));
      saltLength = decodedBytes.length - SHA1_LENGTH;
      if (saltLength <= 0)
      {
        Message message =
          ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD.get(
          storedPassword.toString());
        ErrorLogger.logError(message);
        return false;
      }
      saltBytes = new byte[saltLength];
      System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA1_LENGTH);
      System.arraycopy(decodedBytes, SHA1_LENGTH, saltBytes, 0,
                       saltLength);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      Message message = ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD.get(
          storedPassword.toString(), String.valueOf(e));
      ErrorLogger.logError(message);
      return false;
    }
    // Use the salt to generate a digest based on the provided plain-text value.
    int plainBytesLength = plaintextPassword.length();
    byte[] plainPlusSalt = new byte[plainBytesLength + saltLength];
    plaintextPassword.copyTo(plainPlusSalt);
    System.arraycopy(saltBytes, 0, plainPlusSalt, plainBytesLength,
                     saltLength);
    byte[] userDigestBytes;
    synchronized (factoryLock)
    {
      try
      {
        KeySpec spec = new PBEKeySpec(
            plaintextPassword.toString().toCharArray(), saltBytes,
            iterations, SHA1_LENGTH * 8);
        userDigestBytes = factory.generateSecret(spec).getEncoded();
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        return false;
      }
    }
    return Arrays.equals(digestBytes, userDigestBytes);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean supportsAuthPasswordSyntax()
  {
    // This storage scheme does support the authentication password syntax.
    return true;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public String getAuthPasswordSchemeName()
  {
    return AUTH_PASSWORD_SCHEME_NAME_PBKDF2;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public ByteString encodeAuthPassword(ByteSequence plaintext)
         throws DirectoryException
  {
    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
    byte[] digestBytes;
    int    iterations    = config.getPBKDF2Iterations();
    synchronized(factoryLock)
    {
      try
      {
        random.nextBytes(saltBytes);
        KeySpec spec = new PBEKeySpec(
            plaintext.toString().toCharArray(), saltBytes,
            iterations, SHA1_LENGTH * 8);
        digestBytes = factory.generateSecret(spec).getEncoded();
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
            CLASS_NAME, getExceptionMessage(e));
        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                     message, e);
      }
    }
    // Encode and return the value.
    StringBuilder authPWValue = new StringBuilder();
    authPWValue.append(AUTH_PASSWORD_SCHEME_NAME_PBKDF2);
    authPWValue.append('$');
    authPWValue.append(Integer.toString(iterations));
    authPWValue.append(':');
    authPWValue.append(Base64.encode(saltBytes));
    authPWValue.append('$');
    authPWValue.append(Base64.encode(digestBytes));
    return ByteString.valueOf(authPWValue.toString());
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean authPasswordMatches(ByteSequence plaintextPassword,
                                     String authInfo, String authValue)
  {
    byte[] saltBytes;
    byte[] digestBytes;
    int    iterations;
    try
    {
      int pos = 0;
      int length = authInfo.length();
      while (pos < length && authInfo.charAt(pos) != ':')
      {
        pos++;
      }
      if (pos >= (length - 1) || pos == 0)
        throw new Exception();
      iterations = Integer.parseInt(authInfo.substring(0, pos));
      saltBytes   = Base64.decode(authInfo.substring(pos + 1));
      digestBytes = Base64.decode(authValue);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      return false;
    }
    int plainBytesLength = plaintextPassword.length();
    byte[] plainPlusSalt = new byte[plainBytesLength + saltBytes.length];
    plaintextPassword.copyTo(plainPlusSalt);
    System.arraycopy(saltBytes, 0, plainPlusSalt, plainBytesLength,
                     saltBytes.length);
    byte[] userDigestBytes;
    synchronized (factoryLock)
    {
      try
      {
        KeySpec spec = new PBEKeySpec(
            plaintextPassword.toString().toCharArray(), saltBytes,
            iterations, SHA1_LENGTH * 8);
        userDigestBytes = factory.generateSecret(spec).getEncoded();
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        return false;
      }
    }
    return Arrays.equals(digestBytes, userDigestBytes);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isReversible()
  {
    return false;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public ByteString getPlaintextValue(ByteSequence storedPassword)
         throws DirectoryException
  {
    Message message =
        ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_PBKDF2);
    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public ByteString getAuthPasswordPlaintextValue(String authInfo,
                                                  String authValue)
         throws DirectoryException
  {
    Message message =
        ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_PBKDF2);
    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isStorageSchemeSecure()
  {
    // PBKDF2 should be considered secure.
    return true;
  }
  /**
   * Generates an encoded password string from the given clear-text password.
   * This method is primarily intended for use when it is necessary to generate
   * a password with the server offline (e.g., when setting the initial root
   * user password).
   *
   * @param  passwordBytes  The bytes that make up the clear-text password.
   *
   * @return  The encoded password string, including the scheme name in curly
   *          braces.
   *
   * @throws  DirectoryException  If a problem occurs during processing.
   */
  public static String encodeOffline(byte[] passwordBytes)
         throws DirectoryException
  {
    byte[] saltBytes     = new byte[NUM_SALT_BYTES];
    byte[] digestBytes;
    int    iterations    = 10000;
    try
    {
      SecureRandom.getInstance(SECURE_PRNG_SHA1).nextBytes(saltBytes);
      KeySpec spec = new PBEKeySpec(
          passwordBytes.toString().toCharArray(), saltBytes,
          iterations, SHA1_LENGTH * 8);
      digestBytes = SecretKeyFactory
          .getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2)
          .generateSecret(spec).getEncoded();
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
          CLASS_NAME, getExceptionMessage(e));
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
          message, e);
    }
    // Append the salt to the hashed value and base64-the whole thing.
    byte[] hashPlusSalt = new byte[digestBytes.length + NUM_SALT_BYTES];
    System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
    System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length,
                     NUM_SALT_BYTES);
    return "{" + STORAGE_SCHEME_NAME_PBKDF2 + "}" + iterations + ":" +
      Base64.encode(hashPlusSalt);
  }
}