UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleEnhancedPasswordStorageScheme.java

/*
 * 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
 * docs/licenses/cddl.txt
 * or http://www.opensource.org/licenses/cddl1.php.
 * 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
 * docs/licenses/cddl.txt.  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
 *
 *
 *      Portions Copyright 2013-2024 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;



import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;

import com.unboundid.directory.sdk.common.schema.AttributeType;
import com.unboundid.directory.sdk.common.types.Entry;
import com.unboundid.directory.sdk.ds.api.EnhancedPasswordStorageScheme;
import com.unboundid.directory.sdk.ds.config.PasswordStorageSchemeConfig;
import com.unboundid.directory.sdk.ds.types.DirectoryServerContext;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.Modification;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Base64;
import com.unboundid.util.ByteString;
import com.unboundid.util.ByteStringBuffer;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.StringArgument;



/**
 * This class provides an example of an enhanced password storage scheme.
 * Passwords will be encoded using a salted 256-bit SHA-2 digest, but the salt
 * will be read from an attribute in the user's entry rather than being
 * dynamically generated.  As such, access to the full user entry will be
 * required in order to generate the encoded password.  Also note that the salt
 * will not automatically be generated or written by this password storage
 * scheme implementation so it must already be present in the entry, and if the
 * salt is to be changed then that must be done in conjunction with changing the
 * password.
 * <BR><BR>
 * This password storage scheme has one configuration argument:
 * <UL>
 *   <LI>salt-attribute -- The name or OID of the attribute that will appear in
 *       user entries and will be used as the salt when generating an encoded
 *       password.</LI>
 * </UL>
 */
