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