/* **************************************************************************
* $OpenLDAP: /com/novell/sasl/client/DigestMD5SaslClient.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $
*
* Copyright (C) 2003 Novell, Inc. All Rights Reserved.
*
* THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
* TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
* TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
* AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
* IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
* OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
* PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
* THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
******************************************************************************/
package com.novell.sasl.client;
import org.apache.harmony.javax.security.sasl.*;
import org.apache.harmony.javax.security.auth.callback.*;
import java.security.SecureRandom;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.io.UnsupportedEncodingException;
import java.io.IOException;
import java.util.*;
/**
* Implements the Client portion of DigestMD5 Sasl mechanism.
*/
public class DigestMD5SaslClient implements SaslClient
{
private String m_authorizationId = "";
private String m_protocol = "";
private String m_serverName = "";
private Map m_props;
private CallbackHandler m_cbh;
private int m_state;
private String m_qopValue = "";
private char[] m_HA1 = null;
private String m_digestURI;
private DigestChallenge m_dc;
private String m_clientNonce = "";
private String m_realm = "";
private String m_name = "";
private static final int STATE_INITIAL = 0;
private static final int STATE_DIGEST_RESPONSE_SENT = 1;
private static final int STATE_VALID_SERVER_RESPONSE = 2;
private static final int STATE_INVALID_SERVER_RESPONSE = 3;
private static final int STATE_DISPOSED = 4;
private static final int NONCE_BYTE_COUNT = 32;
private static final int NONCE_HEX_COUNT = 2*NONCE_BYTE_COUNT;
private static final String DIGEST_METHOD = "AUTHENTICATE";
/**
* Creates an DigestMD5SaslClient object using the parameters supplied.
* Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are
* contained in props
*
* @param authorizationId The possibly null protocol-dependent
* identification to be used for authorization. If
* null or empty, the server derives an authorization
* ID from the client's authentication credentials.
* When the SASL authentication completes
* successfully, the specified entity is granted
* access.
*
* @param protocol The non-null string name of the protocol for which
* the authentication is being performed (e.g. "ldap")
*
* @param serverName The non-null fully qualified host name of the server
* to authenticate to
*
* @param props The possibly null set of properties used to select
* the SASL mechanism and to configure the
* authentication exchange of the selected mechanism.
* See the Sasl class for a list of standard properties.
* Other, possibly mechanism-specific, properties can
* be included. Properties not relevant to the selected
* mechanism are ignored.
*
* @param cbh The possibly null callback handler to used by the
* SASL mechanisms to get further information from the
* application/library to complete the authentication.
* For example, a SASL mechanism might require the
* authentication ID, password and realm from the
* caller. The authentication ID is requested by using
* a NameCallback. The password is requested by using
* a PasswordCallback. The realm is requested by using
* a RealmChoiceCallback if there is a list of realms
* to choose from, and by using a RealmCallback if the
* realm must be entered.
*
* @return A possibly null SaslClient created using the
* parameters supplied. If null, this factory cannot
* produce a SaslClient using the parameters supplied.
*
* @exception SaslException If a SaslClient instance cannot be created
* because of an error
*/
public static SaslClient getClient(
String authorizationId,
String protocol,
String serverName,
Map props,
CallbackHandler cbh)
{
String desiredQOP = (String)props.get(Sasl.QOP);
String desiredStrength = (String)props.get(Sasl.STRENGTH);
String serverAuth = (String)props.get(Sasl.SERVER_AUTH);
//only support qop equal to auth
if ((desiredQOP != null) && !"auth".equals(desiredQOP))
return null;
//doesn't support server authentication
if ((serverAuth != null) && !"false".equals(serverAuth))
return null;
//need a callback handler to get the password
if (cbh == null)
return null;
return new DigestMD5SaslClient(authorizationId, protocol,
serverName, props, cbh);
}
/**
* Creates an DigestMD5SaslClient object using the parameters supplied.
* Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are
* contained in props
*
* @param authorizationId The possibly null protocol-dependent
* identification to be used for authorization. If
* null or empty, the server derives an authorization
* ID from the client's authentication credentials.
* When the SASL authentication completes
* successfully, the specified entity is granted
* access.
*
* @param protocol The non-null string name of the protocol for which
* the authentication is being performed (e.g. "ldap")
*
* @param serverName The non-null fully qualified host name of the server
* to authenticate to
*
* @param props The possibly null set of properties used to select
* the SASL mechanism and to configure the
* authentication exchange of the selected mechanism.
* See the Sasl class for a list of standard properties.
* Other, possibly mechanism-specific, properties can
* be included. Properties not relevant to the selected
* mechanism are ignored.
*
* @param cbh The possibly null callback handler to used by the
* SASL mechanisms to get further information from the
* application/library to complete the authentication.
* For example, a SASL mechanism might require the
* authentication ID, password and realm from the
* caller. The authentication ID is requested by using
* a NameCallback. The password is requested by using
* a PasswordCallback. The realm is requested by using
* a RealmChoiceCallback if there is a list of realms
* to choose from, and by using a RealmCallback if the
* realm must be entered.
*
*/
private DigestMD5SaslClient(
String authorizationId,
String protocol,
String serverName,
Map props,
CallbackHandler cbh)
{
m_authorizationId = authorizationId;
m_protocol = protocol;
m_serverName = serverName;
m_props = props;
m_cbh = cbh;
m_state = STATE_INITIAL;
}
/**
* Determines if this mechanism has an optional initial response. If true,
* caller should call evaluateChallenge() with an empty array to get the
* initial response.
*
* @return true if this mechanism has an initial response
*/
public boolean hasInitialResponse()
{
return false;
}
/**
* Determines if the authentication exchange has completed. This method
* may be called at any time, but typically, it will not be called until
* the caller has received indication from the server (in a protocol-
* specific manner) that the exchange has completed.
*
* @return true if the authentication exchange has completed;
* false otherwise.
*/
public boolean isComplete()
{
if ((m_state == STATE_VALID_SERVER_RESPONSE) ||
(m_state == STATE_INVALID_SERVER_RESPONSE) ||
(m_state == STATE_DISPOSED))
return true;
else
return false;
}
/**
* Unwraps a byte array received from the server. This method can be called
* only after the authentication exchange has completed (i.e., when
* isComplete() returns true) and only if the authentication exchange has
* negotiated integrity and/or privacy as the quality of protection;
* otherwise, an IllegalStateException is thrown.
*
* incoming is the contents of the SASL buffer as defined in RFC 2222
* without the leading four octet field that represents the length.
* offset and len specify the portion of incoming to use.
*
* @param incoming A non-null byte array containing the encoded bytes
* from the server
* @param offset The starting position at incoming of the bytes to use
*
* @param len The number of bytes from incoming to use
*
* @return A non-null byte array containing the decoded bytes
*
*/
public byte[] unwrap(
byte[] incoming,
int offset,
int len)
throws SaslException
{
throw new IllegalStateException(
"unwrap: QOP has neither integrity nor privacy>");
}
/**
* Wraps a byte array to be sent to the server. This method can be called
* only after the authentication exchange has completed (i.e., when
* isComplete() returns true) and only if the authentication exchange has
* negotiated integrity and/or privacy as the quality of protection;
* otherwise, an IllegalStateException is thrown.
*
* The result of this method will make up the contents of the SASL buffer as
* defined in RFC 2222 without the leading four octet field that represents
* the length. offset and len specify the portion of outgoing to use.
*
* @param outgoing A non-null byte array containing the bytes to encode
* @param offset The starting position at outgoing of the bytes to use
* @param len The number of bytes from outgoing to use
*
* @return A non-null byte array containing the encoded bytes
*
* @exception SaslException if incoming cannot be successfully unwrapped.
*
* @exception IllegalStateException if the authentication exchange has
* not completed, or if the negotiated quality of
* protection has neither integrity nor privacy.
*/
public byte[] wrap(
byte[] outgoing,
int offset,
int len)
throws SaslException
{
throw new IllegalStateException(
"wrap: QOP has neither integrity nor privacy>");
}
/**
* Retrieves the negotiated property. This method can be called only after
* the authentication exchange has completed (i.e., when isComplete()
* returns true); otherwise, an IllegalStateException is thrown.
*
* @param propName The non-null property name
*
* @return The value of the negotiated property. If null, the property was
* not negotiated or is not applicable to this mechanism.
*
* @exception IllegalStateException if this authentication exchange has
* not completed
*/
public Object getNegotiatedProperty(
String propName)
{
if (m_state != STATE_VALID_SERVER_RESPONSE)
throw new IllegalStateException(
"getNegotiatedProperty: authentication exchange not complete.");
if (Sasl.QOP.equals(propName))
return "auth";
else
return null;
}
/**
* Disposes of any system resources or security-sensitive information the
* SaslClient might be using. Invoking this method invalidates the
* SaslClient instance. This method is idempotent.
*
* @exception SaslException if a problem was encountered while disposing
* of the resources
*/
public void dispose()
throws SaslException
{
if (m_state != STATE_DISPOSED)
{
m_state = STATE_DISPOSED;
}
}
/**
* Evaluates the challenge data and generates a response. If a challenge
* is received from the server during the authentication process, this
* method is called to prepare an appropriate next response to submit to
* the server.
*
* @param challenge The non-null challenge sent from the server. The
* challenge array may have zero length.
*
* @return The possibly null reponse to send to the server. It is null
* if the challenge accompanied a "SUCCESS" status and the
* challenge only contains data for the client to update its
* state and no response needs to be sent to the server.
* The response is a zero-length byte array if the client is to
* send a response with no data.
*
* @exception SaslException If an error occurred while processing the
* challenge or generating a response.
*/
public byte[] evaluateChallenge(
byte[] challenge)
throws SaslException
{
byte[] response = null;
//printState();
switch (m_state)
{
case STATE_INITIAL:
if (challenge.length == 0)
throw new SaslException("response = byte[0]");
else
try
{
response = createDigestResponse(challenge).
getBytes("UTF-8");
m_state = STATE_DIGEST_RESPONSE_SENT;
}
catch (java.io.UnsupportedEncodingException e)
{
throw new SaslException(
"UTF-8 encoding not suppported by platform", e);
}
break;
case STATE_DIGEST_RESPONSE_SENT:
if (checkServerResponseAuth(challenge))
m_state = STATE_VALID_SERVER_RESPONSE;
else
{
m_state = STATE_INVALID_SERVER_RESPONSE;
throw new SaslException("Could not validate response-auth " +
"value from server");
}
break;
case STATE_VALID_SERVER_RESPONSE:
case STATE_INVALID_SERVER_RESPONSE:
throw new SaslException("Authentication sequence is complete");
case STATE_DISPOSED:
throw new SaslException("Client has been disposed");
default:
throw new SaslException("Unknown client state.");
}
return response;
}
/**
* This function takes a 16 byte binary md5-hash value and creates a 32
* character (plus a terminating null character) hex-digit
* representation of binary data.
*
* @param hash 16 byte binary md5-hash value in bytes
*
* @return 32 character (plus a terminating null character) hex-digit
* representation of binary data.
*/
char[] convertToHex(
byte[] hash)
{
int i;
byte j;
byte fifteen = 15;
char[] hex = new char[32];
for (i = 0; i < 16; i++)
{
//convert value of top 4 bits to hex char
hex[i*2] = getHexChar((byte)((hash[i] & 0xf0) >> 4));
//convert value of bottom 4 bits to hex char
hex[(i*2)+1] = getHexChar((byte)(hash[i] & 0x0f));
}
return hex;
}
/**
* Calculates the HA1 portion of the response
*
* @param algorithm Algorith to use.
* @param userName User being authenticated
* @param realm realm information
* @param password password of teh user
* @param nonce nonce value
* @param clientNonce Clients Nonce value
*
* @return HA1 portion of the response in a character array
*
* @exception SaslException If an error occurs
*/
char[] DigestCalcHA1(
String algorithm,
String userName,
String realm,
String password,
String nonce,
String clientNonce) throws SaslException
{
byte[] hash;
try
{
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(userName.getBytes("UTF-8"));
md.update(":".getBytes("UTF-8"));
md.update(realm.getBytes("UTF-8"));
md.update(":".getBytes("UTF-8"));
md.update(password.getBytes("UTF-8"));
hash = md.digest();
if ("md5-sess".equals(algorithm))
{
md.update(hash);
md.update(":".getBytes("UTF-8"));
md.update(nonce.getBytes("UTF-8"));
md.update(":".getBytes("UTF-8"));
md.update(clientNonce.getBytes("UTF-8"));
hash = md.digest();
}
}
catch(NoSuchAlgorithmException e)
{
throw new SaslException("No provider found for MD5 hash", e);
}
catch(UnsupportedEncodingException e)
{
throw new SaslException(
"UTF-8 encoding not supported by platform.", e);
}
return convertToHex(hash);
}
/**
* This function calculates the response-value of the response directive of
* the digest-response as documented in RFC 2831
*
* @param HA1 H(A1)
* @param serverNonce nonce from server
* @param nonceCount 8 hex digits
* @param clientNonce client nonce
* @param qop qop-value: "", "auth", "auth-int"
* @param method method from the request
* @param digestUri requested URL
* @param clientResponseFlag request-digest or response-digest
*
* @return Response-value of the response directive of the digest-response
*
* @exception SaslException If an error occurs
*/
char[] DigestCalcResponse(
char[] HA1, /* H(A1) */
String serverNonce, /* nonce from server */
String nonceCount, /* 8 hex digits */
String clientNonce, /* client nonce */
String qop, /* qop-value: "", "auth", "auth-int" */
String method, /* method from the request */
String digestUri, /* requested URL */
boolean clientResponseFlag) /* request-digest or response-digest */
throws SaslException
{
byte[] HA2;
byte[] respHash;
char[] HA2Hex;
// calculate H(A2)
try
{
MessageDigest md = MessageDigest.getInstance("MD5");
if (clientResponseFlag)
md.update(method.getBytes("UTF-8"));
md.update(":".getBytes("UTF-8"));
md.update(digestUri.getBytes("UTF-8"));
if ("auth-int".equals(qop))
{
md.update(":".getBytes("UTF-8"));
md.update("00000000000000000000000000000000".getBytes("UTF-8"));
}
HA2 = md.digest();
HA2Hex = convertToHex(HA2);
// calculate response
md.update(new String(HA1).getBytes("UTF-8"));
md.update(":".getBytes("UTF-8"));
md.update(serverNonce.getBytes("UTF-8"));
md.update(":".getBytes("UTF-8"));
if (qop.length() > 0)
{
md.update(nonceCount.getBytes("UTF-8"));
md.update(":".getBytes("UTF-8"));
md.update(clientNonce.getBytes("UTF-8"));
md.update(":".getBytes("UTF-8"));
md.update(qop.getBytes("UTF-8"));
md.update(":".getBytes("UTF-8"));
}
md.update(new String(HA2Hex).getBytes("UTF-8"));
respHash = md.digest();
}
catch(NoSuchAlgorithmException e)
{
throw new SaslException("No provider found for MD5 hash", e);
}
catch(UnsupportedEncodingException e)
{
throw new SaslException(
"UTF-8 encoding not supported by platform.", e);
}
return convertToHex(respHash);
}
/**
* Creates the intial response to be sent to the server.
*
* @param challenge Challenge in bytes recived form the Server
*
* @return Initial response to be sent to the server
*/
private String createDigestResponse(
byte[] challenge)
throws SaslException
{
char[] response;
StringBuffer digestResponse = new StringBuffer(512);
int realmSize;
m_dc = new DigestChallenge(challenge);
m_digestURI = m_protocol + "/" + m_serverName;
if ((m_dc.getQop() & DigestChallenge.QOP_AUTH)
== DigestChallenge.QOP_AUTH )
m_qopValue = "auth";
else
throw new SaslException("Client only supports qop of 'auth'");
//get call back information
Callback[] callbacks = new Callback[3];
ArrayList realms = m_dc.getRealms();
realmSize = realms.size();
if (realmSize == 0)
{
callbacks[0] = new RealmCallback("Realm");
}
else if (realmSize == 1)
{
callbacks[0] = new RealmCallback("Realm", (String)realms.get(0));
}
else
{
callbacks[0] =
new RealmChoiceCallback(
"Realm",
(String[])realms.toArray(new String[realmSize]),
0, //the default choice index
false); //no multiple selections
}
callbacks[1] = new PasswordCallback("Password", false);
//false = no echo
if (m_authorizationId == null || m_authorizationId.length() == 0)
callbacks[2] = new NameCallback("Name");
else
callbacks[2] = new NameCallback("Name", m_authorizationId);
try
{
m_cbh.handle(callbacks);
}
catch(UnsupportedCallbackException e)
{
throw new SaslException("Handler does not support" +
" necessary callbacks",e);
}
catch(IOException e)
{
throw new SaslException("IO exception in CallbackHandler.", e);
}
if (realmSize > 1)
{
int[] selections =
((RealmChoiceCallback)callbacks[0]).getSelectedIndexes();
if (selections.length > 0)
m_realm =
((RealmChoiceCallback)callbacks[0]).getChoices()[selections[0]];
else
m_realm = ((RealmChoiceCallback)callbacks[0]).getChoices()[0];
}
else
m_realm = ((RealmCallback)callbacks[0]).getText();
m_clientNonce = getClientNonce();
m_name = ((NameCallback)callbacks[2]).getName();
if (m_name == null)
m_name = ((NameCallback)callbacks[2]).getDefaultName();
if (m_name == null)
throw new SaslException("No user name was specified.");
m_HA1 = DigestCalcHA1(
m_dc.getAlgorithm(),
m_name,
m_realm,
new String(((PasswordCallback)callbacks[1]).getPassword()),
m_dc.getNonce(),
m_clientNonce);
response = DigestCalcResponse(m_HA1,
m_dc.getNonce(),
"00000001",
m_clientNonce,
m_qopValue,
"AUTHENTICATE",
m_digestURI,
true);
digestResponse.append("username=\"");
digestResponse.append(m_authorizationId);
if (0 != m_realm.length())
{
digestResponse.append("\",realm=\"");
digestResponse.append(m_realm);
}
digestResponse.append("\",cnonce=\"");
digestResponse.append(m_clientNonce);
digestResponse.append("\",nc=");
digestResponse.append("00000001"); //nounce count
digestResponse.append(",qop=");
digestResponse.append(m_qopValue);
digestResponse.append(",digest-uri=\"ldap/");
digestResponse.append(m_serverName);
digestResponse.append("\",response=");
digestResponse.append(response);
digestResponse.append(",charset=utf-8,nonce=\"");
digestResponse.append(m_dc.getNonce());
digestResponse.append("\"");
return digestResponse.toString();
}
/**
* This function validates the server response. This step performs a
* modicum of mutual authentication by verifying that the server knows
* the user's password
*
* @param serverResponse Response recived form Server
*
* @return true if the mutual authentication succeeds;
* else return false
*
* @exception SaslException If an error occurs
*/
boolean checkServerResponseAuth(
byte[] serverResponse) throws SaslException
{
char[] response;
ResponseAuth responseAuth = null;
String responseStr;
responseAuth = new ResponseAuth(serverResponse);
response = DigestCalcResponse(m_HA1,
m_dc.getNonce(),
"00000001",
m_clientNonce,
m_qopValue,
DIGEST_METHOD,
m_digestURI,
false);
responseStr = new String(response);
return responseStr.equals(responseAuth.getResponseValue());
}
/**
* This function returns hex character representing the value of the input
*
* @param value Input value in byte
*
* @return Hex value of the Input byte value
*/
private static char getHexChar(
byte value)
{
switch (value)
{
case 0:
return '0';
case 1:
return '1';
case 2:
return '2';
case 3:
return '3';
case 4:
return '4';
case 5:
return '5';
case 6:
return '6';
case 7:
return '7';
case 8:
return '8';
case 9:
return '9';
case 10:
return 'a';
case 11:
return 'b';
case 12:
return 'c';
case 13:
return 'd';
case 14:
return 'e';
case 15:
return 'f';
default:
return 'Z';
}
}
/**
* Calculates the Nonce value of the Client
*
* @return Nonce value of the client
*
* @exception SaslException If an error Occurs
*/
String getClientNonce() throws SaslException
{
byte[] nonceBytes = new byte[NONCE_BYTE_COUNT];
SecureRandom prng;
byte nonceByte;
char[] hexNonce = new char[NONCE_HEX_COUNT];
try
{
prng = SecureRandom.getInstance("SHA1PRNG");
prng.nextBytes(nonceBytes);
for(int i=0; i<NONCE_BYTE_COUNT; i++)
{
//low nibble
hexNonce[i*2] = getHexChar((byte)(nonceBytes[i] & 0x0f));
//high nibble
hexNonce[(i*2)+1] = getHexChar((byte)((nonceBytes[i] & 0xf0)
>> 4));
}
return new String(hexNonce);
}
catch(NoSuchAlgorithmException e)
{
throw new SaslException("No random number generator available", e);
}
}
/**
* Returns the IANA-registered mechanism name of this SASL client.
* (e.g. "CRAM-MD5", "GSSAPI")
*
* @return "DIGEST-MD5"the IANA-registered mechanism name of this SASL
* client.
*/
public String getMechanismName()
{
return "DIGEST-MD5";
}
} //end class DigestMD5SaslClient