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

Ludovic Poitou
26.57.2010 2a09ccc55845e4ff56cf1e8dee0a24fe26725063
Resolves Enhancement request OpenDJ-5: Support Linux md5 crypt storage for password
This changes are adding support for the BSD MD5 crypt hash as part of the CRYPT password storage scheme.
A new parameter has been added to the configuration of the storage scheme to select whether new passwords should be hashed with the unix algo (default) or the md5 one.
When it comes to authentication, the scheme is able to detect the algo (based on the $1$ prefix) and match appropriately.
Unit tests have been added, including test again passwords already hashed on Linux systems.
1 files added
6 files modified
1012 ■■■■■ changed files
opends/resource/config/config.ldif 1 ●●●● patch | view | raw | blame | history
opends/resource/schema/02-config.ldif 6 ●●●●● patch | view | raw | blame | history
opends/src/admin/defn/org/opends/server/admin/std/CryptPasswordStorageSchemeConfiguration.xml 51 ●●●● patch | view | raw | blame | history
opends/src/admin/messages/CryptPasswordStorageSchemeCfgDefn.properties 8 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/CryptPasswordStorageScheme.java 150 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/BSDMD5Crypt.java 291 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/CryptPasswordStorageSchemeTestCase.java 505 ●●●●● patch | view | raw | blame | history
opends/resource/config/config.ldif
@@ -1534,6 +1534,7 @@
cn: CRYPT
ds-cfg-java-class: org.opends.server.extensions.CryptPasswordStorageScheme
ds-cfg-enabled: true
ds-cfg-crypt-password-storage-encryption-algorithm: unix
dn: cn=MD5,cn=Password Storage Schemes,cn=config
objectClass: top
opends/resource/schema/02-config.ldif
@@ -2533,6 +2533,11 @@
  NAME 'ds-cfg-max-ops-interval'
  SYNTAX  1.3.6.1.4.1.1466.115.121.1.15
  X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.999.1
  NAME 'ds-cfg-crypt-password-storage-encryption-algorithm'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  SINGLE-VALUE
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
  NAME 'ds-cfg-access-control-handler'
  SUP top
@@ -3838,6 +3843,7 @@
  NAME 'ds-cfg-crypt-password-storage-scheme'
  SUP ds-cfg-password-storage-scheme
  STRUCTURAL
  MUST ds-cfg-crypt-password-storage-encryption-algorithm
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.143
  NAME 'ds-cfg-md5-password-storage-scheme'
opends/src/admin/defn/org/opends/server/admin/std/CryptPasswordStorageSchemeConfiguration.xml
@@ -24,6 +24,7 @@
  !
  !
  !      Copyright 2007-2008 Sun Microsystems, Inc.
  !      Portions Copyright 2010 ForgeRock AS
  ! -->
<adm:managed-object name="crypt-password-storage-scheme"
  plural-name="crypt-password-storage-schemes"
@@ -34,14 +35,17 @@
  <adm:synopsis>
    The
    <adm:user-friendly-name />
    provides a mechanism for encoding user passwords using the UNIX
    crypt algorithm.
    provides a mechanism for encoding user passwords like Unix crypt does.
    Like on most Unix systems, the password mq be encrypted using different
    algorithm, either UNIX crypt or md5 (bsd).
  </adm:synopsis>
  <adm:description>
    This implementation contains only an implementation for the user
    password syntax, with a storage scheme name of "CRYPT". Even though it
    is a one-way digest, the
    <adm:user-friendly-name />
    This implementation contains an implementation for the user
    password syntax, with a storage scheme name of "CRYPT". Like on most
    Unix, the "CRYPT" storage scheme has different algorithm, the default
    being the UNIX crypt.
    Even though the UNIX crypt is a one-way digest, it
    is relatively weak by today's standards. Because it supports 
    only a 12-bit salt (meaning that there are only 4096 possible ways to 
    encode a given password), it is also vulnerable to dictionary attacks. 
@@ -64,4 +68,39 @@
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
    <adm:property name="crypt-password-storage-encryption-algorithm" mandatory="true">
    <adm:synopsis>
      Specifies the algorithm to use to encrypt new passwords.
    </adm:synopsis>
    <adm:description>
      Select the crypt algorithm to use to encrypt new passwords.
      The value can either be "unix", which means the password is encrypted
      with the UNIX crypt algorithm, or md5 which means the password is
      encrypted with BSD MD5 algorithm and has a $1$ prefix.
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>unix</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:enumeration>
        <adm:value name="unix">
          <adm:synopsis>
            New passwords are encrypted with the UNIX crypt algorithm.
          </adm:synopsis>
        </adm:value>
        <adm:value name="md5">
          <adm:synopsis>
            New passwords are encrypted with the BSD MD5 algorithm.
          </adm:synopsis>
        </adm:value>
      </adm:enumeration>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-crypt-password-storage-encryption-algorithm</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opends/src/admin/messages/CryptPasswordStorageSchemeCfgDefn.properties
