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.security.MessageDigest; 032 import java.security.SecureRandom; 033 import java.text.ParseException; 034 import java.util.ArrayList; 035 import java.util.Arrays; 036 import java.util.List; 037 import java.util.concurrent.locks.Lock; 038 039 import org.opends.messages.Message; 040 import org.opends.server.admin.server.ConfigurationChangeListener; 041 import org.opends.server.admin.std.server.CramMD5SASLMechanismHandlerCfg; 042 import org.opends.server.admin.std.server.SASLMechanismHandlerCfg; 043 import org.opends.server.api.ClientConnection; 044 import org.opends.server.api.IdentityMapper; 045 import org.opends.server.api.SASLMechanismHandler; 046 import org.opends.server.config.ConfigException; 047 import org.opends.server.core.BindOperation; 048 import org.opends.server.core.DirectoryServer; 049 import org.opends.server.core.PasswordPolicyState; 050 import org.opends.server.loggers.debug.DebugTracer; 051 import org.opends.server.protocols.asn1.ASN1OctetString; 052 import org.opends.server.types.AuthenticationInfo; 053 import org.opends.server.types.ByteString; 054 import org.opends.server.types.ConfigChangeResult; 055 import org.opends.server.types.DebugLogLevel; 056 import org.opends.server.types.DirectoryException; 057 import org.opends.server.types.DN; 058 import org.opends.server.types.Entry; 059 import org.opends.server.types.InitializationException; 060 import org.opends.server.types.LockManager; 061 import org.opends.server.types.ResultCode; 062 063 import static org.opends.messages.ExtensionMessages.*; 064 import static org.opends.server.loggers.debug.DebugLogger.*; 065 import static org.opends.server.util.ServerConstants.*; 066 import static org.opends.server.util.StaticUtils.*; 067 068 069 070 /** 071 * This class provides an implementation of a SASL mechanism that uses digest 072 * authentication via CRAM-MD5. This is a password-based mechanism that does 073 * not expose the password itself over the wire but rather uses an MD5 hash that 074 * proves the client knows the password. This is similar to the DIGEST-MD5 075 * mechanism, and the primary differences are that CRAM-MD5 only obtains random 076 * data from the server (whereas DIGEST-MD5 uses random data from both the 077 * server and the client), CRAM-MD5 does not allow for an authorization ID in 078 * addition to the authentication ID where DIGEST-MD5 does, and CRAM-MD5 does 079 * not define any integrity and confidentiality mechanisms where DIGEST-MD5 080 * does. This implementation is based on the proposal defined in 081 * draft-ietf-sasl-crammd5-05. 082 */ 083 public class CRAMMD5SASLMechanismHandler 084 extends SASLMechanismHandler<CramMD5SASLMechanismHandlerCfg> 085 implements ConfigurationChangeListener< 086 CramMD5SASLMechanismHandlerCfg> 087 { 088 /** 089 * The tracer object for the debug logger. 090 */ 091 private static final DebugTracer TRACER = getTracer(); 092 093 // An array filled with the inner pad byte. 094 private byte[] iPad; 095 096 // An array filled with the outer pad byte. 097 private byte[] oPad; 098 099 // The current configuration for this SASL mechanism handler. 100 private CramMD5SASLMechanismHandlerCfg currentConfig; 101 102 // The identity mapper that will be used to map ID strings to user entries. 103 private IdentityMapper<?> identityMapper; 104 105 // The message digest engine that will be used to create the MD5 digests. 106 private MessageDigest md5Digest; 107 108 // The lock that will be used to provide threadsafe access to the message 109 // digest. 110 private Object digestLock; 111 112 // The random number generator that we will use to create the server 113 // challenge. 114 private SecureRandom randomGenerator; 115 116 117 118 /** 119 * Creates a new instance of this SASL mechanism handler. No initialization 120 * should be done in this method, as it should all be performed in the 121 * <CODE>initializeSASLMechanismHandler</CODE> method. 122 */ 123 public CRAMMD5SASLMechanismHandler() 124 { 125 super(); 126 } 127 128 129 130 /** 131 * {@inheritDoc} 132 */ 133 @Override() 134 public void initializeSASLMechanismHandler( 135 CramMD5SASLMechanismHandlerCfg configuration) 136 throws ConfigException, InitializationException 137 { 138 configuration.addCramMD5ChangeListener(this); 139 currentConfig = configuration; 140 141 // Initialize the variables needed for the MD5 digest creation. 142 digestLock = new Object(); 143 randomGenerator = new SecureRandom(); 144 145 try 146 { 147 md5Digest = MessageDigest.getInstance("MD5"); 148 } 149 catch (Exception e) 150 { 151 if (debugEnabled()) 152 { 153 TRACER.debugCaught(DebugLogLevel.ERROR, e); 154 } 155 156 Message message = 157 ERR_SASLCRAMMD5_CANNOT_GET_MESSAGE_DIGEST.get(getExceptionMessage(e)); 158 throw new InitializationException(message, e); 159 } 160 161 162 // Create and fill the iPad and oPad arrays. 163 iPad = new byte[HMAC_MD5_BLOCK_LENGTH]; 164 oPad = new byte[HMAC_MD5_BLOCK_LENGTH]; 165 Arrays.fill(iPad, CRAMMD5_IPAD_BYTE); 166 Arrays.fill(oPad, CRAMMD5_OPAD_BYTE); 167 168 169 // Get the identity mapper that should be used to find users. 170 DN identityMapperDN = configuration.getIdentityMapperDN(); 171 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); 172 173 DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_CRAM_MD5, this); 174 } 175 176 177 178 /** 179 * {@inheritDoc} 180 */ 181 @Override() 182 public void finalizeSASLMechanismHandler() 183 { 184 currentConfig.removeCramMD5ChangeListener(this); 185 DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_CRAM_MD5); 186 } 187 188 189 190 191 /** 192 * {@inheritDoc} 193 */ 194 @Override() 195 public void processSASLBind(BindOperation bindOperation) 196 { 197 // The CRAM-MD5 bind process uses two stages. See if the client provided 198 // any credentials. If not, then we're in the first stage so we'll send the 199 // challenge to the client. 200 ByteString clientCredentials = bindOperation.getSASLCredentials(); 201 ClientConnection clientConnection = bindOperation.getClientConnection(); 202 if (clientCredentials == null) 203 { 204 // The client didn't provide any credentials, so this is the initial 205 // request. Generate some random data to send to the client as the 206 // challenge and store it in the client connection so we can verify the 207 // credentials provided by the client later. 208 byte[] challengeBytes = new byte[16]; 209 randomGenerator.nextBytes(challengeBytes); 210 StringBuilder challengeString = new StringBuilder(18); 211 challengeString.append('<'); 212 for (byte b : challengeBytes) 213 { 214 challengeString.append(byteToLowerHex(b)); 215 } 216 challengeString.append('>'); 217 218 ASN1OctetString challenge = 219 new ASN1OctetString(challengeString.toString()); 220 clientConnection.setSASLAuthStateInfo(challenge); 221 bindOperation.setServerSASLCredentials(challenge); 222 bindOperation.setResultCode(ResultCode.SASL_BIND_IN_PROGRESS); 223 return; 224 } 225 226 227 // If we've gotten here, then the client did provide credentials. First, 228 // make sure that we have a stored version of the credentials associated 229 // with the client connection. If not, then it likely means that the client 230 // is trying to pull a fast one on us. 231 Object saslStateInfo = clientConnection.getSASLAuthStateInfo(); 232 if (saslStateInfo == null) 233 { 234 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 235 236 Message message = ERR_SASLCRAMMD5_NO_STORED_CHALLENGE.get(); 237 bindOperation.setAuthFailureReason(message); 238 return; 239 } 240 241 if (! (saslStateInfo instanceof ASN1OctetString)) 242 { 243 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 244 245 Message message = ERR_SASLCRAMMD5_INVALID_STORED_CHALLENGE.get(); 246 bindOperation.setAuthFailureReason(message); 247 return; 248 } 249 250 ASN1OctetString challenge = (ASN1OctetString) saslStateInfo; 251 252 // Wipe out the stored challenge so it can't be used again. 253 clientConnection.setSASLAuthStateInfo(null); 254 255 256 // Now look at the client credentials and make sure that we can decode them. 257 // It should be a username followed by a space and a digest string. Since 258 // the username itself may contain spaces but the digest string may not, 259 // look for the last space and use it as the delimiter. 260 String credString = clientCredentials.stringValue(); 261 int spacePos = credString.lastIndexOf(' '); 262 if (spacePos < 0) 263 { 264 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 265 266 Message message = ERR_SASLCRAMMD5_NO_SPACE_IN_CREDENTIALS.get(); 267 bindOperation.setAuthFailureReason(message); 268 return; 269 } 270 271 String userName = credString.substring(0, spacePos); 272 String digest = credString.substring(spacePos+1); 273 274 275 // Look at the digest portion of the provided credentials. It must have a 276 // length of exactly 32 bytes and be comprised only of hex characters. 277 if (digest.length() != (2*MD5_DIGEST_LENGTH)) 278 { 279 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 280 281 Message message = ERR_SASLCRAMMD5_INVALID_DIGEST_LENGTH.get( 282 digest.length(), 283 (2*MD5_DIGEST_LENGTH)); 284 bindOperation.setAuthFailureReason(message); 285 return; 286 } 287 288 byte[] digestBytes; 289 try 290 { 291 digestBytes = hexStringToByteArray(digest); 292 } 293 catch (ParseException pe) 294 { 295 if (debugEnabled()) 296 { 297 TRACER.debugCaught(DebugLogLevel.ERROR, pe); 298 } 299 300 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 301 302 Message message = ERR_SASLCRAMMD5_INVALID_DIGEST_CONTENT.get( 303 pe.getMessage()); 304 bindOperation.setAuthFailureReason(message); 305 return; 306 } 307 308 309 // Get the user entry for the authentication ID. Allow for an 310 // authentication ID that is just a username (as per the CRAM-MD5 spec), but 311 // also allow a value in the authzid form specified in RFC 2829. 312 Entry userEntry = null; 313 String lowerUserName = toLowerCase(userName); 314 if (lowerUserName.startsWith("dn:")) 315 { 316 // Try to decode the user DN and retrieve the corresponding entry. 317 DN userDN; 318 try 319 { 320 userDN = DN.decode(userName.substring(3)); 321 } 322 catch (DirectoryException de) 323 { 324 if (debugEnabled()) 325 { 326 TRACER.debugCaught(DebugLogLevel.ERROR, de); 327 } 328 329 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 330 331 Message message = ERR_SASLCRAMMD5_CANNOT_DECODE_USERNAME_AS_DN.get( 332 userName, de.getMessageObject()); 333 bindOperation.setAuthFailureReason(message); 334 return; 335 } 336 337 if (userDN.isNullDN()) 338 { 339 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 340 341 Message message = ERR_SASLCRAMMD5_USERNAME_IS_NULL_DN.get(); 342 bindOperation.setAuthFailureReason(message); 343 return; 344 } 345 346 DN rootDN = DirectoryServer.getActualRootBindDN(userDN); 347 if (rootDN != null) 348 { 349 userDN = rootDN; 350 } 351 352 // Acquire a read lock on the user entry. If this fails, then so will the 353 // authentication. 354 Lock readLock = null; 355 for (int i=0; i < 3; i++) 356 { 357 readLock = LockManager.lockRead(userDN); 358 if (readLock != null) 359 { 360 break; 361 } 362 } 363 364 if (readLock == null) 365 { 366 bindOperation.setResultCode(DirectoryServer.getServerErrorResultCode()); 367 368 Message message = INFO_SASLCRAMMD5_CANNOT_LOCK_ENTRY.get( 369 String.valueOf(userDN)); 370 bindOperation.setAuthFailureReason(message); 371 return; 372 } 373 374 try 375 { 376 userEntry = DirectoryServer.getEntry(userDN); 377 } 378 catch (DirectoryException de) 379 { 380 if (debugEnabled()) 381 { 382 TRACER.debugCaught(DebugLogLevel.ERROR, de); 383 } 384 385 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 386 387 Message message = ERR_SASLCRAMMD5_CANNOT_GET_ENTRY_BY_DN.get( 388 String.valueOf(userDN), de.getMessageObject()); 389 bindOperation.setAuthFailureReason(message); 390 return; 391 } 392 finally 393 { 394 LockManager.unlock(userDN, readLock); 395 } 396 } 397 else 398 { 399 // Use the identity mapper to resolve the username to an entry. 400 if (lowerUserName.startsWith("u:")) 401 { 402 userName = userName.substring(2); 403 } 404 405 try 406 { 407 userEntry = identityMapper.getEntryForID(userName); 408 } 409 catch (DirectoryException de) 410 { 411 if (debugEnabled()) 412 { 413 TRACER.debugCaught(DebugLogLevel.ERROR, de); 414 } 415 416 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 417 418 Message message = ERR_SASLCRAMMD5_CANNOT_MAP_USERNAME.get( 419 String.valueOf(userName), de.getMessageObject()); 420 bindOperation.setAuthFailureReason(message); 421 return; 422 } 423 } 424 425 426 // At this point, we should have a user entry. If we don't then fail. 427 if (userEntry == null) 428 { 429 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 430 431 Message message = ERR_SASLCRAMMD5_NO_MATCHING_ENTRIES.get(userName); 432 bindOperation.setAuthFailureReason(message); 433 return; 434 } 435 else 436 { 437 bindOperation.setSASLAuthUserEntry(userEntry); 438 } 439 440 441 // Get the clear-text passwords from the user entry, if there are any. 442 List<ByteString> clearPasswords; 443 try 444 { 445 PasswordPolicyState pwPolicyState = 446 new PasswordPolicyState(userEntry, false); 447 clearPasswords = pwPolicyState.getClearPasswords(); 448 if ((clearPasswords == null) || clearPasswords.isEmpty()) 449 { 450 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 451 452 Message message = ERR_SASLCRAMMD5_NO_REVERSIBLE_PASSWORDS.get( 453 String.valueOf(userEntry.getDN())); 454 bindOperation.setAuthFailureReason(message); 455 return; 456 } 457 } 458 catch (Exception e) 459 { 460 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 461 462 Message message = ERR_SASLCRAMMD5_CANNOT_GET_REVERSIBLE_PASSWORDS.get( 463 String.valueOf(userEntry.getDN()), 464 String.valueOf(e)); 465 bindOperation.setAuthFailureReason(message); 466 return; 467 } 468 469 470 // Iterate through the clear-text values and see if any of them can be used 471 // in conjunction with the challenge to construct the provided digest. 472 boolean matchFound = false; 473 for (ByteString clearPassword : clearPasswords) 474 { 475 byte[] generatedDigest = generateDigest(clearPassword, challenge); 476 if (Arrays.equals(digestBytes, generatedDigest)) 477 { 478 matchFound = true; 479 break; 480 } 481 } 482 483 if (! matchFound) 484 { 485 bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS); 486 487 Message message = ERR_SASLCRAMMD5_INVALID_PASSWORD.get(); 488 bindOperation.setAuthFailureReason(message); 489 return; 490 } 491 492 493 // If we've gotten here, then the authentication was successful. 494 bindOperation.setResultCode(ResultCode.SUCCESS); 495 496 AuthenticationInfo authInfo = 497 new AuthenticationInfo(userEntry, SASL_MECHANISM_CRAM_MD5, 498 DirectoryServer.isRootDN(userEntry.getDN())); 499 bindOperation.setAuthenticationInfo(authInfo); 500 return; 501 } 502 503 504 505 /** 506 * Generates the appropriate HMAC-MD5 digest for a CRAM-MD5 authentication 507 * with the given information. 508 * 509 * @param password The clear-text password to use when generating the 510 * digest. 511 * @param challenge The server-supplied challenge to use when generating the 512 * digest. 513 * 514 * @return The generated HMAC-MD5 digest for CRAM-MD5 authentication. 515 */ 516 private byte[] generateDigest(ByteString password, ByteString challenge) 517 { 518 // Get the byte arrays backing the password and challenge. 519 byte[] p = password.value(); 520 byte[] c = challenge.value(); 521 522 523 // Grab a lock to protect the MD5 digest generation. 524 synchronized (digestLock) 525 { 526 // If the password is longer than the HMAC-MD5 block length, then use an 527 // MD5 digest of the password rather than the password itself. 528 if (p.length > HMAC_MD5_BLOCK_LENGTH) 529 { 530 p = md5Digest.digest(p); 531 } 532 533 534 // Create byte arrays with data needed for the hash generation. 535 byte[] iPadAndData = new byte[HMAC_MD5_BLOCK_LENGTH + c.length]; 536 System.arraycopy(iPad, 0, iPadAndData, 0, HMAC_MD5_BLOCK_LENGTH); 537 System.arraycopy(c, 0, iPadAndData, HMAC_MD5_BLOCK_LENGTH, c.length); 538 539 byte[] oPadAndHash = new byte[HMAC_MD5_BLOCK_LENGTH + MD5_DIGEST_LENGTH]; 540 System.arraycopy(oPad, 0, oPadAndHash, 0, HMAC_MD5_BLOCK_LENGTH); 541 542 543 // Iterate through the bytes in the key and XOR them with the iPad and 544 // oPad as appropriate. 545 for (int i=0; i < p.length; i++) 546 { 547 iPadAndData[i] ^= p[i]; 548 oPadAndHash[i] ^= p[i]; 549 } 550 551 552 // Copy an MD5 digest of the iPad-XORed key and the data into the array to 553 // be hashed. 554 System.arraycopy(md5Digest.digest(iPadAndData), 0, oPadAndHash, 555 HMAC_MD5_BLOCK_LENGTH, MD5_DIGEST_LENGTH); 556 557 558 // Return an MD5 digest of the resulting array. 559 return md5Digest.digest(oPadAndHash); 560 } 561 } 562 563 564 565 /** 566 * {@inheritDoc} 567 */ 568 @Override() 569 public boolean isPasswordBased(String mechanism) 570 { 571 // This is a password-based mechanism. 572 return true; 573 } 574 575 576 577 /** 578 * {@inheritDoc} 579 */ 580 @Override() 581 public boolean isSecure(String mechanism) 582 { 583 // This may be considered a secure mechanism. 584 return true; 585 } 586 587 588 589 /** 590 * {@inheritDoc} 591 */ 592 @Override() 593 public boolean isConfigurationAcceptable( 594 SASLMechanismHandlerCfg configuration, 595 List<Message> unacceptableReasons) 596 { 597 CramMD5SASLMechanismHandlerCfg config = 598 (CramMD5SASLMechanismHandlerCfg) configuration; 599 return isConfigurationChangeAcceptable(config, unacceptableReasons); 600 } 601 602 603 604 /** 605 * {@inheritDoc} 606 */ 607 public boolean isConfigurationChangeAcceptable( 608 CramMD5SASLMechanismHandlerCfg configuration, 609 List<Message> unacceptableReasons) 610 { 611 return true; 612 } 613 614 615 616 /** 617 * {@inheritDoc} 618 */ 619 public ConfigChangeResult applyConfigurationChange( 620 CramMD5SASLMechanismHandlerCfg configuration) 621 { 622 ResultCode resultCode = ResultCode.SUCCESS; 623 boolean adminActionRequired = false; 624 ArrayList<Message> messages = new ArrayList<Message>(); 625 626 DN identityMapperDN = configuration.getIdentityMapperDN(); 627 identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN); 628 currentConfig = configuration; 629 630 return new ConfigChangeResult(resultCode, adminActionRequired, messages); 631 } 632 } 633