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