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