diff options
Diffstat (limited to 'src/com/novell/sasl/client/DigestMD5SaslClient.java')
-rw-r--r-- | src/com/novell/sasl/client/DigestMD5SaslClient.java | 820 |
1 files changed, 820 insertions, 0 deletions
diff --git a/src/com/novell/sasl/client/DigestMD5SaslClient.java b/src/com/novell/sasl/client/DigestMD5SaslClient.java new file mode 100644 index 0000000..141c96b --- /dev/null +++ b/src/com/novell/sasl/client/DigestMD5SaslClient.java @@ -0,0 +1,820 @@ +/* ************************************************************************** + * $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=\""); + digestResponse.append(m_digestURI); + 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 + |