DigestMD5SASLMechanismHandler.java revision 99faa045b6241c1d2843cce1b7a9d9c97055beae
/*
* 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
* 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
*
*
* Portions Copyright 2006-2007 Sun Microsystems, Inc.
*/
/**
* This class provides an implementation of a SASL mechanism that uses digest
* authentication via DIGEST-MD5. This is a password-based mechanism that does
* not expose the password itself over the wire but rather uses an MD5 hash that
* proves the client knows the password. This is similar to the CRAM-MD5
* mechanism, and the primary differences are that CRAM-MD5 only obtains random
* data from the server whereas DIGEST-MD5 uses random data from both the
* server and the client, CRAM-MD5 does not allow for an authorization ID in
* addition to the authentication ID where DIGEST-MD5 does, and CRAM-MD5 does
* not define any integrity and confidentiality mechanisms where DIGEST-MD5
* does. This implementation is based on the specification in RFC 2831 and
* updates from draft-ietf-sasl-rfc2831bis-06.
*/
public class DigestMD5SASLMechanismHandler
implements ConfigurationChangeListener<
{
// The current configuration for this SASL mechanism handler.
// The DN of the configuration entry for this SASL mechanism handler.
private DN configEntryDN;
// The identity mapper that will be used to map ID strings to user entries.
private IdentityMapper identityMapper;
// The message digest engine that will be used to create the MD5 digests.
private MessageDigest md5Digest;
// The lock that will be used to provide threadsafe access to the message
// digest.
private ReentrantLock digestLock;
// The random number generator that we will use to create the nonce.
private SecureRandom randomGenerator;
/**
* Creates a new instance of this SASL mechanism handler. No initialization
* should be done in this method, as it should all be performed in the
* <CODE>initializeSASLMechanismHandler</CODE> method.
*/
public DigestMD5SASLMechanismHandler()
{
super();
}
/**
* {@inheritDoc}
*/
@Override()
public void initializeSASLMechanismHandler(
{
// Initialize the variables needed for the MD5 digest creation.
digestLock = new ReentrantLock();
randomGenerator = new SecureRandom();
try
{
}
catch (Exception e)
{
if (debugEnabled())
{
}
}
// Get the identity mapper that should be used to find users.
if (identityMapper == null)
{
}
this);
}
/**
* {@inheritDoc}
*/
@Override()
public void finalizeSASLMechanismHandler()
{
}
/**
* {@inheritDoc}
*/
@Override()
{
// The DIGEST-MD5 bind process uses two stages. See if the client provided
// any credentials. If not, then this is an initial authentication so we
// will send a challenge to the client.
{
// Create a buffer to hold the challenge.
// Add the realm to the challenge. If we have a configured realm, then
// use it. Otherwise, add a realm for each suffix defined in the server.
{
{
{
}
}
}
else
{
}
// Generate the nonce. Add it to the challenge and remember it for future
// use.
{
}
// Generate the qop-list and add it to the challenge.
// FIXME -- Add support for integrity and confidentiality. Once we do,
// we'll also want to add the maxbuf and cipher options.
// Add the charset option to indicate that we support UTF-8 values.
// Add the algorithm, which will always be "md5-sess".
// Encode the challenge as an ASN.1 element. The total length of the
// encoded value must be less than 2048 bytes, which should not be a
// problem, but we'll add a safety check just in case.... In the event
// that it does happen, we'll also log an error so it is more noticeable.
{
return;
}
// Store the state information with the client connection so we can use it
// for later validation.
// Prepare the response and return so it will be sent to the client.
return;
}
// If we've gotten here, then the client did provide credentials. This can
// be either an initial or subsequent authentication, but they will both be
// handled identically. First, get the stored client SASL state. If it's
// not there, then fail.
if (saslStateInfo == null)
{
return;
}
if (! (saslStateInfo instanceof DigestMD5StateInfo))
{
return;
}
// Create variables to hold values stored in the client's response. We'll
// also store the base DN because we might need to override it later.
int responseNonceCount = -1;
byte[] responseDigest = null;
// Get a temporary string representation of the SASL credentials using the
// ISO-8859-1 encoding and see if it contains "charset=utf-8". If so, then
// re-parse the credentials using that character set.
try
{
}
catch (Exception e)
{
if (debugEnabled())
{
}
// This isn't necessarily fatal because we're going to retry using UTF-8,
// but we want to log it anyway.
}
if ((credString == null) ||
{
try
{
}
catch (Exception e)
{
if (debugEnabled())
{
}
// This is fatal because either we can't parse the credentials as a
// string at all, or we know we need to do so using UTF-8 and can't.
return;
}
}
// Iterate through the credentials string, parsing the property names and
// their corresponding values.
int pos = 0;
{
if (equalPos < 0)
{
// This is bad because we're not at the end of the string but we don't
return;
}
try
{
}
catch (DirectoryException de)
{
// We couldn't parse the token value, so it must be malformed.
de.getErrorMessage());
return;
}
{
// The value must be the string "utf-8". If not, that's an error.
{
return;
}
}
{
}
{
{
{
return;
}
}
}
{
{
// The nonce provided by the client is incorrect. This could be an
// attempt at a replay or chosen plaintext attack, so we'll close the
// connection. We will put a message in the log but will not send it
// to the client.
return;
}
}
{
}
{
try
{
}
catch (Exception e)
{
if (debugEnabled())
{
}
return;
}
int storedNonce;
try
{
}
catch (Exception e)
{
if (debugEnabled())
{
}
return;
}
{
// The nonce count provided by the client is incorrect. This
// indicates a replay attack, so we'll close the connection. We will
// put a message in the log but we will not send it to the client.
return;
}
}
{
{
// No action necessary.
}
{
// FIXME -- Add support for integrity protection.
return;
}
{
// FIXME -- Add support for confidentiality protection.
return;
}
else
{
// This is an invalid QoP value.
return;
}
}
{
// FIXME -- Add the ability to validate this URI, at least to check the
// hostname.
}
{
try
{
}
catch (ParseException pe)
{
if (debugEnabled())
{
}
return;
}
}
{
// FIXME -- This must always be parsed in UTF-8 even if the charset for
// other elements is ISO 8859-1.
}
{
// FIXME -- Add support for confidentiality and integrity protection.
}
else
{
return;
}
}
// Make sure that all required properties have been specified.
{
return;
}
else if (responseNonce == null)
{
return;
}
else if (responseCNonce == null)
{
return;
}
else if (responseNonceCount < 0)
{
return;
}
else if (responseDigestURI == null)
{
return;
}
else if (responseDigest == null)
{
return;
}
// If a realm has not been specified, then use the empty string.
// FIXME -- Should we reject this if a specific realm is defined?
if (responseRealm == null)
{
responseRealm = "";
}
// Get the user entry for the authentication ID. Allow for an
// authentication ID that is just a username (as per the DIGEST-MD5 spec),
// but also allow a value in the authzid form specified in RFC 2829.
{
// Try to decode the user DN and retrieve the corresponding entry.
try
{
}
catch (DirectoryException de)
{
if (debugEnabled())
{
}
de.getErrorMessage());
return;
}
{
return;
}
{
}
// Acquire a read lock on the user entry. If this fails, then so will the
// authentication.
for (int i=0; i < 3; i++)
{
{
break;
}
}
{
return;
}
try
{
}
catch (DirectoryException de)
{
if (debugEnabled())
{
}
de.getErrorMessage());
return;
}
finally
{
}
}
else
{
// Use the identity mapper to resolve the username to an entry.
{
{
return;
}
}
try
{
}
catch (DirectoryException de)
{
if (debugEnabled())
{
}
de.getErrorMessage());
return;
}
}
// At this point, we should have a user entry. If we don't then fail.
{
return;
}
else
{
}
if (responseAuthzID != null)
{
{
// The authorization ID must not be an empty string.
return;
}
{
{
try
{
}
catch (DirectoryException de)
{
if (debugEnabled())
{
}
de.getErrorMessage());
return;
}
if (actualAuthzDN != null)
{
}
{
{
return;
}
{
authZEntry = null;
}
else
{
try
{
if (authZEntry == null)
{
return;
}
}
catch (DirectoryException de)
{
if (debugEnabled())
{
}
de.getErrorMessage());
return;
}
}
}
}
else
{
{
}
else
{
}
{
authZEntry = null;
}
else
{
try
{
if (authZEntry == null)
{
return;
}
}
catch (DirectoryException de)
{
if (debugEnabled())
{
}
de.getErrorMessage());
return;
}
}
if ((authZEntry == null) ||
{
{
return;
}
}
}
}
}
// Get the clear-text passwords from the user entry, if there are any.
try
{
new PasswordPolicyState(userEntry, false, false);
{
return;
}
}
catch (Exception e)
{
return;
}
// Iterate through the clear-text values and see if any of them can be used
// in conjunction with the challenge to construct the provided digest.
boolean matchFound = false;
byte[] passwordBytes = null;
{
byte[] generatedDigest;
try
{
}
catch (Exception e)
{
if (debugEnabled())
{
}
getExceptionMessage(e));
continue;
}
{
matchFound = true;
break;
}
}
if (! matchFound)
{
return;
}
// Generate the response auth element to include in the response to the
// client.
byte[] responseAuth;
try
{
}
catch (Exception e)
{
if (debugEnabled())
{
}
return;
}
// Make sure to store the updated nonce count with the client connection to
// allow for correct subsequent authentication.
// If we've gotten here, then the authentication was successful. We'll also
// need to include the response auth string in the server SASL credentials.
return;
}
/**
* Generates a new nonce value to use during the DIGEST-MD5 authentication
* process.
*
* @return The nonce that should be used for DIGEST-MD5 authentication.
*/
private String generateNonce()
{
byte[] nonceBytes = new byte[16];
digestLock.lock();
try
{
}
finally
{
digestLock.unlock();
}
}
/**
* Reads the next token from the provided credentials string using the
* provided information. If the token is surrounded by quotation marks, then
* the token returned will not include those quotation marks.
*
* @param credentials The credentials string from which to read the token.
* @param startPos The position of the first character of the token to
* read.
* @param length The total number of characters in the credentials
* string.
* @param token The buffer into which the token is to be placed.
*
* @return The position at which the next token should start, or a value
* greater than or equal to the length of the string if there are no
* more tokens.
*
* @throws DirectoryException If a problem occurs while attempting to read
* the token.
*/
throws DirectoryException
{
// If the position is greater than or equal to the length, then we shouldn't
// do anything.
{
return startPos;
}
// Look at the first character to see if it's an empty string or the string
// is quoted.
boolean isEscaped = false;
boolean isQuoted = false;
if (c == ',')
{
// This must be a zero-length token, so we'll just return the next
// position.
return pos;
}
else if (c == '"')
{
// The string is quoted, so we'll ignore this character, and we'll keep
// reading until we find the unescaped closing quote followed by a comma
// or the end of the string.
isQuoted = true;
}
else if (c == '\\')
{
// The next character is escaped, so we'll take it no matter what.
isEscaped = true;
}
else
{
// The string is not quoted, and this is the first character. Store this
// character and keep reading until we find a comma or the end of the
// string.
}
// Enter a loop, reading until we find the appropriate criteria for the end
// of the token.
{
if (isEscaped)
{
// The previous character was an escape, so we'll take this no matter
// what.
isEscaped = false;
}
else if (c == ',')
{
// If this is a quoted string, then this comma is part of the token.
// Otherwise, it's the end of the token.
if (isQuoted)
{
}
else
{
break;
}
}
else if (c == '"')
{
if (isQuoted)
{
// This should be the end of the token, but in order for it to be
// valid it must be followed by a comma or the end of the string.
{
// We have hit the end of the string, so this is fine.
break;
}
else
{
if (c2 == ',')
{
// We have hit the end of the token, so this is fine.
break;
}
else
{
// We found the closing quote before the end of the token. This
// is not fine.
}
}
}
else
{
// This must be part of the value, so we'll take it.
}
}
else if (c == '\\')
{
// The next character is escaped. We'll set a flag so we know to
// accept it, but will not include the backspace itself.
isEscaped = true;
}
else
{
}
}
return pos;
}
/**
* Generates the appropriate DIGEST-MD5 response for the provided set of
* information.
*
* @param userName The username from the authentication request.
* @param authzID The authorization ID from the request, or
* <CODE>null</CODE> if there is none.
* @param password The clear-text password for the user.
* @param realm The realm for which the authentication is to be
* performed.
* @param nonce The random data generated by the server for use in the
* digest.
* @param cnonce The random data generated by the client for use in the
* digest.
* @param nonceCount The 8-digit hex string indicating the number of times
* the provided nonce has been used by the client.
* @param digestURI The digest URI that specifies the service and host for
* which the authentication is being performed.
* @param qop The quality of protection string for the
* authentication.
* @param charset The character set used to encode the information.
*
* @return The DIGEST-MD5 response for the provided set of information.
*
* @throws UnsupportedEncodingException If the specified character set is
* invalid for some reason.
*/
throws UnsupportedEncodingException
{
digestLock.lock();
try
{
// First, get a hash of "username:realm:password".
// Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
{
}
// Next, get a hash of "AUTHENTICATE:digesturi".
// Get hex string representations of the last two hashes.
// Put together the final string to hash, consisting of
// "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
}
finally
{
digestLock.unlock();
}
}
/**
* Generates the appropriate DIGEST-MD5 rspauth digest using the provided
* information.
*
* @param userName The username from the authentication request.
* @param authzID The authorization ID from the request, or
* <CODE>null</CODE> if there is none.
* @param password The clear-text password for the user.
* @param realm The realm for which the authentication is to be
* performed.
* @param nonce The random data generated by the server for use in the
* digest.
* @param cnonce The random data generated by the client for use in the
* digest.
* @param nonceCount The 8-digit hex string indicating the number of times
* the provided nonce has been used by the client.
* @param digestURI The digest URI that specifies the service and host for
* which the authentication is being performed.
* @param qop The quality of protection string for the
* authentication.
* @param charset The character set used to encode the information.
*
* @return The DIGEST-MD5 response for the provided set of information.
*
* @throws UnsupportedEncodingException If the specified character set is
* invalid for some reason.
*/
throws UnsupportedEncodingException
{
digestLock.lock();
try
{
// First, get a hash of "username:realm:password".
// Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
{
}
// Next, get a hash of "AUTHENTICATE:digesturi".
{
a2String += ":00000000000000000000000000000000";
}
// Get hex string representations of the last two hashes.
// Put together the final string to hash, consisting of
// "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
}
finally
{
digestLock.unlock();
}
}
/**
* Retrieves a hexadecimal string representation of the contents of the
* provided byte array.
*
* @param byteArray The byte array for which to obtain the hexadecimal
* string representation.
*
* @return The hexadecimal string representation of the contents of the
* provided byte array.
*/
{
for (byte b : byteArray)
{
}
}
/**
* {@inheritDoc}
*/
@Override()
{
// This is a password-based mechanism.
return true;
}
/**
* {@inheritDoc}
*/
@Override()
{
// This may be considered a secure mechanism.
return true;
}
/**
* {@inheritDoc}
*/
public boolean isConfigurationChangeAcceptable(
{
boolean configAcceptable = true;
// Get the identity mapper that should be used to find users.
if (newIdentityMapper == null)
{
configAcceptable = false;
}
return configAcceptable;
}
/**
* {@inheritDoc}
*/
{
boolean adminActionRequired = false;
// Get the identity mapper that should be used to find users.
if (newIdentityMapper == null)
{
{
}
}
{
}
}
}