public final class ExampleEnhancedPasswordStorageScheme
       extends EnhancedPasswordStorageScheme
{
  /**
   * The name of the argument that will be used to specify which attribute holds
   * the salt use when encoding the password.
   */
  private static final String ARG_NAME_SALT_ATTRIBUTE = "salt-attribute";



  /**
   * The name of the digest algorithm that will be used to hash passwords.
   */
  private static final String DIGEST_NAME_SHA2_256 = "SHA-256";



  // The name of the attribute that should hold the salt.
  private volatile String saltAttribute = null;

  // The server context for the server in which this extension is running.
  private DirectoryServerContext serverContext = null;



  /**
   * Creates a new instance of this password storage scheme.  All password
   * storage scheme implementations must include a default constructor, but any
   * initialization should generally be done in the
   * {@code initializePasswordStorageScheme} method.
   */
  public ExampleEnhancedPasswordStorageScheme()
  {
    // No implementation required.
  }



  /**
   * Retrieves a human-readable name for this extension.
   *
   * @return  A human-readable name for this extension.
   */
  @Override()
  public String getExtensionName()
  {
    return "Example Enhanced Password Storage Scheme";
  }



  /**
   * Retrieves a human-readable description for this extension.  Each element
   * of the array that is returned will be considered a separate paragraph in
   * generated documentation.
   *
   * @return  A human-readable description for this extension, or {@code null}
   *          or an empty array if no description should be available.
   */
  @Override()
  public String[] getExtensionDescription()
  {
    return new String[]
    {
      "This password storage scheme serves an example that may be used to " +
           "demonstrate the process for creating a third-party enhanced " +
           "password storage scheme.  It will use a salted 256-bit SHA-2 " +
           "encoding, but with the salt obtained from another attribute in " +
           "the user entry rather than generated by the storage scheme."
    };
  }



  /**
   * Updates the provided argument parser to define any configuration arguments
   * which may be used by this password generator.  The argument parser may also
   * be updated to define relationships between arguments (e.g., to specify
   * required, exclusive, or dependent argument sets).
   *
   * @param  parser  The argument parser to be updated with the configuration
   *                 arguments which may be used by this password generator.
   *
   * @throws  ArgumentException  If a problem is encountered while updating the
   *                             provided argument parser.
   */
  @Override()
  public void defineConfigArguments(final ArgumentParser parser)
         throws ArgumentException
  {
    // Add an argument that allows you to specify the attribute that holds the
    // salt for encoding passwords.
    final Character shortIdentifier = null;
    final String    longIdentifier  = ARG_NAME_SALT_ATTRIBUTE;
    final boolean   required        = true;
    final int       maxOccurrences  = 1;
    final String    placeholder     = "{attr}";
    final String    description     = "The name of the attribute that holds " +
         "the salt to use when encoding passwords for the user.";

    parser.addArgument(new StringArgument(shortIdentifier, longIdentifier,
         required, maxOccurrences, placeholder, description));
  }



  /**
   * Initializes this password storage scheme.
   *
   * @param  serverContext  A handle to the server context for the server in
   *                        which this extension is running.
   * @param  config         The general configuration for this password storage
   *                        scheme.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this password storage scheme.
   *
   * @throws  LDAPException  If a problem occurs while initializing this
   *                         password storage scheme.
   */
  @Override()
  public void initializePasswordStorageScheme(
                   final DirectoryServerContext serverContext,
                   final PasswordStorageSchemeConfig config,
                   final ArgumentParser parser)
         throws LDAPException
  {
    serverContext.debugInfo("Beginning password storage scheme initialization");

    this.serverContext = serverContext;

    final StringArgument saltAttrArg =
         (StringArgument) parser.getNamedArgument(ARG_NAME_SALT_ATTRIBUTE);
    saltAttribute = saltAttrArg.getValue();
  }



  /**
   * Indicates whether the configuration contained in the provided argument
   * parser represents a valid configuration for this extension.
   *
   * @param  config               The general configuration for this password
   *                              storage scheme.
   * @param  parser               The argument parser which has been initialized
   *                              with the proposed configuration.
   * @param  unacceptableReasons  A list that can be updated with reasons that
   *                              the proposed configuration is not acceptable.
   *
   * @return  {@code true} if the proposed configuration is acceptable, or
   *          {@code false} if not.
   */
  @Override()
  public boolean isConfigurationAcceptable(
                      final PasswordStorageSchemeConfig config,
                      final ArgumentParser parser,
                      final List<String> unacceptableReasons)
  {
    boolean acceptable = true;

    // The salt attribute must be defined in the server schema.
    final StringArgument saltAttrArg =
         (StringArgument) parser.getNamedArgument(ARG_NAME_SALT_ATTRIBUTE);
    final AttributeType saltAttrType =
         config.getServerContext().getSchema().getAttributeType(
              saltAttrArg.getValue(), false);
    if (saltAttrType == null)
    {
      acceptable = false;
      unacceptableReasons.add("The salt attribute is not defined in the " +
           "server schema.");
    }

    return acceptable;
  }



  /**
   * Attempts to apply the configuration contained in the provided argument
   * parser.
   *
   * @param  config                The general configuration for this password
   *                               storage scheme.
   * @param  parser                The argument parser which has been
   *                               initialized with the new configuration.
   * @param  adminActionsRequired  A list that can be updated with information
   *                               about any administrative actions that may be
   *                               required before one or more of the
   *                               configuration changes will be applied.
   * @param  messages              A list that can be updated with information
   *                               about the result of applying the new
   *                               configuration.
   *
   * @return  A result code that provides information about the result of
   *          attempting to apply the configuration change.
   */
  @Override()
  public ResultCode applyConfiguration(final PasswordStorageSchemeConfig config,
                                       final ArgumentParser parser,
                                       final List<String> adminActionsRequired,
                                       final List<String> messages)
  {
    final StringArgument saltAttrArg =
         (StringArgument) parser.getNamedArgument(ARG_NAME_SALT_ATTRIBUTE);
    saltAttribute = saltAttrArg.getValue();

    return ResultCode.SUCCESS;
  }



  /**
   * Performs any cleanup which may be necessary when this password storage
   * scheme is to be taken out of service.
   */
  @Override()
  public void finalizePasswordStorageScheme()
  {
    // No finalization is required.
  }



  /**
   * Retrieves the name for this password storage scheme.  This will be the
   * identifier which appears in curly braces at the beginning of the encoded
   * password.  The name should not include curly braces.
   *
   * @return  The name for this password storage scheme.
   */
  @Override()
  public String getStorageSchemeName()
  {
    // We'll call this algorithm "ESSHA256", for "externally-salted SHA-256".
    return "ESSHA256";
  }



  /**
   * Indicates whether this password storage scheme encodes passwords in a form
   * that allows the original plaintext value to be obtained from the encoded
   * representation.
   *
   * @return  {@code true} if the original plaintext password may be obtained
   *          from the encoded password, or {@code false} if not.
   */
  @Override()
  public boolean isReversible()
  {
    // This algorithm is not reversible.
    return false;
  }



  /**
   * Indicates whether this password storage scheme encodes passwords in a form
   * that may be considered secure.  A storage scheme should only be considered
   * secure if it is not possible to trivially determine a clear-text value
   * which may be used to generate a given encoded representation.
   *
   * @return  {@code true} if this password storage scheme may be considered
   *          secure, or {@code false} if not.
   */
  @Override()
  public boolean isSecure()
  {
    // This algorithm may be considered secure.
    return true;
  }



  /**
   * Indicates whether this password storage scheme requires access to the user
   * entry in order to perform processing.  If the storage scheme does require
   * access to the user entry (e.g., in order to obtain key or salt information
   * from another attribute, or to use information in the entry to access
   * information in another data store), then some features (e.g., the ability
   * to perform extensible matching with password values, or the ability to use
   * the encode-password tool to encode and compare passwords) may not be
   * available.
   *
   * @return  {@code true} if this password storage scheme requires access to
   *          the user entry, or {@code false} if not.
   */
  @Override()
  public boolean requiresUserEntry()
  {
    // This password storage scheme does require access to the user entry.
    return true;
  }



  /**
   * Encodes the provided plaintext password for this storage scheme,
   * without the name of the associated scheme.  Note that the
   * provided plaintext password should not be altered in any way.
   *
   * @param  plaintext      The plaintext password to be encoded.  It must not
   *                        be {@code null}.  Note that there is no guarantee
   *                        that password validators have yet been invoked for
   *                        this password, so this password storage scheme
   *                        implementation should not make any assumptions about
   *                        the format of the plaintext password or whether it
   *                        will actually be allowed for use in the entry.
   * @param  userEntry      The complete entry for the user for whom the
   *                        password is to be encoded.  This will not be
   *                        {@code null} for schemes in which
   *                        {@link #requiresUserEntry} returns {@code true}.
   * @param  modifications  The set of modifications to be applied to the user
   *                        entry.  This will generally be non-{@code null} only
   *                        for operations that use a modify to change the user
   *                        password.
   * @param  deterministic  Indicates whether the password encoding should be
   *                        deterministic.  If this is {@code true}, then the
   *                        scheme should attempt to generate a consistent
   *                        encoding for the password (e.g., by determining the
   *                        salt from a normalized representation of the user's
   *                        DN).  This may not be supported by all schemes.
   * @param  includeScheme  Indicates whether to include the name of the scheme
   *                        in curly braces before the encoded password.
   *
   * @return  The password that has been encoded using this storage
   *          scheme.
   *
   * @throws  LDAPException  If a problem occurs while attempting to encode the
   *                         password.
   */
  @Override()
  public ByteString encodePassword(final ByteString plaintext,
                                   final Entry userEntry,
                                   final List<Modification> modifications,
                                   final boolean deterministic,
                                   final boolean includeScheme)
         throws LDAPException
  {
    final ByteStringBuffer buffer = new ByteStringBuffer();
    if (includeScheme)
    {
      buffer.append('{');
      buffer.append(getStorageSchemeName());
      buffer.append('}');
    }

    final byte[] saltBytes = getSalt(userEntry, modifications);
    encode(plaintext, saltBytes, buffer);
    return buffer.toByteString();
  }



  /**
   * Indicates whether the provided plaintext password could have been used to
   * generate the given encoded password.
   *
   * @param  plaintextPassword  The plaintext password provided by the user as
   *                            part of a simple bind attempt.
   * @param  storedPassword     The stored password to compare against the
   *                            provided plaintext password.
   * @param  userEntry          The complete entry for the user for whom the
   *                            password is to be validated.  This will not be
   *                            {@code null} for schemes in which
   *                            {@link #requiresUserEntry} returns {@code true}.
   *
   * @return  {@code true} if the provided clear-text password could have been
   *          used to generate the encoded password, or {@code false} if not.
   */
  @Override()
  public boolean passwordMatches(final ByteString plaintextPassword,
                                 final ByteString storedPassword,
                                 final Entry userEntry)
  {
    try
    {
      final byte[] saltBytes = getSalt(userEntry, null);
      final ByteStringBuffer buffer = new ByteStringBuffer();
      encode(plaintextPassword, saltBytes, buffer);

      return Arrays.equals(buffer.toByteArray(), storedPassword.getValue());
    }
    catch (final Exception e)
    {
      serverContext.debugCaught(e);
      return false;
    }
  }



  /**
   * Attempts to determine the plaintext password used to generate the provided
   * encoded password.  This method should only be called if the
   * {@link #isReversible} method returns {@code true}.
   *
   * @param  storedPassword  The password for which to obtain the plaintext
   *                         value.  It should not include the scheme name in
   *                         curly braces.
   * @param  userEntry       The complete entry for the user for whom the
   *                         password is to be validated.  This will not be
   *                         {@code null} for schemes in which
   *                         {@link #requiresUserEntry} returns {@code true}.
   *
   * @return  The plaintext password obtained from the given encoded password.
   *
   * @throws  LDAPException  If this password storage scheme is not reversible,
   *                         or if the provided value could not be decoded to
   *                         its plaintext representation.
   */
  @Override()
  public ByteString getPlaintextValue(final ByteString storedPassword,
                                      final Entry userEntry)
         throws LDAPException
  {
    throw new LDAPException(ResultCode.OTHER,
         "Passwords encoded with the '" + getStorageSchemeName() +
              "' scheme are not reversible.");
  }



  /**
   * Indicates whether this password storage scheme provides the ability to
   * encode passwords in the authentication password syntax as described in RFC
   * 3112.
   *
   * @return  {@code true} if this password storage scheme supports the
   *          authentication password syntax, or {@code false} if not.
   */
  @Override()
  public boolean supportsAuthPasswordSyntax()
  {
    // This password storage scheme does not support the authentication password
    // syntax.  As such, we don't need to implement any of the other methods
    // that deal with it.
    return false;
  }



  /**
   * Retrieves the salt to use for password encoding.  If modifications are
   * available, then they will be checked first.
   *
   * @param  userEntry  The entry for the user for whom the salt is to be
   *                    obtained.
   * @param  mods       An optional set of modifications for the user.
   *
   * @return  The salt to use for password encoding.
   *
   * @throws  LDAPException  If a problem is encountered while obtaining the
   *                         salt.
   */
  private byte[] getSalt(final Entry userEntry, final List<Modification> mods)
          throws LDAPException
  {
    // The user entry must have been provided.  Since requiresUserEntry returns
    // true for this class, it should always be available, but we can guard
    // against it being null just in case.
    if (userEntry == null)
    {
      throw new LDAPException(ResultCode.OTHER,
           "Unable to encode a new " + getStorageSchemeName() +
                " password because no user entry was provided.");
    }


    // If any modifications were provided, then see if any of them affect the
    // salt.  If so, then apply just those modifications to the user entry.
    final Attribute saltAttr;
    if (mods != null)
    {
      final ArrayList<Modification> applyMods =
           new ArrayList<Modification>(mods.size());
      for (final Modification m : mods)
      {
        if (m.getAttributeName().equalsIgnoreCase(saltAttribute))
        {
          applyMods.add(m);
        }
      }

      if (applyMods.isEmpty())
      {
        saltAttr = userEntry.getAttribute(saltAttribute,
             Collections.<String>emptySet());
      }
      else
      {
        com.unboundid.ldap.sdk.Entry sdkEntry = userEntry.toLDAPSDKEntry();
        sdkEntry = com.unboundid.ldap.sdk.Entry.applyModifications(sdkEntry,
             true, applyMods);
        saltAttr = sdkEntry.getAttribute(saltAttribute);
      }
    }
    else
    {
      saltAttr = userEntry.getAttribute(saltAttribute,
           Collections.<String>emptySet());
    }

    if (saltAttr == null)
    {
      throw new LDAPException(ResultCode.OTHER,
           "No salt found in attribute '" + saltAttribute + "' in entry '" +
                userEntry.getDN() + "'.");
    }

    if (saltAttr.size() != 1)
    {
      throw new LDAPException(ResultCode.OTHER,
           "Entry '" + userEntry.getDN() + "' does not have exactly one " +
                "value for salt attribute '" + saltAttribute + "'.");
    }

    return saltAttr.getValueByteArray();
  }



  /**
   * Appends an encoded representation of the given password and salt to the
   * provided buffer.
   *
   * @param  password  The clear-text password to be encoded.
   * @param  salt      The bytes to use as the salt.
   * @param  buffer    The buffer to which the encoded representation should be
   *                   appended.
   *
   * @throws  LDAPException  If a problem is encountered while encoding the
   *                         password.
   */
  private void encode(final ByteString password, final byte[] salt,
                      final ByteStringBuffer buffer)
          throws LDAPException
  {
    // Generate an array with the bytes of the password and the salt.
    final byte[] pwBytes = password.getValue();
    final byte[] pwPlusSalt = new byte[pwBytes.length + salt.length];
    System.arraycopy(pwBytes, 0, pwPlusSalt, 0, pwBytes.length);
    System.arraycopy(salt, 0, pwPlusSalt, pwBytes.length, salt.length);


    // Generate a 256-bit SHA-2 digest of the password plus the salt.
    final MessageDigest sha256Digest;
    try
    {
      sha256Digest = MessageDigest.getInstance(DIGEST_NAME_SHA2_256);
    }
    catch (final Exception e)
    {
      serverContext.debugCaught(e);
      throw new LDAPException(ResultCode.OTHER,
           "Unable to obtain a " + DIGEST_NAME_SHA2_256 + " digest:  " +
                StaticUtils.getExceptionMessage(e),
           e);
    }

    final byte[] digestBytes = sha256Digest.digest(pwPlusSalt);


    // Append a base64-encoded representation of the digest bytes to the given
    // buffer.
    Base64.encode(digestBytes, buffer);
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public Map<List<String>,String> getExamplesArgumentSets()
  {
    final LinkedHashMap<List<String>,String> examples =
         new LinkedHashMap<List<String>,String>(1);

    examples.put(
         Arrays.asList(ARG_NAME_SALT_ATTRIBUTE + "=exampleAttr"),
              "Encode passwords using the 256-bit SHA-2 digest, obtaining " +
                   "salt values from the 'exampleAttr' attribute of the user " +
                   "entry.");

    return examples;
  }
}