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