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