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 legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2012-2015 ForgeRock AS
026 */
027package org.opends.server.tools;
028
029import java.io.BufferedWriter;
030import java.io.File;
031import java.io.FileWriter;
032import java.io.IOException;
033import java.io.UnsupportedEncodingException;
034import java.security.MessageDigest;
035import java.security.PrivilegedExceptionAction;
036import java.security.SecureRandom;
037import java.util.Arrays;
038import java.util.HashMap;
039import java.util.Iterator;
040import java.util.LinkedHashMap;
041import java.util.LinkedList;
042import java.util.List;
043import java.util.Map;
044import java.util.StringTokenizer;
045import java.util.concurrent.atomic.AtomicInteger;
046
047import javax.security.auth.Subject;
048import javax.security.auth.callback.Callback;
049import javax.security.auth.callback.CallbackHandler;
050import javax.security.auth.callback.NameCallback;
051import javax.security.auth.callback.PasswordCallback;
052import javax.security.auth.callback.UnsupportedCallbackException;
053import javax.security.auth.login.LoginContext;
054import javax.security.sasl.Sasl;
055import javax.security.sasl.SaslClient;
056
057import com.forgerock.opendj.cli.ClientException;
058import com.forgerock.opendj.cli.ConsoleApplication;
059import com.forgerock.opendj.cli.ReturnCode;
060
061import org.forgerock.i18n.LocalizableMessage;
062import org.forgerock.opendj.ldap.ByteSequence;
063import org.forgerock.opendj.ldap.ByteString;
064import org.forgerock.opendj.ldap.DecodeException;
065import org.opends.server.protocols.ldap.BindRequestProtocolOp;
066import org.opends.server.protocols.ldap.BindResponseProtocolOp;
067import org.opends.server.protocols.ldap.ExtendedRequestProtocolOp;
068import org.opends.server.protocols.ldap.ExtendedResponseProtocolOp;
069import org.opends.server.protocols.ldap.LDAPMessage;
070import org.opends.server.types.LDAPException;
071import org.opends.server.types.Control;
072import org.opends.server.util.Base64;
073
074import static org.opends.messages.ToolMessages.*;
075import static org.opends.server.protocols.ldap.LDAPConstants.*;
076import static com.forgerock.opendj.cli.ArgumentConstants.*;
077import static org.opends.server.util.ServerConstants.*;
078import static org.opends.server.util.StaticUtils.*;
079
080
081
082/**
083 * This class provides a generic interface that LDAP clients can use to perform
084 * various kinds of authentication to the Directory Server.  This handles both
085 * simple authentication as well as several SASL mechanisms including:
086 * <UL>
087 *   <LI>ANONYMOUS</LI>
088 *   <LI>CRAM-MD5</LI>
089 *   <LI>DIGEST-MD5</LI>
090 *   <LI>EXTERNAL</LI>
091 *   <LI>GSSAPI</LI>
092 *   <LI>PLAIN</LI>
093 * </UL>
094 * <BR><BR>
095 * Note that this implementation is not thread safe, so if the same
096 * <CODE>AuthenticationHandler</CODE> object is to be used concurrently by
097 * multiple threads, it must be externally synchronized.
098 */
099public class LDAPAuthenticationHandler
100       implements PrivilegedExceptionAction<Object>, CallbackHandler
101{
102  /** The bind DN for GSSAPI authentication. */
103  private ByteSequence gssapiBindDN;
104
105  /** The LDAP reader that will be used to read data from the server. */
106  private final LDAPReader reader;
107
108  /** The LDAP writer that will be used to send data to the server. */
109  private final LDAPWriter writer;
110
111  /**
112   * The atomic integer that will be used to obtain message IDs for request
113   * messages.
114   */
115  private final AtomicInteger nextMessageID;
116
117  /** An array filled with the inner pad byte. */
118  private byte[] iPad;
119
120  /** An array filled with the outer pad byte. */
121  private byte[] oPad;
122
123  /** The authentication password for GSSAPI authentication. */
124  private char[] gssapiAuthPW;
125
126  /** The message digest that will be used to create MD5 hashes. */
127  private MessageDigest md5Digest;
128
129  /** The secure random number generator for use by this authentication handler. */
130  private SecureRandom secureRandom;
131
132  /** The authentication ID for GSSAPI authentication. */
133  private String gssapiAuthID;
134
135  /** The authorization ID for GSSAPI authentication. */
136  private String gssapiAuthzID;
137
138  /** The quality of protection for GSSAPI authentication. */
139  private String gssapiQoP;
140
141  /** The host name used to connect to the remote system. */
142  private final String hostName;
143
144  /** The SASL mechanism that will be used for callback authentication. */
145  private String saslMechanism;
146
147
148
149  /**
150   * Creates a new instance of this authentication handler.  All initialization
151   * will be done lazily to avoid unnecessary performance hits, particularly
152   * for cases in which simple authentication will be used as it does not
153   * require any particularly expensive processing.
154   *
155   * @param  reader         The LDAP reader that will be used to read data from
156   *                        the server.
157   * @param  writer         The LDAP writer that will be used to send data to
158   *                        the server.
159   * @param  hostName       The host name used to connect to the remote system
160   *                        (fully-qualified if possible).
161   * @param  nextMessageID  The atomic integer that will be used to obtain
162   *                        message IDs for request messages.
163   */
164  public LDAPAuthenticationHandler(LDAPReader reader, LDAPWriter writer,
165                                   String hostName, AtomicInteger nextMessageID)
166  {
167    this.reader = reader;
168    this.writer = writer;
169    this.hostName      = hostName;
170    this.nextMessageID = nextMessageID;
171
172    md5Digest    = null;
173    secureRandom = null;
174    iPad         = null;
175    oPad         = null;
176  }
177
178
179
180  /**
181   * Retrieves a list of the SASL mechanisms that are supported by this client
182   * library.
183   *
184   * @return  A list of the SASL mechanisms that are supported by this client
185   *          library.
186   */
187  public static String[] getSupportedSASLMechanisms()
188  {
189    return new String[]
190    {
191      SASL_MECHANISM_ANONYMOUS,
192      SASL_MECHANISM_CRAM_MD5,
193      SASL_MECHANISM_DIGEST_MD5,
194      SASL_MECHANISM_EXTERNAL,
195      SASL_MECHANISM_GSSAPI,
196      SASL_MECHANISM_PLAIN
197    };
198  }
199
200
201
202  /**
203   * Retrieves a list of the SASL properties that may be provided for the
204   * specified SASL mechanism, mapped from the property names to their
205   * corresponding descriptions.
206   *
207   * @param  mechanism  The name of the SASL mechanism for which to obtain the
208   *                    list of supported properties.
209   *
210   * @return  A list of the SASL properties that may be provided for the
211   *          specified SASL mechanism, mapped from the property names to their
212   *          corresponding descriptions.
213   */
214  public static LinkedHashMap<String,LocalizableMessage> getSASLProperties(
215          String mechanism)
216  {
217    String upperName = toUpperCase(mechanism);
218    if (upperName.equals(SASL_MECHANISM_ANONYMOUS))
219    {
220      return getSASLAnonymousProperties();
221    }
222    else if (upperName.equals(SASL_MECHANISM_CRAM_MD5))
223    {
224      return getSASLCRAMMD5Properties();
225    }
226    else if (upperName.equals(SASL_MECHANISM_DIGEST_MD5))
227    {
228      return getSASLDigestMD5Properties();
229    }
230    else if (upperName.equals(SASL_MECHANISM_EXTERNAL))
231    {
232      return getSASLExternalProperties();
233    }
234    else if (upperName.equals(SASL_MECHANISM_GSSAPI))
235    {
236      return getSASLGSSAPIProperties();
237    }
238    else if (upperName.equals(SASL_MECHANISM_PLAIN))
239    {
240      return getSASLPlainProperties();
241    }
242    else
243    {
244      // This is an unsupported mechanism.
245      return null;
246    }
247  }
248
249
250
251  /**
252   * Processes a bind using simple authentication with the provided information.
253   * If the bind fails, then an exception will be thrown with information about
254   * the reason for the failure.  If the bind is successful but there may be
255   * some special information that the client should be given, then it will be
256   * returned as a String.
257   *
258   * @param  ldapVersion       The LDAP protocol version to use for the bind
259   *                           request.
260   * @param  bindDN            The DN to use to bind to the Directory Server, or
261   *                           <CODE>null</CODE> if it is to be an anonymous
262   *                           bind.
263   * @param  bindPassword      The password to use to bind to the Directory
264   *                           Server, or <CODE>null</CODE> if it is to be an
265   *                           anonymous bind.
266   * @param  requestControls   The set of controls to include the request to the
267   *                           server.
268   * @param  responseControls  A list to hold the set of controls included in
269   *                           the response from the server.
270   *
271   * @return  A message providing additional information about the bind if
272   *          appropriate, or <CODE>null</CODE> if there is no special
273   *          information available.
274   *
275   * @throws  ClientException  If a client-side problem prevents the bind
276   *                           attempt from succeeding.
277   *
278   * @throws  LDAPException  If the bind fails or some other server-side problem
279   *                         occurs during processing.
280   */
281  public String doSimpleBind(int ldapVersion, ByteSequence bindDN,
282                             ByteSequence bindPassword,
283                             List<Control> requestControls,
284                             List<Control> responseControls)
285         throws ClientException, LDAPException
286  {
287    //Password is empty, set it to ByteString.empty.
288    if (bindPassword == null)
289    {
290        bindPassword = ByteString.empty();
291    }
292
293
294    // Make sure that critical elements aren't null.
295    if (bindDN == null)
296    {
297      bindDN = ByteString.empty();
298    }
299
300
301    // Create the bind request and send it to the server.
302    BindRequestProtocolOp bindRequest =
303         new BindRequestProtocolOp(bindDN.toByteString(), ldapVersion,
304             bindPassword.toByteString());
305    LDAPMessage bindRequestMessage =
306         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
307                         requestControls);
308
309    try
310    {
311      writer.writeMessage(bindRequestMessage);
312    }
313    catch (IOException ioe)
314    {
315      LocalizableMessage message =
316          ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(ioe));
317      throw new ClientException(
318              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
319    }
320    catch (Exception e)
321    {
322      LocalizableMessage message =
323          ERR_LDAPAUTH_CANNOT_SEND_SIMPLE_BIND.get(getExceptionMessage(e));
324      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
325    }
326
327
328    // Read the response from the server.
329    LDAPMessage responseMessage;
330    try
331    {
332      responseMessage = reader.readMessage();
333      if (responseMessage == null)
334      {
335        LocalizableMessage message =
336            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
337        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
338                                  message);
339      }
340    }
341    catch (DecodeException | LDAPException e)
342    {
343      LocalizableMessage message =
344          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
345      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
346    }
347    catch (IOException ioe)
348    {
349      LocalizableMessage message =
350          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
351      throw new ClientException(
352          ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
353    }
354    catch (Exception e)
355    {
356      LocalizableMessage message =
357          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
358      throw new ClientException(
359          ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
360    }
361
362
363    // See if there are any controls in the response.  If so, then add them to
364    // the response controls list.
365    List<Control> respControls = responseMessage.getControls();
366    if (respControls != null && !respControls.isEmpty())
367    {
368      responseControls.addAll(respControls);
369    }
370
371
372    // Look at the protocol op from the response.  If it's a bind response, then
373    // continue.  If it's an extended response, then it could be a notice of
374    // disconnection so check for that.  Otherwise, generate an error.
375    generateError(responseMessage);
376
377
378    BindResponseProtocolOp bindResponse =
379         responseMessage.getBindResponseProtocolOp();
380    int resultCode = bindResponse.getResultCode();
381    if (resultCode == ReturnCode.SUCCESS.get())
382    {
383      // FIXME -- Need to look for things like password expiration warning,
384      // reset notice, etc.
385      return null;
386    }
387
388    // FIXME -- Add support for referrals.
389
390    LocalizableMessage message = ERR_LDAPAUTH_SIMPLE_BIND_FAILED.get();
391    throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
392                            message, bindResponse.getMatchedDN(), null);
393  }
394
395
396
397  /**
398   * Processes a SASL bind using the provided information.  If the bind fails,
399   * then an exception will be thrown with information about the reason for the
400   * failure.  If the bind is successful but there may be some special
401   * information that the client should be given, then it will be returned as a
402   * String.
403   *
404   * @param  bindDN            The DN to use to bind to the Directory Server, or
405   *                           <CODE>null</CODE> if the authentication identity
406   *                           is to be set through some other means.
407   * @param  bindPassword      The password to use to bind to the Directory
408   *                           Server, or <CODE>null</CODE> if this is not a
409   *                           password-based SASL mechanism.
410   * @param  mechanism         The name of the SASL mechanism to use to
411   *                           authenticate to the Directory Server.
412   * @param  saslProperties    A set of additional properties that may be needed
413   *                           to process the SASL bind.
414   * @param  requestControls   The set of controls to include the request to the
415   *                           server.
416   * @param  responseControls  A list to hold the set of controls included in
417   *                           the response from the server.
418   *
419   * @return  A message providing additional information about the bind if
420   *          appropriate, or <CODE>null</CODE> if there is no special
421   *          information available.
422   *
423   * @throws  ClientException  If a client-side problem prevents the bind
424   *                           attempt from succeeding.
425   *
426   * @throws  LDAPException  If the bind fails or some other server-side problem
427   *                         occurs during processing.
428   */
429  public String doSASLBind(ByteSequence bindDN, ByteSequence bindPassword,
430                           String mechanism,
431                           Map<String,List<String>> saslProperties,
432                           List<Control> requestControls,
433                           List<Control> responseControls)
434         throws ClientException, LDAPException
435  {
436    // Make sure that critical elements aren't null.
437    if (bindDN == null)
438    {
439      bindDN = ByteString.empty();
440    }
441
442    if (mechanism == null || mechanism.length() == 0)
443    {
444      LocalizableMessage message = ERR_LDAPAUTH_NO_SASL_MECHANISM.get();
445      throw new ClientException(
446          ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
447    }
448
449
450    // Look at the mechanism name and call the appropriate method to process
451    // the request.
452    saslMechanism = toUpperCase(mechanism);
453    if (saslMechanism.equals(SASL_MECHANISM_ANONYMOUS))
454    {
455      return doSASLAnonymous(bindDN, saslProperties, requestControls,
456                             responseControls);
457    }
458    else if (saslMechanism.equals(SASL_MECHANISM_CRAM_MD5))
459    {
460      return doSASLCRAMMD5(bindDN, bindPassword, saslProperties,
461                           requestControls, responseControls);
462    }
463    else if (saslMechanism.equals(SASL_MECHANISM_DIGEST_MD5))
464    {
465      return doSASLDigestMD5(bindDN, bindPassword, saslProperties,
466                             requestControls, responseControls);
467    }
468    else if (saslMechanism.equals(SASL_MECHANISM_EXTERNAL))
469    {
470      return doSASLExternal(bindDN, saslProperties, requestControls,
471                            responseControls);
472    }
473    else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
474    {
475      return doSASLGSSAPI(bindDN, bindPassword, saslProperties, requestControls,
476                          responseControls);
477    }
478    else if (saslMechanism.equals(SASL_MECHANISM_PLAIN))
479    {
480      return doSASLPlain(bindDN, bindPassword, saslProperties, requestControls,
481                         responseControls);
482    }
483    else
484    {
485      LocalizableMessage message = ERR_LDAPAUTH_UNSUPPORTED_SASL_MECHANISM.get(mechanism);
486      throw new ClientException(
487          ReturnCode.CLIENT_SIDE_AUTH_UNKNOWN, message);
488    }
489  }
490
491
492
493  /**
494   * Processes a SASL ANONYMOUS bind with the provided information.
495   *
496   * @param  bindDN            The DN to use to bind to the Directory Server, or
497   *                           <CODE>null</CODE> if the authentication identity
498   *                           is to be set through some other means.
499   * @param  saslProperties    A set of additional properties that may be needed
500   *                           to process the SASL bind.
501   * @param  requestControls   The set of controls to include the request to the
502   *                           server.
503   * @param  responseControls  A list to hold the set of controls included in
504   *                           the response from the server.
505   *
506   * @return  A message providing additional information about the bind if
507   *          appropriate, or <CODE>null</CODE> if there is no special
508   *          information available.
509   *
510   * @throws  ClientException  If a client-side problem prevents the bind
511   *                           attempt from succeeding.
512   *
513   * @throws  LDAPException  If the bind fails or some other server-side problem
514   *                         occurs during processing.
515   */
516  public String doSASLAnonymous(ByteSequence bindDN,
517                     Map<String,List<String>> saslProperties,
518                     List<Control> requestControls,
519                     List<Control> responseControls)
520         throws ClientException, LDAPException
521  {
522    String trace = null;
523
524
525    // Evaluate the properties provided.  The only one we'll allow is the trace
526    // property, but it is not required.
527    if (saslProperties == null || saslProperties.isEmpty())
528    {
529      // This is fine because there are no required properties for this mechanism.
530    }
531    else
532    {
533      for (String name : saslProperties.keySet())
534      {
535        if (name.equalsIgnoreCase(SASL_PROPERTY_TRACE))
536        {
537          // This is acceptable, and we'll take any single value.
538          List<String> values = saslProperties.get(name);
539          Iterator<String> iterator = values.iterator();
540          if (iterator.hasNext())
541          {
542            trace = iterator.next();
543
544            if (iterator.hasNext())
545            {
546              LocalizableMessage message = ERR_LDAPAUTH_TRACE_SINGLE_VALUED.get();
547              throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
548            }
549          }
550        }
551        else
552        {
553          LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
554              name, SASL_MECHANISM_ANONYMOUS);
555          throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
556                                    message);
557        }
558      }
559    }
560
561
562    // Construct the bind request and send it to the server.
563    ByteString saslCredentials;
564    if (trace == null)
565    {
566      saslCredentials = null;
567    }
568    else
569    {
570      saslCredentials = ByteString.valueOfUtf8(trace);
571    }
572
573    BindRequestProtocolOp bindRequest =
574         new BindRequestProtocolOp(bindDN.toByteString(),
575             SASL_MECHANISM_ANONYMOUS, saslCredentials);
576    LDAPMessage requestMessage =
577         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
578                         requestControls);
579
580    try
581    {
582      writer.writeMessage(requestMessage);
583    }
584    catch (IOException ioe)
585    {
586      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
587          SASL_MECHANISM_ANONYMOUS, getExceptionMessage(ioe));
588      throw new ClientException(
589              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
590    }
591    catch (Exception e)
592    {
593      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
594          SASL_MECHANISM_ANONYMOUS, getExceptionMessage(e));
595      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
596    }
597
598
599    // Read the response from the server.
600    LDAPMessage responseMessage;
601    try
602    {
603      responseMessage = reader.readMessage();
604      if (responseMessage == null)
605      {
606        LocalizableMessage message =
607            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
608        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
609                                  message);
610      }
611    }
612    catch (DecodeException | LDAPException e)
613    {
614      LocalizableMessage message =
615          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
616      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
617    }
618    catch (IOException ioe)
619    {
620      LocalizableMessage message =
621          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
622      throw new ClientException(
623              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
624    }
625    catch (Exception e)
626    {
627      LocalizableMessage message =
628          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
629      throw new ClientException(
630              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
631    }
632
633
634    // See if there are any controls in the response.  If so, then add them to
635    // the response controls list.
636    List<Control> respControls = responseMessage.getControls();
637    if (respControls != null && ! respControls.isEmpty())
638    {
639      responseControls.addAll(respControls);
640    }
641
642
643    // Look at the protocol op from the response.  If it's a bind response, then
644    // continue.  If it's an extended response, then it could be a notice of
645    // disconnection so check for that.  Otherwise, generate an error.
646    generateError(responseMessage);
647
648
649    BindResponseProtocolOp bindResponse =
650         responseMessage.getBindResponseProtocolOp();
651    int resultCode = bindResponse.getResultCode();
652    if (resultCode == ReturnCode.SUCCESS.get())
653    {
654      // FIXME -- Need to look for things like password expiration warning,
655      // reset notice, etc.
656      return null;
657    }
658
659    // FIXME -- Add support for referrals.
660
661    LocalizableMessage message =
662        ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_ANONYMOUS);
663    throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
664                            message, bindResponse.getMatchedDN(), null);
665  }
666
667
668
669  /**
670   * Retrieves the set of properties that a client may provide when performing a
671   * SASL ANONYMOUS bind, mapped from the property names to their corresponding
672   * descriptions.
673   *
674   * @return  The set of properties that a client may provide when performing a
675   *          SASL ANONYMOUS bind, mapped from the property names to their
676   *          corresponding descriptions.
677   */
678  public static LinkedHashMap<String, LocalizableMessage> getSASLAnonymousProperties()
679  {
680    LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(1);
681
682    properties.put(SASL_PROPERTY_TRACE,
683                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_TRACE.get());
684
685    return properties;
686  }
687
688
689
690  /**
691   * Processes a SASL CRAM-MD5 bind with the provided information.
692   *
693   * @param  bindDN            The DN to use to bind to the Directory Server, or
694   *                           <CODE>null</CODE> if the authentication identity
695   *                           is to be set through some other means.
696   * @param  bindPassword      The password to use to bind to the Directory
697   *                           Server.
698   * @param  saslProperties    A set of additional properties that may be needed
699   *                           to process the SASL bind.
700   * @param  requestControls   The set of controls to include the request to the
701   *                           server.
702   * @param  responseControls  A list to hold the set of controls included in
703   *                           the response from the server.
704   *
705   * @return  A message providing additional information about the bind if
706   *          appropriate, or <CODE>null</CODE> if there is no special
707   *          information available.
708   *
709   * @throws  ClientException  If a client-side problem prevents the bind
710   *                           attempt from succeeding.
711   *
712   * @throws  LDAPException  If the bind fails or some other server-side problem
713   *                         occurs during processing.
714   */
715  public String doSASLCRAMMD5(ByteSequence bindDN,
716                     ByteSequence bindPassword,
717                     Map<String,List<String>> saslProperties,
718                     List<Control> requestControls,
719                     List<Control> responseControls)
720         throws ClientException, LDAPException
721  {
722    String authID  = null;
723
724
725    // Evaluate the properties provided.  The authID is required, no other
726    // properties are allowed.
727    if (saslProperties == null || saslProperties.isEmpty())
728    {
729      LocalizableMessage message =
730          ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_CRAM_MD5);
731      throw new ClientException(
732              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
733    }
734
735    for (String name : saslProperties.keySet())
736    {
737      String lowerName = toLowerCase(name);
738
739      if (lowerName.equals(SASL_PROPERTY_AUTHID))
740      {
741        authID = getAuthID(saslProperties, authID, name);
742      }
743      else
744      {
745        LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
746            name, SASL_MECHANISM_CRAM_MD5);
747        throw new ClientException(
748                ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
749      }
750    }
751
752
753    // Make sure that the authID was provided.
754    if (authID == null || authID.length() == 0)
755    {
756      LocalizableMessage message =
757          ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_CRAM_MD5);
758      throw new ClientException(
759              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
760    }
761
762
763    // Set password to ByteString.empty if the password is null.
764    if (bindPassword == null)
765    {
766        bindPassword = ByteString.empty();
767    }
768
769
770    // Construct the initial bind request to send to the server.  In this case,
771    // we'll simply indicate that we want to use CRAM-MD5 so the server will
772    // send us the challenge.
773    BindRequestProtocolOp bindRequest1 =
774         new BindRequestProtocolOp(bindDN.toByteString(),
775             SASL_MECHANISM_CRAM_MD5, null);
776    // FIXME -- Should we include request controls in both stages or just the
777    // second stage?
778    LDAPMessage requestMessage1 =
779         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest1);
780
781    try
782    {
783      writer.writeMessage(requestMessage1);
784    }
785    catch (IOException ioe)
786    {
787      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
788          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
789      throw new ClientException(
790              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
791    }
792    catch (Exception e)
793    {
794      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
795          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
796      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR, message, e);
797    }
798
799
800    // Read the response from the server.
801    LDAPMessage responseMessage1;
802    try
803    {
804      responseMessage1 = reader.readMessage();
805      if (responseMessage1 == null)
806      {
807        LocalizableMessage message =
808            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
809        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
810                                  message);
811      }
812    }
813    catch (DecodeException | LDAPException e)
814    {
815      LocalizableMessage message =
816          ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
817              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
818      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
819    }
820    catch (IOException ioe)
821    {
822      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
823          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
824      throw new ClientException(
825              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
826    }
827    catch (Exception e)
828    {
829      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
830          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
831      throw new ClientException(
832              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
833    }
834
835
836    // Look at the protocol op from the response.  If it's a bind response, then
837    // continue.  If it's an extended response, then it could be a notice of
838    // disconnection so check for that.  Otherwise, generate an error.
839    switch (responseMessage1.getProtocolOpType())
840    {
841      case OP_TYPE_BIND_RESPONSE:
842        // We'll deal with this later.
843        break;
844
845      case OP_TYPE_EXTENDED_RESPONSE:
846        ExtendedResponseProtocolOp extendedResponse =
847             responseMessage1.getExtendedResponseProtocolOp();
848        String responseOID = extendedResponse.getOID();
849        if (responseOID != null &&
850            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
851        {
852          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
853              get(extendedResponse.getResultCode(),
854                  extendedResponse.getErrorMessage());
855          throw new LDAPException(extendedResponse.getResultCode(), message);
856        }
857        else
858        {
859          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
860          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
861        }
862
863      default:
864        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage1.getProtocolOp());
865        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
866    }
867
868
869    // Make sure that the bind response has the "SASL bind in progress" result
870    // code.
871    BindResponseProtocolOp bindResponse1 =
872         responseMessage1.getBindResponseProtocolOp();
873    int resultCode1 = bindResponse1.getResultCode();
874    if (resultCode1 != ReturnCode.SASL_BIND_IN_PROGRESS.get())
875    {
876      LocalizableMessage errorMessage = bindResponse1.getErrorMessage();
877      if (errorMessage == null)
878      {
879        errorMessage = LocalizableMessage.EMPTY;
880      }
881
882      LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE.
883          get(SASL_MECHANISM_CRAM_MD5, resultCode1,
884              ReturnCode.get(resultCode1), errorMessage);
885      throw new LDAPException(resultCode1, errorMessage, message,
886                              bindResponse1.getMatchedDN(), null);
887    }
888
889
890    // Make sure that the bind response contains SASL credentials with the
891    // challenge to use for the next stage of the bind.
892    ByteString serverChallenge = bindResponse1.getServerSASLCredentials();
893    if (serverChallenge == null)
894    {
895      LocalizableMessage message = ERR_LDAPAUTH_NO_CRAMMD5_SERVER_CREDENTIALS.get();
896      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
897    }
898
899
900    // Use the provided password and credentials to generate the CRAM-MD5
901    // response.
902    StringBuilder buffer = new StringBuilder();
903    buffer.append(authID);
904    buffer.append(' ');
905    buffer.append(generateCRAMMD5Digest(bindPassword, serverChallenge));
906
907
908    // Create and send the second bind request to the server.
909    BindRequestProtocolOp bindRequest2 =
910         new BindRequestProtocolOp(bindDN.toByteString(),
911             SASL_MECHANISM_CRAM_MD5, ByteString.valueOfUtf8(buffer.toString()));
912    LDAPMessage requestMessage2 =
913         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2,
914                         requestControls);
915
916    try
917    {
918      writer.writeMessage(requestMessage2);
919    }
920    catch (IOException ioe)
921    {
922      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
923          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
924      throw new ClientException(
925              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
926    }
927    catch (Exception e)
928    {
929      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
930          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
931      throw new ClientException(
932              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
933    }
934
935
936    // Read the response from the server.
937    LDAPMessage responseMessage2;
938    try
939    {
940      responseMessage2 = reader.readMessage();
941      if (responseMessage2 == null)
942      {
943        LocalizableMessage message =
944            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
945        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
946                                  message);
947      }
948    }
949    catch (DecodeException | LDAPException e)
950    {
951      LocalizableMessage message =
952          ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
953              SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
954      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
955    }
956    catch (IOException ioe)
957    {
958      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
959          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(ioe));
960      throw new ClientException(
961              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
962    }
963    catch (Exception e)
964    {
965      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
966          SASL_MECHANISM_CRAM_MD5, getExceptionMessage(e));
967      throw new ClientException(
968              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
969    }
970
971
972    // See if there are any controls in the response.  If so, then add them to
973    // the response controls list.
974    List<Control> respControls = responseMessage2.getControls();
975    if (respControls != null && ! respControls.isEmpty())
976    {
977      responseControls.addAll(respControls);
978    }
979
980
981    // Look at the protocol op from the response.  If it's a bind response, then
982    // continue.  If it's an extended response, then it could be a notice of
983    // disconnection so check for that.  Otherwise, generate an error.
984    switch (responseMessage2.getProtocolOpType())
985    {
986      case OP_TYPE_BIND_RESPONSE:
987        // We'll deal with this later.
988        break;
989
990      case OP_TYPE_EXTENDED_RESPONSE:
991        ExtendedResponseProtocolOp extendedResponse =
992             responseMessage2.getExtendedResponseProtocolOp();
993        String responseOID = extendedResponse.getOID();
994        if (responseOID != null &&
995            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
996        {
997          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
998              get(extendedResponse.getResultCode(),
999                  extendedResponse.getErrorMessage());
1000          throw new LDAPException(extendedResponse.getResultCode(), message);
1001        }
1002        else
1003        {
1004          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
1005          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
1006        }
1007
1008      default:
1009        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage2.getProtocolOp());
1010        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
1011    }
1012
1013
1014    BindResponseProtocolOp bindResponse2 =
1015         responseMessage2.getBindResponseProtocolOp();
1016    int resultCode2 = bindResponse2.getResultCode();
1017    if (resultCode2 == ReturnCode.SUCCESS.get())
1018    {
1019      // FIXME -- Need to look for things like password expiration warning,
1020      // reset notice, etc.
1021      return null;
1022    }
1023
1024    // FIXME -- Add support for referrals.
1025
1026    LocalizableMessage message =
1027        ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_CRAM_MD5);
1028    throw new LDAPException(resultCode2, bindResponse2.getErrorMessage(),
1029                            message, bindResponse2.getMatchedDN(), null);
1030  }
1031
1032
1033
1034  /**
1035   * @param saslProperties
1036   * @param authID
1037   * @param name
1038   * @return
1039   * @throws ClientException
1040   */
1041  private String getAuthID(Map<String, List<String>> saslProperties, String authID, String name) throws ClientException
1042  {
1043    List<String> values = saslProperties.get(name);
1044    Iterator<String> iterator = values.iterator();
1045    if (iterator.hasNext())
1046    {
1047      authID = iterator.next();
1048
1049      if (iterator.hasNext())
1050      {
1051        LocalizableMessage message = ERR_LDAPAUTH_AUTHID_SINGLE_VALUED.get();
1052        throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
1053      }
1054    }
1055    return authID;
1056  }
1057
1058
1059
1060  /**
1061   * Generates the appropriate HMAC-MD5 digest for a CRAM-MD5 authentication
1062   * with the given information.
1063   *
1064   * @param  password   The clear-text password to use when generating the
1065   *                    digest.
1066   * @param  challenge  The server-supplied challenge to use when generating the
1067   *                    digest.
1068   *
1069   * @return  The generated HMAC-MD5 digest for CRAM-MD5 authentication.
1070   *
1071   * @throws  ClientException  If a problem occurs while attempting to perform
1072   *                           the necessary initialization.
1073   */
1074  private String generateCRAMMD5Digest(ByteSequence password,
1075                                       ByteSequence challenge)
1076          throws ClientException
1077  {
1078    // Perform the necessary initialization if it hasn't been done yet.
1079    if (md5Digest == null)
1080    {
1081      try
1082      {
1083        md5Digest = MessageDigest.getInstance("MD5");
1084      }
1085      catch (Exception e)
1086      {
1087        LocalizableMessage message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get(
1088            getExceptionMessage(e));
1089        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
1090                message, e);
1091      }
1092    }
1093
1094    if (iPad == null)
1095    {
1096      iPad = new byte[HMAC_MD5_BLOCK_LENGTH];
1097      oPad = new byte[HMAC_MD5_BLOCK_LENGTH];
1098      Arrays.fill(iPad, CRAMMD5_IPAD_BYTE);
1099      Arrays.fill(oPad, CRAMMD5_OPAD_BYTE);
1100    }
1101
1102
1103    // Get the byte arrays backing the password and challenge.
1104    byte[] p = password.toByteArray();
1105    byte[] c = challenge.toByteArray();
1106
1107
1108    // If the password is longer than the HMAC-MD5 block length, then use an
1109    // MD5 digest of the password rather than the password itself.
1110    if (password.length() > HMAC_MD5_BLOCK_LENGTH)
1111    {
1112      p = md5Digest.digest(p);
1113    }
1114
1115
1116    // Create byte arrays with data needed for the hash generation.
1117    byte[] iPadAndData = new byte[HMAC_MD5_BLOCK_LENGTH + c.length];
1118    System.arraycopy(iPad, 0, iPadAndData, 0, HMAC_MD5_BLOCK_LENGTH);
1119    System.arraycopy(c, 0, iPadAndData, HMAC_MD5_BLOCK_LENGTH, c.length);
1120
1121    byte[] oPadAndHash = new byte[HMAC_MD5_BLOCK_LENGTH + MD5_DIGEST_LENGTH];
1122    System.arraycopy(oPad, 0, oPadAndHash, 0, HMAC_MD5_BLOCK_LENGTH);
1123
1124
1125    // Iterate through the bytes in the key and XOR them with the iPad and
1126    // oPad as appropriate.
1127    for (int i=0; i < p.length; i++)
1128    {
1129      iPadAndData[i] ^= p[i];
1130      oPadAndHash[i] ^= p[i];
1131    }
1132
1133
1134    // Copy an MD5 digest of the iPad-XORed key and the data into the array to
1135    // be hashed.
1136    System.arraycopy(md5Digest.digest(iPadAndData), 0, oPadAndHash,
1137                     HMAC_MD5_BLOCK_LENGTH, MD5_DIGEST_LENGTH);
1138
1139
1140    // Calculate an MD5 digest of the resulting array and get the corresponding
1141    // hex string representation.
1142    byte[] digestBytes = md5Digest.digest(oPadAndHash);
1143
1144    StringBuilder hexDigest = new StringBuilder(2*digestBytes.length);
1145    for (byte b : digestBytes)
1146    {
1147      hexDigest.append(byteToLowerHex(b));
1148    }
1149
1150    return hexDigest.toString();
1151  }
1152
1153
1154
1155  /**
1156   * Retrieves the set of properties that a client may provide when performing a
1157   * SASL CRAM-MD5 bind, mapped from the property names to their corresponding
1158   * descriptions.
1159   *
1160   * @return  The set of properties that a client may provide when performing a
1161   *          SASL CRAM-MD5 bind, mapped from the property names to their
1162   *          corresponding descriptions.
1163   */
1164  public static LinkedHashMap<String,LocalizableMessage> getSASLCRAMMD5Properties()
1165  {
1166    LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(1);
1167
1168    properties.put(SASL_PROPERTY_AUTHID,
1169                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
1170
1171    return properties;
1172  }
1173
1174
1175
1176  /**
1177   * Processes a SASL DIGEST-MD5 bind with the provided information.
1178   *
1179   * @param  bindDN            The DN to use to bind to the Directory Server, or
1180   *                           <CODE>null</CODE> if the authentication identity
1181   *                           is to be set through some other means.
1182   * @param  bindPassword      The password to use to bind to the Directory
1183   *                           Server.
1184   * @param  saslProperties    A set of additional properties that may be needed
1185   *                           to process the SASL bind.
1186   * @param  requestControls   The set of controls to include the request to the
1187   *                           server.
1188   * @param  responseControls  A list to hold the set of controls included in
1189   *                           the response from the server.
1190   *
1191   * @return  A message providing additional information about the bind if
1192   *          appropriate, or <CODE>null</CODE> if there is no special
1193   *          information available.
1194   *
1195   * @throws  ClientException  If a client-side problem prevents the bind
1196   *                           attempt from succeeding.
1197   *
1198   * @throws  LDAPException  If the bind fails or some other server-side problem
1199   *                         occurs during processing.
1200   */
1201  public String doSASLDigestMD5(ByteSequence bindDN,
1202                     ByteSequence bindPassword,
1203                     Map<String,List<String>> saslProperties,
1204                     List<Control> requestControls,
1205                     List<Control> responseControls)
1206         throws ClientException, LDAPException
1207  {
1208    String  authID               = null;
1209    String  realm                = null;
1210    String  qop                  = "auth";
1211    String  digestURI            = "ldap/" + hostName;
1212    String  authzID              = null;
1213    boolean realmSetFromProperty = false;
1214
1215
1216    // Evaluate the properties provided.  The authID is required.  The realm,
1217    // QoP, digest URI, and authzID are optional.
1218    if (saslProperties == null || saslProperties.isEmpty())
1219    {
1220      LocalizableMessage message =
1221          ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_DIGEST_MD5);
1222      throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
1223    }
1224
1225    for (String name : saslProperties.keySet())
1226    {
1227      String lowerName = toLowerCase(name);
1228
1229      if (lowerName.equals(SASL_PROPERTY_AUTHID))
1230      {
1231        authID = getAuthID(saslProperties, authID, name);
1232      }
1233      else if (lowerName.equals(SASL_PROPERTY_REALM))
1234      {
1235        List<String> values = saslProperties.get(name);
1236        Iterator<String> iterator = values.iterator();
1237        if (iterator.hasNext())
1238        {
1239          realm                = iterator.next();
1240          realmSetFromProperty = true;
1241
1242          if (iterator.hasNext())
1243          {
1244            LocalizableMessage message = ERR_LDAPAUTH_REALM_SINGLE_VALUED.get();
1245            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1246                                      message);
1247          }
1248        }
1249      }
1250      else if (lowerName.equals(SASL_PROPERTY_QOP))
1251      {
1252        List<String> values = saslProperties.get(name);
1253        Iterator<String> iterator = values.iterator();
1254        if (iterator.hasNext())
1255        {
1256          qop = toLowerCase(iterator.next());
1257
1258          if (iterator.hasNext())
1259          {
1260            LocalizableMessage message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get();
1261            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1262                                      message);
1263          }
1264
1265          if (qop.equals("auth"))
1266          {
1267            // This is always fine.
1268          }
1269          else if (qop.equals("auth-int") || qop.equals("auth-conf"))
1270          {
1271            // FIXME -- Add support for integrity and confidentiality.
1272            LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(qop);
1273            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1274                                      message);
1275          }
1276          else
1277          {
1278            // This is an illegal value.
1279            LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_INVALID_QOP.get(qop);
1280            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1281                                      message);
1282          }
1283        }
1284      }
1285      else if (lowerName.equals(SASL_PROPERTY_DIGEST_URI))
1286      {
1287        List<String> values = saslProperties.get(name);
1288        Iterator<String> iterator = values.iterator();
1289        if (iterator.hasNext())
1290        {
1291          digestURI = toLowerCase(iterator.next());
1292
1293          if (iterator.hasNext())
1294          {
1295            LocalizableMessage message = ERR_LDAPAUTH_DIGEST_URI_SINGLE_VALUED.get();
1296            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1297                                      message);
1298          }
1299        }
1300      }
1301      else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
1302      {
1303        List<String> values = saslProperties.get(name);
1304        Iterator<String> iterator = values.iterator();
1305        if (iterator.hasNext())
1306        {
1307          authzID = toLowerCase(iterator.next());
1308
1309          if (iterator.hasNext())
1310          {
1311            LocalizableMessage message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get();
1312            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1313                                      message);
1314          }
1315        }
1316      }
1317      else
1318      {
1319        LocalizableMessage message = ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(
1320            name, SASL_MECHANISM_DIGEST_MD5);
1321        throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1322                message);
1323      }
1324    }
1325
1326
1327    // Make sure that the authID was provided.
1328    if (authID == null || authID.length() == 0)
1329    {
1330      LocalizableMessage message =
1331          ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_DIGEST_MD5);
1332      throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1333              message);
1334    }
1335
1336
1337    // Set password to ByteString.empty if the password is null.
1338    if (bindPassword == null)
1339    {
1340        bindPassword = ByteString.empty();
1341    }
1342
1343
1344    // Construct the initial bind request to send to the server.  In this case,
1345    // we'll simply indicate that we want to use DIGEST-MD5 so the server will
1346    // send us the challenge.
1347    BindRequestProtocolOp bindRequest1 =
1348         new BindRequestProtocolOp(bindDN.toByteString(),
1349             SASL_MECHANISM_DIGEST_MD5, null);
1350    // FIXME -- Should we include request controls in both stages or just the
1351    // second stage?
1352    LDAPMessage requestMessage1 =
1353         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest1);
1354
1355    try
1356    {
1357      writer.writeMessage(requestMessage1);
1358    }
1359    catch (IOException ioe)
1360    {
1361      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
1362          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
1363      throw new ClientException(
1364              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1365    }
1366    catch (Exception e)
1367    {
1368      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_INITIAL_SASL_BIND.get(
1369          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
1370      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
1371                                message, e);
1372    }
1373
1374
1375    // Read the response from the server.
1376    LDAPMessage responseMessage1;
1377    try
1378    {
1379      responseMessage1 = reader.readMessage();
1380      if (responseMessage1 == null)
1381      {
1382        LocalizableMessage message =
1383            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
1384        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
1385                                  message);
1386      }
1387    }
1388    catch (DecodeException | LDAPException e)
1389    {
1390      LocalizableMessage message =
1391          ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
1392              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
1393      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
1394    }
1395    catch (IOException ioe)
1396    {
1397      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
1398          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
1399      throw new ClientException(
1400              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1401    }
1402    catch (Exception e)
1403    {
1404      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_INITIAL_BIND_RESPONSE.get(
1405          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
1406      throw new ClientException(
1407              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1408    }
1409
1410
1411    // Look at the protocol op from the response.  If it's a bind response, then
1412    // continue.  If it's an extended response, then it could be a notice of
1413    // disconnection so check for that.  Otherwise, generate an error.
1414    switch (responseMessage1.getProtocolOpType())
1415    {
1416      case OP_TYPE_BIND_RESPONSE:
1417        // We'll deal with this later.
1418        break;
1419
1420      case OP_TYPE_EXTENDED_RESPONSE:
1421        ExtendedResponseProtocolOp extendedResponse =
1422             responseMessage1.getExtendedResponseProtocolOp();
1423        String responseOID = extendedResponse.getOID();
1424        if (responseOID != null &&
1425            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
1426        {
1427          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
1428              get(extendedResponse.getResultCode(),
1429                  extendedResponse.getErrorMessage());
1430          throw new LDAPException(extendedResponse.getResultCode(), message);
1431        }
1432        else
1433        {
1434          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
1435          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
1436        }
1437
1438      default:
1439        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage1.getProtocolOp());
1440        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
1441    }
1442
1443
1444    // Make sure that the bind response has the "SASL bind in progress" result
1445    // code.
1446    BindResponseProtocolOp bindResponse1 =
1447         responseMessage1.getBindResponseProtocolOp();
1448    int resultCode1 = bindResponse1.getResultCode();
1449    if (resultCode1 != ReturnCode.SASL_BIND_IN_PROGRESS.get())
1450    {
1451      LocalizableMessage errorMessage = bindResponse1.getErrorMessage();
1452      if (errorMessage == null)
1453      {
1454        errorMessage = LocalizableMessage.EMPTY;
1455      }
1456
1457      LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_INITIAL_BIND_RESPONSE.
1458          get(SASL_MECHANISM_DIGEST_MD5, resultCode1,
1459              ReturnCode.get(resultCode1), errorMessage);
1460      throw new LDAPException(resultCode1, errorMessage, message,
1461                              bindResponse1.getMatchedDN(), null);
1462    }
1463
1464
1465    // Make sure that the bind response contains SASL credentials with the
1466    // information to use for the next stage of the bind.
1467    ByteString serverCredentials =
1468         bindResponse1.getServerSASLCredentials();
1469    if (serverCredentials == null)
1470    {
1471      LocalizableMessage message = ERR_LDAPAUTH_NO_DIGESTMD5_SERVER_CREDENTIALS.get();
1472      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1473    }
1474
1475
1476    // Parse the server SASL credentials to get the necessary information.  In
1477    // particular, look at the realm, the nonce, the QoP modes, and the charset.
1478    // We'll only care about the realm if none was provided in the SASL
1479    // properties and only one was provided in the server SASL credentials.
1480    String  credString = serverCredentials.toString();
1481    String  lowerCreds = toLowerCase(credString);
1482    String  nonce      = null;
1483    boolean useUTF8    = false;
1484    int     pos        = 0;
1485    int     length     = credString.length();
1486    while (pos < length)
1487    {
1488      int equalPos = credString.indexOf('=', pos+1);
1489      if (equalPos < 0)
1490      {
1491        // This is bad because we're not at the end of the string but we don't
1492        // have a name/value delimiter.
1493        LocalizableMessage message =
1494            ERR_LDAPAUTH_DIGESTMD5_INVALID_TOKEN_IN_CREDENTIALS.get(
1495                    credString, pos);
1496        throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1497      }
1498
1499
1500      String tokenName  = lowerCreds.substring(pos, equalPos);
1501
1502      StringBuilder valueBuffer = new StringBuilder();
1503      pos = readToken(credString, equalPos+1, length, valueBuffer);
1504      String tokenValue = valueBuffer.toString();
1505
1506      if (tokenName.equals("charset"))
1507      {
1508        // The value must be the string "utf-8".  If not, that's an error.
1509        if (! tokenValue.equalsIgnoreCase("utf-8"))
1510        {
1511          LocalizableMessage message =
1512              ERR_LDAPAUTH_DIGESTMD5_INVALID_CHARSET.get(tokenValue);
1513          throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1514        }
1515
1516        useUTF8 = true;
1517      }
1518      else if (tokenName.equals("realm"))
1519      {
1520        // This will only be of interest to us if there is only a single realm
1521        // in the server credentials and none was provided as a client-side
1522        // property.
1523        if (! realmSetFromProperty)
1524        {
1525          if (realm == null)
1526          {
1527            // No other realm was specified, so we'll use this one for now.
1528            realm = tokenValue;
1529          }
1530          else
1531          {
1532            // This must mean that there are multiple realms in the server
1533            // credentials.  In that case, we'll not provide any realm at all.
1534            // To make sure that happens, pretend that the client specified the
1535            // realm.
1536            realm                = null;
1537            realmSetFromProperty = true;
1538          }
1539        }
1540      }
1541      else if (tokenName.equals("nonce"))
1542      {
1543        nonce = tokenValue;
1544      }
1545      else if (tokenName.equals("qop"))
1546      {
1547        // The QoP modes provided by the server should be a comma-delimited
1548        // list.  Decode that list and make sure the QoP we have chosen is in
1549        // that list.
1550        StringTokenizer tokenizer = new StringTokenizer(tokenValue, ",");
1551        LinkedList<String> qopModes = new LinkedList<>();
1552        while (tokenizer.hasMoreTokens())
1553        {
1554          qopModes.add(toLowerCase(tokenizer.nextToken().trim()));
1555        }
1556
1557        if (! qopModes.contains(qop))
1558        {
1559          LocalizableMessage message = ERR_LDAPAUTH_REQUESTED_QOP_NOT_SUPPORTED_BY_SERVER.
1560              get(qop, tokenValue);
1561          throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
1562                                    message);
1563        }
1564      }
1565      else
1566      {
1567        // Other values may have been provided, but they aren't of interest to
1568        // us because they shouldn't change anything about the way we encode the
1569        // second part of the request.  Rather than attempt to examine them,
1570        // we'll assume that the server sent a valid response.
1571      }
1572    }
1573
1574
1575    // Make sure that the nonce was included in the response from the server.
1576    if (nonce == null)
1577    {
1578      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_NONCE.get();
1579      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1580    }
1581
1582
1583    // Generate the cnonce that we will use for this request.
1584    String cnonce = generateCNonce();
1585
1586
1587    // Generate the response digest, and initialize the necessary remaining
1588    // variables to use in the generation of that digest.
1589    String nonceCount = "00000001";
1590    String charset    = useUTF8 ? "UTF-8" : "ISO-8859-1";
1591    String responseDigest;
1592    try
1593    {
1594      responseDigest = generateDigestMD5Response(authID, authzID,
1595                                                 bindPassword, realm,
1596                                                 nonce, cnonce, nonceCount,
1597                                                 digestURI, qop, charset);
1598    }
1599    catch (ClientException ce)
1600    {
1601      throw ce;
1602    }
1603    catch (Exception e)
1604    {
1605      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_CANNOT_CREATE_RESPONSE_DIGEST.
1606          get(getExceptionMessage(e));
1607      throw new ClientException(
1608              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1609    }
1610
1611
1612    // Generate the SASL credentials for the second bind request.
1613    StringBuilder credBuffer = new StringBuilder();
1614    credBuffer.append("username=\"");
1615    credBuffer.append(authID);
1616    credBuffer.append("\"");
1617
1618    if (realm != null)
1619    {
1620      credBuffer.append(",realm=\"");
1621      credBuffer.append(realm);
1622      credBuffer.append("\"");
1623    }
1624
1625    credBuffer.append(",nonce=\"");
1626    credBuffer.append(nonce);
1627    credBuffer.append("\",cnonce=\"");
1628    credBuffer.append(cnonce);
1629    credBuffer.append("\",nc=");
1630    credBuffer.append(nonceCount);
1631    credBuffer.append(",qop=");
1632    credBuffer.append(qop);
1633    credBuffer.append(",digest-uri=\"");
1634    credBuffer.append(digestURI);
1635    credBuffer.append("\",response=");
1636    credBuffer.append(responseDigest);
1637
1638    if (useUTF8)
1639    {
1640      credBuffer.append(",charset=utf-8");
1641    }
1642
1643    if (authzID != null)
1644    {
1645      credBuffer.append(",authzid=\"");
1646      credBuffer.append(authzID);
1647      credBuffer.append("\"");
1648    }
1649
1650
1651    // Generate and send the second bind request.
1652    BindRequestProtocolOp bindRequest2 =
1653         new BindRequestProtocolOp(bindDN.toByteString(),
1654             SASL_MECHANISM_DIGEST_MD5,
1655             ByteString.valueOfUtf8(credBuffer.toString()));
1656    LDAPMessage requestMessage2 =
1657         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest2,
1658                         requestControls);
1659
1660    try
1661    {
1662      writer.writeMessage(requestMessage2);
1663    }
1664    catch (IOException ioe)
1665    {
1666      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
1667          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
1668      throw new ClientException(
1669              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1670    }
1671    catch (Exception e)
1672    {
1673      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SECOND_SASL_BIND.get(
1674          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
1675      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
1676                                message, e);
1677    }
1678
1679
1680    // Read the response from the server.
1681    LDAPMessage responseMessage2;
1682    try
1683    {
1684      responseMessage2 = reader.readMessage();
1685      if (responseMessage2 == null)
1686      {
1687        LocalizableMessage message =
1688            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
1689        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
1690                                  message);
1691      }
1692    }
1693    catch (DecodeException | LDAPException e)
1694    {
1695      LocalizableMessage message =
1696          ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1697              SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
1698      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
1699    }
1700    catch (IOException ioe)
1701    {
1702      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1703          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(ioe));
1704      throw new ClientException(
1705              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
1706    }
1707    catch (Exception e)
1708    {
1709      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_SECOND_BIND_RESPONSE.get(
1710          SASL_MECHANISM_DIGEST_MD5, getExceptionMessage(e));
1711      throw new ClientException(
1712              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
1713    }
1714
1715
1716    // See if there are any controls in the response.  If so, then add them to
1717    // the response controls list.
1718    List<Control> respControls = responseMessage2.getControls();
1719    if (respControls != null && ! respControls.isEmpty())
1720    {
1721      responseControls.addAll(respControls);
1722    }
1723
1724
1725    // Look at the protocol op from the response.  If it's a bind response, then
1726    // continue.  If it's an extended response, then it could be a notice of
1727    // disconnection so check for that.  Otherwise, generate an error.
1728    switch (responseMessage2.getProtocolOpType())
1729    {
1730      case OP_TYPE_BIND_RESPONSE:
1731        // We'll deal with this later.
1732        break;
1733
1734      case OP_TYPE_EXTENDED_RESPONSE:
1735        ExtendedResponseProtocolOp extendedResponse =
1736             responseMessage2.getExtendedResponseProtocolOp();
1737        String responseOID = extendedResponse.getOID();
1738        if (responseOID != null &&
1739            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
1740        {
1741          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
1742              get(extendedResponse.getResultCode(),
1743                  extendedResponse.getErrorMessage());
1744          throw new LDAPException(extendedResponse.getResultCode(), message);
1745        }
1746        else
1747        {
1748          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
1749          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
1750        }
1751
1752      default:
1753        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage2.getProtocolOp());
1754        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
1755    }
1756
1757
1758    BindResponseProtocolOp bindResponse2 =
1759         responseMessage2.getBindResponseProtocolOp();
1760    int resultCode2 = bindResponse2.getResultCode();
1761    if (resultCode2 != ReturnCode.SUCCESS.get())
1762    {
1763      // FIXME -- Add support for referrals.
1764
1765      LocalizableMessage message =
1766          ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_DIGEST_MD5);
1767      throw new LDAPException(resultCode2, bindResponse2.getErrorMessage(),
1768                              message, bindResponse2.getMatchedDN(),
1769                              null);
1770    }
1771
1772
1773    // Make sure that the bind response included server SASL credentials with
1774    // the appropriate rspauth value.
1775    ByteString rspAuthCreds = bindResponse2.getServerSASLCredentials();
1776    if (rspAuthCreds == null)
1777    {
1778      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get();
1779      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1780    }
1781
1782    String credStr = toLowerCase(rspAuthCreds.toString());
1783    if (! credStr.startsWith("rspauth="))
1784    {
1785      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_NO_RSPAUTH_CREDS.get();
1786      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1787    }
1788
1789
1790    byte[] serverRspAuth;
1791    try
1792    {
1793      serverRspAuth = hexStringToByteArray(credStr.substring(8));
1794    }
1795    catch (Exception e)
1796    {
1797      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_DECODE_RSPAUTH.get(
1798          getExceptionMessage(e));
1799      throw new LDAPException(ReturnCode.PROTOCOL_ERROR.get(), message);
1800    }
1801
1802    byte[] clientRspAuth;
1803    try
1804    {
1805      clientRspAuth =
1806           generateDigestMD5RspAuth(authID, authzID, bindPassword,
1807                                    realm, nonce, cnonce, nonceCount, digestURI,
1808                                    qop, charset);
1809    }
1810    catch (Exception e)
1811    {
1812      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_COULD_NOT_CALCULATE_RSPAUTH.get(
1813          getExceptionMessage(e));
1814      throw new ClientException(
1815              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
1816    }
1817
1818    if (! Arrays.equals(serverRspAuth, clientRspAuth))
1819    {
1820      LocalizableMessage message = ERR_LDAPAUTH_DIGESTMD5_RSPAUTH_MISMATCH.get();
1821      throw new ClientException(
1822              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
1823    }
1824
1825    // FIXME -- Need to look for things like password expiration warning,
1826    // reset notice, etc.
1827    return null;
1828  }
1829
1830
1831
1832  /**
1833   * Reads the next token from the provided credentials string using the
1834   * provided information.  If the token is surrounded by quotation marks, then
1835   * the token returned will not include those quotation marks.
1836   *
1837   * @param  credentials  The credentials string from which to read the token.
1838   * @param  startPos     The position of the first character of the token to
1839   *                      read.
1840   * @param  length       The total number of characters in the credentials
1841   *                      string.
1842   * @param  token        The buffer into which the token is to be placed.
1843   *
1844   * @return  The position at which the next token should start, or a value
1845   *          greater than or equal to the length of the string if there are no
1846   *          more tokens.
1847   *
1848   * @throws  LDAPException  If a problem occurs while attempting to read the
1849   *                         token.
1850   */
1851  private int readToken(String credentials, int startPos, int length,
1852                        StringBuilder token)
1853          throws LDAPException
1854  {
1855    // If the position is greater than or equal to the length, then we shouldn't
1856    // do anything.
1857    if (startPos >= length)
1858    {
1859      return startPos;
1860    }
1861
1862
1863    // Look at the first character to see if it's an empty string or the string
1864    // is quoted.
1865    boolean isEscaped = false;
1866    boolean isQuoted  = false;
1867    int     pos       = startPos;
1868    char    c         = credentials.charAt(pos++);
1869
1870    if (c == ',')
1871    {
1872      // This must be a zero-length token, so we'll just return the next
1873      // position.
1874      return pos;
1875    }
1876    else if (c == '"')
1877    {
1878      // The string is quoted, so we'll ignore this character, and we'll keep
1879      // reading until we find the unescaped closing quote followed by a comma
1880      // or the end of the string.
1881      isQuoted = true;
1882    }
1883    else if (c == '\\')
1884    {
1885      // The next character is escaped, so we'll take it no matter what.
1886      isEscaped = true;
1887    }
1888    else
1889    {
1890      // The string is not quoted, and this is the first character.  Store this
1891      // character and keep reading until we find a comma or the end of the
1892      // string.
1893      token.append(c);
1894    }
1895
1896
1897    // Enter a loop, reading until we find the appropriate criteria for the end
1898    // of the token.
1899    while (pos < length)
1900    {
1901      c = credentials.charAt(pos++);
1902
1903      if (isEscaped)
1904      {
1905        // The previous character was an escape, so we'll take this no matter
1906        // what.
1907        token.append(c);
1908        isEscaped = false;
1909      }
1910      else if (c == ',')
1911      {
1912        // If this is a quoted string, then this comma is part of the token.
1913        // Otherwise, it's the end of the token.
1914        if (isQuoted)
1915        {
1916          token.append(c);
1917        }
1918        else
1919        {
1920          break;
1921        }
1922      }
1923      else if (c == '"')
1924      {
1925        if (isQuoted)
1926        {
1927          // This should be the end of the token, but in order for it to be
1928          // valid it must be followed by a comma or the end of the string.
1929          if (pos >= length)
1930          {
1931            // We have hit the end of the string, so this is fine.
1932            break;
1933          }
1934          else
1935          {
1936            char c2 = credentials.charAt(pos++);
1937            if (c2 == ',')
1938            {
1939              // We have hit the end of the token, so this is fine.
1940              break;
1941            }
1942            else
1943            {
1944              // We found the closing quote before the end of the token.  This
1945              // is not fine.
1946              LocalizableMessage message =
1947                  ERR_LDAPAUTH_DIGESTMD5_INVALID_CLOSING_QUOTE_POS.get(pos-2);
1948              throw new LDAPException(ReturnCode.INVALID_CREDENTIALS.get(),
1949                                      message);
1950            }
1951          }
1952        }
1953        else
1954        {
1955          // This must be part of the value, so we'll take it.
1956          token.append(c);
1957        }
1958      }
1959      else if (c == '\\')
1960      {
1961        // The next character is escaped.  We'll set a flag so we know to
1962        // accept it, but will not include the backspace itself.
1963        isEscaped = true;
1964      }
1965      else
1966      {
1967        token.append(c);
1968      }
1969    }
1970
1971
1972    return pos;
1973  }
1974
1975
1976
1977  /**
1978   * Generates a cnonce value to use during the DIGEST-MD5 authentication
1979   * process.
1980   *
1981   * @return  The cnonce that should be used for DIGEST-MD5 authentication.
1982   */
1983  private String generateCNonce()
1984  {
1985    if (secureRandom == null)
1986    {
1987      secureRandom = new SecureRandom();
1988    }
1989
1990    byte[] cnonceBytes = new byte[16];
1991    secureRandom.nextBytes(cnonceBytes);
1992
1993    return Base64.encode(cnonceBytes);
1994  }
1995
1996
1997
1998  /**
1999   * Generates the appropriate DIGEST-MD5 response for the provided set of
2000   * information.
2001   *
2002   * @param  authID    The username from the authentication request.
2003   * @param  authzID     The authorization ID from the request, or
2004   *                     <CODE>null</CODE> if there is none.
2005   * @param  password    The clear-text password for the user.
2006   * @param  realm       The realm for which the authentication is to be
2007   *                     performed.
2008   * @param  nonce       The random data generated by the server for use in the
2009   *                     digest.
2010   * @param  cnonce      The random data generated by the client for use in the
2011   *                     digest.
2012   * @param  nonceCount  The 8-digit hex string indicating the number of times
2013   *                     the provided nonce has been used by the client.
2014   * @param  digestURI   The digest URI that specifies the service and host for
2015   *                     which the authentication is being performed.
2016   * @param  qop         The quality of protection string for the
2017   *                     authentication.
2018   * @param  charset     The character set used to encode the information.
2019   *
2020   * @return  The DIGEST-MD5 response for the provided set of information.
2021   *
2022   * @throws  ClientException  If a problem occurs while attempting to
2023   *                           initialize the MD5 digest.
2024   *
2025   * @throws  UnsupportedEncodingException  If the specified character set is
2026   *                                        invalid for some reason.
2027   */
2028  private String generateDigestMD5Response(String authID, String authzID,
2029                                           ByteSequence password, String realm,
2030                                           String nonce, String cnonce,
2031                                           String nonceCount, String digestURI,
2032                                           String qop, String charset)
2033          throws ClientException, UnsupportedEncodingException
2034  {
2035    // Perform the necessary initialization if it hasn't been done yet.
2036    if (md5Digest == null)
2037    {
2038      try
2039      {
2040        md5Digest = MessageDigest.getInstance("MD5");
2041      }
2042      catch (Exception e)
2043      {
2044        LocalizableMessage message = ERR_LDAPAUTH_CANNOT_INITIALIZE_MD5_DIGEST.get(
2045            getExceptionMessage(e));
2046        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
2047                message, e);
2048      }
2049    }
2050
2051
2052    // Get a hash of "username:realm:password".
2053    StringBuilder a1String1 = new StringBuilder();
2054    a1String1.append(authID);
2055    a1String1.append(':');
2056    a1String1.append((realm == null) ? "" : realm);
2057    a1String1.append(':');
2058
2059    byte[] a1Bytes1a = a1String1.toString().getBytes(charset);
2060    byte[] a1Bytes1  = new byte[a1Bytes1a.length + password.length()];
2061    System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length);
2062    password.copyTo(a1Bytes1, a1Bytes1a.length);
2063    byte[] urpHash = md5Digest.digest(a1Bytes1);
2064
2065
2066    // Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
2067    StringBuilder a1String2 = new StringBuilder();
2068    a1String2.append(':');
2069    a1String2.append(nonce);
2070    a1String2.append(':');
2071    a1String2.append(cnonce);
2072    if (authzID != null)
2073    {
2074      a1String2.append(':');
2075      a1String2.append(authzID);
2076    }
2077    byte[] a1Bytes2a = a1String2.toString().getBytes(charset);
2078    byte[] a1Bytes2  = new byte[urpHash.length + a1Bytes2a.length];
2079    System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length);
2080    System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length, a1Bytes2a.length);
2081    byte[] a1Hash = md5Digest.digest(a1Bytes2);
2082
2083
2084    // Next, get a hash of "AUTHENTICATE:digesturi".
2085    byte[] a2Bytes = ("AUTHENTICATE:" + digestURI).getBytes(charset);
2086    byte[] a2Hash  = md5Digest.digest(a2Bytes);
2087
2088
2089    // Get hex string representations of the last two hashes.
2090    String a1HashHex = getHexString(a1Hash);
2091    String a2HashHex = getHexString(a2Hash);
2092
2093
2094    // Put together the final string to hash, consisting of
2095    // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
2096    StringBuilder kdStr = new StringBuilder();
2097    kdStr.append(a1HashHex);
2098    kdStr.append(':');
2099    kdStr.append(nonce);
2100    kdStr.append(':');
2101    kdStr.append(nonceCount);
2102    kdStr.append(':');
2103    kdStr.append(cnonce);
2104    kdStr.append(':');
2105    kdStr.append(qop);
2106    kdStr.append(':');
2107    kdStr.append(a2HashHex);
2108
2109    return getHexString(md5Digest.digest(kdStr.toString().getBytes(charset)));
2110  }
2111
2112
2113
2114  /**
2115   * Generates the appropriate DIGEST-MD5 rspauth digest using the provided
2116   * information.
2117   *
2118   * @param  authID      The username from the authentication request.
2119   * @param  authzID     The authorization ID from the request, or
2120   *                     <CODE>null</CODE> if there is none.
2121   * @param  password    The clear-text password for the user.
2122   * @param  realm       The realm for which the authentication is to be
2123   *                     performed.
2124   * @param  nonce       The random data generated by the server for use in the
2125   *                     digest.
2126   * @param  cnonce      The random data generated by the client for use in the
2127   *                     digest.
2128   * @param  nonceCount  The 8-digit hex string indicating the number of times
2129   *                     the provided nonce has been used by the client.
2130   * @param  digestURI   The digest URI that specifies the service and host for
2131   *                     which the authentication is being performed.
2132   * @param  qop         The quality of protection string for the
2133   *                     authentication.
2134   * @param  charset     The character set used to encode the information.
2135   *
2136   * @return  The DIGEST-MD5 response for the provided set of information.
2137   *
2138   * @throws  UnsupportedEncodingException  If the specified character set is
2139   *                                        invalid for some reason.
2140   */
2141  public byte[] generateDigestMD5RspAuth(String authID, String authzID,
2142                                         ByteSequence password, String realm,
2143                                         String nonce, String cnonce,
2144                                         String nonceCount, String digestURI,
2145                                         String qop, String charset)
2146         throws UnsupportedEncodingException
2147  {
2148    // First, get a hash of "username:realm:password".
2149    StringBuilder a1String1 = new StringBuilder();
2150    a1String1.append(authID);
2151    a1String1.append(':');
2152    a1String1.append(realm);
2153    a1String1.append(':');
2154
2155    byte[] a1Bytes1a = a1String1.toString().getBytes(charset);
2156    byte[] a1Bytes1  = new byte[a1Bytes1a.length + password.length()];
2157    System.arraycopy(a1Bytes1a, 0, a1Bytes1, 0, a1Bytes1a.length);
2158    password.copyTo(a1Bytes1, a1Bytes1a.length);
2159    byte[] urpHash = md5Digest.digest(a1Bytes1);
2160
2161
2162    // Next, get a hash of "urpHash:nonce:cnonce[:authzid]".
2163    StringBuilder a1String2 = new StringBuilder();
2164    a1String2.append(':');
2165    a1String2.append(nonce);
2166    a1String2.append(':');
2167    a1String2.append(cnonce);
2168    if (authzID != null)
2169    {
2170      a1String2.append(':');
2171      a1String2.append(authzID);
2172    }
2173    byte[] a1Bytes2a = a1String2.toString().getBytes(charset);
2174    byte[] a1Bytes2  = new byte[urpHash.length + a1Bytes2a.length];
2175    System.arraycopy(urpHash, 0, a1Bytes2, 0, urpHash.length);
2176    System.arraycopy(a1Bytes2a, 0, a1Bytes2, urpHash.length,
2177                     a1Bytes2a.length);
2178    byte[] a1Hash = md5Digest.digest(a1Bytes2);
2179
2180
2181    // Next, get a hash of "AUTHENTICATE:digesturi".
2182    String a2String = ":" + digestURI;
2183    if (qop.equals("auth-int") || qop.equals("auth-conf"))
2184    {
2185      a2String += ":00000000000000000000000000000000";
2186    }
2187    byte[] a2Bytes = a2String.getBytes(charset);
2188    byte[] a2Hash  = md5Digest.digest(a2Bytes);
2189
2190
2191    // Get hex string representations of the last two hashes.
2192    String a1HashHex = getHexString(a1Hash);
2193    String a2HashHex = getHexString(a2Hash);
2194
2195
2196    // Put together the final string to hash, consisting of
2197    // "a1HashHex:nonce:nonceCount:cnonce:qop:a2HashHex" and get its digest.
2198    StringBuilder kdStr = new StringBuilder();
2199    kdStr.append(a1HashHex);
2200    kdStr.append(':');
2201    kdStr.append(nonce);
2202    kdStr.append(':');
2203    kdStr.append(nonceCount);
2204    kdStr.append(':');
2205    kdStr.append(cnonce);
2206    kdStr.append(':');
2207    kdStr.append(qop);
2208    kdStr.append(':');
2209    kdStr.append(a2HashHex);
2210    return md5Digest.digest(kdStr.toString().getBytes(charset));
2211  }
2212
2213
2214
2215  /**
2216   * Retrieves a hexadecimal string representation of the contents of the
2217   * provided byte array.
2218   *
2219   * @param  byteArray  The byte array for which to obtain the hexadecimal
2220   *                    string representation.
2221   *
2222   * @return  The hexadecimal string representation of the contents of the
2223   *          provided byte array.
2224   */
2225  private String getHexString(byte[] byteArray)
2226  {
2227    StringBuilder buffer = new StringBuilder(2*byteArray.length);
2228    for (byte b : byteArray)
2229    {
2230      buffer.append(byteToLowerHex(b));
2231    }
2232
2233    return buffer.toString();
2234  }
2235
2236
2237
2238  /**
2239   * Retrieves the set of properties that a client may provide when performing a
2240   * SASL DIGEST-MD5 bind, mapped from the property names to their corresponding
2241   * descriptions.
2242   *
2243   * @return  The set of properties that a client may provide when performing a
2244   *          SASL DIGEST-MD5 bind, mapped from the property names to their
2245   *          corresponding descriptions.
2246   */
2247  public static LinkedHashMap<String,LocalizableMessage> getSASLDigestMD5Properties()
2248  {
2249    LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(5);
2250
2251    properties.put(SASL_PROPERTY_AUTHID,
2252                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
2253    properties.put(SASL_PROPERTY_REALM,
2254                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get());
2255    properties.put(SASL_PROPERTY_QOP,
2256                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_QOP.get());
2257    properties.put(SASL_PROPERTY_DIGEST_URI,
2258                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_DIGEST_URI.get());
2259    properties.put(SASL_PROPERTY_AUTHZID,
2260                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
2261
2262    return properties;
2263  }
2264
2265
2266
2267  /**
2268   * Processes a SASL EXTERNAL bind with the provided information.
2269   *
2270   * @param  bindDN            The DN to use to bind to the Directory Server, or
2271   *                           <CODE>null</CODE> if the authentication identity
2272   *                           is to be set through some other means.
2273   * @param  saslProperties    A set of additional properties that may be needed
2274   *                           to process the SASL bind.  SASL EXTERNAL does not
2275   *                           take any properties, so this should be empty or
2276   *                           <CODE>null</CODE>.
2277   * @param  requestControls   The set of controls to include the request to the
2278   *                           server.
2279   * @param  responseControls  A list to hold the set of controls included in
2280   *                           the response from the server.
2281   *
2282   * @return  A message providing additional information about the bind if
2283   *          appropriate, or <CODE>null</CODE> if there is no special
2284   *          information available.
2285   *
2286   * @throws  ClientException  If a client-side problem prevents the bind
2287   *                           attempt from succeeding.
2288   *
2289   * @throws  LDAPException  If the bind fails or some other server-side problem
2290   *                         occurs during processing.
2291   */
2292  public String doSASLExternal(ByteSequence bindDN,
2293                     Map<String,List<String>> saslProperties,
2294                     List<Control> requestControls,
2295                     List<Control> responseControls)
2296         throws ClientException, LDAPException
2297  {
2298    // Make sure that no SASL properties were provided.
2299    if (saslProperties != null && ! saslProperties.isEmpty())
2300    {
2301      LocalizableMessage message =
2302          ERR_LDAPAUTH_NO_ALLOWED_SASL_PROPERTIES.get(SASL_MECHANISM_EXTERNAL);
2303      throw new ClientException(
2304              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2305    }
2306
2307
2308    // Construct the bind request and send it to the server.
2309    BindRequestProtocolOp bindRequest =
2310         new BindRequestProtocolOp(bindDN.toByteString(),
2311             SASL_MECHANISM_EXTERNAL, null);
2312    LDAPMessage requestMessage =
2313         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
2314                         requestControls);
2315
2316    try
2317    {
2318      writer.writeMessage(requestMessage);
2319    }
2320    catch (IOException ioe)
2321    {
2322      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
2323          SASL_MECHANISM_EXTERNAL, getExceptionMessage(ioe));
2324      throw new ClientException(
2325              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
2326    }
2327    catch (Exception e)
2328    {
2329      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
2330          SASL_MECHANISM_EXTERNAL, getExceptionMessage(e));
2331      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
2332                                message, e);
2333    }
2334
2335
2336    // Read the response from the server.
2337    LDAPMessage responseMessage;
2338    try
2339    {
2340      responseMessage = reader.readMessage();
2341      if (responseMessage == null)
2342      {
2343        LocalizableMessage message =
2344            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
2345        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
2346                                  message);
2347      }
2348    }
2349    catch (DecodeException | LDAPException e)
2350    {
2351      LocalizableMessage message =
2352          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
2353      throw new ClientException(
2354              ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
2355    }
2356    catch (IOException ioe)
2357    {
2358      LocalizableMessage message =
2359          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
2360      throw new ClientException(
2361              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
2362    }
2363    catch (Exception e)
2364    {
2365      LocalizableMessage message =
2366          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
2367      throw new ClientException(
2368              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2369    }
2370
2371
2372    // See if there are any controls in the response.  If so, then add them to
2373    // the response controls list.
2374    List<Control> respControls = responseMessage.getControls();
2375    if (respControls != null && ! respControls.isEmpty())
2376    {
2377      responseControls.addAll(respControls);
2378    }
2379
2380
2381    // Look at the protocol op from the response.  If it's a bind response, then
2382    // continue.  If it's an extended response, then it could be a notice of
2383    // disconnection so check for that.  Otherwise, generate an error.
2384    switch (responseMessage.getProtocolOpType())
2385    {
2386      case OP_TYPE_BIND_RESPONSE:
2387        // We'll deal with this later.
2388        break;
2389
2390      case OP_TYPE_EXTENDED_RESPONSE:
2391        ExtendedResponseProtocolOp extendedResponse =
2392             responseMessage.getExtendedResponseProtocolOp();
2393        String responseOID = extendedResponse.getOID();
2394        if (responseOID != null &&
2395            responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
2396        {
2397          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
2398              get(extendedResponse.getResultCode(),
2399                  extendedResponse.getErrorMessage());
2400          throw new LDAPException(extendedResponse.getResultCode(), message);
2401        }
2402        else
2403        {
2404          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
2405          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
2406        }
2407
2408      default:
2409        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp());
2410        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
2411    }
2412
2413
2414    BindResponseProtocolOp bindResponse =
2415         responseMessage.getBindResponseProtocolOp();
2416    int resultCode = bindResponse.getResultCode();
2417    if (resultCode == ReturnCode.SUCCESS.get())
2418    {
2419      // FIXME -- Need to look for things like password expiration warning,
2420      // reset notice, etc.
2421      return null;
2422    }
2423
2424    // FIXME -- Add support for referrals.
2425
2426    LocalizableMessage message =
2427        ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_EXTERNAL);
2428    throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
2429                            message, bindResponse.getMatchedDN(), null);
2430  }
2431
2432
2433
2434  /**
2435   * Retrieves the set of properties that a client may provide when performing a
2436   * SASL EXTERNAL bind, mapped from the property names to their corresponding
2437   * descriptions.
2438   *
2439   * @return  The set of properties that a client may provide when performing a
2440   *          SASL EXTERNAL bind, mapped from the property names to their
2441   *          corresponding descriptions.
2442   */
2443  public static LinkedHashMap<String,LocalizableMessage> getSASLExternalProperties()
2444  {
2445    // There are no properties for the SASL EXTERNAL mechanism.
2446    return new LinkedHashMap<>(0);
2447  }
2448
2449
2450
2451  /**
2452   * Processes a SASL GSSAPI bind with the provided information.
2453   *
2454   * @param  bindDN            The DN to use to bind to the Directory Server, or
2455   *                           <CODE>null</CODE> if the authentication identity
2456   *                           is to be set through some other means.
2457   * @param  bindPassword      The password to use to bind to the Directory
2458   *                           Server.
2459   * @param  saslProperties    A set of additional properties that may be needed
2460   *                           to process the SASL bind.  SASL EXTERNAL does not
2461   *                           take any properties, so this should be empty or
2462   *                           <CODE>null</CODE>.
2463   * @param  requestControls   The set of controls to include the request to the
2464   *                           server.
2465   * @param  responseControls  A list to hold the set of controls included in
2466   *                           the response from the server.
2467   *
2468   * @return  A message providing additional information about the bind if
2469   *          appropriate, or <CODE>null</CODE> if there is no special
2470   *          information available.
2471   *
2472   * @throws  ClientException  If a client-side problem prevents the bind
2473   *                           attempt from succeeding.
2474   *
2475   * @throws  LDAPException  If the bind fails or some other server-side problem
2476   *                         occurs during processing.
2477   */
2478  public String doSASLGSSAPI(ByteSequence bindDN,
2479                     ByteSequence bindPassword,
2480                     Map<String,List<String>> saslProperties,
2481                     List<Control> requestControls,
2482                     List<Control> responseControls)
2483         throws ClientException, LDAPException
2484  {
2485    String kdc     = null;
2486    String realm   = null;
2487
2488    gssapiBindDN  = bindDN;
2489    gssapiAuthID  = null;
2490    gssapiAuthzID = null;
2491    gssapiQoP     = "auth";
2492
2493    if (bindPassword == null)
2494    {
2495      gssapiAuthPW = null;
2496    }
2497    else
2498    {
2499      gssapiAuthPW = bindPassword.toString().toCharArray();
2500    }
2501
2502
2503    // Evaluate the properties provided.  The authID is required.  The authzID,
2504    // KDC, QoP, and realm are optional.
2505    if (saslProperties == null || saslProperties.isEmpty())
2506    {
2507      LocalizableMessage message =
2508          ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_GSSAPI);
2509      throw new ClientException(
2510              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2511    }
2512
2513    for (String name : saslProperties.keySet())
2514    {
2515      String lowerName = toLowerCase(name);
2516
2517      if (lowerName.equals(SASL_PROPERTY_AUTHID))
2518      {
2519        List<String> values = saslProperties.get(name);
2520        Iterator<String> iterator = values.iterator();
2521        if (iterator.hasNext())
2522        {
2523          gssapiAuthID = iterator.next();
2524
2525          if (iterator.hasNext())
2526          {
2527            LocalizableMessage message = ERR_LDAPAUTH_AUTHID_SINGLE_VALUED.get();
2528            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2529          }
2530        }
2531      }
2532      else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
2533      {
2534        List<String> values = saslProperties.get(name);
2535        Iterator<String> iterator = values.iterator();
2536        if (iterator.hasNext())
2537        {
2538          gssapiAuthzID = iterator.next();
2539
2540          if (iterator.hasNext())
2541          {
2542            LocalizableMessage message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get();
2543            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
2544                                      message);
2545          }
2546        }
2547      }
2548      else if (lowerName.equals(SASL_PROPERTY_KDC))
2549      {
2550        List<String> values = saslProperties.get(name);
2551        Iterator<String> iterator = values.iterator();
2552        if (iterator.hasNext())
2553        {
2554          kdc = iterator.next();
2555
2556          if (iterator.hasNext())
2557          {
2558            LocalizableMessage message = ERR_LDAPAUTH_KDC_SINGLE_VALUED.get();
2559            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
2560                                      message);
2561          }
2562        }
2563      }
2564      else if (lowerName.equals(SASL_PROPERTY_QOP))
2565      {
2566        List<String> values = saslProperties.get(name);
2567        Iterator<String> iterator = values.iterator();
2568        if (iterator.hasNext())
2569        {
2570          gssapiQoP = toLowerCase(iterator.next());
2571
2572          if (iterator.hasNext())
2573          {
2574            LocalizableMessage message = ERR_LDAPAUTH_QOP_SINGLE_VALUED.get();
2575            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
2576                                      message);
2577          }
2578
2579          if (gssapiQoP.equals("auth"))
2580          {
2581            // This is always fine.
2582          }
2583          else if (gssapiQoP.equals("auth-int") ||
2584                   gssapiQoP.equals("auth-conf"))
2585          {
2586            // FIXME -- Add support for integrity and confidentiality.
2587            LocalizableMessage message =
2588                ERR_LDAPAUTH_DIGESTMD5_QOP_NOT_SUPPORTED.get(gssapiQoP);
2589            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
2590                                      message);
2591          }
2592          else
2593          {
2594            // This is an illegal value.
2595            LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_INVALID_QOP.get(gssapiQoP);
2596            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
2597                                      message);
2598          }
2599        }
2600      }
2601      else if (lowerName.equals(SASL_PROPERTY_REALM))
2602      {
2603        List<String> values = saslProperties.get(name);
2604        Iterator<String> iterator = values.iterator();
2605        if (iterator.hasNext())
2606        {
2607          realm = iterator.next();
2608
2609          if (iterator.hasNext())
2610          {
2611            LocalizableMessage message = ERR_LDAPAUTH_REALM_SINGLE_VALUED.get();
2612            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
2613                                      message);
2614          }
2615        }
2616      }
2617      else
2618      {
2619        LocalizableMessage message =
2620            ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_GSSAPI);
2621        throw new ClientException(
2622                ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2623      }
2624    }
2625
2626
2627    // Make sure that the authID was provided.
2628    if (gssapiAuthID == null || gssapiAuthID.length() == 0)
2629    {
2630      LocalizableMessage message =
2631          ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_GSSAPI);
2632      throw new ClientException(
2633              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2634    }
2635
2636
2637    // See if an authzID was provided.  If not, then use the authID.
2638    if (gssapiAuthzID == null)
2639    {
2640      gssapiAuthzID = gssapiAuthID;
2641    }
2642
2643
2644    // See if the realm and/or KDC were specified.  If so, then set properties
2645    // that will allow them to be used.  Otherwise, we'll hope that the
2646    // underlying system has a valid Kerberos client configuration.
2647    if (realm != null)
2648    {
2649      System.setProperty(KRBV_PROPERTY_REALM, realm);
2650    }
2651
2652    if (kdc != null)
2653    {
2654      System.setProperty(KRBV_PROPERTY_KDC, kdc);
2655    }
2656
2657
2658    // Since we're going to be using JAAS behind the scenes, we need to have a
2659    // JAAS configuration.  Rather than always requiring the user to provide it,
2660    // we'll write one to a temporary file that will be deleted when the JVM
2661    // exits.
2662    String configFileName;
2663    try
2664    {
2665      File tempFile = File.createTempFile("login", "conf");
2666      configFileName = tempFile.getAbsolutePath();
2667      tempFile.deleteOnExit();
2668      BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false));
2669
2670      w.write(getClass().getName() + " {");
2671      w.newLine();
2672
2673      w.write("  com.sun.security.auth.module.Krb5LoginModule required " +
2674              "client=TRUE useTicketCache=TRUE;");
2675      w.newLine();
2676
2677      w.write("};");
2678      w.newLine();
2679
2680      w.flush();
2681      w.close();
2682    }
2683    catch (Exception e)
2684    {
2685      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_JAAS_CONFIG.get(
2686          getExceptionMessage(e));
2687      throw new ClientException(
2688              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2689    }
2690
2691    System.setProperty(JAAS_PROPERTY_CONFIG_FILE, configFileName);
2692    System.setProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY, "true");
2693
2694
2695    // The rest of this code must be executed via JAAS, so it will have to go
2696    // in the "run" method.
2697    LoginContext loginContext;
2698    try
2699    {
2700      loginContext = new LoginContext(getClass().getName(), this);
2701      loginContext.login();
2702    }
2703    catch (Exception e)
2704    {
2705      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_LOCAL_AUTHENTICATION_FAILED.get(
2706          getExceptionMessage(e));
2707      throw new ClientException(
2708              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2709    }
2710
2711    try
2712    {
2713      Subject.doAs(loginContext.getSubject(), this);
2714    }
2715    catch (Exception e)
2716    {
2717      if (e instanceof ClientException)
2718      {
2719        throw (ClientException) e;
2720      }
2721      else if (e instanceof LDAPException)
2722      {
2723        throw (LDAPException) e;
2724      }
2725
2726      LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_REMOTE_AUTHENTICATION_FAILED.get(
2727              getExceptionMessage(e));
2728      throw new ClientException(
2729              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2730    }
2731
2732
2733    // FIXME --  Need to make sure we handle request and response controls
2734    // properly, and also check for any possible message to send back to the
2735    // client.
2736    return null;
2737  }
2738
2739
2740
2741  /**
2742   * Retrieves the set of properties that a client may provide when performing a
2743   * SASL EXTERNAL bind, mapped from the property names to their corresponding
2744   * descriptions.
2745   *
2746   * @return  The set of properties that a client may provide when performing a
2747   *          SASL EXTERNAL bind, mapped from the property names to their
2748   *          corresponding descriptions.
2749   */
2750  public static LinkedHashMap<String,LocalizableMessage> getSASLGSSAPIProperties()
2751  {
2752    LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(4);
2753
2754    properties.put(SASL_PROPERTY_AUTHID,
2755                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
2756    properties.put(SASL_PROPERTY_AUTHZID,
2757                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
2758    properties.put(SASL_PROPERTY_KDC,
2759                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_KDC.get());
2760    properties.put(SASL_PROPERTY_REALM,
2761                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_REALM.get());
2762
2763    return properties;
2764  }
2765
2766
2767
2768  /**
2769   * Processes a SASL PLAIN bind with the provided information.
2770   *
2771   * @param  bindDN            The DN to use to bind to the Directory Server, or
2772   *                           <CODE>null</CODE> if the authentication identity
2773   *                           is to be set through some other means.
2774   * @param  bindPassword      The password to use to bind to the Directory
2775   *                           Server.
2776   * @param  saslProperties    A set of additional properties that may be needed
2777   *                           to process the SASL bind.
2778   * @param  requestControls   The set of controls to include the request to the
2779   *                           server.
2780   * @param  responseControls  A list to hold the set of controls included in
2781   *                           the response from the server.
2782   *
2783   * @return  A message providing additional information about the bind if
2784   *          appropriate, or <CODE>null</CODE> if there is no special
2785   *          information available.
2786   *
2787   * @throws  ClientException  If a client-side problem prevents the bind
2788   *                           attempt from succeeding.
2789   *
2790   * @throws  LDAPException  If the bind fails or some other server-side problem
2791   *                         occurs during processing.
2792   */
2793  public String doSASLPlain(ByteSequence bindDN,
2794                     ByteSequence bindPassword,
2795                     Map<String,List<String>> saslProperties,
2796                     List<Control> requestControls,
2797                     List<Control> responseControls)
2798         throws ClientException, LDAPException
2799  {
2800    String authID  = null;
2801    String authzID = null;
2802
2803
2804    // Evaluate the properties provided.  The authID is required, and authzID is
2805    // optional.
2806    if (saslProperties == null || saslProperties.isEmpty())
2807    {
2808      LocalizableMessage message =
2809          ERR_LDAPAUTH_NO_SASL_PROPERTIES.get(SASL_MECHANISM_PLAIN);
2810      throw new ClientException(
2811              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2812    }
2813
2814    for (String name : saslProperties.keySet())
2815    {
2816      String lowerName = toLowerCase(name);
2817
2818      if (lowerName.equals(SASL_PROPERTY_AUTHID))
2819      {
2820        authID = getAuthID(saslProperties, authID, name);
2821      }
2822      else if (lowerName.equals(SASL_PROPERTY_AUTHZID))
2823      {
2824        List<String> values = saslProperties.get(name);
2825        Iterator<String> iterator = values.iterator();
2826        if (iterator.hasNext())
2827        {
2828          authzID = iterator.next();
2829
2830          if (iterator.hasNext())
2831          {
2832            LocalizableMessage message = ERR_LDAPAUTH_AUTHZID_SINGLE_VALUED.get();
2833            throw new ClientException(ReturnCode.CLIENT_SIDE_PARAM_ERROR,
2834                                      message);
2835          }
2836        }
2837      }
2838      else
2839      {
2840        LocalizableMessage message =
2841            ERR_LDAPAUTH_INVALID_SASL_PROPERTY.get(name, SASL_MECHANISM_PLAIN);
2842        throw new ClientException(
2843                ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2844      }
2845    }
2846
2847
2848    // Make sure that at least the authID was provided.
2849    if (authID == null || authID.length() == 0)
2850    {
2851      LocalizableMessage message =
2852          ERR_LDAPAUTH_SASL_AUTHID_REQUIRED.get(SASL_MECHANISM_PLAIN);
2853      throw new ClientException(
2854              ReturnCode.CLIENT_SIDE_PARAM_ERROR, message);
2855    }
2856
2857
2858    // Set password to ByteString.empty if the password is null.
2859    if (bindPassword == null)
2860    {
2861        bindPassword = ByteString.empty();
2862    }
2863
2864
2865    // Construct the bind request and send it to the server.
2866    StringBuilder credBuffer = new StringBuilder();
2867    if (authzID != null)
2868    {
2869      credBuffer.append(authzID);
2870    }
2871    credBuffer.append('\u0000');
2872    credBuffer.append(authID);
2873    credBuffer.append('\u0000');
2874    credBuffer.append(bindPassword.toString());
2875
2876    ByteString saslCredentials =
2877        ByteString.valueOfUtf8(credBuffer.toString());
2878    BindRequestProtocolOp bindRequest =
2879         new BindRequestProtocolOp(bindDN.toByteString(), SASL_MECHANISM_PLAIN,
2880                                saslCredentials);
2881    LDAPMessage requestMessage =
2882         new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest,
2883                         requestControls);
2884
2885    try
2886    {
2887      writer.writeMessage(requestMessage);
2888    }
2889    catch (IOException ioe)
2890    {
2891      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
2892          SASL_MECHANISM_PLAIN, getExceptionMessage(ioe));
2893      throw new ClientException(
2894              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
2895    }
2896    catch (Exception e)
2897    {
2898      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
2899          SASL_MECHANISM_PLAIN, getExceptionMessage(e));
2900      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
2901                                message, e);
2902    }
2903
2904
2905    // Read the response from the server.
2906    LDAPMessage responseMessage;
2907    try
2908    {
2909      responseMessage = reader.readMessage();
2910      if (responseMessage == null)
2911      {
2912        LocalizableMessage message =
2913            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
2914        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
2915                                  message);
2916      }
2917    }
2918    catch (DecodeException | LDAPException e)
2919    {
2920      LocalizableMessage message =
2921          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
2922      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
2923    }
2924    catch (IOException ioe)
2925    {
2926      LocalizableMessage message =
2927          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(ioe));
2928      throw new ClientException(
2929              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
2930    }
2931    catch (Exception e)
2932    {
2933      LocalizableMessage message =
2934          ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
2935      throw new ClientException(
2936              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
2937    }
2938
2939
2940    // See if there are any controls in the response.  If so, then add them to
2941    // the response controls list.
2942    List<Control> respControls = responseMessage.getControls();
2943    if (respControls != null && !respControls.isEmpty())
2944    {
2945      responseControls.addAll(respControls);
2946    }
2947
2948
2949    // Look at the protocol op from the response.  If it's a bind response, then
2950    // continue.  If it's an extended response, then it could be a notice of
2951    // disconnection so check for that.  Otherwise, generate an error.
2952    generateError(responseMessage);
2953
2954
2955    BindResponseProtocolOp bindResponse =
2956         responseMessage.getBindResponseProtocolOp();
2957    int resultCode = bindResponse.getResultCode();
2958    if (resultCode == ReturnCode.SUCCESS.get())
2959    {
2960      // FIXME -- Need to look for things like password expiration warning,
2961      // reset notice, etc.
2962      return null;
2963    }
2964
2965    // FIXME -- Add support for referrals.
2966
2967    LocalizableMessage message = ERR_LDAPAUTH_SASL_BIND_FAILED.get(SASL_MECHANISM_PLAIN);
2968    throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
2969                            message, bindResponse.getMatchedDN(), null);
2970  }
2971
2972
2973
2974  /**
2975   * Retrieves the set of properties that a client may provide when performing a
2976   * SASL PLAIN bind, mapped from the property names to their corresponding
2977   * descriptions.
2978   *
2979   * @return  The set of properties that a client may provide when performing a
2980   *          SASL PLAIN bind, mapped from the property names to their
2981   *          corresponding descriptions.
2982   */
2983  public static LinkedHashMap<String,LocalizableMessage> getSASLPlainProperties()
2984  {
2985    LinkedHashMap<String,LocalizableMessage> properties = new LinkedHashMap<>(2);
2986
2987    properties.put(SASL_PROPERTY_AUTHID,
2988                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHID.get());
2989    properties.put(SASL_PROPERTY_AUTHZID,
2990                   INFO_LDAPAUTH_PROPERTY_DESCRIPTION_AUTHZID.get());
2991
2992    return properties;
2993  }
2994
2995
2996
2997  /**
2998   * Performs a privileged operation under JAAS so that the local authentication
2999   * information can be available for the SASL bind to the Directory Server.
3000   *
3001   * @return  A placeholder object in order to comply with the
3002   *          <CODE>PrivilegedExceptionAction</CODE> interface.
3003   *
3004   * @throws  ClientException  If a client-side problem occurs during the bind
3005   *                           processing.
3006   *
3007   * @throws  LDAPException  If a server-side problem occurs during the bind
3008   *                         processing.
3009   */
3010  @Override
3011  public Object run()
3012         throws ClientException, LDAPException
3013  {
3014    if (saslMechanism == null)
3015    {
3016      LocalizableMessage message = ERR_LDAPAUTH_NONSASL_RUN_INVOCATION.get(getBacktrace());
3017      throw new ClientException(
3018              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
3019    }
3020    else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
3021    {
3022      // Create the property map that will be used by the internal SASL handler.
3023      HashMap<String,String> saslProperties = new HashMap<>();
3024      saslProperties.put(Sasl.QOP, gssapiQoP);
3025      saslProperties.put(Sasl.SERVER_AUTH, "true");
3026
3027
3028      // Create the SASL client that we will use to actually perform the
3029      // authentication.
3030      SaslClient saslClient;
3031      try
3032      {
3033        saslClient =
3034             Sasl.createSaslClient(new String[] { SASL_MECHANISM_GSSAPI },
3035                                   gssapiAuthzID, "ldap", hostName,
3036                                   saslProperties, this);
3037      }
3038      catch (Exception e)
3039      {
3040        LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_SASL_CLIENT.get(
3041            getExceptionMessage(e));
3042        throw new ClientException(
3043                ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
3044      }
3045
3046
3047      // Get the SASL credentials to include in the initial bind request.
3048      ByteString saslCredentials;
3049      if (saslClient.hasInitialResponse())
3050      {
3051        try
3052        {
3053          byte[] credBytes = saslClient.evaluateChallenge(new byte[0]);
3054          saslCredentials = ByteString.wrap(credBytes);
3055        }
3056        catch (Exception e)
3057        {
3058          LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_CREATE_INITIAL_CHALLENGE.
3059              get(getExceptionMessage(e));
3060          throw new ClientException(
3061                  ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
3062                                    message, e);
3063        }
3064      }
3065      else
3066      {
3067        saslCredentials = null;
3068      }
3069
3070
3071      BindRequestProtocolOp bindRequest =
3072           new BindRequestProtocolOp(gssapiBindDN.toByteString(),
3073               SASL_MECHANISM_GSSAPI, saslCredentials);
3074      // FIXME -- Add controls here?
3075      LDAPMessage requestMessage =
3076           new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest);
3077
3078      try
3079      {
3080        writer.writeMessage(requestMessage);
3081      }
3082      catch (IOException ioe)
3083      {
3084        LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
3085            SASL_MECHANISM_GSSAPI, getExceptionMessage(ioe));
3086        throw new ClientException(
3087                ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
3088      }
3089      catch (Exception e)
3090      {
3091        LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
3092            SASL_MECHANISM_GSSAPI, getExceptionMessage(e));
3093        throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
3094                                  message, e);
3095      }
3096
3097
3098      // Read the response from the server.
3099      LDAPMessage responseMessage;
3100      try
3101      {
3102        responseMessage = reader.readMessage();
3103        if (responseMessage == null)
3104        {
3105          LocalizableMessage message =
3106              ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
3107          throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
3108                                    message);
3109        }
3110      }
3111      catch (DecodeException | LDAPException e)
3112      {
3113        LocalizableMessage message =
3114            ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
3115        throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
3116      }
3117      catch (IOException ioe)
3118      {
3119        LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
3120            getExceptionMessage(ioe));
3121        throw new ClientException(
3122                ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
3123      }
3124      catch (Exception e)
3125      {
3126        LocalizableMessage message =
3127            ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
3128        throw new ClientException(
3129                ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
3130      }
3131
3132
3133      // FIXME -- Handle response controls.
3134
3135
3136      // Look at the protocol op from the response.  If it's a bind response,
3137      // then continue.  If it's an extended response, then it could be a notice
3138      // of disconnection so check for that.  Otherwise, generate an error.
3139      generateError(responseMessage);
3140
3141
3142      while (true)
3143      {
3144        BindResponseProtocolOp bindResponse =
3145             responseMessage.getBindResponseProtocolOp();
3146        int resultCode = bindResponse.getResultCode();
3147        if (resultCode == ReturnCode.SUCCESS.get())
3148        {
3149          // We should be done after this, but we still need to look for and
3150          // handle the server SASL credentials.
3151          ByteString serverSASLCredentials =
3152               bindResponse.getServerSASLCredentials();
3153          if (serverSASLCredentials != null)
3154          {
3155            try
3156            {
3157              saslClient.evaluateChallenge(serverSASLCredentials.toByteArray());
3158            }
3159            catch (Exception e)
3160            {
3161              LocalizableMessage message =
3162                  ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.
3163                    get(getExceptionMessage(e));
3164              throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
3165                                        message, e);
3166            }
3167          }
3168
3169
3170          // Just to be sure, check that the login really is complete.
3171          if (! saslClient.isComplete())
3172          {
3173            LocalizableMessage message =
3174                ERR_LDAPAUTH_GSSAPI_UNEXPECTED_SUCCESS_RESPONSE.get();
3175            throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
3176                                      message);
3177          }
3178
3179          break;
3180        }
3181        else if (resultCode == ReturnCode.SASL_BIND_IN_PROGRESS.get())
3182        {
3183          // Read the response and process the server SASL credentials.
3184          ByteString serverSASLCredentials =
3185               bindResponse.getServerSASLCredentials();
3186          byte[] credBytes;
3187          try
3188          {
3189            if (serverSASLCredentials == null)
3190            {
3191              credBytes = saslClient.evaluateChallenge(new byte[0]);
3192            }
3193            else
3194            {
3195              credBytes = saslClient.evaluateChallenge(
3196                  serverSASLCredentials.toByteArray());
3197            }
3198          }
3199          catch (Exception e)
3200          {
3201            LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_CANNOT_VALIDATE_SERVER_CREDS.
3202                get(getExceptionMessage(e));
3203            throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
3204                                      message, e);
3205          }
3206
3207
3208          // Send the next bind in the sequence to the server.
3209          bindRequest =
3210               new BindRequestProtocolOp(gssapiBindDN.toByteString(),
3211                   SASL_MECHANISM_GSSAPI, ByteString.wrap(credBytes));
3212          // FIXME -- Add controls here?
3213          requestMessage =
3214               new LDAPMessage(nextMessageID.getAndIncrement(), bindRequest);
3215
3216
3217          try
3218          {
3219            writer.writeMessage(requestMessage);
3220          }
3221          catch (IOException ioe)
3222          {
3223            LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
3224                SASL_MECHANISM_GSSAPI, getExceptionMessage(ioe));
3225            throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
3226                                      message, ioe);
3227          }
3228          catch (Exception e)
3229          {
3230            LocalizableMessage message = ERR_LDAPAUTH_CANNOT_SEND_SASL_BIND.get(
3231                SASL_MECHANISM_GSSAPI, getExceptionMessage(e));
3232            throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
3233                                      message, e);
3234          }
3235
3236
3237          // Read the response from the server.
3238          try
3239          {
3240            responseMessage = reader.readMessage();
3241            if (responseMessage == null)
3242            {
3243              LocalizableMessage message =
3244                  ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
3245              throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
3246                                        message);
3247            }
3248          }
3249          catch (DecodeException | LDAPException e)
3250          {
3251            LocalizableMessage message =
3252                ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(getExceptionMessage(e));
3253            throw new ClientException(
3254                ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
3255          }
3256          catch (IOException ioe)
3257          {
3258            LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
3259                getExceptionMessage(ioe));
3260            throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
3261                                      message, ioe);
3262          }
3263          catch (Exception e)
3264          {
3265            LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_BIND_RESPONSE.get(
3266                getExceptionMessage(e));
3267            throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR,
3268                                      message, e);
3269          }
3270
3271
3272          // FIXME -- Handle response controls.
3273
3274
3275          // Look at the protocol op from the response.  If it's a bind
3276          // response, then continue.  If it's an extended response, then it
3277          // could be a notice of disconnection so check for that.  Otherwise,
3278          // generate an error.
3279          generateError(responseMessage);
3280        }
3281        else
3282        {
3283          // This is an error.
3284          LocalizableMessage message = ERR_LDAPAUTH_GSSAPI_BIND_FAILED.get();
3285          throw new LDAPException(resultCode, bindResponse.getErrorMessage(),
3286                                  message, bindResponse.getMatchedDN(),
3287                                  null);
3288        }
3289      }
3290    }
3291    else
3292    {
3293      LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RUN_INVOCATION.get(
3294          saslMechanism, getBacktrace());
3295      throw new ClientException(
3296              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
3297    }
3298
3299
3300    // FIXME -- Need to look for things like password expiration warning, reset
3301    // notice, etc.
3302    return null;
3303  }
3304
3305  private void generateError(LDAPMessage responseMessage) throws LDAPException, ClientException
3306  {
3307    switch (responseMessage.getProtocolOpType())
3308    {
3309      case OP_TYPE_BIND_RESPONSE:
3310        // We'll deal with this later.
3311        break;
3312
3313      case OP_TYPE_EXTENDED_RESPONSE:
3314        ExtendedResponseProtocolOp extendedResponse =
3315             responseMessage.getExtendedResponseProtocolOp();
3316        String responseOID = extendedResponse.getOID();
3317        if (OID_NOTICE_OF_DISCONNECTION.equals(responseOID))
3318        {
3319          LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.
3320              get(extendedResponse.getResultCode(), extendedResponse.getErrorMessage());
3321          throw new LDAPException(extendedResponse.getResultCode(), message);
3322        }
3323        else
3324        {
3325          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_EXTENDED_RESPONSE.get(extendedResponse);
3326          throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
3327        }
3328
3329      default:
3330        LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp());
3331        throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
3332    }
3333  }
3334
3335  /**
3336   * Handles the authentication callbacks to provide information needed by the
3337   * JAAS login process.
3338   *
3339   * @param  callbacks  The callbacks needed to provide information for the JAAS
3340   *                    login process.
3341   *
3342   * @throws  UnsupportedCallbackException  If an unexpected callback is
3343   *                                        included in the provided set.
3344   */
3345  @Override
3346  public void handle(Callback[] callbacks)
3347         throws UnsupportedCallbackException
3348  {
3349    if (saslMechanism ==  null)
3350    {
3351      LocalizableMessage message =
3352          ERR_LDAPAUTH_NONSASL_CALLBACK_INVOCATION.get(getBacktrace());
3353      throw new UnsupportedCallbackException(callbacks[0], message.toString());
3354    }
3355    else if (saslMechanism.equals(SASL_MECHANISM_GSSAPI))
3356    {
3357      for (Callback cb : callbacks)
3358      {
3359        if (cb instanceof NameCallback)
3360        {
3361          ((NameCallback) cb).setName(gssapiAuthID);
3362        }
3363        else if (cb instanceof PasswordCallback)
3364        {
3365          if (gssapiAuthPW == null)
3366          {
3367            System.out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(gssapiAuthID));
3368            try
3369            {
3370              gssapiAuthPW = ConsoleApplication.readPassword();
3371            }
3372            catch (ClientException e)
3373            {
3374              throw new UnsupportedCallbackException(cb, e.getLocalizedMessage());
3375            }
3376          }
3377
3378          ((PasswordCallback) cb).setPassword(gssapiAuthPW);
3379        }
3380        else
3381        {
3382          LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_GSSAPI_CALLBACK.get(cb);
3383          throw new UnsupportedCallbackException(cb, message.toString());
3384        }
3385      }
3386    }
3387    else
3388    {
3389      LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_CALLBACK_INVOCATION.get(
3390          saslMechanism, getBacktrace());
3391      throw new UnsupportedCallbackException(callbacks[0], message.toString());
3392    }
3393  }
3394
3395
3396
3397  /**
3398   * Uses the "Who Am I?" extended operation to request that the server provide
3399   * the client with the authorization identity for this connection.
3400   *
3401   * @return  An ASN.1 octet string containing the authorization identity, or
3402   *          <CODE>null</CODE> if the client is not authenticated or is
3403   *          authenticated anonymously.
3404   *
3405   * @throws  ClientException  If a client-side problem occurs during the
3406   *                           request processing.
3407   *
3408   * @throws  LDAPException  If a server-side problem occurs during the request
3409   *                         processing.
3410   */
3411  public ByteString requestAuthorizationIdentity()
3412         throws ClientException, LDAPException
3413  {
3414    // Construct the extended request and send it to the server.
3415    ExtendedRequestProtocolOp extendedRequest =
3416         new ExtendedRequestProtocolOp(OID_WHO_AM_I_REQUEST);
3417    LDAPMessage requestMessage =
3418         new LDAPMessage(nextMessageID.getAndIncrement(), extendedRequest);
3419
3420    try
3421    {
3422      writer.writeMessage(requestMessage);
3423    }
3424    catch (IOException ioe)
3425    {
3426      LocalizableMessage message =
3427          ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(ioe));
3428      throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
3429              message, ioe);
3430    }
3431    catch (Exception e)
3432    {
3433      LocalizableMessage message =
3434          ERR_LDAPAUTH_CANNOT_SEND_WHOAMI_REQUEST.get(getExceptionMessage(e));
3435      throw new ClientException(ReturnCode.CLIENT_SIDE_ENCODING_ERROR,
3436                                message, e);
3437    }
3438
3439
3440    // Read the response from the server.
3441    LDAPMessage responseMessage;
3442    try
3443    {
3444      responseMessage = reader.readMessage();
3445      if (responseMessage == null)
3446      {
3447        LocalizableMessage message =
3448            ERR_LDAPAUTH_CONNECTION_CLOSED_WITHOUT_BIND_RESPONSE.get();
3449        throw new ClientException(ReturnCode.CLIENT_SIDE_SERVER_DOWN,
3450                                  message);
3451      }
3452    }
3453    catch (DecodeException | LDAPException e)
3454    {
3455      LocalizableMessage message =
3456          ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(e));
3457      throw new ClientException(ReturnCode.CLIENT_SIDE_DECODING_ERROR, message, e);
3458    }
3459    catch (IOException ioe)
3460    {
3461      LocalizableMessage message = ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(
3462          getExceptionMessage(ioe));
3463      throw new ClientException(
3464              ReturnCode.CLIENT_SIDE_SERVER_DOWN, message, ioe);
3465    }
3466    catch (Exception e)
3467    {
3468      LocalizableMessage message =
3469          ERR_LDAPAUTH_CANNOT_READ_WHOAMI_RESPONSE.get(getExceptionMessage(e));
3470      throw new ClientException(
3471              ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message, e);
3472    }
3473
3474
3475    // If the protocol op isn't an extended response, then that's a problem.
3476    if (responseMessage.getProtocolOpType() != OP_TYPE_EXTENDED_RESPONSE)
3477    {
3478      LocalizableMessage message = ERR_LDAPAUTH_UNEXPECTED_RESPONSE.get(responseMessage.getProtocolOp());
3479      throw new ClientException(ReturnCode.CLIENT_SIDE_LOCAL_ERROR, message);
3480    }
3481
3482
3483    // Get the extended response and see if it has the "notice of disconnection"
3484    // OID.  If so, then the server is closing the connection.
3485    ExtendedResponseProtocolOp extendedResponse =
3486         responseMessage.getExtendedResponseProtocolOp();
3487    String responseOID = extendedResponse.getOID();
3488    if (OID_NOTICE_OF_DISCONNECTION.equals(responseOID))
3489    {
3490      LocalizableMessage message = ERR_LDAPAUTH_SERVER_DISCONNECT.get(
3491          extendedResponse.getResultCode(), extendedResponse.getErrorMessage());
3492      throw new LDAPException(extendedResponse.getResultCode(), message);
3493    }
3494
3495
3496    // It isn't a notice of disconnection so it must be the "Who Am I?"
3497    // response and the value would be the authorization ID.  However, first
3498    // check that it was successful.  If it was not, then fail.
3499    int resultCode = extendedResponse.getResultCode();
3500    if (resultCode != ReturnCode.SUCCESS.get())
3501    {
3502      LocalizableMessage message = ERR_LDAPAUTH_WHOAMI_FAILED.get();
3503      throw new LDAPException(resultCode, extendedResponse.getErrorMessage(),
3504                              message, extendedResponse.getMatchedDN(),
3505                              null);
3506    }
3507
3508
3509    // Get the authorization ID (if there is one) and return it to the caller.
3510    ByteString authzID = extendedResponse.getValue();
3511    if (authzID == null || authzID.length() == 0)
3512    {
3513      return null;
3514    }
3515
3516    String valueString = authzID.toString();
3517    if (valueString == null || valueString.length() == 0 ||
3518        valueString.equalsIgnoreCase("dn:"))
3519    {
3520      return null;
3521    }
3522
3523    return authzID;
3524  }
3525}