From e1b78d96d01a01bb9e537a5c2428198e6c994a64 Mon Sep 17 00:00:00 2001
From: Chris Ridd <chris.ridd@forgerock.com>
Date: Wed, 20 Feb 2013 14:09:09 +0000
Subject: [PATCH] Fix OPENDJ-510 Add support for PBKDF2 password storage scheme

---
 opends/resource/schema/02-config.ldif                                                          |   12 
 opends/src/server/org/opends/server/extensions/PBKDF2PasswordStorageScheme.java                |  582 ++++++++++++++++++++++++++++++++++++++++++++
 opends/src/admin/messages/PBKDF2PasswordStorageSchemeCfgDefn.properties                        |    7 
 opends/resource/admin/abbreviations.xsl                                                        |    3 
 opends/src/admin/defn/org/opends/server/admin/std/PBKDF2PasswordStorageSchemeConfiguration.xml |   78 ++++++
 opends/resource/config/config.ldif                                                             |   10 
 opends/src/server/org/opends/server/extensions/ExtensionsConstants.java                        |   34 ++
 7 files changed, 724 insertions(+), 2 deletions(-)

diff --git a/opends/resource/admin/abbreviations.xsl b/opends/resource/admin/abbreviations.xsl
index 256ba7d..6c02a0d 100644
--- a/opends/resource/admin/abbreviations.xsl
+++ b/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>
diff --git a/opends/resource/config/config.ldif b/opends/resource/config/config.ldif
index f16c7d3..5699800 100644
--- a/opends/resource/config/config.ldif
+++ b/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
diff --git a/opends/resource/schema/02-config.ldif b/opends/resource/schema/02-config.ldif
index 5a59874..3b3bd97 100644
--- a/opends/resource/schema/02-config.ldif
+++ b/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' )
diff --git a/opends/src/admin/defn/org/opends/server/admin/std/PBKDF2PasswordStorageSchemeConfiguration.xml b/opends/src/admin/defn/org/opends/server/admin/std/PBKDF2PasswordStorageSchemeConfiguration.xml
new file mode 100644
index 0000000..862d01d
--- /dev/null
+++ b/opends/src/admin/defn/org/opends/server/admin/std/PBKDF2PasswordStorageSchemeConfiguration.xml
@@ -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>
diff --git a/opends/src/admin/messages/PBKDF2PasswordStorageSchemeCfgDefn.properties b/opends/src/admin/messages/PBKDF2PasswordStorageSchemeCfgDefn.properties
new file mode 100644
index 0000000..dbb3818
--- /dev/null
+++ b/opends/src/admin/messages/PBKDF2PasswordStorageSchemeCfgDefn.properties
@@ -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.
diff --git a/opends/src/server/org/opends/server/extensions/ExtensionsConstants.java b/opends/src/server/org/opends/server/extensions/ExtensionsConstants.java
index 3353282..782c887 100644
--- a/opends/src/server/org/opends/server/extensions/ExtensionsConstants.java
+++ b/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";
diff --git a/opends/src/server/org/opends/server/extensions/PBKDF2PasswordStorageScheme.java b/opends/src/server/org/opends/server/extensions/PBKDF2PasswordStorageScheme.java
new file mode 100644
index 0000000..37dbf7c
--- /dev/null
+++ b/opends/src/server/org/opends/server/extensions/PBKDF2PasswordStorageScheme.java
@@ -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);
+  }
+
+}

--
Gitblit v1.10.0