001/*
002 * Copyright 2007-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2014 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 */
021package com.unboundid.ldap.sdk;
022
023
024
025import java.util.ArrayList;
026import java.util.List;
027import java.util.logging.Level;
028import javax.security.auth.callback.Callback;
029import javax.security.auth.callback.CallbackHandler;
030import javax.security.auth.callback.NameCallback;
031import javax.security.auth.callback.PasswordCallback;
032import javax.security.sasl.Sasl;
033import javax.security.sasl.SaslClient;
034
035import com.unboundid.asn1.ASN1OctetString;
036import com.unboundid.util.DebugType;
037import com.unboundid.util.InternalUseOnly;
038import com.unboundid.util.NotMutable;
039import com.unboundid.util.ThreadSafety;
040import com.unboundid.util.ThreadSafetyLevel;
041
042import static com.unboundid.ldap.sdk.LDAPMessages.*;
043import static com.unboundid.util.Debug.*;
044import static com.unboundid.util.StaticUtils.*;
045import 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)
094public 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}