/* * 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-2023 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; } }