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