001    /*
002     * CDDL HEADER START
003     *
004     * The contents of this file are subject to the terms of the
005     * Common Development and Distribution License, Version 1.0 only
006     * (the "License").  You may not use this file except in compliance
007     * with the License.
008     *
009     * You can obtain a copy of the license at
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.extensions;
028    
029    
030    
031    import java.io.UnsupportedEncodingException;
032    import java.security.MessageDigest;
033    import java.security.SecureRandom;
034    import java.text.ParseException;
035    import java.util.ArrayList;
036    import java.util.Arrays;
037    import java.util.Iterator;
038    import java.util.List;
039    import java.util.Map;
040    import java.util.concurrent.locks.Lock;
041    
042    import org.opends.messages.Message;
043    import org.opends.server.admin.server.ConfigurationChangeListener;
044    import org.opends.server.admin.std.server.DigestMD5SASLMechanismHandlerCfg;
045    import org.opends.server.admin.std.server.SASLMechanismHandlerCfg;
046    import org.opends.server.api.Backend;
047    import org.opends.server.api.ClientConnection;
048    import org.opends.server.api.IdentityMapper;
049    import org.opends.server.api.SASLMechanismHandler;
050    import org.opends.server.config.ConfigException;
051    import org.opends.server.core.BindOperation;
052    import org.opends.server.core.DirectoryServer;
053    import org.opends.server.core.PasswordPolicyState;
054    import org.opends.server.loggers.debug.DebugTracer;
055    import org.opends.server.protocols.asn1.ASN1OctetString;
056    import org.opends.server.protocols.internal.InternalClientConnection;
057    import org.opends.server.types.AuthenticationInfo;
058    import org.opends.server.types.ByteString;
059    import org.opends.server.types.ConfigChangeResult;
060    import org.opends.server.types.DebugLogLevel;
061    import org.opends.server.types.DirectoryException;
062    import org.opends.server.types.DisconnectReason;
063    import org.opends.server.types.DN;
064    import org.opends.server.types.Entry;
065    import org.opends.server.types.InitializationException;
066    import org.opends.server.types.LockManager;
067    import org.opends.server.types.Privilege;
068    import org.opends.server.types.ResultCode;
069    import org.opends.server.util.Base64;
070    
071    import static org.opends.messages.ExtensionMessages.*;
072    import static org.opends.server.loggers.ErrorLogger.*;
073    import static org.opends.server.loggers.debug.DebugLogger.*;
074    import static org.opends.server.util.ServerConstants.*;
075    import static org.opends.server.util.StaticUtils.*;
076    
077    
078    
079    /**
080     * This class provides an implementation of a SASL mechanism that uses digest
081     * authentication via DIGEST-MD5.  This is a password-based mechanism that does
082     * not expose the password itself over the wire but rather uses an MD5 hash that
083     * proves the client knows the password.  This is similar to the CRAM-MD5
084     * mechanism, and the primary differences are that CRAM-MD5 only obtains random
085     * data from the server whereas DIGEST-MD5 uses random data from both the
086     * server and the client, CRAM-MD5 does not allow for an authorization ID in
087     * addition to the authentication ID where DIGEST-MD5 does, and CRAM-MD5 does
088     * not define any integrity and confidentiality mechanisms where DIGEST-MD5
089     * does.  This implementation is based on the specification in RFC 2831 and
090     * updates from draft-ietf-sasl-rfc2831bis-06.
091     */
092    public class DigestMD5SASLMechanismHandler
093           extends SASLMechanismHandler<DigestMD5SASLMechanismHandlerCfg>
094           implements ConfigurationChangeListener<
095                           DigestMD5SASLMechanismHandlerCfg>
096    {
097      /**
098       * The tracer object for the debug logger.
099       */
100      private static final DebugTracer TRACER = getTracer();
101    
102      // The current configuration for this SASL mechanism handler.
103      private DigestMD5SASLMechanismHandlerCfg currentConfig;
104    
105      // The identity mapper that will be used to map ID strings to user entries.
106      private IdentityMapper<?> identityMapper;
107    
108      // The message digest engine that will be used to create the MD5 digests.
109      private MessageDigest md5Digest;
110    
111      // The lock that will be used to provide threadsafe access to the message
112      // digest.
113      private Object digestLock;
114    
115      // The random number generator that we will use to create the nonce.
116      private SecureRandom randomGenerator;
117    
118    
119    
120      /**
121       * Creates a new instance of this SASL mechanism handler.  No initialization
122       * should be done in this method, as it should all be performed in the
123       * <CODE>initializeSASLMechanismHandler</CODE> method.
124       */
125      public DigestMD5SASLMechanismHandler()
126      {
127        super();
128      }
129    
130    
131    
132      /**
133       * {@inheritDoc}
134       */
135      @Override()
136      public void initializeSASLMechanismHandler(
137                       DigestMD5SASLMechanismHandlerCfg configuration)
138             throws ConfigException, InitializationException
139      {
140        configuration.addDigestMD5ChangeListener(this);
141        currentConfig = configuration;
142    
143    
144        // Initialize the variables needed for the MD5 digest creation.
145        digestLock      = new Object();
146        randomGenerator = new SecureRandom();
147    
148        try
149        {
150          md5Digest = MessageDigest.getInstance("MD5");
151        }
152        catch (Exception e)
153        {
154          if (debugEnabled())
155          {
156            TRACER.debugCaught(DebugLogLevel.ERROR, e);
157          }
158    
159          Message message = ERR_SASLDIGESTMD5_CANNOT_GET_MESSAGE_DIGEST.get(
160              getExceptionMessage(e));
161          throw new InitializationException(message, e);
162        }
163    
164    
165        // Get the identity mapper that should be used to find users.
166        DN identityMapperDN = configuration.getIdentityMapperDN();
167        identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
168    
169    
170        DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_DIGEST_MD5,
171                                                     this);
172      }
173    
174    
175    
176      /**
177       * {@inheritDoc}
178       */
179      @Override()
180      public void finalizeSASLMechanismHandler()
181      {
182        currentConfig.removeDigestMD5ChangeListener(this);
183        DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_DIGEST_MD5);
184      }
185    
186    
187    
188    
189      /**
190       * {@inheritDoc}
191       */
192      @Override()
193      public void processSASLBind(BindOperation bindOperation)
194      {
195        DigestMD5SASLMechanismHandlerCfg config = currentConfig;
196        IdentityMapper<?> identityMapper = this.identityMapper;
197        String realm = config.getRealm();
198    
199    
200        // The DIGEST-MD5 bind process uses two stages.  See if we have any state
201        // information from the first stage to determine whether this is a
202        // continuation of an existing bind or an initial authentication.  Note that
203        // this implementation does not support subsequent authentication, so even
204        // if the client provided credentials for the bind, it will be treated as an
205        // initial authentication if there is no existing state.
206        boolean initialAuth = true;
207        ClientConnection clientConnection  = bindOperation.getClientConnection();
208        Object saslStateInfo = clientConnection.getSASLAuthStateInfo();
209        if ((saslStateInfo != null) &&
210            (saslStateInfo instanceof DigestMD5StateInfo))
211        {
212          initialAuth = false;
213        }
214    
215        if (initialAuth)
216        {
217          // Create a buffer to hold the challenge.
218          StringBuilder challengeBuffer = new StringBuilder();
219    
220    
221          // Add the realm to the challenge.  If we have a configured realm, then
222          // use it.  Otherwise, add a realm for each suffix defined in the server.
223          if (realm == null)
224          {
225            Map<DN,Backend> suffixes = DirectoryServer.getPublicNamingContexts();
226            if (! suffixes.isEmpty())
227            {
228              Iterator<DN> iterator = suffixes.keySet().iterator();
229              challengeBuffer.append("realm=\"");
230              challengeBuffer.append(iterator.next().toNormalizedString());
231              challengeBuffer.append("\"");
232    
233              while (iterator.hasNext())
234              {
235                challengeBuffer.append(",realm=\"");
236                challengeBuffer.append(iterator.next().toNormalizedString());
237                challengeBuffer.append("\"");
238              }
239            }
240          }
241          else
242          {
243            challengeBuffer.append("realm=\"");
244            challengeBuffer.append(realm);
245            challengeBuffer.append("\"");
246          }
247    
248    
249          // Generate the nonce.  Add it to the challenge and remember it for future
250          // use.
251          String nonce = generateNonce();
252          if (challengeBuffer.length() > 0)
253          {
254            challengeBuffer.append(",");
255          }
256          challengeBuffer.append("nonce=\"");
257          challengeBuffer.append(nonce);
258          challengeBuffer.append("\"");
259    
260    
261          // Generate the qop-list and add it to the challenge.
262          // FIXME -- Add support for integrity and confidentiality.  Once we do,
263          //          we'll also want to add the maxbuf and cipher options.
264          challengeBuffer.append(",qop=\"auth\"");
265    
266    
267          // Add the charset option to indicate that we support UTF-8 values.
268          challengeBuffer.append(",charset=utf-8");
269    
270    
271          // Add the algorithm, which will always be "md5-sess".
272          challengeBuffer.append(",algorithm=md5-sess");
273    
274    
275          // Encode the challenge as an ASN.1 element.  The total length of the
276          // encoded value must be less than 2048 bytes, which should not be a
277          // problem, but we'll add a safety check just in case....  In the event
278          // that it does happen, we'll also log an error so it is more noticeable.
279          ASN1OctetString challenge =
280               new ASN1OctetString(challengeBuffer.toString());
281          if (challenge.value().length >= 2048)
282          {
283            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
284    
285            Message message = WARN_SASLDIGESTMD5_CHALLENGE_TOO_LONG.get(
286                    challenge.value().length);
287            bindOperation.setAuthFailureReason(message);
288    
289            logError(message);
290            return;
291          }
292    
293    
294          // Store the state information with the client connection so we can use it
295          // for later validation.
296          DigestMD5StateInfo stateInfo = new DigestMD5StateInfo(nonce, "00000000");
297          clientConnection.setSASLAuthStateInfo(stateInfo);
298    
299    
300          // Prepare the response and return so it will be sent to the client.
301          bindOperation.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS);
302          bindOperation.setServerSASLCredentials(challenge);
303          return;
304        }
305    
306    
307        // If we've gotten here, then we have existing SASL state information for
308        // this client.  Make sure that the client also provided credentials.
309        ASN1OctetString clientCredentials = bindOperation.getSASLCredentials();
310        if ((clientCredentials == null) || (clientCredentials.value().length == 0))
311        {
312          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
313    
314          Message message = ERR_SASLDIGESTMD5_NO_CREDENTIALS.get();
315          bindOperation.setAuthFailureReason(message);
316          return;
317        }
318    
319    
320        // Parse the SASL state information.  Also, since there are only ever two
321        // stages of a DIGEST-MD5 bind, clear the SASL state information stored in
322        // the client connection because it shouldn't be used anymore regardless of
323        // whether the bind succeeds or fails.  Note that if we do add support for
324        // subsequent authentication in the future, then we will probably need to
325        // keep state information in the client connection, but even then it will
326        // be different from what's already there.
327        DigestMD5StateInfo stateInfo = (DigestMD5StateInfo) saslStateInfo;
328        clientConnection.setSASLAuthStateInfo(null);
329    
330    
331        // Create variables to hold values stored in the client's response.  We'll
332        // also store the base DN because we might need to override it later.
333        String responseUserName      = null;
334        String responseRealm         = null;
335        String responseNonce         = null;
336        String responseCNonce        = null;
337        int    responseNonceCount    = -1;
338        String responseNonceCountStr = null;
339        String responseQoP           = "auth";
340        String responseDigestURI     = null;
341        byte[] responseDigest        = null;
342        String responseCharset       = "ISO-8859-1";
343        String responseAuthzID       = null;
344    
345    
346        // Get a temporary string representation of the SASL credentials using the
347        // ISO-8859-1 encoding and see if it contains "charset=utf-8".  If so, then
348        // re-parse the credentials using that character set.
349        byte[] credBytes  = clientCredentials.value();
350        String credString = null;
351        String lowerCreds = null;
352        try
353        {
354          credString = new String(credBytes, responseCharset);
355          lowerCreds = toLowerCase(credString);
356        }
357        catch (Exception e)
358        {
359          if (debugEnabled())
360          {
361            TRACER.debugCaught(DebugLogLevel.ERROR, e);
362          }
363    
364          // This isn't necessarily fatal because we're going to retry using UTF-8,
365          // but we want to log it anyway.
366          logError(WARN_SASLDIGESTMD5_CANNOT_PARSE_ISO_CREDENTIALS.get(
367              responseCharset, getExceptionMessage(e)));
368        }
369    
370        if ((credString == null) ||
371            (lowerCreds.indexOf("charset=utf-8") >= 0))
372        {
373          try
374          {
375            credString = new String(credBytes, "UTF-8");
376            lowerCreds = toLowerCase(credString);
377          }
378          catch (Exception e)
379          {
380            if (debugEnabled())
381            {
382              TRACER.debugCaught(DebugLogLevel.ERROR, e);
383            }
384    
385            // This is fatal because either we can't parse the credentials as a
386            // string at all, or we know we need to do so using UTF-8 and can't.
387            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
388    
389            Message message = WARN_SASLDIGESTMD5_CANNOT_PARSE_UTF8_CREDENTIALS.get(
390                    getExceptionMessage(e));
391            bindOperation.setAuthFailureReason(message);
392            return;
393          }
394        }
395    
396    
397        // Iterate through the credentials string, parsing the property names and
398        // their corresponding values.
399        int pos    = 0;
400        int length = credString.length();
401        while (pos < length)
402        {
403          int equalPos = credString.indexOf('=', pos+1);
404          if (equalPos < 0)
405          {
406            // This is bad because we're not at the end of the string but we don't
407            // have a name/value delimiter.
408            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
409    
410            Message message = ERR_SASLDIGESTMD5_INVALID_TOKEN_IN_CREDENTIALS.get(
411                    credString, pos);
412            bindOperation.setAuthFailureReason(message);
413            return;
414          }
415    
416    
417          String tokenName  = lowerCreds.substring(pos, equalPos);
418    
419          String tokenValue;
420          try
421          {
422            StringBuilder valueBuffer = new StringBuilder();
423            pos = readToken(credString, equalPos+1, length, valueBuffer);
424            tokenValue = valueBuffer.toString();
425          }
426          catch (DirectoryException de)
427          {
428            // We couldn't parse the token value, so it must be malformed.
429            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
430            bindOperation.setAuthFailureReason(
431                    de.getMessageObject());
432            return;
433          }
434    
435          if (tokenName.equals("charset"))
436          {
437            // The value must be the string "utf-8".  If not, that's an error.
438            if (! tokenValue.equalsIgnoreCase("utf-8"))
439            {
440              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
441    
442              Message message = ERR_SASLDIGESTMD5_INVALID_CHARSET.get(tokenValue);
443              bindOperation.setAuthFailureReason(message);
444              return;
445            }
446          }
447          else if (tokenName.equals("username"))
448          {
449            responseUserName = tokenValue;
450          }
451          else if (tokenName.equals("realm"))
452          {
453            responseRealm = tokenValue;
454            if (realm != null)
455            {
456              if (! responseRealm.equals(realm))
457              {
458                bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
459    
460                Message message =
461                        ERR_SASLDIGESTMD5_INVALID_REALM.get(responseRealm);
462                bindOperation.setAuthFailureReason(message);
463                return;
464              }
465            }
466          }
467          else if (tokenName.equals("nonce"))
468          {
469            responseNonce = tokenValue;
470            String requestNonce = stateInfo.getNonce();
471            if (! responseNonce.equals(requestNonce))
472            {
473              // The nonce provided by the client is incorrect.  This could be an
474              // attempt at a replay or chosen plaintext attack, so we'll close the
475              // connection.  We will put a message in the log but will not send it
476              // to the client.
477              Message message = ERR_SASLDIGESTMD5_INVALID_NONCE.get();
478              clientConnection.disconnect(DisconnectReason.SECURITY_PROBLEM, false,
479                      message);
480              return;
481            }
482          }
483          else if (tokenName.equals("cnonce"))
484          {
485            responseCNonce = tokenValue;
486          }
487          else if (tokenName.equals("nc"))
488          {
489            try
490            {
491              responseNonceCountStr = tokenValue;
492              responseNonceCount    = Integer.parseInt(responseNonceCountStr, 16);
493            }
494            catch (Exception e)
495            {
496              if (debugEnabled())
497              {
498                TRACER.debugCaught(DebugLogLevel.ERROR, e);
499              }
500    
501              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
502    
503              Message message = ERR_SASLDIGESTMD5_CANNOT_DECODE_NONCE_COUNT.get(
504                      tokenValue);
505              bindOperation.setAuthFailureReason(message);
506              return;
507            }
508    
509            int storedNonce;
510            try
511            {
512              storedNonce = Integer.parseInt(stateInfo.getNonceCount(), 16);
513            }
514            catch (Exception e)
515            {
516              if (debugEnabled())
517              {
518                TRACER.debugCaught(DebugLogLevel.ERROR, e);
519              }
520    
521              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
522    
523              Message message =
524                      ERR_SASLDIGESTMD5_CANNOT_DECODE_STORED_NONCE_COUNT.get(
525                              getExceptionMessage(e));
526              bindOperation.setAuthFailureReason(message);
527              return;
528            }
529    
530            if (responseNonceCount != (storedNonce + 1))
531            {
532              // The nonce count provided by the client is incorrect.  This
533              // indicates a replay attack, so we'll close the connection.  We will
534              // put a message in the log but we will not send it to the client.
535              Message message = ERR_SASLDIGESTMD5_INVALID_NONCE_COUNT.get();
536              clientConnection.disconnect(DisconnectReason.SECURITY_PROBLEM, false,
537                      message);
538              return;
539            }
540          }
541          else if (tokenName.equals("qop"))
542          {
543            responseQoP = tokenValue;
544    
545            if (responseQoP.equals("auth"))
546            {
547              // No action necessary.
548            }
549            else if (responseQoP.equals("auth-int"))
550            {
551              // FIXME -- Add support for integrity protection.
552              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
553    
554              Message message = ERR_SASLDIGESTMD5_INTEGRITY_NOT_SUPPORTED.get();
555              bindOperation.setAuthFailureReason(message);
556              return;
557            }
558            else if (responseQoP.equals("auth-conf"))
559            {
560              // FIXME -- Add support for confidentiality protection.
561              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
562    
563              Message message =
564                      ERR_SASLDIGESTMD5_CONFIDENTIALITY_NOT_SUPPORTED.get();
565              bindOperation.setAuthFailureReason(message);
566              return;
567            }
568            else
569            {
570              // This is an invalid QoP value.
571              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
572    
573              Message message = ERR_SASLDIGESTMD5_INVALID_QOP.get(responseQoP);
574              bindOperation.setAuthFailureReason(message);
575              return;
576            }
577          }
578          else if (tokenName.equals("digest-uri"))
579          {
580            responseDigestURI = tokenValue;
581    
582            String serverFQDN = config.getServerFqdn();
583            if ((serverFQDN != null) && (serverFQDN.length() > 0))
584            {
585              // If a server FQDN is populated, then we'll use it to validate the
586              // digest-uri, which should be in the form "ldap/serverfqdn".
587              String expectedDigestURI = "ldap/" + serverFQDN;
588              if (! expectedDigestURI.equalsIgnoreCase(responseDigestURI))
589              {
590                bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
591    
592                Message message = ERR_SASLDIGESTMD5_INVALID_DIGEST_URI.get(
593                        responseDigestURI, expectedDigestURI);
594                bindOperation.setAuthFailureReason(message);
595                return;
596              }
597            }
598          }
599          else if (tokenName.equals("response"))
600          {
601            try
602            {
603              responseDigest = hexStringToByteArray(tokenValue);
604            }
605            catch (ParseException pe)
606            {
607              if (debugEnabled())
608              {
609                TRACER.debugCaught(DebugLogLevel.ERROR, pe);
610              }
611    
612              Message message =
613                      ERR_SASLDIGESTMD5_CANNOT_PARSE_RESPONSE_DIGEST.get(
614                              getExceptionMessage(pe));
615              bindOperation.setAuthFailureReason(message);
616              return;
617            }
618          }
619          else if (tokenName.equals("authzid"))
620          {
621            responseAuthzID = tokenValue;
622    
623            // FIXME -- This must always be parsed in UTF-8 even if the charset for
624            // other elements is ISO 8859-1.
625          }
626          else if (tokenName.equals("maxbuf") || tokenName.equals("cipher"))
627          {
628            // FIXME -- Add support for confidentiality and integrity protection.
629          }
630          else
631          {
632            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
633    
634            Message message = ERR_SASLDIGESTMD5_INVALID_RESPONSE_TOKEN.get(
635                    tokenName);
636            bindOperation.setAuthFailureReason(message);
637            return;
638          }
639        }
640    
641    
642        // Make sure that all required properties have been specified.
643        if ((responseUserName == null) || (responseUserName.length() == 0))
644        {
645          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
646    
647          Message message = ERR_SASLDIGESTMD5_NO_USERNAME_IN_RESPONSE.get();
648          bindOperation.setAuthFailureReason(message);
649          return;
650        }
651        else if (responseNonce == null)
652        {
653          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
654    
655          Message message = ERR_SASLDIGESTMD5_NO_NONCE_IN_RESPONSE.get();
656          bindOperation.setAuthFailureReason(message);
657          return;
658        }
659        else if (responseCNonce == null)
660        {
661          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
662    
663          Message message = ERR_SASLDIGESTMD5_NO_CNONCE_IN_RESPONSE.get();
664          bindOperation.setAuthFailureReason(message);
665          return;
666        }
667        else if (responseNonceCount < 0)
668        {
669          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
670    
671          Message message = ERR_SASLDIGESTMD5_NO_NONCE_COUNT_IN_RESPONSE.get();
672          bindOperation.setAuthFailureReason(message);
673          return;
674        }
675        else if (responseDigest == null)
676        {
677          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
678    
679          Message message = ERR_SASLDIGESTMD5_NO_DIGEST_IN_RESPONSE.get();
680          bindOperation.setAuthFailureReason(message);
681          return;
682        }
683    
684    
685        // Slight departure from draft-ietf-sasl-rfc2831bis-06 in order to
686        // support legacy/broken client implementations, such as Solaris
687        // Native LDAP Client, which omit digest-uri directive. the presence
688        // of digest-uri directive erroneously read "may" in the RFC and has
689        // been fixed later in the DRAFT to read "must". if the client does
690        // not include digest-uri directive use the empty string instead.
691        if (responseDigestURI == null)
692        {
693          responseDigestURI = "";
694        }
695    
696    
697        // If a realm has not been specified, then use the empty string.
698        // FIXME -- Should we reject this if a specific realm is defined?
699        if (responseRealm == null)
700        {
701          responseRealm = "";
702        }
703    
704    
705        // Get the user entry for the authentication ID.  Allow for an
706        // authentication ID that is just a username (as per the DIGEST-MD5 spec),
707        // but also allow a value in the authzid form specified in RFC 2829.
708        Entry  userEntry    = null;
709        String lowerUserName = toLowerCase(responseUserName);
710        if (lowerUserName.startsWith("dn:"))
711        {
712          // Try to decode the user DN and retrieve the corresponding entry.
713          DN userDN;
714          try
715          {
716            userDN = DN.decode(responseUserName.substring(3));
717          }
718          catch (DirectoryException de)
719          {
720            if (debugEnabled())
721            {
722              TRACER.debugCaught(DebugLogLevel.ERROR, de);
723            }
724    
725            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
726    
727            Message message = ERR_SASLDIGESTMD5_CANNOT_DECODE_USERNAME_AS_DN.get(
728                    responseUserName, de.getMessageObject());
729            bindOperation.setAuthFailureReason(message);
730            return;
731          }
732    
733          if (userDN.isNullDN())
734          {
735            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
736    
737            Message message = ERR_SASLDIGESTMD5_USERNAME_IS_NULL_DN.get();
738            bindOperation.setAuthFailureReason(message);
739            return;
740          }
741    
742          DN rootDN = DirectoryServer.getActualRootBindDN(userDN);
743          if (rootDN != null)
744          {
745            userDN = rootDN;
746          }
747    
748          // Acquire a read lock on the user entry.  If this fails, then so will the
749          // authentication.
750          Lock readLock = null;
751          for (int i=0; i < 3; i++)
752          {
753            readLock = LockManager.lockRead(userDN);
754            if (readLock != null)
755            {
756              break;
757            }
758          }
759    
760          if (readLock == null)
761          {
762            bindOperation.setResultCode(DirectoryServer.getServerErrorResultCode());
763    
764            Message message = INFO_SASLDIGESTMD5_CANNOT_LOCK_ENTRY.get(
765                    String.valueOf(userDN));
766            bindOperation.setAuthFailureReason(message);
767            return;
768          }
769    
770          try
771          {
772            userEntry = DirectoryServer.getEntry(userDN);
773          }
774          catch (DirectoryException de)
775          {
776            if (debugEnabled())
777            {
778              TRACER.debugCaught(DebugLogLevel.ERROR, de);
779            }
780    
781            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
782    
783            Message message = ERR_SASLDIGESTMD5_CANNOT_GET_ENTRY_BY_DN.get(
784                    String.valueOf(userDN), de.getMessageObject());
785            bindOperation.setAuthFailureReason(message);
786            return;
787          }
788          finally
789          {
790            LockManager.unlock(userDN, readLock);
791          }
792        }
793        else
794        {
795          // Use the identity mapper to resolve the username to an entry.
796          String userName = responseUserName;
797          if (lowerUserName.startsWith("u:"))
798          {
799            if (lowerUserName.equals("u:"))
800            {
801              bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
802    
803              Message message = ERR_SASLDIGESTMD5_ZERO_LENGTH_USERNAME.get();
804              bindOperation.setAuthFailureReason(message);
805              return;
806            }
807    
808            userName = responseUserName.substring(2);
809          }
810    
811    
812          try
813          {
814            userEntry = identityMapper.getEntryForID(userName);
815          }
816          catch (DirectoryException de)
817          {
818            if (debugEnabled())
819            {
820              TRACER.debugCaught(DebugLogLevel.ERROR, de);
821            }
822    
823            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
824    
825            Message message = ERR_SASLDIGESTMD5_CANNOT_MAP_USERNAME.get(
826                    String.valueOf(responseUserName), de.getMessageObject());
827            bindOperation.setAuthFailureReason(message);
828            return;
829          }
830        }
831    
832    
833        // At this point, we should have a user entry.  If we don't then fail.
834        if (userEntry == null)
835        {
836          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
837    
838          Message message =
839                  ERR_SASLDIGESTMD5_NO_MATCHING_ENTRIES.get(responseUserName);
840          bindOperation.setAuthFailureReason(message);
841          return;
842        }
843        else
844        {
845          bindOperation.setSASLAuthUserEntry(userEntry);
846        }
847    
848    
849        Entry authZEntry = userEntry;
850        if (responseAuthzID != null)
851        {
852          if (responseAuthzID.length() == 0)
853          {
854            // The authorization ID must not be an empty string.
855            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
856    
857            Message message = ERR_SASLDIGESTMD5_EMPTY_AUTHZID.get();
858            bindOperation.setAuthFailureReason(message);
859            return;
860          }
861          else if (! responseAuthzID.equals(responseUserName))
862          {
863            String lowerAuthzID = toLowerCase(responseAuthzID);
864    
865            if (lowerAuthzID.startsWith("dn:"))
866            {
867              DN authzDN;
868              try
869              {
870                authzDN = DN.decode(responseAuthzID.substring(3));
871              }
872              catch (DirectoryException de)
873              {
874                if (debugEnabled())
875                {
876                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
877                }
878    
879                bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
880    
881                Message message = ERR_SASLDIGESTMD5_AUTHZID_INVALID_DN.get(
882                        responseAuthzID, de.getMessageObject());
883                bindOperation.setAuthFailureReason(message);
884                return;
885              }
886    
887              DN actualAuthzDN = DirectoryServer.getActualRootBindDN(authzDN);
888              if (actualAuthzDN != null)
889              {
890                authzDN = actualAuthzDN;
891              }
892    
893              if (! authzDN.equals(userEntry.getDN()))
894              {
895                AuthenticationInfo tempAuthInfo =
896                  new AuthenticationInfo(userEntry,
897                           DirectoryServer.isRootDN(userEntry.getDN()));
898                InternalClientConnection tempConn =
899                     new InternalClientConnection(tempAuthInfo);
900                if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation))
901                {
902                  bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
903    
904                  Message message =
905                          ERR_SASLDIGESTMD5_AUTHZID_INSUFFICIENT_PRIVILEGES.get(
906                                  String.valueOf(userEntry.getDN()));
907                  bindOperation.setAuthFailureReason(message);
908                  return;
909                }
910    
911                if (authzDN.isNullDN())
912                {
913                  authZEntry = null;
914                }
915                else
916                {
917                  try
918                  {
919                    authZEntry = DirectoryServer.getEntry(authzDN);
920                    if (authZEntry == null)
921                    {
922                      bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
923    
924                      Message message = ERR_SASLDIGESTMD5_AUTHZID_NO_SUCH_ENTRY.get(
925                              String.valueOf(authzDN));
926                      bindOperation.setAuthFailureReason(message);
927                      return;
928                    }
929                  }
930                  catch (DirectoryException de)
931                  {
932                    if (debugEnabled())
933                    {
934                      TRACER.debugCaught(DebugLogLevel.ERROR, de);
935                    }
936    
937                    bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
938    
939                    Message message = ERR_SASLDIGESTMD5_AUTHZID_CANNOT_GET_ENTRY
940                            .get(String.valueOf(authzDN), de.getMessageObject());
941                    bindOperation.setAuthFailureReason(message);
942                    return;
943                  }
944                }
945              }
946            }
947            else
948            {
949              String idStr;
950              if (lowerAuthzID.startsWith("u:"))
951              {
952                idStr = responseAuthzID.substring(2);
953              }
954              else
955              {
956                idStr = responseAuthzID;
957              }
958    
959              if (idStr.length() == 0)
960              {
961                authZEntry = null;
962              }
963              else
964              {
965                try
966                {
967                  authZEntry = identityMapper.getEntryForID(idStr);
968                  if (authZEntry == null)
969                  {
970                    bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
971    
972                    Message message = ERR_SASLDIGESTMD5_AUTHZID_NO_MAPPED_ENTRY.get(
973                            responseAuthzID);
974                    bindOperation.setAuthFailureReason(message);
975                    return;
976                  }
977                }
978                catch (DirectoryException de)
979                {
980                  if (debugEnabled())
981                  {
982                    TRACER.debugCaught(DebugLogLevel.ERROR, de);
983                  }
984    
985                  bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
986    
987                  Message message = ERR_SASLDIGESTMD5_CANNOT_MAP_AUTHZID.get(
988                          responseAuthzID, de.getMessageObject());
989                  bindOperation.setAuthFailureReason(message);
990                  return;
991                }
992              }
993    
994              if ((authZEntry == null) ||
995                  (! authZEntry.getDN().equals(userEntry.getDN())))
996              {
997                AuthenticationInfo tempAuthInfo =
998                  new AuthenticationInfo(userEntry,
999                           DirectoryServer.isRootDN(userEntry.getDN()));
1000                InternalClientConnection tempConn =
1001                     new InternalClientConnection(tempAuthInfo);
1002                if (! tempConn.hasPrivilege(Privilege.PROXIED_AUTH, bindOperation))
1003                {
1004                  bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
1005    
1006                  Message message =
1007                          ERR_SASLDIGESTMD5_AUTHZID_INSUFFICIENT_PRIVILEGES.get(
1008                                  String.valueOf(userEntry.getDN()));
1009                  bindOperation.setAuthFailureReason(message);
1010                  return;
1011                }
1012              }
1013            }
1014          }
1015        }
1016    
1017    
1018        // Get the clear-text passwords from the user entry, if there are any.
1019        List<ByteString> clearPasswords;
1020        try
1021        {
1022          PasswordPolicyState pwPolicyState =
1023               new PasswordPolicyState(userEntry, false);
1024          clearPasswords = pwPolicyState.getClearPasswords();
1025          if ((clearPasswords == null) || clearPasswords.isEmpty())
1026          {
1027            bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
1028    
1029            Message message = ERR_SASLDIGESTMD5_NO_REVERSIBLE_PASSWORDS.get(
1030                    String.valueOf(userEntry.getDN()));
1031            bindOperation.setAuthFailureReason(message);
1032            return;
1033          }
1034        }
1035        catch (Exception e)
1036        {
1037          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
1038    
1039          Message message = ERR_SASLDIGESTMD5_CANNOT_GET_REVERSIBLE_PASSWORDS.get(
1040                  String.valueOf(userEntry.getDN()),
1041                  String.valueOf(e));
1042          bindOperation.setAuthFailureReason(message);
1043          return;
1044        }
1045    
1046    
1047        // Iterate through the clear-text values and see if any of them can be used
1048        // in conjunction with the challenge to construct the provided digest.
1049        boolean matchFound    = false;
1050        byte[]  passwordBytes = null;
1051        for (ByteString clearPassword : clearPasswords)
1052        {
1053          byte[] generatedDigest;
1054          try
1055          {
1056            generatedDigest =
1057                 generateResponseDigest(responseUserName, responseAuthzID,
1058                                        clearPassword.value(), responseRealm,
1059                                        responseNonce, responseCNonce,
1060                                        responseNonceCountStr, responseDigestURI,
1061                                        responseQoP, responseCharset);
1062          }
1063          catch (Exception e)
1064          {
1065            if (debugEnabled())
1066            {
1067              TRACER.debugCaught(DebugLogLevel.ERROR, e);
1068            }
1069    
1070            logError(WARN_SASLDIGESTMD5_CANNOT_GENERATE_RESPONSE_DIGEST.get(
1071                getExceptionMessage(e)));
1072            continue;
1073          }
1074    
1075          if (Arrays.equals(responseDigest, generatedDigest))
1076          {
1077            matchFound    = true;
1078            passwordBytes = clearPassword.value();
1079            break;
1080          }
1081        }
1082    
1083        if (! matchFound)
1084        {
1085          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
1086    
1087          Message message = ERR_SASLDIGESTMD5_INVALID_CREDENTIALS.get();
1088          bindOperation.setAuthFailureReason(message);
1089          return;
1090        }
1091    
1092    
1093        // Generate the response auth element to include in the response to the
1094        // client.
1095        byte[] responseAuth;
1096        try
1097        {
1098          responseAuth =
1099               generateResponseAuthDigest(responseUserName, responseAuthzID,
1100                                          passwordBytes, responseRealm,
1101                                          responseNonce, responseCNonce,
1102                                          responseNonceCountStr, responseDigestURI,
1103                                          responseQoP, responseCharset);
1104        }
1105        catch (Exception e)
1106        {
1107          if (debugEnabled())
1108          {
1109            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1110          }
1111    
1112          bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
1113    
1114          Message message =
1115                  ERR_SASLDIGESTMD5_CANNOT_GENERATE_RESPONSE_AUTH_DIGEST.get(
1116                          getExceptionMessage(e));
1117          bindOperation.setAuthFailureReason(message);
1118          return;
1119        }
1120    
1121        ASN1OctetString responseAuthStr =
1122             new ASN1OctetString("rspauth=" + getHexString(responseAuth));
1123    
1124    
1125        // Make sure to store the updated nonce count with the client connection to
1126        // allow for correct subsequent authentication.
1127        stateInfo.setNonceCount(responseNonceCountStr);
1128    
1129    
1130        // If we've gotten here, then the authentication was successful.  We'll also
1131        // need to include the response auth string in the server SASL credentials.
1132        bindOperation.setResultCode(ResultCode.SUCCESS);
1133        bindOperation.setServerSASLCredentials(responseAuthStr);
1134    
1135    
1136        AuthenticationInfo authInfo =
1137             new AuthenticationInfo(userEntry, authZEntry,
1138                                    SASL_MECHANISM_DIGEST_MD5,
1139                                    DirectoryServer.isRootDN(userEntry.getDN()));
1140        bindOperation.setAuthenticationInfo(authInfo);
1141        return;
1142      }
1143    
1144    
1145    
1146      /**
1147       * Generates a new nonce value to use during the DIGEST-MD5 authentication
1148       * process.
1149       *
1150       * @return  The nonce that should be used for DIGEST-MD5 authentication.
1151       */
1152      private String generateNonce()
1153      {
1154        byte[] nonceBytes = new byte[16];
1155        randomGenerator.nextBytes(nonceBytes);
1156        return Base64.encode(nonceBytes);
1157      }
1158    
1159    
1160    
1161      /**
1162       * Reads the next token from the provided credentials string using the
1163       * provided information.  If the token is surrounded by quotation marks, then
1164       * the token returned will not include those quotation marks.
1165       *
1166       * @param  credentials  The credentials string from which to read the token.
1167       * @param  startPos     The position of the first character of the token to
1168       *                      read.
1169       * @param  length       The total number of characters in the credentials
1170       *                      string.
1171       * @param  token        The buffer into which the token is to be placed.
1172       *
1173       * @return  The position at which the next token should start, or a value
1174       *          greater than or equal to the length of the string if there are no
1175       *          more tokens.
1176       *
1177       * @throws  DirectoryException  If a problem occurs while attempting to read
1178       *                              the token.
1179       */
1180      private int readToken(String credentials, int startPos, int length,
1181                            StringBuilder token)
1182              throws DirectoryException
1183      {
1184        // If the position is greater than or equal to the length, then we shouldn't
1185        // do anything.
1186        if (startPos >= length)
1187        {
1188          return startPos;
1189        }
1190    
1191    
1192        // Look at the first character to see if it's an empty string or the string
1193        // is quoted.
1194        boolean isEscaped = false;
1195        boolean isQuoted  = false;
1196        int     pos       = startPos;
1197        char    c         = credentials.charAt(pos++);
1198    
1199        if (c == ',')
1200        {
1201          // This must be a zero-length token, so we'll just return the next
1202          // position.
1203          return pos;
1204        }
1205        else if (c == '"')
1206        {
1207          // The string is quoted, so we'll ignore this character, and we'll keep
1208          // reading until we find the unescaped closing quote followed by a comma
1209          // or the end of the string.
1210          isQuoted = true;
1211        }
1212        else if (c == '\\')
1213        {
1214          // The next character is escaped, so we'll take it no matter what.
1215          isEscaped = true;
1216        }
1217        else
1218        {
1219          // The string is not quoted, and this is the first character.  Store this
1220          // character and keep reading until we find a comma or the end of the
1221          // string.
1222          token.append(c);
1223        }
1224    
1225    
1226        // Enter a loop, reading until we find the appropriate criteria for the end
1227        // of the token.
1228        while (pos < length)
1229        {
1230          c = credentials.charAt(pos++);
1231    
1232          if (isEscaped)
1233          {
1234            // The previous character was an escape, so we'll take this no matter
1235            // what.
1236            token.append(c);
1237            isEscaped = false;
1238          }
1239          else if (c == ',')
1240          {
1241            // If this is a quoted string, then this comma is part of the token.
1242            // Otherwise, it's the end of the token.
1243            if (isQuoted)
1244            {
1245              token.append(c);
1246            }
1247            else
1248            {
1249              break;
1250            }
1251          }
1252          else if (c == '"')
1253          {
1254            if (isQuoted)
1255            {
1256              // This should be the end of the token, but in order for it to be
1257              // valid it must be followed by a comma or the end of the string.
1258              if (pos >= length)
1259              {
1260                // We have hit the end of the string, so this is fine.
1261                break;
1262              }
1263              else
1264              {
1265                char c2 = credentials.charAt(pos++);
1266                if (c2 == ',')
1267                {
1268                  // We have hit the end of the token, so this is fine.
1269                  break;
1270                }
1271                else
1272                {
1273                  // We found the closing quote before the end of the token.  This
1274                  // is not fine.
1275                  Message message =
1276                      ERR_SASLDIGESTMD5_INVALID_CLOSING_QUOTE_POS.get((pos-2));
1277                  throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
1278                                               message);
1279                }
1280              }
1281            }
1282            else
1283            {
1284              // This must be part of the value, so we'll take it.
1285              token.append(c);
1286            }
1287          }
1288          else if (c == '\\')
1289          {
1290            // The next character is escaped.  We'll set a flag so we know to
1291            // accept it, but will not include the backspace itself.
1292            isEscaped = true;
1293          }
1294          else
1295          {
1296            token.append(c);
1297          }
1298        }
1299    
1300    
1301        return pos;
1302      }
1303    
1304    
1305    
1306      /**
1307       * Generates the appropriate DIGEST-MD5 response for the provided set of
1308       * information.
1309       *
1310       * @param  userName    The username from the authentication request.
1311       * @param  authzID     The authorization ID from the request, or
1312       *                     <CODE>null</CODE> if there is none.
1313       * @param  password    The clear-text password for the user.
1314       * @param  realm       The realm for which the authentication is to be
1315       *                     performed.
1316       * @param  nonce       The random data generated by the server for use in the
1317       *                     digest.
1318       * @param  cnonce      The random data generated by the client for use in the
1319       *                     digest.
1320       * @param  nonceCount  The 8-digit hex string indicating the number of times
1321       *                     the provided nonce has been used by the client.
1322       * @param  digestURI   The digest URI that specifies the service and host for
1323       *                     which the authentication is being performed.
1324       * @param  qop         The quality of protection string for the
1325       *                     authentication.
1326       * @param  charset     The character set used to encode the information.
1327       *
1328       * @return  The DIGEST-MD5 response for the provided set of information.
1329       *
1330       * @throws  UnsupportedEncodingException  If the specified character set is
1331       *                                        invalid for some reason.
1332       */
1333      public byte[] generateResponseDigest(String userName, String authzID,
1334                                           byte[] password, String realm,
1335                                           String nonce, String cnonce,
1336                                           String nonceCount, String digestURI,
1337                                           String qop, String charset)
1338             throws UnsupportedEncodingException
1339      {
1340        synchronized (digestLock)
1341        {
1342          // First, get a hash of "username:realm:password".
1343          StringBuilder a1String1 = new StringBuilder();
1344          a1String1.append(userName);
1345          a1String1.append(':');
1346          a1String1.append(realm);
1347          a1String1.append(':');
1348    
1349          byte[] a1Bytes1a = a1String1.toString().getBytes(charset);
1350          byte[] a1Bytes1  = new byte[a1Bytes1a.length + password.length];
1351          System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length);
1352          System.arraycopy(password, 0, a1Bytes1, a1Bytes1a.length,
1353                           password.length);
1354          byte[] urpHash = md5Digest.digest(a1Bytes1);
1355    
1356    
1357          // Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
1358          StringBuilder a1String2 = new StringBuilder();
1359          a1String2.append(':');
1360          a1String2.append(nonce);
1361          a1String2.append(':');
1362          a1String2.append(cnonce);
1363          if (authzID != null)
1364          {
1365            a1String2.append(':');
1366            a1String2.append(authzID);
1367          }
1368          byte[] a1Bytes2a = a1String2.toString().getBytes(charset);
1369          byte[] a1Bytes2  = new byte[urpHash.length + a1Bytes2a.length];
1370          System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length);
1371          System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length,
1372                           a1Bytes2a.length);
1373          byte[] a1Hash = md5Digest.digest(a1Bytes2);
1374    
1375    
1376          // Next, get a hash of "AUTHENTICATE:digesturi".
1377          byte[] a2Bytes = ("AUTHENTICATE:" + digestURI).getBytes(charset);
1378          byte[] a2Hash  = md5Digest.digest(a2Bytes);
1379    
1380    
1381          // Get hex string representations of the last two hashes.
1382          String a1HashHex = getHexString(a1Hash);
1383          String a2HashHex = getHexString(a2Hash);
1384    
1385    
1386          // Put together the final string to hash, consisting of
1387          // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
1388          StringBuilder kdString = new StringBuilder();
1389          kdString.append(a1HashHex);
1390          kdString.append(':');
1391          kdString.append(nonce);
1392          kdString.append(':');
1393          kdString.append(nonceCount);
1394          kdString.append(':');
1395          kdString.append(cnonce);
1396          kdString.append(':');
1397          kdString.append(qop);
1398          kdString.append(':');
1399          kdString.append(a2HashHex);
1400          return md5Digest.digest(kdString.toString().getBytes(charset));
1401        }
1402      }
1403    
1404    
1405    
1406      /**
1407       * Generates the appropriate DIGEST-MD5 rspauth digest using the provided
1408       * information.
1409       *
1410       * @param  userName    The username from the authentication request.
1411       * @param  authzID     The authorization ID from the request, or
1412       *                     <CODE>null</CODE> if there is none.
1413       * @param  password    The clear-text password for the user.
1414       * @param  realm       The realm for which the authentication is to be
1415       *                     performed.
1416       * @param  nonce       The random data generated by the server for use in the
1417       *                     digest.
1418       * @param  cnonce      The random data generated by the client for use in the
1419       *                     digest.
1420       * @param  nonceCount  The 8-digit hex string indicating the number of times
1421       *                     the provided nonce has been used by the client.
1422       * @param  digestURI   The digest URI that specifies the service and host for
1423       *                     which the authentication is being performed.
1424       * @param  qop         The quality of protection string for the
1425       *                     authentication.
1426       * @param  charset     The character set used to encode the information.
1427       *
1428       * @return  The DIGEST-MD5 response for the provided set of information.
1429       *
1430       * @throws  UnsupportedEncodingException  If the specified character set is
1431       *                                        invalid for some reason.
1432       */
1433      public byte[] generateResponseAuthDigest(String userName, String authzID,
1434                                               byte[] password, String realm,
1435                                               String nonce, String cnonce,
1436                                               String nonceCount, String digestURI,
1437                                               String qop, String charset)
1438             throws UnsupportedEncodingException
1439      {
1440        synchronized (digestLock)
1441        {
1442          // First, get a hash of "username:realm:password".
1443          StringBuilder a1String1 = new StringBuilder();
1444          a1String1.append(userName);
1445          a1String1.append(':');
1446          a1String1.append(realm);
1447          a1String1.append(':');
1448    
1449          byte[] a1Bytes1a = a1String1.toString().getBytes(charset);
1450          byte[] a1Bytes1  = new byte[a1Bytes1a.length + password.length];
1451          System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length);
1452          System.arraycopy(password, 0, a1Bytes1, a1Bytes1a.length,
1453                           password.length);
1454          byte[] urpHash = md5Digest.digest(a1Bytes1);
1455    
1456    
1457          // Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
1458          StringBuilder a1String2 = new StringBuilder();
1459          a1String2.append(':');
1460          a1String2.append(nonce);
1461          a1String2.append(':');
1462          a1String2.append(cnonce);
1463          if (authzID != null)
1464          {
1465            a1String2.append(':');
1466            a1String2.append(authzID);
1467          }
1468          byte[] a1Bytes2a = a1String2.toString().getBytes(charset);
1469          byte[] a1Bytes2  = new byte[urpHash.length + a1Bytes2a.length];
1470          System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length);
1471          System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length,
1472                           a1Bytes2a.length);
1473          byte[] a1Hash = md5Digest.digest(a1Bytes2);
1474    
1475    
1476          // Next, get a hash of "AUTHENTICATE:digesturi".
1477          String a2String = ":" + digestURI;
1478          if (qop.equals("auth-int") || qop.equals("auth-conf"))
1479          {
1480            a2String += ":00000000000000000000000000000000";
1481          }
1482          byte[] a2Bytes = a2String.getBytes(charset);
1483          byte[] a2Hash  = md5Digest.digest(a2Bytes);
1484    
1485    
1486          // Get hex string representations of the last two hashes.
1487          String a1HashHex = getHexString(a1Hash);
1488          String a2HashHex = getHexString(a2Hash);
1489    
1490    
1491          // Put together the final string to hash, consisting of
1492          // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
1493          StringBuilder kdString = new StringBuilder();
1494          kdString.append(a1HashHex);
1495          kdString.append(':');
1496          kdString.append(nonce);
1497          kdString.append(':');
1498          kdString.append(nonceCount);
1499          kdString.append(':');
1500          kdString.append(cnonce);
1501          kdString.append(':');
1502          kdString.append(qop);
1503          kdString.append(':');
1504          kdString.append(a2HashHex);
1505          return md5Digest.digest(kdString.toString().getBytes(charset));
1506        }
1507      }
1508    
1509    
1510    
1511      /**
1512       * Retrieves a hexadecimal string representation of the contents of the
1513       * provided byte array.
1514       *
1515       * @param  byteArray  The byte array for which to obtain the hexadecimal
1516       *                    string representation.
1517       *
1518       * @return  The hexadecimal string representation of the contents of the
1519       *          provided byte array.
1520       */
1521      private String getHexString(byte[] byteArray)
1522      {
1523        StringBuilder buffer = new StringBuilder(2*byteArray.length);
1524        for (byte b : byteArray)
1525        {
1526          buffer.append(byteToLowerHex(b));
1527        }
1528    
1529        return buffer.toString();
1530      }
1531    
1532    
1533    
1534      /**
1535       * {@inheritDoc}
1536       */
1537      @Override()
1538      public boolean isPasswordBased(String mechanism)
1539      {
1540        // This is a password-based mechanism.
1541        return true;
1542      }
1543    
1544    
1545    
1546      /**
1547       * {@inheritDoc}
1548       */
1549      @Override()
1550      public boolean isSecure(String mechanism)
1551      {
1552        // This may be considered a secure mechanism.
1553        return true;
1554      }
1555    
1556    
1557    
1558      /**
1559       * {@inheritDoc}
1560       */
1561      @Override()
1562      public boolean isConfigurationAcceptable(
1563                          SASLMechanismHandlerCfg configuration,
1564                          List<Message> unacceptableReasons)
1565      {
1566        DigestMD5SASLMechanismHandlerCfg config =
1567             (DigestMD5SASLMechanismHandlerCfg) configuration;
1568        return isConfigurationChangeAcceptable(config, unacceptableReasons);
1569      }
1570    
1571    
1572    
1573      /**
1574       * {@inheritDoc}
1575       */
1576      public boolean isConfigurationChangeAcceptable(
1577                          DigestMD5SASLMechanismHandlerCfg configuration,
1578                          List<Message> unacceptableReasons)
1579      {
1580        return true;
1581      }
1582    
1583    
1584    
1585      /**
1586       * {@inheritDoc}
1587       */
1588      public ConfigChangeResult applyConfigurationChange(
1589                  DigestMD5SASLMechanismHandlerCfg configuration)
1590      {
1591        ResultCode        resultCode          = ResultCode.SUCCESS;
1592        boolean           adminActionRequired = false;
1593        ArrayList<Message> messages            = new ArrayList<Message>();
1594    
1595        // Get the identity mapper that should be used to find users.
1596        DN identityMapperDN = configuration.getIdentityMapperDN();
1597        identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
1598        currentConfig  = configuration;
1599    
1600        return new ConfigChangeResult(resultCode, adminActionRequired, messages);
1601      }
1602    }
1603