/*
* 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
*
*
* Copyright 2013-2016 UnboundID Corp.
*/
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 SHA-1 digest, but rather than
* dynamically generating the salt. As such, access to the full 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_SHA1 = "SHA-1";
// 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 SHA-1 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 "ESSHA1", for "externally-salted SHA-1".
return "ESSHA1";
}
/**
* 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 SHA-1 digest of the password plus the salt.
final MessageDigest sha1Digest;
try
{
sha1Digest = MessageDigest.getInstance(DIGEST_NAME_SHA1);
}
catch (final Exception e)
{
serverContext.debugCaught(e);
throw new LDAPException(ResultCode.OTHER,
"Unable to obtain a " + DIGEST_NAME_SHA1 + " digest: " +
StaticUtils.getExceptionMessage(e),
e);
}
final byte[] digestBytes = sha1Digest.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 SHA-1 digest, obtaining salt " +
"values from the 'exampleAttr' attribute of the user " +
"entry.");
return examples;
}
}
|