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