001    /*
002     * Copyright 2007-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2016 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.ldap.sdk;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.List;
027    import java.util.logging.Level;
028    import javax.security.auth.callback.Callback;
029    import javax.security.auth.callback.CallbackHandler;
030    import javax.security.auth.callback.NameCallback;
031    import javax.security.auth.callback.PasswordCallback;
032    import javax.security.sasl.Sasl;
033    import javax.security.sasl.SaslClient;
034    
035    import com.unboundid.asn1.ASN1OctetString;
036    import com.unboundid.util.DebugType;
037    import com.unboundid.util.InternalUseOnly;
038    import com.unboundid.util.NotMutable;
039    import com.unboundid.util.ThreadSafety;
040    import com.unboundid.util.ThreadSafetyLevel;
041    
042    import static com.unboundid.ldap.sdk.LDAPMessages.*;
043    import static com.unboundid.util.Debug.*;
044    import static com.unboundid.util.StaticUtils.*;
045    import static com.unboundid.util.Validator.*;
046    
047    
048    
049    /**
050     * This class provides a SASL CRAM-MD5 bind request implementation as described
051     * in draft-ietf-sasl-crammd5.  The CRAM-MD5 mechanism can be used to
052     * authenticate over an insecure channel without exposing the credentials
053     * (although it requires that the server have access to the clear-text
054     * password).    It is similar to DIGEST-MD5, but does not provide as many
055     * options, and provides slightly weaker protection because the client does not
056     * contribute any of the random data used during bind processing.
057     * <BR><BR>
058     * Elements included in a CRAM-MD5 bind request include:
059     * <UL>
060     *   <LI>Authentication ID -- A string which identifies the user that is
061     *       attempting to authenticate.  It should be an "authzId" value as
062     *       described in section 5.2.1.8 of
063     *       <A HREF="http://www.ietf.org/rfc/rfc4513.txt">RFC 4513</A>.  That is,
064     *       it should be either "dn:" followed by the distinguished name of the
065     *       target user, or "u:" followed by the username.  If the "u:" form is
066     *       used, then the mechanism used to resolve the provided username to an
067     *       entry may vary from server to server.</LI>
068     *   <LI>Password -- The clear-text password for the target user.</LI>
069     * </UL>
070     * <H2>Example</H2>
071     * The following example demonstrates the process for performing a CRAM-MD5
072     * bind against a directory server with a username of "john.doe" and a password
073     * of "password":
074     * <PRE>
075     * CRAMMD5BindRequest bindRequest =
076     *      new CRAMMD5BindRequest("u:john.doe", "password");
077     * BindResult bindResult;
078     * try
079     * {
080     *   bindResult = connection.bind(bindRequest);
081     *   // If we get here, then the bind was successful.
082     * }
083     * catch (LDAPException le)
084     * {
085     *   // The bind failed for some reason.
086     *   bindResult = new BindResult(le.toLDAPResult());
087     *   ResultCode resultCode = le.getResultCode();
088     *   String errorMessageFromServer = le.getDiagnosticMessage();
089     * }
090     * </PRE>
091     */
092    @NotMutable()
093    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
094    public final class CRAMMD5BindRequest
095           extends SASLBindRequest
096           implements CallbackHandler
097    {
098      /**
099       * The name for the CRAM-MD5 SASL mechanism.
100       */
101      public static final String CRAMMD5_MECHANISM_NAME = "CRAM-MD5";
102    
103    
104    
105      /**
106       * The serial version UID for this serializable class.
107       */
108      private static final long serialVersionUID = -4556570436768136483L;
109    
110    
111    
112      // The password for this bind request.
113      private final ASN1OctetString password;
114    
115      // The message ID from the last LDAP message sent from this request.
116      private int messageID = -1;
117    
118      // A list that will be updated with messages about any unhandled callbacks
119      // encountered during processing.
120      private final List<String> unhandledCallbackMessages;
121    
122      // The authentication ID string for this bind request.
123      private final String authenticationID;
124    
125    
126    
127      /**
128       * Creates a new SASL CRAM-MD5 bind request with the provided authentication
129       * ID and password.  It will not include any controls.
130       *
131       * @param  authenticationID  The authentication ID for this bind request.  It
132       *                           must not be {@code null}.
133       * @param  password          The password for this bind request.  It must not
134       *                           be {@code null}.
135       */
136      public CRAMMD5BindRequest(final String authenticationID,
137                                final String password)
138      {
139        this(authenticationID, new ASN1OctetString(password), NO_CONTROLS);
140    
141        ensureNotNull(password);
142      }
143    
144    
145    
146      /**
147       * Creates a new SASL CRAM-MD5 bind request with the provided authentication
148       * ID and password.  It will not include any controls.
149       *
150       * @param  authenticationID  The authentication ID for this bind request.  It
151       *                           must not be {@code null}.
152       * @param  password          The password for this bind request.  It must not
153       *                           be {@code null}.
154       */
155      public CRAMMD5BindRequest(final String authenticationID,
156                                final byte[] password)
157      {
158        this(authenticationID, new ASN1OctetString(password), NO_CONTROLS);
159    
160        ensureNotNull(password);
161      }
162    
163    
164    
165      /**
166       * Creates a new SASL CRAM-MD5 bind request with the provided authentication
167       * ID and password.  It will not include any controls.
168       *
169       * @param  authenticationID  The authentication ID for this bind request.  It
170       *                           must not be {@code null}.
171       * @param  password          The password for this bind request.  It must not
172       *                           be {@code null}.
173       */
174      public CRAMMD5BindRequest(final String authenticationID,
175                                final ASN1OctetString password)
176      {
177        this(authenticationID, password, NO_CONTROLS);
178      }
179    
180    
181    
182      /**
183       * Creates a new SASL CRAM-MD5 bind request with the provided authentication
184       * ID, password, and set of controls.
185       *
186       * @param  authenticationID  The authentication ID for this bind request.  It
187       *                           must not be {@code null}.
188       * @param  password          The password for this bind request.  It must not
189       *                           be {@code null}.
190       * @param  controls          The set of controls to include in the request.
191       */
192      public CRAMMD5BindRequest(final String authenticationID,
193                                final String password, final Control... controls)
194      {
195        this(authenticationID, new ASN1OctetString(password), controls);
196    
197        ensureNotNull(password);
198      }
199    
200    
201    
202      /**
203       * Creates a new SASL CRAM-MD5 bind request with the provided authentication
204       * ID, password, and set of controls.
205       *
206       * @param  authenticationID  The authentication ID for this bind request.  It
207       *                           must not be {@code null}.
208       * @param  password          The password for this bind request.  It must not
209       *                           be {@code null}.
210       * @param  controls          The set of controls to include in the request.
211       */
212      public CRAMMD5BindRequest(final String authenticationID,
213                                final byte[] password, final Control... controls)
214      {
215        this(authenticationID, new ASN1OctetString(password), controls);
216    
217        ensureNotNull(password);
218      }
219    
220    
221    
222      /**
223       * Creates a new SASL CRAM-MD5 bind request with the provided authentication
224       * ID, password, and set of controls.
225       *
226       * @param  authenticationID  The authentication ID for this bind request.  It
227       *                           must not be {@code null}.
228       * @param  password          The password for this bind request.  It must not
229       *                           be {@code null}.
230       * @param  controls          The set of controls to include in the request.
231       */
232      public CRAMMD5BindRequest(final String authenticationID,
233                                final ASN1OctetString password,
234                                final Control... controls)
235      {
236        super(controls);
237    
238        ensureNotNull(authenticationID, password);
239    
240        this.authenticationID = authenticationID;
241        this.password         = password;
242    
243        unhandledCallbackMessages = new ArrayList<String>(5);
244      }
245    
246    
247    
248      /**
249       * {@inheritDoc}
250       */
251      @Override()
252      public String getSASLMechanismName()
253      {
254        return CRAMMD5_MECHANISM_NAME;
255      }
256    
257    
258    
259      /**
260       * Retrieves the authentication ID for this bind request.
261       *
262       * @return  The authentication ID for this bind request.
263       */
264      public String getAuthenticationID()
265      {
266        return authenticationID;
267      }
268    
269    
270    
271      /**
272       * Retrieves the string representation of the password for this bind request.
273       *
274       * @return  The string representation of the password for this bind request.
275       */
276      public String getPasswordString()
277      {
278        return password.stringValue();
279      }
280    
281    
282    
283      /**
284       * Retrieves the bytes that comprise the the password for this bind request.
285       *
286       * @return  The bytes that comprise the password for this bind request.
287       */
288      public byte[] getPasswordBytes()
289      {
290        return password.getValue();
291      }
292    
293    
294    
295      /**
296       * Sends this bind request to the target server over the provided connection
297       * and returns the corresponding response.
298       *
299       * @param  connection  The connection to use to send this bind request to the
300       *                     server and read the associated response.
301       * @param  depth       The current referral depth for this request.  It should
302       *                     always be one for the initial request, and should only
303       *                     be incremented when following referrals.
304       *
305       * @return  The bind response read from the server.
306       *
307       * @throws  LDAPException  If a problem occurs while sending the request or
308       *                         reading the response.
309       */
310      @Override()
311      protected BindResult process(final LDAPConnection connection, final int depth)
312                throws LDAPException
313      {
314        unhandledCallbackMessages.clear();
315    
316        final SaslClient saslClient;
317        final String[] mechanisms = { CRAMMD5_MECHANISM_NAME };
318    
319        try
320        {
321          saslClient = Sasl.createSaslClient(mechanisms, null, "ldap",
322                                             connection.getConnectedAddress(), null,
323                                             this);
324        }
325        catch (Exception e)
326        {
327          debugException(e);
328          throw new LDAPException(ResultCode.LOCAL_ERROR,
329               ERR_CRAMMD5_CANNOT_CREATE_SASL_CLIENT.get(getExceptionMessage(e)),
330               e);
331        }
332    
333        final SASLHelper helper = new SASLHelper(this, connection,
334             CRAMMD5_MECHANISM_NAME, saslClient, getControls(),
335             getResponseTimeoutMillis(connection), unhandledCallbackMessages);
336    
337        try
338        {
339          return helper.processSASLBind();
340        }
341        finally
342        {
343          messageID = helper.getMessageID();
344        }
345      }
346    
347    
348    
349      /**
350       * {@inheritDoc}
351       */
352      @Override()
353      public CRAMMD5BindRequest getRebindRequest(final String host, final int port)
354      {
355        return new CRAMMD5BindRequest(authenticationID, password, getControls());
356      }
357    
358    
359    
360      /**
361       * Handles any necessary callbacks required for SASL authentication.
362       *
363       * @param  callbacks  The set of callbacks to be handled.
364       */
365      @InternalUseOnly()
366      public void handle(final Callback[] callbacks)
367      {
368        for (final Callback callback : callbacks)
369        {
370          if (callback instanceof NameCallback)
371          {
372            ((NameCallback) callback).setName(authenticationID);
373          }
374          else if (callback instanceof PasswordCallback)
375          {
376            ((PasswordCallback) callback).setPassword(
377                 password.stringValue().toCharArray());
378          }
379          else
380          {
381            // This is an unexpected callback.
382            if (debugEnabled(DebugType.LDAP))
383            {
384              debug(Level.WARNING, DebugType.LDAP,
385                    "Unexpected CRAM-MD5 SASL callback of type " +
386                    callback.getClass().getName());
387            }
388    
389            unhandledCallbackMessages.add(ERR_CRAMMD5_UNEXPECTED_CALLBACK.get(
390                 callback.getClass().getName()));
391          }
392        }
393      }
394    
395    
396    
397      /**
398       * {@inheritDoc}
399       */
400      @Override()
401      public int getLastMessageID()
402      {
403        return messageID;
404      }
405    
406    
407    
408      /**
409       * {@inheritDoc}
410       */
411      @Override()
412      public CRAMMD5BindRequest duplicate()
413      {
414        return duplicate(getControls());
415      }
416    
417    
418    
419      /**
420       * {@inheritDoc}
421       */
422      @Override()
423      public CRAMMD5BindRequest duplicate(final Control[] controls)
424      {
425        final CRAMMD5BindRequest bindRequest =
426             new CRAMMD5BindRequest(authenticationID, password, controls);
427        bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
428        return bindRequest;
429      }
430    
431    
432    
433      /**
434       * {@inheritDoc}
435       */
436      @Override()
437      public void toString(final StringBuilder buffer)
438      {
439        buffer.append("CRAMMD5BindRequest(authenticationID='");
440        buffer.append(authenticationID);
441        buffer.append('\'');
442    
443        final Control[] controls = getControls();
444        if (controls.length > 0)
445        {
446          buffer.append(", controls={");
447          for (int i=0; i < controls.length; i++)
448          {
449            if (i > 0)
450            {
451              buffer.append(", ");
452            }
453    
454            buffer.append(controls[i]);
455          }
456          buffer.append('}');
457        }
458    
459        buffer.append(')');
460      }
461    
462    
463    
464      /**
465       * {@inheritDoc}
466       */
467      @Override()
468      public void toCode(final List<String> lineList, final String requestID,
469                         final int indentSpaces, final boolean includeProcessing)
470      {
471        // Create the request variable.
472        final ArrayList<ToCodeArgHelper> constructorArgs =
473             new ArrayList<ToCodeArgHelper>(3);
474        constructorArgs.add(ToCodeArgHelper.createString(authenticationID,
475             "Authentication ID"));
476        constructorArgs.add(ToCodeArgHelper.createString("---redacted-password---",
477             "Bind Password"));
478    
479        final Control[] controls = getControls();
480        if (controls.length > 0)
481        {
482          constructorArgs.add(ToCodeArgHelper.createControlArray(controls,
483               "Bind Controls"));
484        }
485    
486        ToCodeHelper.generateMethodCall(lineList, indentSpaces,
487             "CRAMMD5BindRequest", requestID + "Request",
488             "new CRAMMD5BindRequest", constructorArgs);
489    
490    
491        // Add lines for processing the request and obtaining the result.
492        if (includeProcessing)
493        {
494          // Generate a string with the appropriate indent.
495          final StringBuilder buffer = new StringBuilder();
496          for (int i=0; i < indentSpaces; i++)
497          {
498            buffer.append(' ');
499          }
500          final String indent = buffer.toString();
501    
502          lineList.add("");
503          lineList.add(indent + "try");
504          lineList.add(indent + '{');
505          lineList.add(indent + "  BindResult " + requestID +
506               "Result = connection.bind(" + requestID + "Request);");
507          lineList.add(indent + "  // The bind was processed successfully.");
508          lineList.add(indent + '}');
509          lineList.add(indent + "catch (LDAPException e)");
510          lineList.add(indent + '{');
511          lineList.add(indent + "  // The bind failed.  Maybe the following will " +
512               "help explain why.");
513          lineList.add(indent + "  // Note that the connection is now likely in " +
514               "an unauthenticated state.");
515          lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
516          lineList.add(indent + "  String message = e.getMessage();");
517          lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
518          lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
519          lineList.add(indent + "  Control[] responseControls = " +
520               "e.getResponseControls();");
521          lineList.add(indent + '}');
522        }
523      }
524    }