001/*
002 * Copyright 2007-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 Ping Identity Corporation
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.Debug;
037import com.unboundid.util.DebugType;
038import com.unboundid.util.InternalUseOnly;
039import com.unboundid.util.NotMutable;
040import com.unboundid.util.StaticUtils;
041import com.unboundid.util.ThreadSafety;
042import com.unboundid.util.ThreadSafetyLevel;
043import com.unboundid.util.Validator;
044
045import static com.unboundid.ldap.sdk.LDAPMessages.*;
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    Validator.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    Validator.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    Validator.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    Validator.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    Validator.ensureNotNull(authenticationID, password);
239
240    this.authenticationID = authenticationID;
241    this.password         = password;
242
243    unhandledCallbackMessages = new ArrayList<>(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
318    try
319    {
320      final String[] mechanisms = { CRAMMD5_MECHANISM_NAME };
321      saslClient = Sasl.createSaslClient(mechanisms, null, "ldap",
322                                         connection.getConnectedAddress(), null,
323                                         this);
324    }
325    catch (final Exception e)
326    {
327      Debug.debugException(e);
328      throw new LDAPException(ResultCode.LOCAL_ERROR,
329           ERR_CRAMMD5_CANNOT_CREATE_SASL_CLIENT.get(
330                StaticUtils.getExceptionMessage(e)),
331           e);
332    }
333
334    final SASLHelper helper = new SASLHelper(this, connection,
335         CRAMMD5_MECHANISM_NAME, saslClient, getControls(),
336         getResponseTimeoutMillis(connection), unhandledCallbackMessages);
337
338    try
339    {
340      return helper.processSASLBind();
341    }
342    finally
343    {
344      messageID = helper.getMessageID();
345    }
346  }
347
348
349
350  /**
351   * {@inheritDoc}
352   */
353  @Override()
354  public CRAMMD5BindRequest getRebindRequest(final String host, final int port)
355  {
356    return new CRAMMD5BindRequest(authenticationID, password, getControls());
357  }
358
359
360
361  /**
362   * Handles any necessary callbacks required for SASL authentication.
363   *
364   * @param  callbacks  The set of callbacks to be handled.
365   */
366  @InternalUseOnly()
367  @Override()
368  public void handle(final Callback[] callbacks)
369  {
370    for (final Callback callback : callbacks)
371    {
372      if (callback instanceof NameCallback)
373      {
374        ((NameCallback) callback).setName(authenticationID);
375      }
376      else if (callback instanceof PasswordCallback)
377      {
378        ((PasswordCallback) callback).setPassword(
379             password.stringValue().toCharArray());
380      }
381      else
382      {
383        // This is an unexpected callback.
384        if (Debug.debugEnabled(DebugType.LDAP))
385        {
386          Debug.debug(Level.WARNING, DebugType.LDAP,
387               "Unexpected CRAM-MD5 SASL callback of type " +
388                    callback.getClass().getName());
389        }
390
391        unhandledCallbackMessages.add(ERR_CRAMMD5_UNEXPECTED_CALLBACK.get(
392             callback.getClass().getName()));
393      }
394    }
395  }
396
397
398
399  /**
400   * {@inheritDoc}
401   */
402  @Override()
403  public int getLastMessageID()
404  {
405    return messageID;
406  }
407
408
409
410  /**
411   * {@inheritDoc}
412   */
413  @Override()
414  public CRAMMD5BindRequest duplicate()
415  {
416    return duplicate(getControls());
417  }
418
419
420
421  /**
422   * {@inheritDoc}
423   */
424  @Override()
425  public CRAMMD5BindRequest duplicate(final Control[] controls)
426  {
427    final CRAMMD5BindRequest bindRequest =
428         new CRAMMD5BindRequest(authenticationID, password, controls);
429    bindRequest.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
430    return bindRequest;
431  }
432
433
434
435  /**
436   * {@inheritDoc}
437   */
438  @Override()
439  public void toString(final StringBuilder buffer)
440  {
441    buffer.append("CRAMMD5BindRequest(authenticationID='");
442    buffer.append(authenticationID);
443    buffer.append('\'');
444
445    final Control[] controls = getControls();
446    if (controls.length > 0)
447    {
448      buffer.append(", controls={");
449      for (int i=0; i < controls.length; i++)
450      {
451        if (i > 0)
452        {
453          buffer.append(", ");
454        }
455
456        buffer.append(controls[i]);
457      }
458      buffer.append('}');
459    }
460
461    buffer.append(')');
462  }
463
464
465
466  /**
467   * {@inheritDoc}
468   */
469  @Override()
470  public void toCode(final List<String> lineList, final String requestID,
471                     final int indentSpaces, final boolean includeProcessing)
472  {
473    // Create the request variable.
474    final ArrayList<ToCodeArgHelper> constructorArgs = new ArrayList<>(3);
475    constructorArgs.add(ToCodeArgHelper.createString(authenticationID,
476         "Authentication ID"));
477    constructorArgs.add(ToCodeArgHelper.createString("---redacted-password---",
478         "Bind Password"));
479
480    final Control[] controls = getControls();
481    if (controls.length > 0)
482    {
483      constructorArgs.add(ToCodeArgHelper.createControlArray(controls,
484           "Bind Controls"));
485    }
486
487    ToCodeHelper.generateMethodCall(lineList, indentSpaces,
488         "CRAMMD5BindRequest", requestID + "Request",
489         "new CRAMMD5BindRequest", constructorArgs);
490
491
492    // Add lines for processing the request and obtaining the result.
493    if (includeProcessing)
494    {
495      // Generate a string with the appropriate indent.
496      final StringBuilder buffer = new StringBuilder();
497      for (int i=0; i < indentSpaces; i++)
498      {
499        buffer.append(' ');
500      }
501      final String indent = buffer.toString();
502
503      lineList.add("");
504      lineList.add(indent + "try");
505      lineList.add(indent + '{');
506      lineList.add(indent + "  BindResult " + requestID +
507           "Result = connection.bind(" + requestID + "Request);");
508      lineList.add(indent + "  // The bind was processed successfully.");
509      lineList.add(indent + '}');
510      lineList.add(indent + "catch (LDAPException e)");
511      lineList.add(indent + '{');
512      lineList.add(indent + "  // The bind failed.  Maybe the following will " +
513           "help explain why.");
514      lineList.add(indent + "  // Note that the connection is now likely in " +
515           "an unauthenticated state.");
516      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
517      lineList.add(indent + "  String message = e.getMessage();");
518      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
519      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
520      lineList.add(indent + "  Control[] responseControls = " +
521           "e.getResponseControls();");
522      lineList.add(indent + '}');
523    }
524  }
525}