@@ -1,6 +1,10 @@
user-friendly-name=Crypt Password Storage Scheme
user-friendly-plural-name=Crypt Password Storage Schemes
synopsis=The Crypt Password Storage Scheme provides a mechanism for encoding user passwords using the UNIX crypt algorithm.
description=This implementation contains only an implementation for the user password syntax, with a storage scheme name of "CRYPT". Even though it is a one-way digest, the Crypt Password Storage Scheme is relatively weak by today's standards. Because it supports only a 12-bit salt (meaning that there are only 4096 possible ways to encode a given password), it is also vulnerable to dictionary attacks. You should therefore use this storage scheme only in cases where an external application expects to retrieve the password and verify it outside of the directory, rather than by performing an LDAP bind.
synopsis=The Crypt Password Storage Scheme provides a mechanism for encoding user passwords like Unix crypt does. Like on most Unix systems, the password mq be encrypted using different algorithm, either UNIX crypt or md5 (bsd).
description=This implementation contains an implementation for the user password syntax, with a storage scheme name of "CRYPT". Like on most Unix, the "CRYPT" storage scheme has different algorithm, the default being the UNIX crypt. Even though the UNIX crypt is a one-way digest, it is relatively weak by today's standards. Because it supports only a 12-bit salt (meaning that there are only 4096 possible ways to encode a given password), it is also vulnerable to dictionary attacks. You should therefore use this storage scheme only in cases where an external application expects to retrieve the password and verify it outside of the directory, rather than by performing an LDAP bind.
property.crypt-password-storage-encryption-algorithm.synopsis=Specifies the algorithm to use to encrypt new passwords.
property.crypt-password-storage-encryption-algorithm.description=Select the crypt algorithm to use to encrypt new passwords. The value can either be "unix", which means the password is encrypted with the UNIX crypt algorithm, or md5 which means the password is encrypted with BSD MD5 algorithm and has a $1$ prefix.
property.crypt-password-storage-encryption-algorithm.syntax.enumeration.value.md5.synopsis=New passwords are encrypted with the BSD MD5 algorithm.
property.crypt-password-storage-encryption-algorithm.syntax.enumeration.value.unix.synopsis=New passwords are encrypted with the UNIX crypt algorithm.
property.enabled.synopsis=Indicates whether the Crypt Password Storage Scheme is enabled for use.
property.java-class.synopsis=Specifies the fully-qualified name of the Java class that provides the Crypt Password Storage Scheme implementation.
opends/src/server/org/opends/server/extensions/CryptPasswordStorageScheme.java
@@ -23,20 +23,27 @@
 *
 *
 *      Copyright 2008 Sun Microsystems, Inc.
 *      Portions Copyright 2010 ForgeRock AS
 *
 */
package org.opends.server.extensions;
import java.util.List;
import java.util.ArrayList;
import java.util.Random;
import org.opends.messages.Message;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.PasswordStorageSchemeCfg;
import org.opends.server.admin.std.server.CryptPasswordStorageSchemeCfg;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.*;
import org.opends.server.util.Crypt;
import org.opends.server.util.BSDMD5Crypt;
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.extensions.ExtensionsConstants.*;
@@ -54,6 +61,7 @@
 */
