1/* **************************************************************************
2 * $OpenLDAP: /com/novell/sasl/client/DigestMD5SaslClient.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $
3 *
4 * Copyright (C) 2003 Novell, Inc. All Rights Reserved.
5 *
6 * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
7 * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
8 * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
9 * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
10 * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
11 * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
12 * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
13 * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
14 ******************************************************************************/
15package com.novell.sasl.client;
16
17import org.apache.harmony.javax.security.sasl.*;
18import org.apache.harmony.javax.security.auth.callback.*;
19import java.security.SecureRandom;
20import java.security.MessageDigest;
21import java.security.NoSuchAlgorithmException;
22import java.io.UnsupportedEncodingException;
23import java.io.IOException;
24import java.util.*;
25
26/**
27 * Implements the Client portion of DigestMD5 Sasl mechanism.
28 */
29public class DigestMD5SaslClient implements SaslClient
30{
31    private String           m_authorizationId = "";
32    private String           m_protocol = "";
33    private String           m_serverName = "";
34    private Map              m_props;
35    private CallbackHandler  m_cbh;
36    private int              m_state;
37    private String           m_qopValue = "";
38    private char[]              m_HA1 = null;
39    private String           m_digestURI;
40    private DigestChallenge  m_dc;
41    private String           m_clientNonce = "";
42    private String           m_realm = "";
43    private String           m_name = "";
44
45    private static final int   STATE_INITIAL = 0;
46    private static final int   STATE_DIGEST_RESPONSE_SENT = 1;
47    private static final int   STATE_VALID_SERVER_RESPONSE = 2;
48    private static final int   STATE_INVALID_SERVER_RESPONSE = 3;
49    private static final int   STATE_DISPOSED = 4;
50
51    private static final int   NONCE_BYTE_COUNT = 32;
52    private static final int   NONCE_HEX_COUNT = 2*NONCE_BYTE_COUNT;
53
54    private static final String DIGEST_METHOD = "AUTHENTICATE";
55
56    /**
57     * Creates an DigestMD5SaslClient object using the parameters supplied.
58     * Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are
59     * contained in props
60     *
61     * @param authorizationId  The possibly null protocol-dependent
62     *                     identification to be used for authorization. If
63     *                     null or empty, the server derives an authorization
64     *                     ID from the client's authentication credentials.
65     *                     When the SASL authentication completes
66     *                     successfully, the specified entity is granted
67     *                     access.
68     *
69     * @param protocol     The non-null string name of the protocol for which
70     *                     the authentication is being performed (e.g. "ldap")
71     *
72     * @param serverName   The non-null fully qualified host name of the server
73     *                     to authenticate to
74     *
75     * @param props        The possibly null set of properties used to select
76     *                     the SASL mechanism and to configure the
77     *                     authentication exchange of the selected mechanism.
78     *                     See the Sasl class for a list of standard properties.
79     *                     Other, possibly mechanism-specific, properties can
80     *                     be included. Properties not relevant to the selected
81     *                     mechanism are ignored.
82     *
83     * @param cbh          The possibly null callback handler to used by the
84     *                     SASL mechanisms to get further information from the
85     *                     application/library to complete the authentication.
86     *                     For example, a SASL mechanism might require the
87     *                     authentication ID, password and realm from the
88     *                     caller. The authentication ID is requested by using
89     *                     a NameCallback. The password is requested by using
90     *                     a PasswordCallback. The realm is requested by using
91     *                     a RealmChoiceCallback if there is a list of realms
92     *                     to choose from, and by using a RealmCallback if the
93     *                     realm must be entered.
94     *
95     * @return            A possibly null SaslClient created using the
96     *                     parameters supplied. If null, this factory cannot
97     *                     produce a SaslClient using the parameters supplied.
98     *
99     * @exception SaslException  If a SaslClient instance cannot be created
100     *                     because of an error
101     */
102    public static SaslClient getClient(
103        String          authorizationId,
104        String          protocol,
105        String          serverName,
106        Map             props,
107        CallbackHandler cbh)
108    {
109        String desiredQOP = (String)props.get(Sasl.QOP);
110        String desiredStrength = (String)props.get(Sasl.STRENGTH);
111        String serverAuth = (String)props.get(Sasl.SERVER_AUTH);
112
113        //only support qop equal to auth
114        if ((desiredQOP != null) && !"auth".equals(desiredQOP))
115            return null;
116
117        //doesn't support server authentication
118        if ((serverAuth != null) && !"false".equals(serverAuth))
119            return null;
120
121        //need a callback handler to get the password
122        if (cbh == null)
123            return null;
124
125        return new DigestMD5SaslClient(authorizationId, protocol,
126                                       serverName, props, cbh);
127    }
128
129    /**
130     * Creates an DigestMD5SaslClient object using the parameters supplied.
131     * Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are
132     * contained in props
133     *
134     * @param authorizationId  The possibly null protocol-dependent
135     *                     identification to be used for authorization. If
136     *                     null or empty, the server derives an authorization
137     *                     ID from the client's authentication credentials.
138     *                     When the SASL authentication completes
139     *                     successfully, the specified entity is granted
140     *                     access.
141     *
142     * @param protocol     The non-null string name of the protocol for which
143     *                     the authentication is being performed (e.g. "ldap")
144     *
145     * @param serverName   The non-null fully qualified host name of the server
146     *                     to authenticate to
147     *
148     * @param props        The possibly null set of properties used to select
149     *                     the SASL mechanism and to configure the
150     *                     authentication exchange of the selected mechanism.
151     *                     See the Sasl class for a list of standard properties.
152     *                     Other, possibly mechanism-specific, properties can
153     *                     be included. Properties not relevant to the selected
154     *                     mechanism are ignored.
155     *
156     * @param cbh          The possibly null callback handler to used by the
157     *                     SASL mechanisms to get further information from the
158     *                     application/library to complete the authentication.
159     *                     For example, a SASL mechanism might require the
160     *                     authentication ID, password and realm from the
161     *                     caller. The authentication ID is requested by using
162     *                     a NameCallback. The password is requested by using
163     *                     a PasswordCallback. The realm is requested by using
164     *                     a RealmChoiceCallback if there is a list of realms
165     *                     to choose from, and by using a RealmCallback if the
166     *                     realm must be entered.
167     *
168     */
169    private  DigestMD5SaslClient(
170        String          authorizationId,
171        String          protocol,
172        String          serverName,
173        Map             props,
174        CallbackHandler cbh)
175    {
176        m_authorizationId = authorizationId;
177        m_protocol = protocol;
178        m_serverName = serverName;
179        m_props = props;
180        m_cbh = cbh;
181
182        m_state = STATE_INITIAL;
183    }
184
185    /**
186     * Determines if this mechanism has an optional initial response. If true,
187     * caller should call evaluateChallenge() with an empty array to get the
188     * initial response.
189     *
190     * @return  true if this mechanism has an initial response
191     */
192    public boolean hasInitialResponse()
193    {
194        return false;
195    }
196
197    /**
198     * Determines if the authentication exchange has completed. This method
199     * may be called at any time, but typically, it will not be called until
200     * the caller has received indication from the server (in a protocol-
201     * specific manner) that the exchange has completed.
202     *
203     * @return  true if the authentication exchange has completed;
204     *           false otherwise.
205     */
206    public boolean isComplete()
207    {
208        if ((m_state == STATE_VALID_SERVER_RESPONSE) ||
209            (m_state == STATE_INVALID_SERVER_RESPONSE) ||
210            (m_state == STATE_DISPOSED))
211            return true;
212        else
213            return false;
214    }
215
216    /**
217     * Unwraps a byte array received from the server. This method can be called
218     * only after the authentication exchange has completed (i.e., when
219     * isComplete() returns true) and only if the authentication exchange has
220     * negotiated integrity and/or privacy as the quality of protection;
221     * otherwise, an IllegalStateException is thrown.
222     *
223     * incoming is the contents of the SASL buffer as defined in RFC 2222
224     * without the leading four octet field that represents the length.
225     * offset and len specify the portion of incoming to use.
226     *
227     * @param incoming   A non-null byte array containing the encoded bytes
228     *                   from the server
229     * @param offset     The starting position at incoming of the bytes to use
230     *
231     * @param len        The number of bytes from incoming to use
232     *
233     * @return           A non-null byte array containing the decoded bytes
234     *
235     */
236    public byte[] unwrap(
237        byte[] incoming,
238        int    offset,
239        int    len)
240            throws SaslException
241    {
242        throw new IllegalStateException(
243         "unwrap: QOP has neither integrity nor privacy>");
244    }
245
246    /**
247     * Wraps a byte array to be sent to the server. This method can be called
248     * only after the authentication exchange has completed (i.e., when
249     * isComplete() returns true) and only if the authentication exchange has
250     * negotiated integrity and/or privacy as the quality of protection;
251     * otherwise, an IllegalStateException is thrown.
252     *
253     * The result of this method will make up the contents of the SASL buffer as
254     * defined in RFC 2222 without the leading four octet field that represents
255     * the length. offset and len specify the portion of outgoing to use.
256     *
257     * @param outgoing   A non-null byte array containing the bytes to encode
258     * @param offset     The starting position at outgoing of the bytes to use
259     * @param len        The number of bytes from outgoing to use
260     *
261     * @return A non-null byte array containing the encoded bytes
262     *
263     * @exception SaslException  if incoming cannot be successfully unwrapped.
264     *
265     * @exception IllegalStateException   if the authentication exchange has
266     *                   not completed, or if the negotiated quality of
267     *                   protection has neither integrity nor privacy.
268     */
269    public byte[] wrap(
270        byte[]  outgoing,
271        int     offset,
272        int     len)
273            throws SaslException
274    {
275        throw new IllegalStateException(
276         "wrap: QOP has neither integrity nor privacy>");
277    }
278
279    /**
280     * Retrieves the negotiated property. This method can be called only after
281     * the authentication exchange has completed (i.e., when isComplete()
282     * returns true); otherwise, an IllegalStateException is thrown.
283     *
284     * @param propName   The non-null property name
285     *
286     * @return  The value of the negotiated property. If null, the property was
287     *          not negotiated or is not applicable to this mechanism.
288     *
289     * @exception IllegalStateException   if this authentication exchange has
290     *                                    not completed
291     */
292    public Object getNegotiatedProperty(
293        String propName)
294    {
295        if (m_state != STATE_VALID_SERVER_RESPONSE)
296            throw new IllegalStateException(
297             "getNegotiatedProperty: authentication exchange not complete.");
298
299        if (Sasl.QOP.equals(propName))
300            return "auth";
301        else
302            return null;
303    }
304
305    /**
306     * Disposes of any system resources or security-sensitive information the
307     * SaslClient might be using. Invoking this method invalidates the
308     * SaslClient instance. This method is idempotent.
309     *
310     * @exception SaslException  if a problem was encountered while disposing
311     *                           of the resources
312     */
313    public void dispose()
314            throws SaslException
315    {
316        if (m_state != STATE_DISPOSED)
317        {
318            m_state = STATE_DISPOSED;
319        }
320    }
321
322    /**
323     * Evaluates the challenge data and generates a response. If a challenge
324     * is received from the server during the authentication process, this
325     * method is called to prepare an appropriate next response to submit to
326     * the server.
327     *
328     * @param challenge  The non-null challenge sent from the server. The
329     *                   challenge array may have zero length.
330     *
331     * @return    The possibly null reponse to send to the server. It is null
332     *            if the challenge accompanied a "SUCCESS" status and the
333     *            challenge only contains data for the client to update its
334     *            state and no response needs to be sent to the server.
335     *            The response is a zero-length byte array if the client is to
336     *            send a response with no data.
337     *
338     * @exception SaslException   If an error occurred while processing the
339     *                            challenge or generating a response.
340     */
341    public byte[] evaluateChallenge(
342        byte[] challenge)
343            throws SaslException
344    {
345        byte[] response = null;
346
347        //printState();
348        switch (m_state)
349        {
350        case STATE_INITIAL:
351            if (challenge.length == 0)
352                throw new SaslException("response = byte[0]");
353            else
354                try
355                {
356                    response = createDigestResponse(challenge).
357                                                           getBytes("UTF-8");
358                    m_state = STATE_DIGEST_RESPONSE_SENT;
359                }
360                catch (java.io.UnsupportedEncodingException e)
361                {
362                    throw new SaslException(
363                     "UTF-8 encoding not suppported by platform", e);
364                }
365            break;
366        case STATE_DIGEST_RESPONSE_SENT:
367            if (checkServerResponseAuth(challenge))
368                m_state = STATE_VALID_SERVER_RESPONSE;
369            else
370            {
371                m_state = STATE_INVALID_SERVER_RESPONSE;
372                throw new SaslException("Could not validate response-auth " +
373                                        "value from server");
374            }
375            break;
376        case STATE_VALID_SERVER_RESPONSE:
377        case STATE_INVALID_SERVER_RESPONSE:
378            throw new SaslException("Authentication sequence is complete");
379        case STATE_DISPOSED:
380            throw new SaslException("Client has been disposed");
381        default:
382            throw new SaslException("Unknown client state.");
383        }
384
385        return response;
386    }
387
388    /**
389     * This function takes a 16 byte binary md5-hash value and creates a 32
390     * character (plus    a terminating null character) hex-digit
391     * representation of binary data.
392     *
393     * @param hash  16 byte binary md5-hash value in bytes
394     *
395     * @return   32 character (plus    a terminating null character) hex-digit
396     *           representation of binary data.
397     */
398    char[] convertToHex(
399        byte[] hash)
400    {
401        int          i;
402        byte         j;
403        byte         fifteen = 15;
404        char[]      hex = new char[32];
405
406        for (i = 0; i < 16; i++)
407        {
408            //convert value of top 4 bits to hex char
409            hex[i*2] = getHexChar((byte)((hash[i] & 0xf0) >> 4));
410            //convert value of bottom 4 bits to hex char
411            hex[(i*2)+1] = getHexChar((byte)(hash[i] & 0x0f));
412        }
413
414        return hex;
415    }
416
417    /**
418     * Calculates the HA1 portion of the response
419     *
420     * @param  algorithm   Algorith to use.
421     * @param  userName    User being authenticated
422     * @param  realm       realm information
423     * @param  password    password of teh user
424     * @param  nonce       nonce value
425     * @param  clientNonce Clients Nonce value
426     *
427     * @return  HA1 portion of the response in a character array
428     *
429     * @exception SaslException  If an error occurs
430     */
431    char[] DigestCalcHA1(
432        String   algorithm,
433        String   userName,
434        String   realm,
435        String   password,
436        String   nonce,
437        String   clientNonce) throws SaslException
438    {
439        byte[]        hash;
440
441        try
442        {
443            MessageDigest md = MessageDigest.getInstance("MD5");
444
445            md.update(userName.getBytes("UTF-8"));
446            md.update(":".getBytes("UTF-8"));
447            md.update(realm.getBytes("UTF-8"));
448            md.update(":".getBytes("UTF-8"));
449            md.update(password.getBytes("UTF-8"));
450            hash = md.digest();
451
452            if ("md5-sess".equals(algorithm))
453            {
454                md.update(hash);
455                md.update(":".getBytes("UTF-8"));
456                md.update(nonce.getBytes("UTF-8"));
457                md.update(":".getBytes("UTF-8"));
458                md.update(clientNonce.getBytes("UTF-8"));
459                hash = md.digest();
460            }
461        }
462        catch(NoSuchAlgorithmException e)
463        {
464            throw new SaslException("No provider found for MD5 hash", e);
465        }
466        catch(UnsupportedEncodingException e)
467        {
468            throw new SaslException(
469             "UTF-8 encoding not supported by platform.", e);
470        }
471
472        return convertToHex(hash);
473    }
474
475
476    /**
477     * This function calculates the response-value of the response directive of
478     * the digest-response as documented in RFC 2831
479     *
480     * @param  HA1           H(A1)
481     * @param  serverNonce   nonce from server
482     * @param  nonceCount    8 hex digits
483     * @param  clientNonce   client nonce
484     * @param  qop           qop-value: "", "auth", "auth-int"
485     * @param  method        method from the request
486     * @param  digestUri     requested URL
487     * @param  clientResponseFlag request-digest or response-digest
488     *
489     * @return Response-value of the response directive of the digest-response
490     *
491     * @exception SaslException  If an error occurs
492     */
493    char[] DigestCalcResponse(
494        char[]      HA1,            /* H(A1) */
495        String      serverNonce,    /* nonce from server */
496        String      nonceCount,     /* 8 hex digits */
497        String      clientNonce,    /* client nonce */
498        String      qop,            /* qop-value: "", "auth", "auth-int" */
499        String      method,         /* method from the request */
500        String      digestUri,      /* requested URL */
501        boolean     clientResponseFlag) /* request-digest or response-digest */
502            throws SaslException
503    {
504        byte[]             HA2;
505        byte[]             respHash;
506        char[]             HA2Hex;
507
508        // calculate H(A2)
509        try
510        {
511            MessageDigest md = MessageDigest.getInstance("MD5");
512            if (clientResponseFlag)
513                  md.update(method.getBytes("UTF-8"));
514            md.update(":".getBytes("UTF-8"));
515            md.update(digestUri.getBytes("UTF-8"));
516            if ("auth-int".equals(qop))
517            {
518                md.update(":".getBytes("UTF-8"));
519                md.update("00000000000000000000000000000000".getBytes("UTF-8"));
520            }
521            HA2 = md.digest();
522            HA2Hex = convertToHex(HA2);
523
524            // calculate response
525            md.update(new String(HA1).getBytes("UTF-8"));
526            md.update(":".getBytes("UTF-8"));
527            md.update(serverNonce.getBytes("UTF-8"));
528            md.update(":".getBytes("UTF-8"));
529            if (qop.length() > 0)
530            {
531                md.update(nonceCount.getBytes("UTF-8"));
532                md.update(":".getBytes("UTF-8"));
533                md.update(clientNonce.getBytes("UTF-8"));
534                md.update(":".getBytes("UTF-8"));
535                md.update(qop.getBytes("UTF-8"));
536                md.update(":".getBytes("UTF-8"));
537            }
538            md.update(new String(HA2Hex).getBytes("UTF-8"));
539            respHash = md.digest();
540        }
541        catch(NoSuchAlgorithmException e)
542        {
543            throw new SaslException("No provider found for MD5 hash", e);
544        }
545        catch(UnsupportedEncodingException e)
546        {
547            throw new SaslException(
548             "UTF-8 encoding not supported by platform.", e);
549        }
550
551        return convertToHex(respHash);
552    }
553
554
555    /**
556     * Creates the intial response to be sent to the server.
557     *
558     * @param challenge  Challenge in bytes recived form the Server
559     *
560     * @return Initial response to be sent to the server
561     */
562    private String createDigestResponse(
563        byte[] challenge)
564            throws SaslException
565    {
566        char[]            response;
567        StringBuffer    digestResponse = new StringBuffer(512);
568        int             realmSize;
569
570        m_dc = new DigestChallenge(challenge);
571
572        m_digestURI = m_protocol + "/" + m_serverName;
573
574        if ((m_dc.getQop() & DigestChallenge.QOP_AUTH)
575            == DigestChallenge.QOP_AUTH )
576            m_qopValue = "auth";
577        else
578            throw new SaslException("Client only supports qop of 'auth'");
579
580        //get call back information
581        Callback[] callbacks = new Callback[3];
582        ArrayList realms = m_dc.getRealms();
583        realmSize = realms.size();
584        if (realmSize == 0)
585        {
586            callbacks[0] = new RealmCallback("Realm");
587        }
588        else if (realmSize == 1)
589        {
590            callbacks[0] = new RealmCallback("Realm", (String)realms.get(0));
591        }
592        else
593        {
594            callbacks[0] =
595             new RealmChoiceCallback(
596                         "Realm",
597                         (String[])realms.toArray(new String[realmSize]),
598                          0,      //the default choice index
599                          false); //no multiple selections
600        }
601
602        callbacks[1] = new PasswordCallback("Password", false);
603        //false = no echo
604
605        if (m_authorizationId == null || m_authorizationId.length() == 0)
606            callbacks[2] = new NameCallback("Name");
607        else
608            callbacks[2] = new NameCallback("Name", m_authorizationId);
609
610        try
611        {
612            m_cbh.handle(callbacks);
613        }
614        catch(UnsupportedCallbackException e)
615        {
616            throw new SaslException("Handler does not support" +
617                                          " necessary callbacks",e);
618        }
619        catch(IOException e)
620        {
621            throw new SaslException("IO exception in CallbackHandler.", e);
622        }
623
624        if (realmSize > 1)
625        {
626            int[] selections =
627             ((RealmChoiceCallback)callbacks[0]).getSelectedIndexes();
628
629            if (selections.length > 0)
630                m_realm =
631                ((RealmChoiceCallback)callbacks[0]).getChoices()[selections[0]];
632            else
633                m_realm = ((RealmChoiceCallback)callbacks[0]).getChoices()[0];
634        }
635        else
636            m_realm = ((RealmCallback)callbacks[0]).getText();
637
638        m_clientNonce = getClientNonce();
639
640        m_name = ((NameCallback)callbacks[2]).getName();
641        if (m_name == null)
642            m_name = ((NameCallback)callbacks[2]).getDefaultName();
643        if (m_name == null)
644            throw new SaslException("No user name was specified.");
645
646        m_HA1 = DigestCalcHA1(
647                      m_dc.getAlgorithm(),
648                      m_name,
649                      m_realm,
650                      new String(((PasswordCallback)callbacks[1]).getPassword()),
651                      m_dc.getNonce(),
652                      m_clientNonce);
653
654        response = DigestCalcResponse(m_HA1,
655                                      m_dc.getNonce(),
656                                      "00000001",
657                                      m_clientNonce,
658                                      m_qopValue,
659                                      "AUTHENTICATE",
660                                      m_digestURI,
661                                      true);
662
663        digestResponse.append("username=\"");
664        digestResponse.append(m_authorizationId);
665        if (0 != m_realm.length())
666        {
667            digestResponse.append("\",realm=\"");
668            digestResponse.append(m_realm);
669        }
670        digestResponse.append("\",cnonce=\"");
671        digestResponse.append(m_clientNonce);
672        digestResponse.append("\",nc=");
673        digestResponse.append("00000001"); //nounce count
674        digestResponse.append(",qop=");
675        digestResponse.append(m_qopValue);
676        digestResponse.append(",digest-uri=\"ldap/");
677        digestResponse.append(m_serverName);
678        digestResponse.append("\",response=");
679        digestResponse.append(response);
680        digestResponse.append(",charset=utf-8,nonce=\"");
681        digestResponse.append(m_dc.getNonce());
682        digestResponse.append("\"");
683
684        return digestResponse.toString();
685     }
686
687
688    /**
689     * This function validates the server response. This step performs a
690     * modicum of mutual authentication by verifying that the server knows
691     * the user's password
692     *
693     * @param  serverResponse  Response recived form Server
694     *
695     * @return  true if the mutual authentication succeeds;
696     *          else return false
697     *
698     * @exception SaslException  If an error occurs
699     */
700    boolean checkServerResponseAuth(
701            byte[]  serverResponse) throws SaslException
702    {
703        char[]           response;
704        ResponseAuth  responseAuth = null;
705        String        responseStr;
706
707        responseAuth = new ResponseAuth(serverResponse);
708
709        response = DigestCalcResponse(m_HA1,
710                                  m_dc.getNonce(),
711                                  "00000001",
712                                  m_clientNonce,
713                                  m_qopValue,
714                                  DIGEST_METHOD,
715                                  m_digestURI,
716                                  false);
717
718        responseStr = new String(response);
719
720        return responseStr.equals(responseAuth.getResponseValue());
721    }
722
723
724    /**
725     * This function returns hex character representing the value of the input
726     *
727     * @param value Input value in byte
728     *
729     * @return Hex value of the Input byte value
730     */
731    private static char getHexChar(
732        byte    value)
733    {
734        switch (value)
735        {
736        case 0:
737            return '0';
738        case 1:
739            return '1';
740        case 2:
741            return '2';
742        case 3:
743            return '3';
744        case 4:
745            return '4';
746        case 5:
747            return '5';
748        case 6:
749            return '6';
750        case 7:
751            return '7';
752        case 8:
753            return '8';
754        case 9:
755            return '9';
756        case 10:
757            return 'a';
758        case 11:
759            return 'b';
760        case 12:
761            return 'c';
762        case 13:
763            return 'd';
764        case 14:
765            return 'e';
766        case 15:
767            return 'f';
768        default:
769            return 'Z';
770        }
771    }
772
773    /**
774     * Calculates the Nonce value of the Client
775     *
776     * @return   Nonce value of the client
777     *
778     * @exception   SaslException If an error Occurs
779     */
780    String getClientNonce() throws SaslException
781    {
782        byte[]          nonceBytes = new byte[NONCE_BYTE_COUNT];
783        SecureRandom    prng;
784        byte            nonceByte;
785        char[]          hexNonce = new char[NONCE_HEX_COUNT];
786
787        try
788        {
789            prng = SecureRandom.getInstance("SHA1PRNG");
790            prng.nextBytes(nonceBytes);
791            for(int i=0; i<NONCE_BYTE_COUNT; i++)
792            {
793                //low nibble
794                hexNonce[i*2] = getHexChar((byte)(nonceBytes[i] & 0x0f));
795                //high nibble
796                hexNonce[(i*2)+1] = getHexChar((byte)((nonceBytes[i] & 0xf0)
797                                                                      >> 4));
798            }
799            return new String(hexNonce);
800        }
801        catch(NoSuchAlgorithmException e)
802        {
803            throw new SaslException("No random number generator available", e);
804        }
805    }
806
807    /**
808     * Returns the IANA-registered mechanism name of this SASL client.
809     *  (e.g. "CRAM-MD5", "GSSAPI")
810     *
811     * @return  "DIGEST-MD5"the IANA-registered mechanism name of this SASL
812     *          client.
813     */
814    public String getMechanismName()
815    {
816        return "DIGEST-MD5";
817    }
818
819} //end class DigestMD5SaslClient
820
821