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

Luca Leonardo Scorcia
11.31.2022 dde03e77e46c7717d502b7c4fd596d78c6ec92ba
Implement PBKDF2-HMAC-SHA256 and PBKDF-HMAC-SHA512 password encoding schemes (#227) (#228)

5 files modified
9 files added
1073 ■■■■ changed files
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PBKDF2HmacSHA256PasswordStorageSchemeConfiguration.xml 48 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PBKDF2HmacSHA512PasswordStorageSchemeConfiguration.xml 48 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PBKDF2PasswordStorageSchemeConfiguration.xml 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/resource/config/config.ldif 18 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/admin/messages/PBKDF2HmacSHA256PasswordStorageSchemeCfgDefn.properties 7 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/admin/messages/PBKDF2HmacSHA512PasswordStorageSchemeCfgDefn.properties 7 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/admin/messages/PBKDF2PasswordStorageSchemeCfgDefn.properties 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/AbstractPBKDF2PasswordStorageScheme.java 338 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/ExtensionsConstants.java 34 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/PBKDF2HmacSHA256PasswordStorageScheme.java 53 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/PBKDF2HmacSHA512PasswordStorageScheme.java 53 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/PBKDF2PasswordStorageScheme.java 317 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2HmacSHA256PasswordStorageSchemeTestCase.java 73 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2HmacSHA512PasswordStorageSchemeTestCase.java 73 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PBKDF2HmacSHA256PasswordStorageSchemeConfiguration.xml
New file
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
  The contents of this file are subject to the terms of the Common Development and
  Distribution License (the License). You may not use this file except in compliance with the
  License.
  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
  specific language governing permission and limitations under the License.
  When distributing Covered Software, include this CDDL Header Notice in each file and include
  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
  Header, with the fields enclosed by brackets [] replaced by your own identifying
  information: "Portions Copyright [year] [name of copyright owner]".
  Copyright 2013 ForgeRock AS.
  ! -->
<adm:managed-object name="pbkdf2-hmac-sha256-password-storage-scheme"
  plural-name="pbkdf2-hmac-sha256-password-storage-schemes"
  package="org.forgerock.opendj.server.config"
  extends="pbkdf2-password-storage-scheme"
  xmlns:adm="http://opendj.forgerock.org/admin"
  xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
  <adm:synopsis>
    The
    <adm:user-friendly-name />
    provides a mechanism for encoding user passwords using the
    PBKDF2-HMAC-SHA256 message digest algorithm.
  </adm:synopsis>
  <adm:description>
    This scheme contains an implementation for the user password syntax,
    with a storage scheme name of "PBKDF2-HMAC-SHA256".
  </adm:description>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>ds-cfg-pbkdf2-hmac-sha256-password-storage-scheme</ldap:name>
      <ldap:superior>ds-cfg-pbkdf2-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.PBKDF2HmacSHA256PasswordStorageScheme
        </adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
</adm:managed-object>
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PBKDF2HmacSHA512PasswordStorageSchemeConfiguration.xml
New file
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
  The contents of this file are subject to the terms of the Common Development and
  Distribution License (the License). You may not use this file except in compliance with the
  License.
  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
  specific language governing permission and limitations under the License.
  When distributing Covered Software, include this CDDL Header Notice in each file and include
  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
  Header, with the fields enclosed by brackets [] replaced by your own identifying
  information: "Portions Copyright [year] [name of copyright owner]".
  Copyright 2013 ForgeRock AS.
  ! -->
<adm:managed-object name="pbkdf2-hmac-sha512-password-storage-scheme"
  plural-name="pbkdf2-hmac-sha512-password-storage-schemes"
  package="org.forgerock.opendj.server.config"
  extends="pbkdf2-password-storage-scheme"
  xmlns:adm="http://opendj.forgerock.org/admin"
  xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
  <adm:synopsis>
    The
    <adm:user-friendly-name />
    provides a mechanism for encoding user passwords using the
    PBKDF2-HMAC-SHA512 message digest algorithm.
  </adm:synopsis>
  <adm:description>
    This scheme contains an implementation for the user password syntax,
    with a storage scheme name of "PBKDF2-HMAC-SHA512".
  </adm:description>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>ds-cfg-pbkdf2-hmac-sha512-password-storage-scheme</ldap:name>
      <ldap:superior>ds-cfg-pbkdf2-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.PBKDF2HmacSHA512PasswordStorageScheme
        </adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
</adm:managed-object>
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PBKDF2PasswordStorageSchemeConfiguration.xml
@@ -24,7 +24,7 @@
    The
    <adm:user-friendly-name />
    provides a mechanism for encoding user passwords using the
    PBKDF2 message digest algorithm.
    PBKDF2 message digest algorithm. PBKDF2 is the shortname for PBKDF2-HMAC-SHA1.
  </adm:synopsis>
  <adm:description>
    This scheme contains an implementation for the user password syntax,
opendj-server-legacy/resource/config/config.ldif
@@ -1119,6 +1119,24 @@
ds-cfg-java-class: org.opends.server.extensions.PBKDF2PasswordStorageScheme
ds-cfg-enabled: true
dn: cn=PBKDF2-HMAC-SHA256,cn=Password Storage Schemes,cn=config
objectClass: top
objectClass: ds-cfg-password-storage-scheme
objectClass: ds-cfg-pbkdf2-password-storage-scheme
objectClass: ds-cfg-pbkdf2-hmac-sha256-password-storage-scheme
cn: PBKDF2-HMAC-SHA256
ds-cfg-java-class: org.opends.server.extensions.PBKDF2HmacSHA256PasswordStorageScheme
ds-cfg-enabled: true
dn: cn=PBKDF2-HMAC-SHA512,cn=Password Storage Schemes,cn=config
objectClass: top
objectClass: ds-cfg-password-storage-scheme
objectClass: ds-cfg-pbkdf2-password-storage-scheme
objectClass: ds-cfg-pbkdf2-hmac-sha512-password-storage-scheme
cn: PBKDF2-HMAC-SHA512
ds-cfg-java-class: org.opends.server.extensions.PBKDF2HmacSHA512PasswordStorageScheme
ds-cfg-enabled: true
dn: cn=PKCS5S2,cn=Password Storage Schemes,cn=config
objectClass: top
objectClass: ds-cfg-password-storage-scheme
opendj-server-legacy/src/admin/messages/PBKDF2HmacSHA256PasswordStorageSchemeCfgDefn.properties
New file
@@ -0,0 +1,7 @@
user-friendly-name=PBKDF2-HMAC-SHA256 Password Storage Scheme
user-friendly-plural-name=PBKDF2-HMAC-SHA256 Password Storage Schemes
synopsis=The PBKDF2-HMAC-SHA256 Password Storage Scheme provides a mechanism for encoding user passwords using the PBKDF2-HMAC-SHA256 message digest algorithm.
description=This scheme contains an implementation for the user password syntax, with a storage scheme name of "PBKDF2-HMAC-SHA256".
property.enabled.synopsis=Indicates whether the PBKDF2-HMAC-SHA256 Password Storage Scheme is enabled for use.
property.java-class.synopsis=Specifies the fully-qualified name of the Java class that provides the PBKDF2-HMAC-SHA256 Password Storage Scheme implementation.
property.pbkdf2-iterations.synopsis=The number of algorithm iterations to make. NIST recommends at least 1000.
opendj-server-legacy/src/admin/messages/PBKDF2HmacSHA512PasswordStorageSchemeCfgDefn.properties
New file
@@ -0,0 +1,7 @@
user-friendly-name=PBKDF2-HMAC-SHA512 Password Storage Scheme
user-friendly-plural-name=PBKDF2-HMAC-SHA512 Password Storage Schemes
synopsis=The PBKDF2-HMAC-SHA512 Password Storage Scheme provides a mechanism for encoding user passwords using the PBKDF2-HMAC-SHA512 message digest algorithm.
description=This scheme contains an implementation for the user password syntax, with a storage scheme name of "PBKDF2-HMAC-SHA512".
property.enabled.synopsis=Indicates whether the PBKDF2-HMAC-SHA512 Password Storage Scheme is enabled for use.
property.java-class.synopsis=Specifies the fully-qualified name of the Java class that provides the PBKDF2-HMAC-SHA512 Password Storage Scheme implementation.
property.pbkdf2-iterations.synopsis=The number of algorithm iterations to make. NIST recommends at least 1000.
opendj-server-legacy/src/admin/messages/PBKDF2PasswordStorageSchemeCfgDefn.properties
@@ -1,6 +1,6 @@
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.
synopsis=The PBKDF2 Password Storage Scheme provides a mechanism for encoding user passwords using the PBKDF2 message digest algorithm. PBKDF2 is the shortname for PBKDF2-HMAC-SHA1.
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.
opendj-server-legacy/src/main/java/org/opends/server/extensions/AbstractPBKDF2PasswordStorageScheme.java
New file
@@ -0,0 +1,338 @@
package org.opends.server.extensions;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.ldap.Base64;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.server.config.server.PBKDF2PasswordStorageSchemeCfg;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.List;
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.util.StaticUtils.getExceptionMessage;
abstract class AbstractPBKDF2PasswordStorageScheme
        extends PasswordStorageScheme<PBKDF2PasswordStorageSchemeCfg>
        implements ConfigurationChangeListener<PBKDF2PasswordStorageSchemeCfg>
{
    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
    /** 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 chosen digest algorithm. */
    private SecretKeyFactory keyFactory;
    /** 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 AbstractPBKDF2PasswordStorageScheme()
    {
        super();
    }
    @Override
    public void initializePasswordStorageScheme(PBKDF2PasswordStorageSchemeCfg configuration)
            throws ConfigException, InitializationException
    {
        try
        {
            random = new SecureRandom();
            // Initialize the digest algorithm
            keyFactory = SecretKeyFactory.getInstance(getMessageDigestAlgorithm());
        }
        catch (NoSuchAlgorithmException e)
        {
            throw new InitializationException(null);
        }
        this.config = configuration;
        config.addPBKDF2ChangeListener(this);
    }
    @Override
    public boolean isConfigurationChangeAcceptable(PBKDF2PasswordStorageSchemeCfg configuration,
                                                   List<LocalizableMessage> unacceptableReasons)
    {
        return true;
    }
    @Override
    public ConfigChangeResult applyConfigurationChange(PBKDF2PasswordStorageSchemeCfg configuration)
    {
        this.config = configuration;
        return new ConfigChangeResult();
    }
    @Override
    public abstract String getStorageSchemeName();
    @Override
    public ByteString encodePassword(ByteSequence plaintext)
            throws DirectoryException
    {
        byte[] saltBytes      = new byte[NUM_SALT_BYTES];
        int    iterations     = config.getPBKDF2Iterations();
        SecretKey digest = encodeWithRandomSalt(plaintext, saltBytes, iterations);
        byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digest.getEncoded());
        return ByteString.valueOfUtf8(iterations + ":" + Base64.encode(hashPlusSalt));
    }
    @Override
    public ByteString encodePasswordWithScheme(ByteSequence plaintext)
            throws DirectoryException
    {
        return ByteString.valueOfUtf8('{' + getStorageSchemeName() + '}' + encodePassword(plaintext));
    }
    @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 bytes as the salt.
        try
        {
            final String stored = storedPassword.toString();
            final int pos = stored.indexOf(':');
            if (pos == -1)
            {
                throw new Exception();
            }
            final int iterations = Integer.parseInt(stored.substring(0, pos));
            byte[] decodedBytes = Base64.decode(stored.substring(pos + 1)).toByteArray();
            int digestLength = getDigestSize();
            final int saltLength = decodedBytes.length - digestLength;
            if (saltLength <= 0) {
                logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword);
                return false;
            }
            final byte[] digestBytes = new byte[digestLength];
            final byte[] saltBytes = new byte[saltLength];
            System.arraycopy(decodedBytes, 0, digestBytes, 0, digestLength);
            System.arraycopy(decodedBytes, digestLength, saltBytes, 0, saltLength);
            return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
        }
        catch (Exception e)
        {
            logger.traceException(e);
            logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e);
            return false;
        }
    }
    @Override
    public boolean supportsAuthPasswordSyntax()
    {
        return true;
    }
    @Override
    public abstract String getAuthPasswordSchemeName();
    abstract String getMessageDigestAlgorithm();
    abstract int getDigestSize();
    @Override
    public ByteString encodeAuthPassword(ByteSequence plaintext)
            throws DirectoryException
    {
        byte[] saltBytes      = new byte[NUM_SALT_BYTES];
        int    iterations     = config.getPBKDF2Iterations();
        SecretKey digest = encodeWithRandomSalt(plaintext, saltBytes, iterations);
        byte[] digestBytes = digest.getEncoded();
        // Encode and return the value.
        return ByteString.valueOfUtf8(getAuthPasswordSchemeName() + '$'
                + iterations + ':' + Base64.encode(saltBytes) + '$' + Base64.encode(digestBytes));
    }
    @Override
    public boolean authPasswordMatches(ByteSequence plaintextPassword, String authInfo, String authValue)
    {
        try
        {
            int pos = authInfo.indexOf(':');
            if (pos == -1)
            {
                throw new Exception();
            }
            int iterations = Integer.parseInt(authInfo.substring(0, pos));
            byte[] saltBytes   = Base64.decode(authInfo.substring(pos + 1)).toByteArray();
            byte[] digestBytes = Base64.decode(authValue).toByteArray();
            return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
        }
        catch (Exception e)
        {
            logger.traceException(e);
            return false;
        }
    }
    @Override
    public boolean isReversible()
    {
        return false;
    }
    @Override
    public ByteString getPlaintextValue(ByteSequence storedPassword)
            throws DirectoryException
    {
        LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(getStorageSchemeName());
        throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
    }
    @Override
    public ByteString getAuthPasswordPlaintextValue(String authInfo, String authValue)
            throws DirectoryException
    {
        LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(getAuthPasswordSchemeName());
        throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
    }
    @Override
    public boolean isStorageSchemeSecure()
    {
        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.
     * @param schemeName The storage scheme name
     * @param digestAlgo The digest algorithm name
     * @param digestSize The digest algorithm size in bytes
     * @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, String schemeName, String digestAlgo, int digestSize)
            throws DirectoryException
    {
        byte[] saltBytes      = new byte[NUM_SALT_BYTES];
        final int iterations  = 10000;
        SecureRandom random = new SecureRandom();
        random.nextBytes(saltBytes);
        final ByteString password = ByteString.wrap(passwordBytes);
        char[] plaintextChars = password.toString().toCharArray();
        PBEKeySpec spec = null;
        try {
            SecretKeyFactory factory = SecretKeyFactory.getInstance(digestAlgo);
            spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, digestSize * 8);
            SecretKey digest = factory.generateSecret(spec);
            byte[] digestBytes = digest.getEncoded();
            byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes);
            return '{' + schemeName + '}' + iterations + ':' + Base64.encode(hashPlusSalt);
        } catch (InvalidKeySpecException | NoSuchAlgorithmException e)
        {
            throw cannotEncodePassword(schemeName, e);
        }
        finally {
            Arrays.fill(plaintextChars, '0');
            if (spec != null)
                spec.clearPassword();
        }
    }
    private SecretKey encodeWithRandomSalt(ByteString plaintext, byte[] saltBytes, int iterations)
            throws DirectoryException
    {
        random.nextBytes(saltBytes);
        return encodeWithRandomSalt(plaintext, saltBytes, iterations);
    }
    private SecretKey encodeWithSalt(ByteSequence plaintext, byte[] saltBytes, int iterations)
            throws DirectoryException
    {
        final char[] plaintextChars = plaintext.toString().toCharArray();
        PBEKeySpec spec = null;
        try
        {
            spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, getDigestSize() * 8);
            return keyFactory.generateSecret(spec);
        }
        catch (Exception e)
        {
            throw cannotEncodePassword(getStorageSchemeName(), e);
        }
        finally
        {
            Arrays.fill(plaintextChars, '0');
            if (spec != null)
                spec.clearPassword();
        }
    }
    private boolean encodeAndMatch(ByteSequence plaintext, byte[] saltBytes, byte[] digestBytes, int iterations)
    {
        try
        {
            final SecretKey userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations);
            return Arrays.equals(digestBytes, userDigestBytes.getEncoded());
        }
        catch (Exception e)
        {
            return false;
        }
    }
    private SecretKey encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes, int iterations)
            throws DirectoryException
    {
        random.nextBytes(saltBytes);
        return encodeWithSalt(plaintext, saltBytes, iterations);
    }
    private static DirectoryException cannotEncodePassword(String storageSchemeName, Exception e)
    {
        logger.traceException(e);
        LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
                storageSchemeName, getExceptionMessage(e));
        return new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), message, e);
    }
    private static byte[] concatenateHashPlusSalt(byte[] saltBytes, byte[] digestBytes) {
        final byte[] hashPlusSalt = new byte[digestBytes.length + saltBytes.length];
        System.arraycopy(digestBytes, 0, hashPlusSalt, 0, digestBytes.length);
        System.arraycopy(saltBytes, 0, hashPlusSalt, digestBytes.length, saltBytes.length);
        return hashPlusSalt;
    }
}
opendj-server-legacy/src/main/java/org/opends/server/extensions/ExtensionsConstants.java
@@ -73,6 +73,17 @@
   */
  public static final String AUTH_PASSWORD_SCHEME_NAME_PBKDF2 = "PBKDF2";
  /**
   * The authentication password scheme name for use with passwords encoded in a
   * PBKDF2 representation.
   */
  public static final String AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA256 = "PBKDF2-HMAC-SHA256";
  /**
   * The authentication password scheme name for use with passwords encoded in a
   * PBKDF2 representation.
   */
  public static final String AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA512 = "PBKDF2-HMAC-SHA512";
  /**
   * The authentication password scheme name for use with passwords encoded in a
@@ -128,7 +139,19 @@
  public static final String MESSAGE_DIGEST_ALGORITHM_PBKDF2 =
       "PBKDF2WithHmacSHA1";
  /**
   * The name of the message digest algorithm that should be used to generate
   * PBKDF2 hashes.
   */
  public static final String MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA256 =
          "PBKDF2WithHmacSHA256";
  /**
   * The name of the message digest algorithm that should be used to generate
   * PBKDF2 hashes.
   */
  public static final String MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA512 =
          "PBKDF2WithHmacSHA512";
  /**
   * The name of the pseudo-random number generator using SHA-1.
@@ -328,6 +351,17 @@
   */
  public static final String STORAGE_SCHEME_NAME_PBKDF2 = "PBKDF2";
  /**
   * The password storage scheme name that will be used for passwords stored in
   * a PBKDF2 representation.
   */
  public static final String STORAGE_SCHEME_NAME_PBKDF2_HMAC_SHA256 = "PBKDF2-HMAC-SHA256";
  /**
   * The password storage scheme name that will be used for passwords stored in
   * a PBKDF2 representation.
   */
  public static final String STORAGE_SCHEME_NAME_PBKDF2_HMAC_SHA512 = "PBKDF2-HMAC-SHA512";
  /**
   * The password storage scheme name that will be used for passwords stored in
opendj-server-legacy/src/main/java/org/opends/server/extensions/PBKDF2HmacSHA256PasswordStorageScheme.java
New file
@@ -0,0 +1,53 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2013-2016 ForgeRock AS.
 */
package org.opends.server.extensions;
import org.opends.server.types.DirectoryException;
import static org.opends.server.extensions.ExtensionsConstants.*;
/**
 * 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 PBKDF2HmacSHA256PasswordStorageScheme
    extends AbstractPBKDF2PasswordStorageScheme
{
  public String getStorageSchemeName() {
    return STORAGE_SCHEME_NAME_PBKDF2_HMAC_SHA256;
  }
  public String getAuthPasswordSchemeName() {
    return AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA256;
  }
  String getMessageDigestAlgorithm() {
    return MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA256;
  }
  int getDigestSize() {
    return 32;
  }
  public static String encodeOffline(byte[] passwordBytes) throws DirectoryException {
    return encodeOffline(passwordBytes,
            AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA256, MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA256, 32);
  }
}
opendj-server-legacy/src/main/java/org/opends/server/extensions/PBKDF2HmacSHA512PasswordStorageScheme.java
New file
@@ -0,0 +1,53 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2013-2016 ForgeRock AS.
 */
package org.opends.server.extensions;
import org.opends.server.types.DirectoryException;
import static org.opends.server.extensions.ExtensionsConstants.*;
/**
 * 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 PBKDF2HmacSHA512PasswordStorageScheme
    extends AbstractPBKDF2PasswordStorageScheme
{
  public String getStorageSchemeName() {
    return STORAGE_SCHEME_NAME_PBKDF2_HMAC_SHA512;
  }
  public String getAuthPasswordSchemeName() {
    return AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA512;
  }
  String getMessageDigestAlgorithm() {
    return MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA512;
  }
  int getDigestSize() {
    return 64;
  }
  public static String encodeOffline(byte[] passwordBytes) throws DirectoryException {
    return encodeOffline(passwordBytes,
            AUTH_PASSWORD_SCHEME_NAME_PBKDF2_HMAC_SHA512, MESSAGE_DIGEST_ALGORITHM_PBKDF2_HMAC_SHA512, 64);
  }
}
opendj-server-legacy/src/main/java/org/opends/server/extensions/PBKDF2PasswordStorageScheme.java
@@ -15,33 +15,9 @@
 */
package org.opends.server.extensions;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.List;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.Base64;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.server.config.server.PBKDF2PasswordStorageSchemeCfg;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.core.DirectoryServer;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.extensions.ExtensionsConstants.*;
import static org.opends.server.util.StaticUtils.*;
/**
 * This class defines a Directory Server password storage scheme based on the
@@ -52,300 +28,25 @@
 * implementation uses a configurable number of iterations.
 */
public class PBKDF2PasswordStorageScheme
    extends PasswordStorageScheme<PBKDF2PasswordStorageSchemeCfg>
    implements ConfigurationChangeListener<PBKDF2PasswordStorageSchemeCfg>
    extends AbstractPBKDF2PasswordStorageScheme
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  /** 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 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();
  }
  @Override
  public void initializePasswordStorageScheme(PBKDF2PasswordStorageSchemeCfg configuration)
      throws ConfigException, InitializationException
  {
    try
    {
      random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
      // Just try to verify if the algorithm is supported.
      SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
    }
    catch (NoSuchAlgorithmException e)
    {
      throw new InitializationException(null);
    }
    this.config = configuration;
    config.addPBKDF2ChangeListener(this);
  }
  @Override
  public boolean isConfigurationChangeAcceptable(PBKDF2PasswordStorageSchemeCfg configuration,
                                                 List<LocalizableMessage> unacceptableReasons)
  {
    return true;
  }
  @Override
  public ConfigChangeResult applyConfigurationChange(PBKDF2PasswordStorageSchemeCfg configuration)
  {
    this.config = configuration;
    return new ConfigChangeResult();
  }
  @Override
  public String getStorageSchemeName()
  {
  public String getStorageSchemeName() {
    return STORAGE_SCHEME_NAME_PBKDF2;
  }
  @Override
  public ByteString encodePassword(ByteSequence plaintext)
      throws DirectoryException
  {
    byte[] saltBytes      = new byte[NUM_SALT_BYTES];
    int    iterations     = config.getPBKDF2Iterations();
    byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations,random);
    byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes);
    return ByteString.valueOfUtf8(iterations + ":" + Base64.encode(hashPlusSalt));
  }
  @Override
  public ByteString encodePasswordWithScheme(ByteSequence plaintext)
      throws DirectoryException
  {
    return ByteString.valueOfUtf8('{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + encodePassword(plaintext));
  }
  @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.
    try
    {
      final String stored = storedPassword.toString();
      final int pos = stored.indexOf(':');
      if (pos == -1)
      {
        throw new Exception();
      }
      final int iterations = Integer.parseInt(stored.substring(0, pos));
      byte[] decodedBytes = Base64.decode(stored.substring(pos + 1)).toByteArray();
      final int saltLength = decodedBytes.length - SHA1_LENGTH;
      if (saltLength <= 0)
      {
        logger.error(ERR_PWSCHEME_INVALID_BASE64_DECODED_STORED_PASSWORD, storedPassword);
        return false;
      }
      final byte[] digestBytes = new byte[SHA1_LENGTH];
      final byte[] saltBytes = new byte[saltLength];
      System.arraycopy(decodedBytes, 0, digestBytes, 0, SHA1_LENGTH);
      System.arraycopy(decodedBytes, SHA1_LENGTH, saltBytes, 0, saltLength);
      return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
    }
    catch (Exception e)
    {
      logger.traceException(e);
      logger.error(ERR_PWSCHEME_CANNOT_BASE64_DECODE_STORED_PASSWORD, storedPassword, e);
      return false;
    }
  }
  @Override
  public boolean supportsAuthPasswordSyntax()
  {
    return true;
  }
  @Override
  public String getAuthPasswordSchemeName()
  {
  public String getAuthPasswordSchemeName() {
    return AUTH_PASSWORD_SCHEME_NAME_PBKDF2;
  }
  @Override
  public ByteString encodeAuthPassword(ByteSequence plaintext)
      throws DirectoryException
  {
    byte[] saltBytes      = new byte[NUM_SALT_BYTES];
    int    iterations     = config.getPBKDF2Iterations();
    byte[] digestBytes = encodeWithRandomSalt(plaintext, saltBytes, iterations,random);
    // Encode and return the value.
    return ByteString.valueOfUtf8(AUTH_PASSWORD_SCHEME_NAME_PBKDF2 + '$'
        + iterations + ':' + Base64.encode(saltBytes) + '$' + Base64.encode(digestBytes));
  String getMessageDigestAlgorithm() {
    return MESSAGE_DIGEST_ALGORITHM_PBKDF2;
  }
  @Override
  public boolean authPasswordMatches(ByteSequence plaintextPassword, String authInfo, String authValue)
  {
    try
    {
      int pos = authInfo.indexOf(':');
      if (pos == -1)
      {
        throw new Exception();
      }
      int iterations = Integer.parseInt(authInfo.substring(0, pos));
      byte[] saltBytes   = Base64.decode(authInfo.substring(pos + 1)).toByteArray();
      byte[] digestBytes = Base64.decode(authValue).toByteArray();
      return encodeAndMatch(plaintextPassword, saltBytes, digestBytes, iterations);
    }
    catch (Exception e)
    {
      logger.traceException(e);
      return false;
    }
  int getDigestSize() {
    return 20;
  }
  @Override
  public boolean isReversible()
  {
    return false;
  }
  @Override
  public ByteString getPlaintextValue(ByteSequence storedPassword)
      throws DirectoryException
  {
    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(STORAGE_SCHEME_NAME_PBKDF2);
    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
  }
  @Override
  public ByteString getAuthPasswordPlaintextValue(String authInfo, String authValue)
      throws DirectoryException
  {
    LocalizableMessage message = ERR_PWSCHEME_NOT_REVERSIBLE.get(AUTH_PASSWORD_SCHEME_NAME_PBKDF2);
    throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message);
  }
  @Override
  public boolean isStorageSchemeSecure()
  {
    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];
    int    iterations     = 10000;
    final ByteString password = ByteString.wrap(passwordBytes);
    byte[] digestBytes = encodeWithRandomSalt(password, saltBytes, iterations);
    byte[] hashPlusSalt = concatenateHashPlusSalt(saltBytes, digestBytes);
    return '{' + STORAGE_SCHEME_NAME_PBKDF2 + '}' + iterations + ':' + Base64.encode(hashPlusSalt);
  }
  private static byte[] encodeWithRandomSalt(ByteString plaintext, byte[] saltBytes, int iterations)
      throws DirectoryException
  {
    try
    {
      final SecureRandom random = SecureRandom.getInstance(SECURE_PRNG_SHA1);
      return encodeWithRandomSalt(plaintext, saltBytes, iterations, random);
    }
    catch (DirectoryException e)
    {
      throw e;
    }
    catch (Exception e)
    {
      throw cannotEncodePassword(e);
    }
  }
  private static byte[] encodeWithSalt(ByteSequence plaintext, byte[] saltBytes, int iterations)
      throws DirectoryException
  {
    final char[] plaintextChars = plaintext.toString().toCharArray();
    try
    {
      final SecretKeyFactory factory = SecretKeyFactory.getInstance(MESSAGE_DIGEST_ALGORITHM_PBKDF2);
      KeySpec spec = new PBEKeySpec(plaintextChars, saltBytes, iterations, SHA1_LENGTH * 8);
      return factory.generateSecret(spec).getEncoded();
    }
    catch (Exception e)
    {
      throw cannotEncodePassword(e);
    }
    finally
    {
      Arrays.fill(plaintextChars, '0');
    }
  }
  private boolean encodeAndMatch(ByteSequence plaintext, byte[] saltBytes, byte[] digestBytes, int iterations)
  {
    try
    {
      final byte[] userDigestBytes = encodeWithSalt(plaintext, saltBytes, iterations);
      return Arrays.equals(digestBytes, userDigestBytes);
    }
    catch (Exception e)
    {
      return false;
    }
  }
  private static byte[] encodeWithRandomSalt(ByteSequence plaintext, byte[] saltBytes,
                                             int iterations, SecureRandom random)
      throws DirectoryException
  {
    random.nextBytes(saltBytes);
    return encodeWithSalt(plaintext, saltBytes, iterations);
  }
  private static DirectoryException cannotEncodePassword(Exception e)
  {
    logger.traceException(e);
    LocalizableMessage message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(CLASS_NAME, getExceptionMessage(e));
    return new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), message, e);
  }
  private static byte[] concatenateHashPlusSalt(byte[] saltBytes, byte[] digestBytes) {
    final 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 hashPlusSalt;
  public static String encodeOffline(byte[] passwordBytes) throws DirectoryException {
    return encodeOffline(passwordBytes, AUTH_PASSWORD_SCHEME_NAME_PBKDF2, MESSAGE_DIGEST_ALGORITHM_PBKDF2, 20);
  }
}
opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2HmacSHA256PasswordStorageSchemeTestCase.java
New file
@@ -0,0 +1,73 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2014-2016 ForgeRock AS.
 */
package org.opends.server.extensions;
import org.forgerock.opendj.server.config.meta.PBKDF2PasswordStorageSchemeCfgDefn;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.types.DirectoryException;
import org.testng.annotations.DataProvider;
/**
 * A set of test cases for the PBKDF2-HMAC-SHA256 password storage scheme.
 */
@SuppressWarnings("javadoc")
public class PBKDF2HmacSHA256PasswordStorageSchemeTestCase
       extends PasswordStorageSchemeTestCase
{
  /** Creates a new instance of this storage scheme test case.   */
  public PBKDF2HmacSHA256PasswordStorageSchemeTestCase()
  {
    super("cn=PBKDF2-HMAC-SHA256,cn=Password Storage Schemes,cn=config");
  }
  /**
   * Retrieves a set of passwords that may be used to test the password storage scheme.
   *
   * @return  A set of passwords that may be used to test the password storage scheme.
   */
  @Override
  @DataProvider(name = "testPasswords")
  public Object[][] getTestPasswords()
  {
    final Object[][] testPasswords = super.getTestPasswords();
    // JDK Bug 6879540. Empty passwords are not accepted when generating PBESpecKey.
    // The bug is present in Java 6 and some version of Java 7.
    final int newLength = testPasswords.length - 2;
    final Object[][] results = new Object[newLength][];
    System.arraycopy(testPasswords, 2, results, 0, newLength);
    return results;
  }
  /**
   * Retrieves an initialized instance of this password storage scheme.
   *
   * @return  An initialized instance of this password storage scheme.
   */
  @Override
  protected PasswordStorageScheme<?> getScheme() throws Exception
  {
    return InitializationUtils.initializePasswordStorageScheme(
        new PBKDF2HmacSHA256PasswordStorageScheme(), configEntry, PBKDF2PasswordStorageSchemeCfgDefn.getInstance());
  }
  @Override
  protected String encodeOffline(final byte[] plaintextBytes) throws DirectoryException
  {
    return PBKDF2HmacSHA256PasswordStorageScheme.encodeOffline(plaintextBytes);
  }
}
opendj-server-legacy/src/test/java/org/opends/server/extensions/PBKDF2HmacSHA512PasswordStorageSchemeTestCase.java
New file
@@ -0,0 +1,73 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2014-2016 ForgeRock AS.
 */
package org.opends.server.extensions;
import org.forgerock.opendj.server.config.meta.PBKDF2PasswordStorageSchemeCfgDefn;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.types.DirectoryException;
import org.testng.annotations.DataProvider;
/**
 * A set of test cases for the PBKDF2-HMAC-SHA512 password storage scheme.
 */
@SuppressWarnings("javadoc")
public class PBKDF2HmacSHA512PasswordStorageSchemeTestCase
       extends PasswordStorageSchemeTestCase
{
  /** Creates a new instance of this storage scheme test case.   */
  public PBKDF2HmacSHA512PasswordStorageSchemeTestCase()
  {
    super("cn=PBKDF2-HMAC-SHA512,cn=Password Storage Schemes,cn=config");
  }
  /**
   * Retrieves a set of passwords that may be used to test the password storage scheme.
   *
   * @return  A set of passwords that may be used to test the password storage scheme.
   */
  @Override
  @DataProvider(name = "testPasswords")
  public Object[][] getTestPasswords()
  {
    final Object[][] testPasswords = super.getTestPasswords();
    // JDK Bug 6879540. Empty passwords are not accepted when generating PBESpecKey.
    // The bug is present in Java 6 and some version of Java 7.
    final int newLength = testPasswords.length - 2;
    final Object[][] results = new Object[newLength][];
    System.arraycopy(testPasswords, 2, results, 0, newLength);
    return results;
  }
  /**
   * Retrieves an initialized instance of this password storage scheme.
   *
   * @return  An initialized instance of this password storage scheme.
   */
  @Override
  protected PasswordStorageScheme<?> getScheme() throws Exception
  {
    return InitializationUtils.initializePasswordStorageScheme(
        new PBKDF2HmacSHA512PasswordStorageScheme(), configEntry, PBKDF2PasswordStorageSchemeCfgDefn.getInstance());
  }
  @Override
  protected String encodeOffline(final byte[] plaintextBytes) throws DirectoryException
  {
    return PBKDF2HmacSHA512PasswordStorageScheme.encodeOffline(plaintextBytes);
  }
}