public class CryptPasswordStorageScheme
       extends PasswordStorageScheme<CryptPasswordStorageSchemeCfg>
       implements ConfigurationChangeListener<CryptPasswordStorageSchemeCfg>
{
  /**
   * The fully-qualified name of this class for debugging purposes.
@@ -61,6 +69,11 @@
  private static final String CLASS_NAME =
       "org.opends.server.extensions.CryptPasswordStorageScheme";
  /*
   * The current configuration for the CryptPasswordStorageScheme
   */
  private CryptPasswordStorageSchemeCfg currentConfig;
  /**
   * An array of values that can be used to create salt characters
   * when encoding new crypt hashes.
@@ -72,7 +85,7 @@
  private final Random randomSaltIndex = new Random();
  private final Object saltLock = new Object();
  private final Crypt crypt = new Crypt();
  private final BSDMD5Crypt bsdmd5crypt = new BSDMD5Crypt();
  /**
@@ -93,7 +106,10 @@
  public void initializePasswordStorageScheme(
                   CryptPasswordStorageSchemeCfg configuration)
         throws ConfigException, InitializationException {
    // Nothing to configure
    configuration.addCryptChangeListener(this);
    currentConfig = configuration;
  }
  /**
@@ -107,10 +123,10 @@
  /**
   * {@inheritDoc}
   * Encrypt plaintext password with the Unix Crypt algorithm.
   */
  @Override()
  public ByteString encodePassword(ByteSequence plaintext)
  private ByteString unixCryptEncodePassword(ByteSequence plaintext)
         throws DirectoryException
  {
@@ -133,7 +149,6 @@
    return ByteString.wrap(digestBytes);
  }
  /**
   * Return a random 2-byte salt.
   *
@@ -152,6 +167,44 @@
    }
  }
  private ByteString md5CryptEncodePassword(ByteSequence plaintext)
         throws DirectoryException
  {
    String output;
    try
    {
      output = bsdmd5crypt.crypt(plaintext.toString());
    }
    catch (Exception e)
    {
      Message message = ERR_PWSCHEME_CANNOT_ENCODE_PASSWORD.get(
          CLASS_NAME, stackTraceToSingleLineString(e));
      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
                                   message, e);
    }
    return ByteString.valueOf(output);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public ByteString encodePassword(ByteSequence plaintext)
         throws DirectoryException
  {
    ByteString bytes = null;
    switch (currentConfig.getCryptPasswordStorageEncryptionAlgorithm())
    {
      case UNIX:
        bytes = unixCryptEncodePassword(plaintext);
        break;
      case MD5:
        bytes = md5CryptEncodePassword(plaintext);
        break;
    }
    return bytes;
  }
  /**
   * {@inheritDoc}
@@ -171,13 +224,10 @@
    return ByteString.valueOf(buffer.toString());
  }
  /**
   * {@inheritDoc}
   * Matches passwords encrypted with the Unix Crypt algorithm.
   */
  @Override()
  public boolean passwordMatches(ByteSequence plaintextPassword,
  private boolean unixCryptPasswordMatches(ByteSequence plaintextPassword,
                                 ByteSequence storedPassword)
  {
    // TODO: Can we avoid this copy?
@@ -201,7 +251,39 @@
    return userPWDigestBytes.equals(storedPassword);
  }
  private boolean md5CryptPasswordMatches(ByteSequence plaintextPassword,
                                 ByteSequence storedPassword)
  {
    String storedString = storedPassword.toString();
    try
    {
      String userString   = bsdmd5crypt.crypt(plaintextPassword.toString(),
        storedString);
      return userString.equals(storedString);
    }
    catch (Exception e)
    {
      return false;
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean passwordMatches(ByteSequence plaintextPassword,
                                 ByteSequence storedPassword)
  {
    String storedString = storedPassword.toString();
    if (storedString.startsWith(BSDMD5Crypt.getMagicString()))
    {
      return md5CryptPasswordMatches(plaintextPassword, storedPassword);
    }
    else
    {
      return unixCryptPasswordMatches(plaintextPassword, storedPassword);
    }
  }
  /**
   * {@inheritDoc}
@@ -276,7 +358,7 @@
         throws DirectoryException
  {
    Message message =
        ERR_PWSCHEME_DOES_NOT_SUPPORT_AUTH_PASSWORD.get(getStorageSchemeName());
      ERR_PWSCHEME_DOES_NOT_SUPPORT_AUTH_PASSWORD.get(getStorageSchemeName());
    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
  }
@@ -303,5 +385,47 @@
    return false;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isConfigurationAcceptable(
          PasswordStorageSchemeCfg configuration,
          List<Message> unacceptableReasons)
  {
    CryptPasswordStorageSchemeCfg config =
            (CryptPasswordStorageSchemeCfg) configuration;
    return isConfigurationChangeAcceptable(config, unacceptableReasons);
}
  /**
   * {@inheritDoc}
   */
  public boolean isConfigurationChangeAcceptable(
                      CryptPasswordStorageSchemeCfg configuration,
                      List<Message> unacceptableReasons)
  {
    // If we've gotten this far, then we'll accept the change.
    return true;
  }
  /**
   * {@inheritDoc}
   */
  public ConfigChangeResult applyConfigurationChange(
                      CryptPasswordStorageSchemeCfg configuration)
  {
    ResultCode        resultCode          = ResultCode.SUCCESS;
    boolean           adminActionRequired = false;
    ArrayList<Message> messages            = new ArrayList<Message>();
    currentConfig = configuration;
    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
  }
}
opends/src/server/org/opends/server/util/BSDMD5Crypt.java
New file
@@ -0,0 +1,291 @@
/*
 * 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 2010 ForgeRock AS
 *
 * BSD-compatible md5 password crypt
 * Ported to Java from C based on crypt-md5.c by Poul-Henning Kamp,
 * which was distributed with the following notice:
 * ----------------------------------------------------------------------------
 * "THE BEER-WARE LICENSE" (Revision 42):
 * <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
 * can do whatever you want with this stuff. If we meet some day, and you think
 * this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
 * ----------------------------------------------------------------------------
 */
package org.opends.server.util;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
/**
 * BSD MD5 Crypt algorithm, ported from C.
 *
 * @author ludo
 */
public final class BSDMD5Crypt {
  private final static String magic = "$1$";
  private final static int saltLength = 8;
  private final static String itoa64 =
          "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
  private static String intTo64(int value, int length)
  {
    StringBuilder output = new StringBuilder();
    while (--length >= 0)
    {
      output.append(itoa64.charAt((value & 0x3f)));
      value >>= 6;
    }
    return (output.toString());
  }
  /* Clear bytes, equivalent of memset */
  static private void clearBytes(byte bytes[])
  {
    for (int i = 0; i < bytes.length; i++)
    {
      bytes[i] = 0;
    }
  }
  /**
   * Encode the supplied password in BSD MD5 crypt form, using
   * a random salt.
   *
   * @param password A password to encode.
   *
   * @return An encrypted string.
   *
   * @throws NoSuchAlgorithmException If the MD5 algorithm is not supported.
   *
   */
  static public String crypt(String password)
          throws NoSuchAlgorithmException
  {
    SecureRandom randomGenerator = new SecureRandom();
    StringBuilder salt = new StringBuilder();
    /* Generate some random salt */
    while (salt.length() < saltLength)
    {
      int index = (int) (randomGenerator.nextFloat() * itoa64.length());
      salt.append(itoa64.charAt(index));
    }
    return BSDMD5Crypt.crypt(password, salt.toString());
  }
  /**
   * Encode the supplied password in BSD MD5 crypt form, using
   * provided salt.
   *
   * @param password A password to encode.
   *
   * @param salt A salt string of any size, of which only the first
   * 8 bytes will be considered.
   *
   * @return An encrypted string.
   *
   * @throws NoSuchAlgorithmException If the MD5 algorithm is not supported.
   *
   */
  static public String crypt(String password, String salt)
          throws NoSuchAlgorithmException
  {
    MessageDigest ctx, ctx1;
    byte digest1[], digest[];
    /* First skip the magic string */
    if (salt.startsWith(magic))
    {
      salt = salt.substring(magic.length());
    }
    /* Salt stops at the first $, max saltLength chars */
    int saltEnd = salt.indexOf('$');
    if (saltEnd != -1)
    {
      salt = salt.substring(0, saltEnd);
    }
    if (salt.length() > saltLength)
    {
      salt = salt.substring(0, saltLength);
    }
    ctx = MessageDigest.getInstance("MD5");
    /* The password first, since that is what is most unknown */
    ctx.update(password.getBytes());
    /* Then our magic string */
    ctx.update(magic.getBytes());
    /* Then the raw salt */
    ctx.update(salt.getBytes());
    /* Then just as many characters of the MD5(password,salt,password) */
    ctx1 = MessageDigest.getInstance("MD5");
    ctx1.update(password.getBytes());
    ctx1.update(salt.getBytes());
    ctx1.update(password.getBytes());
    digest1 = ctx1.digest();
    for (int pl = password.length(); pl > 0; pl -= 16)
    {
      ctx.update(digest1, 0, pl > 16 ? 16 : pl);
    }
    /* Don't leave anything around in vm they could use. */
    clearBytes(digest1);
    /* Then something really weird... */
    for (int i = password.length(); i != 0; i >>= 1)
    {
      if ((i & 1) != 0)
      {
        ctx.update(digest1[0]);
      } else
      {
        ctx.update(password.getBytes()[0]);
      }
    }
    /* Now make the output string */
    StringBuilder output = new StringBuilder();
    output.append(magic);
    output.append(salt);
    output.append("$");
    digest = ctx.digest();
    /*
     * and now, just to make sure things don't run too fast
     * On a 60 MHz Pentium this takes 34 msec, so you would
     * need 30 seconds to build a 1000 entry dictionary...
     */
    for (int i = 0; i < 1000; i++)
    {
      ctx1 = MessageDigest.getInstance("MD5");
      if ((i & 1) != 0)
      {
        ctx1.update(password.getBytes());
      } else
      {
        ctx1.update(digest);
      }
      if ((i % 3) != 0)
      {
        ctx1.update(salt.getBytes());
      }
      if ((i % 7) != 0)
      {
        ctx1.update(password.getBytes());
      }
      if ((i & 1) != 0)
      {
        ctx1.update(digest);
      } else
      {
        ctx1.update(password.getBytes());
      }
      digest = ctx1.digest();
    }
    int l;
    l = ((digest[0] & 0xff) << 16) | ((digest[6] & 0xff) << 8)
            | (digest[12] & 0xff);
    output.append(intTo64(l, 4));
    l = ((digest[1] & 0xff) << 16) | ((digest[7] & 0xff) << 8)
            | (digest[13] & 0xff);
    output.append(intTo64(l, 4));
    l = ((digest[2] & 0xff) << 16) | ((digest[8] & 0xff) << 8)
            | (digest[14] & 0xff);
    output.append(intTo64(l, 4));
    l = ((digest[3] & 0xff) << 16) | ((digest[9] & 0xff) << 8)
            | (digest[15] & 0xff);
    output.append(intTo64(l, 4));
    l = ((digest[4] & 0xff) << 16) | ((digest[10] & 0xff) << 8)
            | (digest[5] & 0xff);
    output.append(intTo64(l, 4));
    l = (digest[11] & 0xff);
    output.append(intTo64(l, 2));
    /* Don't leave anything around in vm they could use. */
    clearBytes(digest);
    ctx = null;
    ctx1 = null;
    return output.toString();
  }
  /**
   * Getter to the BSD MD5 magic string.
   *
   * @return the magic string for this crypt algorithm
   */
  static public String getMagicString()
  {
    return magic;
  }
  /**
   * Main test method.
   *
   * @param argv The array of test arguments
   *
   */
  static public void main(String argv[])
  {
    if ((argv.length < 1) || (argv.length > 2))
    {
      System.err.println("Usage: BSDMD5Crypt password salt");
      System.exit(1);
    }
    try
    {
      if (argv.length == 2)
      {
        System.out.println(BSDMD5Crypt.crypt(argv[0], argv[1]));
      } else
      {
        System.out.println(BSDMD5Crypt.crypt(argv[0]));
      }
    } catch (Exception e)
    {
      System.err.println(e.getMessage().toString());
      System.exit(1);
    }
    System.exit(0);
  }
}
opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/CryptPasswordStorageSchemeTestCase.java
@@ -23,30 +23,504 @@
 *
 *
 *      Copyright 2008 Sun Microsystems, Inc.
 *      Portions Copyright 2010 ForgeRock AS
 */
package org.opends.server.extensions;
import static org.testng.Assert.*;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.ArrayList;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.server.AdminTestCaseUtils;
import org.opends.server.admin.std.meta.CryptPasswordStorageSchemeCfgDefn;
import org.opends.server.admin.std.server.CryptPasswordStorageSchemeCfg;
import org.opends.server.config.ConfigEntry;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyOperation;
import org.opends.server.core.PasswordPolicy;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.schema.AuthPasswordSyntax;
import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.types.Attributes;
import org.opends.server.types.ByteString;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.Modification;
import org.opends.server.types.ModificationType;
import org.opends.server.types.ResultCode;
/**
 * A set of test cases for the salted MD5 password storage scheme.
 * A set of test cases for the crypt password storage scheme.
 */
public class CryptPasswordStorageSchemeTestCase
       extends PasswordStorageSchemeTestCase
       extends ExtensionsTestCase
{
  // The configuration entry for this password storage scheme.
  private ConfigEntry configEntry;
  // The string representation of the DN of the configuration entry for this
  // password storage scheme.
  private static final String configDNString =
          "cn=Crypt,cn=Password Storage Schemes,cn=config";
  /**
   * Creates a new instance of this storage scheme test case.
   * Creates a new instance of this crypt password storage scheme test
   * case with the provided information.
   */
  public CryptPasswordStorageSchemeTestCase()
  {
    super("cn=Crypt,cn=Password Storage Schemes,cn=config");
    super();
    this.configEntry    = null;
  }
  /**
   * Ensures that the Directory Server is started before running any of these
   * tests.
   */
  @BeforeClass()
  public void startServer()
         throws Exception
  {
    TestCaseUtils.startServer();
    configEntry = DirectoryServer.getConfigEntry(DN.decode(configDNString));
  }
  /**
   * 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.
   */
  @DataProvider(name = "testPasswords")
  public Object[][] getTestPasswords()
  {
    return new Object[][]
    {
      new Object[] { ByteString.empty() },
      new Object[] { ByteString.valueOf("") },
      new Object[] { ByteString.valueOf("\u0000") },
      new Object[] { ByteString.valueOf("\t") },
      new Object[] { ByteString.valueOf("\n") },
      new Object[] { ByteString.valueOf("\r\n") },
      new Object[] { ByteString.valueOf(" ") },
      new Object[] { ByteString.valueOf("Test1\tTest2\tTest3") },
      new Object[] { ByteString.valueOf("Test1\nTest2\nTest3") },
      new Object[] { ByteString.valueOf("Test1\r\nTest2\r\nTest3") },
      new Object[] { ByteString.valueOf("a") },
      new Object[] { ByteString.valueOf("ab") },
      new Object[] { ByteString.valueOf("abc") },
      new Object[] { ByteString.valueOf("abcd") },
      new Object[] { ByteString.valueOf("abcde") },
      new Object[] { ByteString.valueOf("abcdef") },
      new Object[] { ByteString.valueOf("abcdefg") },
      new Object[] { ByteString.valueOf("abcdefgh") },
      new Object[] { ByteString.valueOf("The Quick Brown Fox Jumps Over " +
                                         "The Lazy Dog") },
      new Object[] { ByteString.valueOf("\u00BFD\u00F3nde est\u00E1 el " +
                                         "ba\u00F1o?") }
    };
  }
  /**
   * Creates an instance of the password storage scheme, uses it to encode the
   * provided password, and ensures that the encoded value is correct.
   *
   * @param  plaintext  The plain-text version of the password to encode.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testPasswords")
  public void testUnixStorageScheme(ByteString plaintext)
         throws Exception
  {
    CryptPasswordStorageScheme scheme = getScheme("unix");
    assertNotNull(scheme);
    assertNotNull(scheme.getStorageSchemeName());
    ByteString encodedPassword = scheme.encodePassword(plaintext);
    assertNotNull(encodedPassword);
    assertTrue(scheme.passwordMatches(plaintext, encodedPassword));
    assertFalse(scheme.passwordMatches(plaintext,
                                       ByteString.valueOf("garbage")));
    ByteString schemeEncodedPassword =
         scheme.encodePasswordWithScheme(plaintext);
    String[] pwComponents = UserPasswordSyntax.decodeUserPassword(
                                 schemeEncodedPassword.toString());
    assertNotNull(pwComponents);
    if (scheme.supportsAuthPasswordSyntax())
    {
      assertNotNull(scheme.getAuthPasswordSchemeName());
      ByteString encodedAuthPassword = scheme.encodeAuthPassword(plaintext);
      StringBuilder[] authPWComponents =
           AuthPasswordSyntax.decodeAuthPassword(
                encodedAuthPassword.toString());
      assertTrue(scheme.authPasswordMatches(plaintext,
                                            authPWComponents[1].toString(),
                                            authPWComponents[2].toString()));
      assertFalse(scheme.authPasswordMatches(plaintext, ",", "foo"));
      assertFalse(scheme.authPasswordMatches(plaintext, "foo", ","));
    }
    else
    {
      try
      {
        scheme.encodeAuthPassword(plaintext);
        throw new Exception("Expected encodedAuthPassword to fail for scheme " +
                            scheme.getStorageSchemeName() +
                            " because it doesn't support auth passwords.");
      }
      catch (DirectoryException de)
      {
        // This was expected.
      }
      assertFalse(scheme.authPasswordMatches(plaintext, "foo", "bar"));
    }
    if (scheme.isReversible())
    {
      assertEquals(scheme.getPlaintextValue(encodedPassword), plaintext);
    }
    else
    {
      try
      {
        scheme.getPlaintextValue(encodedPassword);
        throw new Exception("Expected getPlaintextValue to fail for scheme " +
                            scheme.getStorageSchemeName() +
                            " because it is not reversible.");
      }
      catch (DirectoryException de)
      {
        // This was expected.
      }
    }
    scheme.isStorageSchemeSecure();
  }
  /**
   * Creates an instance of the password storage scheme, uses it to encode the
   * provided password, and ensures that the encoded value is correct.
   *
   * @param  plaintext  The plain-text version of the password to encode.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testPasswords")
  public void testMD5StorageScheme(ByteString plaintext)
         throws Exception
  {
    CryptPasswordStorageScheme scheme = getScheme("md5");
    assertNotNull(scheme);
    assertNotNull(scheme.getStorageSchemeName());
    ByteString encodedPassword = scheme.encodePassword(plaintext);
    assertNotNull(encodedPassword);
    assertTrue(scheme.passwordMatches(plaintext, encodedPassword));
    assertFalse(scheme.passwordMatches(plaintext,
                                       ByteString.valueOf("garbage")));
    ByteString schemeEncodedPassword =
         scheme.encodePasswordWithScheme(plaintext);
    String[] pwComponents = UserPasswordSyntax.decodeUserPassword(
                                 schemeEncodedPassword.toString());
    assertNotNull(pwComponents);
    if (scheme.supportsAuthPasswordSyntax())
    {
      assertNotNull(scheme.getAuthPasswordSchemeName());
      ByteString encodedAuthPassword = scheme.encodeAuthPassword(plaintext);
      StringBuilder[] authPWComponents =
           AuthPasswordSyntax.decodeAuthPassword(
                encodedAuthPassword.toString());
      assertTrue(scheme.authPasswordMatches(plaintext,
                                            authPWComponents[1].toString(),
                                            authPWComponents[2].toString()));
      assertFalse(scheme.authPasswordMatches(plaintext, ",", "foo"));
      assertFalse(scheme.authPasswordMatches(plaintext, "foo", ","));
    }
    else
    {
      try
      {
        scheme.encodeAuthPassword(plaintext);
        throw new Exception("Expected encodedAuthPassword to fail for scheme " +
                            scheme.getStorageSchemeName() +
                            " because it doesn't support auth passwords.");
      }
      catch (DirectoryException de)
      {
        // This was expected.
      }
      assertFalse(scheme.authPasswordMatches(plaintext, "foo", "bar"));
    }
    if (scheme.isReversible())
    {
      assertEquals(scheme.getPlaintextValue(encodedPassword), plaintext);
    }
    else
    {
      try
      {
        scheme.getPlaintextValue(encodedPassword);
        throw new Exception("Expected getPlaintextValue to fail for scheme " +
                            scheme.getStorageSchemeName() +
                            " because it is not reversible.");
      }
      catch (DirectoryException de)
      {
        // This was expected.
      }
    }
    scheme.isStorageSchemeSecure();
  }
  @DataProvider
  public Object[][] passwordsForBinding()
  {
    return new Object[][]
    {
      // In the case of a clear-text password, these values will be shoved
      // un-excaped into an LDIF file, so make sure they don't include \n
      // or other characters that will cause LDIF parsing errors.
      // We really don't need many test cases here, since that functionality
      // is tested above.
      new Object[] { ByteString.valueOf("a") },
      new Object[] { ByteString.valueOf("abcdefgh") },
      new Object[] { ByteString.valueOf("abcdefghi") },
    };
  }
  /**
   * An end-to-end test that verifies that we can set a pre-encoded password
   * in a user entry, and then bind as that user using the cleartext password.
   */
  @Test(dataProvider = "passwordsForBinding")
  public void testSettingUnixEncodedPassword(ByteString plainPassword)
          throws Exception
  {
    // Start/clear-out the memory backend
    TestCaseUtils.initializeTestBackend(true);
    boolean allowPreencodedDefault = setAllowPreencodedPasswords(true);
    try {
      CryptPasswordStorageScheme scheme = getScheme("unix");
      ByteString schemeEncodedPassword =
           scheme.encodePasswordWithScheme(plainPassword);
      //
      // This code creates a user with the encoded password,
      // and then verifies that they can bind with the raw password.
      //
      Entry userEntry = TestCaseUtils.makeEntry(
           "dn: uid=test.user,o=test",
           "objectClass: top",
           "objectClass: person",
           "objectClass: organizationalPerson",
           "objectClass: inetOrgPerson",
           "uid: test.user",
           "givenName: Test",
           "sn: User",
           "cn: Test User",
           "ds-privilege-name: bypass-acl",
           "userPassword: " + schemeEncodedPassword.toString());
      // Add the entry
      TestCaseUtils.addEntry(userEntry);
      assertTrue(TestCaseUtils.canBind("uid=test.user,o=test",
                 plainPassword.toString()),
                 "Failed to bind when pre-encoded password = \"" +
                         schemeEncodedPassword.toString() + "\" and " +
                         "plaintext password = \"" +
                         plainPassword.toString() + "\"");
    } finally {
      setAllowPreencodedPasswords(allowPreencodedDefault);
    }
  }
  /**
   * An end-to-end test that verifies that we can set a pre-encoded password
   * in a user entry, and then bind as that user using the cleartext password.
   */
  @Test(dataProvider = "passwordsForBinding")
  public void testSettingMD5EncodedPassword(ByteString plainPassword)
          throws Exception
  {
    // Start/clear-out the memory backend
    TestCaseUtils.initializeTestBackend(true);
    boolean allowPreencodedDefault = setAllowPreencodedPasswords(true);
    try {
      CryptPasswordStorageScheme scheme = getScheme("md5");
      ByteString schemeEncodedPassword =
           scheme.encodePasswordWithScheme(plainPassword);
      //
      // This code creates a user with the encoded password,
      // and then verifies that they can bind with the raw password.
      //
      Entry userEntry = TestCaseUtils.makeEntry(
           "dn: uid=test.user,o=test",
           "objectClass: top",
           "objectClass: person",
           "objectClass: organizationalPerson",
           "objectClass: inetOrgPerson",
           "uid: test.user",
           "givenName: Test",
           "sn: User",
           "cn: Test User",
           "ds-privilege-name: bypass-acl",
           "userPassword: " + schemeEncodedPassword.toString());
      // Add the entry
      TestCaseUtils.addEntry(userEntry);
      assertTrue(TestCaseUtils.canBind("uid=test.user,o=test",
                 plainPassword.toString()),
                 "Failed to bind when pre-encoded password = \"" +
                         schemeEncodedPassword.toString() + "\" and " +
                         "plaintext password = \"" +
                         plainPassword.toString() + "\"");
    } finally {
      setAllowPreencodedPasswords(allowPreencodedDefault);
    }
  }
  /**
   * Sets whether or not to allow pre-encoded password values for the
   * current password storage scheme and returns the previous value so that
   * it can be restored.
   *
   * @param allowPreencoded whether or not to allow pre-encoded passwords
   * @return the previous value for the allow preencoded passwords
   */
  private boolean setAllowPreencodedPasswords(boolean allowPreencoded)
          throws Exception
  {
    // This code was borrowed from
    // PasswordPolicyTestCase.testAllowPreEncodedPasswordsAuth
    boolean previousValue = false;
    try {
      DN dn = DN.decode("cn=Default Password Policy,cn=Password Policies,cn=config");
      PasswordPolicy p = DirectoryServer.getPasswordPolicy(dn);
      previousValue = p.allowPreEncodedPasswords();
      String attr  = "ds-cfg-allow-pre-encoded-passwords";
      ArrayList<Modification> mods = new ArrayList<Modification>();
      mods.add(new Modification(ModificationType.REPLACE,
          Attributes.create(attr, String.valueOf(allowPreencoded))));
      InternalClientConnection conn =
           InternalClientConnection.getRootConnection();
      ModifyOperation modifyOperation = conn.processModify(dn, mods);
      assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
      p = DirectoryServer.getPasswordPolicy(dn);
      assertEquals(p.allowPreEncodedPasswords(), allowPreencoded);
    } catch (Exception e) {
      System.err.println("Failed to set ds-cfg-allow-pre-encoded-passwords " +
                         " to " + allowPreencoded);
      e.printStackTrace();
      throw e;
    }
    return previousValue;
  }
  /**
   * Retrieves a set of passwords (plain and md5 encrypted) that may
   * be used to test the BSDMD5 algorithm of Crypt Password Storage scheme
   * compatibility with Linux versions.
   * The encrypted version has been generated by openssl passwd -1
   * command on MacOS X.
   *
   * @return  A set of couple (cleartext, md5 encryped) passwords that
   *          may be used to test BSD MD5 algorithm of the Crypt password
   *          storage scheme.
   */
  @DataProvider(name = "testBSDMD5Passwords")
  public Object[][] getTestBSDMD5Passwords()
         throws Exception
  {
    return new Object[][]
    {
      new Object[] { "secret12", "{CRYPT}$1$X40CcMaA$dd3ndknBLcpkED4/RciyD1" },
      new Object[] { "#1 Strong Password!", "{CRYPT}$1$7jHbWKyy$gAmpOSdaYVap55MwsQnK5/" },
      new Object[] { "foo", "{CRYPT}$1$ac/Z7Q3s$5kTVLqMSq9KMqUVyEBfiw0" }
    };
  }
  @Test(dataProvider = "testBSDMD5Passwords")
  public void testAuthBSDMD5Passwords(
          String plaintextPassword,
          String md5EncodedPassword) throws Exception
  {
      // Start/clear-out the memory backend
    TestCaseUtils.initializeTestBackend(true);
    boolean allowPreencodedDefault = setAllowPreencodedPasswords(true);
    try {
      Entry userEntry = TestCaseUtils.makeEntry(
       "dn: uid=testMD5.user,o=test",
       "objectClass: top",
       "objectClass: person",
       "objectClass: organizationalPerson",
       "objectClass: inetOrgPerson",
       "uid: testMD5.user",
       "givenName: TestMD5",
       "sn: User",
       "cn: TestMD5 User",
       "userPassword: " + md5EncodedPassword);
      // Add the entry
      TestCaseUtils.addEntry(userEntry);
      assertTrue(TestCaseUtils.canBind("uid=testMD5.user,o=test",
                  plaintextPassword),
               "Failed to bind when pre-encoded password = \"" +
               md5EncodedPassword + "\" and " +
               "plaintext password = \"" +
               plaintextPassword + "\"" );
    } finally {
      setAllowPreencodedPasswords(allowPreencodedDefault);
    }
  }
  /**
   * Retrieves an initialized instance of this password storage scheme.
@@ -55,12 +529,27 @@
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  protected PasswordStorageScheme getScheme()
  private CryptPasswordStorageScheme getScheme(String algo)
         throws Exception
  {
    CryptPasswordStorageScheme scheme =
         new CryptPasswordStorageScheme();
    scheme.initializePasswordStorageScheme(null);
    Entry e = TestCaseUtils.makeEntry(
      "dn: cn=CRYPT,cn=Password Storage Schemes,cn=config",
      "objectClass: top",
      "objectClass: ds-cfg-password-storage-scheme",
      "objectClass: ds-cfg-crypt-password-storage-scheme",
      "cn: CRYPT",
      "ds-cfg-java-class: org.opends.server.extensions.CryptPasswordStorageScheme",
      "ds-cfg-enabled: true",
      "ds-cfg-crypt-password-storage-encryption-algrithm: " + algo
);
    CryptPasswordStorageSchemeCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              CryptPasswordStorageSchemeCfgDefn.getInstance(),
              e);
    scheme.initializePasswordStorageScheme(configuration);
    return scheme;
  }
}