/* **************************************************************************
 * $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.
     * @throws 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.
     * <p/>
     * 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.
     * <p/>
     * 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
     * @throws SaslException         if incoming cannot be successfully unwrapped.
     * @throws 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.
     * @throws 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.
     *
     * @throws 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.
     * @throws 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
     * @throws 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
     * @throws 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
     * @throws 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
     * @throws 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

