001/*
002 * Copyright 2011-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2011-2018 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.util;
022
023
024
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.TreeMap;
031
032import com.unboundid.ldap.sdk.ANONYMOUSBindRequest;
033import com.unboundid.ldap.sdk.Control;
034import com.unboundid.ldap.sdk.CRAMMD5BindRequest;
035import com.unboundid.ldap.sdk.DIGESTMD5BindRequest;
036import com.unboundid.ldap.sdk.DIGESTMD5BindRequestProperties;
037import com.unboundid.ldap.sdk.EXTERNALBindRequest;
038import com.unboundid.ldap.sdk.GSSAPIBindRequest;
039import com.unboundid.ldap.sdk.GSSAPIBindRequestProperties;
040import com.unboundid.ldap.sdk.LDAPException;
041import com.unboundid.ldap.sdk.PLAINBindRequest;
042import com.unboundid.ldap.sdk.ResultCode;
043import com.unboundid.ldap.sdk.SASLBindRequest;
044import com.unboundid.ldap.sdk.SASLQualityOfProtection;
045import com.unboundid.ldap.sdk.unboundidds.SingleUseTOTPBindRequest;
046import com.unboundid.ldap.sdk.unboundidds.UnboundIDDeliveredOTPBindRequest;
047import com.unboundid.ldap.sdk.unboundidds.UnboundIDTOTPBindRequest;
048import com.unboundid.ldap.sdk.unboundidds.UnboundIDYubiKeyOTPBindRequest;
049
050import static com.unboundid.util.StaticUtils.*;
051import static com.unboundid.util.UtilityMessages.*;
052
053
054
055/**
056 * This class provides a utility that may be used to help process SASL bind
057 * operations using the LDAP SDK.
058 */
059@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
060public final class SASLUtils
061{
062  /**
063   * The name of the SASL option that specifies the authentication ID.  It may
064   * be used in conjunction with the CRAM-MD5, DIGEST-MD5, GSSAPI, and PLAIN
065   * mechanisms.
066   */
067  public static final String SASL_OPTION_AUTH_ID = "authID";
068
069
070
071  /**
072   * The name of the SASL option that specifies the authorization ID.  It may
073   * be used in conjunction with the DIGEST-MD5, GSSAPI, and PLAIN mechanisms.
074   */
075  public static final String SASL_OPTION_AUTHZ_ID = "authzID";
076
077
078
079  /**
080   * The name of the SASL option that specifies the path to the JAAS config
081   * file.  It may be used in conjunction with the GSSAPI mechanism.
082   */
083  public static final String SASL_OPTION_CONFIG_FILE = "configFile";
084
085
086
087  /**
088   * The name of the SASL option that indicates whether debugging should be
089   * enabled.  It may be used in conjunction with the GSSAPI mechanism.
090   */
091  public static final String SASL_OPTION_DEBUG = "debug";
092
093
094
095  /**
096   * The name of the SASL option that specifies the KDC address.  It may be used
097   * in conjunction with the GSSAPI mechanism.
098   */
099  public static final String SASL_OPTION_KDC_ADDRESS = "kdcAddress";
100
101
102
103
104  /**
105   * The name of the SASL option that specifies the desired SASL mechanism to
106   * use to authenticate to the server.
107   */
108  public static final String SASL_OPTION_MECHANISM = "mech";
109
110
111
112  /**
113   * The name of the SASL option that specifies a one-time password.  It may be
114   * used in conjunction with the UNBOUNDID-DELIVERED-OTP and
115   * UNBOUNDID-YUBIKEY-OTP mechanisms.
116   */
117  public static final String SASL_OPTION_OTP = "otp";
118
119
120
121  /**
122   * The name of the SASL option that may be used to indicate whether to
123   * prompt for a static password.  It may be used in conjunction with the
124   * UNBOUNDID-TOTP and UNBOUNDID-YUBIKEY-OTP mechanisms.
125   */
126  public static final String SASL_OPTION_PROMPT_FOR_STATIC_PW =
127       "promptForStaticPassword";
128
129
130
131  /**
132   * The name of the SASL option that specifies the GSSAPI service principal
133   * protocol.  It may be used in conjunction with the GSSAPI mechanism.
134   */
135  public static final String SASL_OPTION_PROTOCOL = "protocol";
136
137
138
139  /**
140   * The name of the SASL option that specifies the quality of protection that
141   * should be used for communication that occurs after the authentication has
142   * completed.
143   */
144  public static final String SASL_OPTION_QOP = "qop";
145
146
147
148  /**
149   * The name of the SASL option that specifies the realm name.  It may be used
150   * in conjunction with the DIGEST-MD5 and GSSAPI mechanisms.
151   */
152  public static final String SASL_OPTION_REALM = "realm";
153
154
155
156  /**
157   * The name of the SASL option that indicates whether to require an existing
158   * Kerberos session from the ticket cache.  It may be used in conjunction with
159   * the GSSAPI mechanism.
160   */
161  public static final String SASL_OPTION_REQUIRE_CACHE = "requireCache";
162
163
164
165  /**
166   * The name of the SASL option that indicates whether to attempt to renew the
167   * Kerberos TGT for an existing session.  It may be used in conjunction with
168   * the GSSAPI mechanism.
169   */
170  public static final String SASL_OPTION_RENEW_TGT = "renewTGT";
171
172
173
174  /**
175   * The name of the SASL option that specifies the path to the Kerberos ticket
176   * cache to use.  It may be used in conjunction with the GSSAPI mechanism.
177   */
178  public static final String SASL_OPTION_TICKET_CACHE_PATH = "ticketCache";
179
180
181
182  /**
183   * The name of the SASL option that specifies the TOTP authentication code.
184   * It may be used in conjunction with the UNBOUNDID-TOTP mechanism.
185   */
186  public static final String SASL_OPTION_TOTP_PASSWORD = "totpPassword";
187
188
189
190  /**
191   * The name of the SASL option that specifies the trace string.  It may be
192   * used in conjunction with the ANONYMOUS mechanism.
193   */
194  public static final String SASL_OPTION_TRACE = "trace";
195
196
197
198  /**
199   * The name of the SASL option that specifies whether to use a Kerberos ticket
200   * cache.  It may be used in conjunction with the GSSAPI mechanism.
201   */
202  public static final String SASL_OPTION_USE_TICKET_CACHE = "useTicketCache";
203
204
205
206  /**
207   * A map with information about all supported SASL mechanisms, mapped from
208   * lowercase mechanism name to an object with information about that
209   * mechanism.
210   */
211  private static final Map<String,SASLMechanismInfo> SASL_MECHANISMS;
212
213
214
215  static
216  {
217    final TreeMap<String,SASLMechanismInfo> m =
218         new TreeMap<String,SASLMechanismInfo>();
219
220    m.put(toLowerCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME),
221         new SASLMechanismInfo(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME,
222              INFO_SASL_ANONYMOUS_DESCRIPTION.get(), false, false,
223              new SASLOption(SASL_OPTION_TRACE,
224                   INFO_SASL_ANONYMOUS_OPTION_TRACE.get(), false, false)));
225
226    m.put(toLowerCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME),
227         new SASLMechanismInfo(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME,
228              INFO_SASL_CRAM_MD5_DESCRIPTION.get(), true, true,
229              new SASLOption(SASL_OPTION_AUTH_ID,
230                   INFO_SASL_CRAM_MD5_OPTION_AUTH_ID.get(), true, false)));
231
232    m.put(toLowerCase(DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME),
233         new SASLMechanismInfo(DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME,
234              INFO_SASL_DIGEST_MD5_DESCRIPTION.get(), true, true,
235              new SASLOption(SASL_OPTION_AUTH_ID,
236                   INFO_SASL_DIGEST_MD5_OPTION_AUTH_ID.get(), true, false),
237              new SASLOption(SASL_OPTION_AUTHZ_ID,
238                   INFO_SASL_DIGEST_MD5_OPTION_AUTHZ_ID.get(), false, false),
239              new SASLOption(SASL_OPTION_REALM,
240                   INFO_SASL_DIGEST_MD5_OPTION_REALM.get(), false, false),
241              new SASLOption(SASL_OPTION_QOP,
242                   INFO_SASL_DIGEST_MD5_OPTION_QOP.get(), false, false)));
243
244    m.put(toLowerCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME),
245         new SASLMechanismInfo(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME,
246              INFO_SASL_EXTERNAL_DESCRIPTION.get(), false, false));
247
248    m.put(toLowerCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME),
249         new SASLMechanismInfo(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME,
250              INFO_SASL_GSSAPI_DESCRIPTION.get(), true, false,
251              new SASLOption(SASL_OPTION_AUTH_ID,
252                   INFO_SASL_GSSAPI_OPTION_AUTH_ID.get(), true, false),
253              new SASLOption(SASL_OPTION_AUTHZ_ID,
254                   INFO_SASL_GSSAPI_OPTION_AUTHZ_ID.get(), false, false),
255              new SASLOption(SASL_OPTION_CONFIG_FILE,
256                   INFO_SASL_GSSAPI_OPTION_CONFIG_FILE.get(), false, false),
257              new SASLOption(SASL_OPTION_DEBUG,
258                   INFO_SASL_GSSAPI_OPTION_DEBUG.get(), false, false),
259              new SASLOption(SASL_OPTION_KDC_ADDRESS,
260                   INFO_SASL_GSSAPI_OPTION_KDC_ADDRESS.get(), false, false),
261              new SASLOption(SASL_OPTION_PROTOCOL,
262                   INFO_SASL_GSSAPI_OPTION_PROTOCOL.get(), false, false),
263              new SASLOption(SASL_OPTION_REALM,
264                   INFO_SASL_GSSAPI_OPTION_REALM.get(), false, false),
265              new SASLOption(SASL_OPTION_QOP,
266                   INFO_SASL_GSSAPI_OPTION_QOP.get(), false, false),
267              new SASLOption(SASL_OPTION_RENEW_TGT,
268                   INFO_SASL_GSSAPI_OPTION_RENEW_TGT.get(), false, false),
269              new SASLOption(SASL_OPTION_REQUIRE_CACHE,
270                   INFO_SASL_GSSAPI_OPTION_REQUIRE_TICKET_CACHE.get(), false,
271                   false),
272              new SASLOption(SASL_OPTION_TICKET_CACHE_PATH,
273                   INFO_SASL_GSSAPI_OPTION_TICKET_CACHE.get(), false, false),
274              new SASLOption(SASL_OPTION_USE_TICKET_CACHE,
275                   INFO_SASL_GSSAPI_OPTION_USE_TICKET_CACHE.get(), false,
276                   false)));
277
278    m.put(toLowerCase(PLAINBindRequest.PLAIN_MECHANISM_NAME),
279         new SASLMechanismInfo(PLAINBindRequest.PLAIN_MECHANISM_NAME,
280              INFO_SASL_PLAIN_DESCRIPTION.get(), true, true,
281              new SASLOption(SASL_OPTION_AUTH_ID,
282                   INFO_SASL_PLAIN_OPTION_AUTH_ID.get(), true, false),
283              new SASLOption(SASL_OPTION_AUTHZ_ID,
284                   INFO_SASL_PLAIN_OPTION_AUTHZ_ID.get(), false, false)));
285
286    m.put(
287         StaticUtils.toLowerCase(
288              UnboundIDDeliveredOTPBindRequest.
289                   UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME),
290         new SASLMechanismInfo(
291              UnboundIDDeliveredOTPBindRequest.
292                   UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME,
293              INFO_SASL_UNBOUNDID_DELIVERED_OTP_DESCRIPTION.get(), false, false,
294              new SASLOption(SASL_OPTION_AUTH_ID,
295                   INFO_SASL_UNBOUNDID_TOTP_OPTION_AUTH_ID.get(), true, false),
296              new SASLOption(SASL_OPTION_AUTHZ_ID,
297                   INFO_SASL_UNBOUNDID_TOTP_OPTION_AUTHZ_ID.get(), false,
298                   false),
299              new SASLOption(SASL_OPTION_OTP,
300                   INFO_SASL_UNBOUNDID_DELIVERED_OTP_OPTION_OTP.get(), true,
301                   false)));
302
303    m.put(
304         StaticUtils.toLowerCase(
305              UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME),
306         new SASLMechanismInfo(
307              UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME,
308              INFO_SASL_UNBOUNDID_TOTP_DESCRIPTION.get(), true, false,
309              new SASLOption(SASL_OPTION_AUTH_ID,
310                   INFO_SASL_UNBOUNDID_TOTP_OPTION_AUTH_ID.get(), true, false),
311              new SASLOption(SASL_OPTION_AUTHZ_ID,
312                   INFO_SASL_UNBOUNDID_TOTP_OPTION_AUTHZ_ID.get(), false,
313                   false),
314              new SASLOption(SASL_OPTION_PROMPT_FOR_STATIC_PW,
315                   INFO_SASL_UNBOUNDID_TOTP_OPTION_PROMPT_FOR_PW.get(), false,
316                   false),
317              new SASLOption(SASL_OPTION_TOTP_PASSWORD,
318                   INFO_SASL_UNBOUNDID_TOTP_OPTION_TOTP_PASSWORD.get(), true,
319                   false)));
320
321    m.put(
322         StaticUtils.toLowerCase(
323              UnboundIDYubiKeyOTPBindRequest.
324                   UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME),
325         new SASLMechanismInfo(
326              UnboundIDYubiKeyOTPBindRequest.
327                   UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME,
328              INFO_SASL_UNBOUNDID_YUBIKEY_OTP_DESCRIPTION.get(), true, false,
329              new SASLOption(SASL_OPTION_AUTH_ID,
330                   INFO_SASL_UNBOUNDID_YUBIKEY_OTP_OPTION_AUTH_ID.get(), true,
331                   false),
332              new SASLOption(SASL_OPTION_AUTHZ_ID,
333                   INFO_SASL_UNBOUNDID_YUBIKEY_OTP_OPTION_AUTHZ_ID.get(), false,
334                   false),
335              new SASLOption(SASL_OPTION_OTP,
336                   INFO_SASL_UNBOUNDID_YUBIKEY_OTP_OPTION_OTP.get(), true,
337                   false),
338              new SASLOption(SASL_OPTION_PROMPT_FOR_STATIC_PW,
339                   INFO_SASL_UNBOUNDID_YUBIKEY_OTP_OPTION_PROMPT_FOR_PW.get(),
340                   false, false)));
341
342    SASL_MECHANISMS = Collections.unmodifiableMap(m);
343  }
344
345
346
347  /**
348   * Prevent this utility class from being instantiated.
349   */
350  private SASLUtils()
351  {
352    // No implementation required.
353  }
354
355
356
357  /**
358   * Retrieves information about the SASL mechanisms supported for use by this
359   * class.
360   *
361   * @return  Information about the SASL mechanisms supported for use by this
362   *          class.
363   */
364  public static List<SASLMechanismInfo> getSupportedSASLMechanisms()
365  {
366    return Collections.unmodifiableList(new ArrayList<SASLMechanismInfo>(
367         SASL_MECHANISMS.values()));
368  }
369
370
371
372  /**
373   * Retrieves information about the specified SASL mechanism.
374   *
375   * @param  mechanism  The name of the SASL mechanism for which to retrieve
376   *                    information.  It will not be treated in a case-sensitive
377   *                    manner.
378   *
379   * @return  Information about the requested SASL mechanism, or {@code null} if
380   *          no information about the specified mechanism is available.
381   */
382  public static SASLMechanismInfo getSASLMechanismInfo(final String mechanism)
383  {
384    return SASL_MECHANISMS.get(toLowerCase(mechanism));
385  }
386
387
388
389  /**
390   * Creates a new SASL bind request using the provided information.
391   *
392   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
393   *                    SASL mechanisms, this should be {@code null}, since the
394   *                    identity of the target user should be specified in some
395   *                    other way (e.g., via an "authID" SASL option).
396   * @param  password   The password to use for the SASL bind request.  It may
397   *                    be {@code null} if no password is required for the
398   *                    desired SASL mechanism.
399   * @param  mechanism  The name of the SASL mechanism to use.  It may be
400   *                    {@code null} if the provided set of options contains a
401   *                    "mech" option to specify the desired SASL option.
402   * @param  options    The set of SASL options to use when creating the bind
403   *                    request, in the form "name=value".  It may be
404   *                    {@code null} or empty if no SASL options are needed and
405   *                    a value was provided for the {@code mechanism} argument.
406   *                    If the set of SASL options includes a "mech" option,
407   *                    then the {@code mechanism} argument must be {@code null}
408   *                    or have a value that matches the value of the "mech"
409   *                    SASL option.
410   *
411   * @return  The SASL bind request created using the provided information.
412   *
413   * @throws  LDAPException  If a problem is encountered while trying to create
414   *                         the SASL bind request.
415   */
416  public static SASLBindRequest createBindRequest(final String bindDN,
417                                                  final String password,
418                                                  final String mechanism,
419                                                  final String... options)
420         throws LDAPException
421  {
422    return createBindRequest(bindDN,
423         (password == null ? null : getBytes(password)), mechanism,
424         StaticUtils.toList(options));
425  }
426
427
428
429  /**
430   * Creates a new SASL bind request using the provided information.
431   *
432   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
433   *                    SASL mechanisms, this should be {@code null}, since the
434   *                    identity of the target user should be specified in some
435   *                    other way (e.g., via an "authID" SASL option).
436   * @param  password   The password to use for the SASL bind request.  It may
437   *                    be {@code null} if no password is required for the
438   *                    desired SASL mechanism.
439   * @param  mechanism  The name of the SASL mechanism to use.  It may be
440   *                    {@code null} if the provided set of options contains a
441   *                    "mech" option to specify the desired SASL option.
442   * @param  options    The set of SASL options to use when creating the bind
443   *                    request, in the form "name=value".  It may be
444   *                    {@code null} or empty if no SASL options are needed and
445   *                    a value was provided for the {@code mechanism} argument.
446   *                    If the set of SASL options includes a "mech" option,
447   *                    then the {@code mechanism} argument must be {@code null}
448   *                    or have a value that matches the value of the "mech"
449   *                    SASL option.
450   * @param  controls   The set of controls to include in the request.
451   *
452   * @return  The SASL bind request created using the provided information.
453   *
454   * @throws  LDAPException  If a problem is encountered while trying to create
455   *                         the SASL bind request.
456   */
457  public static SASLBindRequest createBindRequest(final String bindDN,
458                                                  final String password,
459                                                  final String mechanism,
460                                                  final List<String> options,
461                                                  final Control... controls)
462         throws LDAPException
463  {
464    return createBindRequest(bindDN,
465         (password == null ? null : getBytes(password)), mechanism, options,
466         controls);
467  }
468
469
470
471  /**
472   * Creates a new SASL bind request using the provided information.
473   *
474   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
475   *                    SASL mechanisms, this should be {@code null}, since the
476   *                    identity of the target user should be specified in some
477   *                    other way (e.g., via an "authID" SASL option).
478   * @param  password   The password to use for the SASL bind request.  It may
479   *                    be {@code null} if no password is required for the
480   *                    desired SASL mechanism.
481   * @param  mechanism  The name of the SASL mechanism to use.  It may be
482   *                    {@code null} if the provided set of options contains a
483   *                    "mech" option to specify the desired SASL option.
484   * @param  options    The set of SASL options to use when creating the bind
485   *                    request, in the form "name=value".  It may be
486   *                    {@code null} or empty if no SASL options are needed and
487   *                    a value was provided for the {@code mechanism} argument.
488   *                    If the set of SASL options includes a "mech" option,
489   *                    then the {@code mechanism} argument must be {@code null}
490   *                    or have a value that matches the value of the "mech"
491   *                    SASL option.
492   *
493   * @return  The SASL bind request created using the provided information.
494   *
495   * @throws  LDAPException  If a problem is encountered while trying to create
496   *                         the SASL bind request.
497   */
498  public static SASLBindRequest createBindRequest(final String bindDN,
499                                                  final byte[] password,
500                                                  final String mechanism,
501                                                  final String... options)
502         throws LDAPException
503  {
504    return createBindRequest(bindDN, password, mechanism,
505         StaticUtils.toList(options));
506  }
507
508
509
510  /**
511   * Creates a new SASL bind request using the provided information.
512   *
513   * @param  bindDN     The bind DN to use for the SASL bind request.  For most
514   *                    SASL mechanisms, this should be {@code null}, since the
515   *                    identity of the target user should be specified in some
516   *                    other way (e.g., via an "authID" SASL option).
517   * @param  password   The password to use for the SASL bind request.  It may
518   *                    be {@code null} if no password is required for the
519   *                    desired SASL mechanism.
520   * @param  mechanism  The name of the SASL mechanism to use.  It may be
521   *                    {@code null} if the provided set of options contains a
522   *                    "mech" option to specify the desired SASL option.
523   * @param  options    The set of SASL options to use when creating the bind
524   *                    request, in the form "name=value".  It may be
525   *                    {@code null} or empty if no SASL options are needed and
526   *                    a value was provided for the {@code mechanism} argument.
527   *                    If the set of SASL options includes a "mech" option,
528   *                    then the {@code mechanism} argument must be {@code null}
529   *                    or have a value that matches the value of the "mech"
530   *                    SASL option.
531   * @param  controls   The set of controls to include in the request.
532   *
533   * @return  The SASL bind request created using the provided information.
534   *
535   * @throws  LDAPException  If a problem is encountered while trying to create
536   *                         the SASL bind request.
537   */
538  public static SASLBindRequest createBindRequest(final String bindDN,
539                                                  final byte[] password,
540                                                  final String mechanism,
541                                                  final List<String> options,
542                                                  final Control... controls)
543         throws LDAPException
544  {
545    return createBindRequest(bindDN, password, false, null, mechanism, options,
546         controls);
547  }
548
549
550
551  /**
552   * Creates a new SASL bind request using the provided information.
553   *
554   * @param  bindDN             The bind DN to use for the SASL bind request.
555   *                            For most SASL mechanisms, this should be
556   *                            {@code null}, since the identity of the target
557   *                            user should be specified in some other way
558   *                            (e.g., via an "authID" SASL option).
559   * @param  password           The password to use for the SASL bind request.
560   *                            It may be {@code null} if no password is
561   *                            required for the desired SASL mechanism.
562   * @param  promptForPassword  Indicates whether to interactively prompt for
563   *                            the password if one is needed but none was
564   *                            provided.
565   * @param  tool               The command-line tool whose input and output
566   *                            streams should be used when prompting for the
567   *                            bind password.  It may be {@code null} if
568   *                            {@code promptForPassword} is {@code false}.
569   * @param  mechanism          The name of the SASL mechanism to use.  It may
570   *                            be {@code null} if the provided set of options
571   *                            contains a "mech" option to specify the desired
572   *                            SASL option.
573   * @param  options            The set of SASL options to use when creating the
574   *                            bind request, in the form "name=value".  It may
575   *                            be {@code null} or empty if no SASL options are
576   *                            needed and a value was provided for the
577   *                            {@code mechanism} argument.  If the set of SASL
578   *                            options includes a "mech" option, then the
579   *                            {@code mechanism} argument must be {@code null}
580   *                            or have a value that matches the value of the
581   *                            "mech" SASL option.
582   * @param  controls           The set of controls to include in the request.
583   *
584   * @return  The SASL bind request created using the provided information.
585   *
586   * @throws  LDAPException  If a problem is encountered while trying to create
587   *                         the SASL bind request.
588   */
589  public static SASLBindRequest createBindRequest(final String bindDN,
590                                     final byte[] password,
591                                     final boolean promptForPassword,
592                                     final CommandLineTool tool,
593                                     final String mechanism,
594                                     final List<String> options,
595                                     final Control... controls)
596         throws LDAPException
597  {
598    if (promptForPassword)
599    {
600      Validator.ensureNotNull(tool);
601    }
602
603    // Parse the provided set of options to ensure that they are properly
604    // formatted in name-value form, and extract the SASL mechanism.
605    final String mech;
606    final Map<String,String> optionsMap = parseOptions(options);
607    final String mechOption =
608         optionsMap.remove(toLowerCase(SASL_OPTION_MECHANISM));
609    if (mechOption != null)
610    {
611      mech = mechOption;
612      if ((mechanism != null) && (! mech.equalsIgnoreCase(mechanism)))
613      {
614        throw new LDAPException(ResultCode.PARAM_ERROR,
615             ERR_SASL_OPTION_MECH_CONFLICT.get(mechanism, mech));
616      }
617    }
618    else
619    {
620      mech = mechanism;
621    }
622
623    if (mech == null)
624    {
625      throw new LDAPException(ResultCode.PARAM_ERROR,
626           ERR_SASL_OPTION_NO_MECH.get());
627    }
628
629    if (mech.equalsIgnoreCase(ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME))
630    {
631      return createANONYMOUSBindRequest(password, optionsMap, controls);
632    }
633    else if (mech.equalsIgnoreCase(CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME))
634    {
635      return createCRAMMD5BindRequest(password, promptForPassword, tool,
636           optionsMap, controls);
637    }
638    else if (mech.equalsIgnoreCase(
639                  DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME))
640    {
641      return createDIGESTMD5BindRequest(password, promptForPassword, tool,
642           optionsMap, controls);
643    }
644    else if (mech.equalsIgnoreCase(EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME))
645    {
646      return createEXTERNALBindRequest(password, optionsMap, controls);
647    }
648    else if (mech.equalsIgnoreCase(GSSAPIBindRequest.GSSAPI_MECHANISM_NAME))
649    {
650      return createGSSAPIBindRequest(password, promptForPassword, tool,
651           optionsMap, controls);
652    }
653    else if (mech.equalsIgnoreCase(PLAINBindRequest.PLAIN_MECHANISM_NAME))
654    {
655      return createPLAINBindRequest(password, promptForPassword, tool,
656           optionsMap, controls);
657    }
658    else if (mech.equalsIgnoreCase(UnboundIDDeliveredOTPBindRequest.
659             UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME))
660    {
661      return createUNBOUNDIDDeliveredOTPBindRequest(password, optionsMap,
662           controls);
663    }
664    else if (mech.equalsIgnoreCase(
665             UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME))
666    {
667      return createUNBOUNDIDTOTPBindRequest(password, tool, optionsMap,
668           controls);
669    }
670    else if (mech.equalsIgnoreCase(
671         UnboundIDYubiKeyOTPBindRequest.UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME))
672    {
673      return createUNBOUNDIDYUBIKEYOTPBindRequest(password, tool, optionsMap,
674           controls);
675    }
676    else
677    {
678      throw new LDAPException(ResultCode.PARAM_ERROR,
679           ERR_SASL_OPTION_UNSUPPORTED_MECH.get(mech));
680    }
681  }
682
683
684
685  /**
686   * Creates a SASL ANONYMOUS bind request using the provided set of options.
687   *
688   * @param  password  The password to use for the bind request.
689   * @param  options   The set of SASL options for the bind request.
690   * @param  controls  The set of controls to include in the request.
691   *
692   * @return  The SASL ANONYMOUS bind request that was created.
693   *
694   * @throws  LDAPException  If a problem is encountered while trying to create
695   *                         the SASL bind request.
696   */
697  private static ANONYMOUSBindRequest createANONYMOUSBindRequest(
698                                           final byte[] password,
699                                           final Map<String,String> options,
700                                           final Control[] controls)
701          throws LDAPException
702  {
703    if (password != null)
704    {
705      throw new LDAPException(ResultCode.PARAM_ERROR,
706           ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
707                ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME));
708    }
709
710
711    // The trace option is optional.
712    final String trace = options.remove(toLowerCase(SASL_OPTION_TRACE));
713
714    // Ensure no unsupported options were provided.
715    ensureNoUnsupportedOptions(options,
716         ANONYMOUSBindRequest.ANONYMOUS_MECHANISM_NAME);
717
718    return new ANONYMOUSBindRequest(trace, controls);
719  }
720
721
722
723  /**
724   * Creates a SASL CRAM-MD5 bind request using the provided password and set of
725   * options.
726   *
727   * @param  password           The password to use for the bind request.
728   * @param  promptForPassword  Indicates whether to interactively prompt for
729   *                            the password if one is needed but none was
730   *                            provided.
731   * @param  tool               The command-line tool whose input and output
732   *                            streams should be used when prompting for the
733   *                            bind password.  It may be {@code null} if
734   *                            {@code promptForPassword} is {@code false}.
735   * @param  options            The set of SASL options for the bind request.
736   * @param  controls           The set of controls to include in the request.
737   *
738   * @return  The SASL CRAM-MD5 bind request that was created.
739   *
740   * @throws  LDAPException  If a problem is encountered while trying to create
741   *                         the SASL bind request.
742   */
743  private static CRAMMD5BindRequest createCRAMMD5BindRequest(
744                                         final byte[] password,
745                                         final boolean promptForPassword,
746                                         final CommandLineTool tool,
747                                         final Map<String,String> options,
748                                         final Control[] controls)
749          throws LDAPException
750  {
751    final byte[] pw;
752    if (password == null)
753    {
754      if (promptForPassword)
755      {
756        tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
757        pw = PasswordReader.readPassword();
758        tool.getOriginalOut().println();
759      }
760      else
761      {
762        throw new LDAPException(ResultCode.PARAM_ERROR,
763             ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
764                  CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
765      }
766    }
767    else
768    {
769      pw = password;
770    }
771
772
773    // The authID option is required.
774    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
775    if (authID == null)
776    {
777      throw new LDAPException(ResultCode.PARAM_ERROR,
778           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
779                CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
780    }
781
782
783    // Ensure no unsupported options were provided.
784    ensureNoUnsupportedOptions(options,
785         CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME);
786
787    return new CRAMMD5BindRequest(authID, pw, controls);
788  }
789
790
791
792  /**
793   * Creates a SASL DIGEST-MD5 bind request using the provided password and set
794   * of options.
795   *
796   * @param  password           The password to use for the bind request.
797   * @param  promptForPassword  Indicates whether to interactively prompt for
798   *                            the password if one is needed but none was
799   *                            provided.
800   * @param  tool               The command-line tool whose input and output
801   *                            streams should be used when prompting for the
802   *                            bind password.  It may be {@code null} if
803   *                            {@code promptForPassword} is {@code false}.
804   * @param  options            The set of SASL options for the bind request.
805   * @param  controls           The set of controls to include in the request.
806   *
807   * @return  The SASL DIGEST-MD5 bind request that was created.
808   *
809   * @throws  LDAPException  If a problem is encountered while trying to create
810   *                         the SASL bind request.
811   */
812  private static DIGESTMD5BindRequest createDIGESTMD5BindRequest(
813                                           final byte[] password,
814                                           final boolean promptForPassword,
815                                           final CommandLineTool tool,
816                                           final Map<String,String> options,
817                                           final Control[] controls)
818          throws LDAPException
819  {
820    final byte[] pw;
821    if (password == null)
822    {
823      if (promptForPassword)
824      {
825        tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
826        pw = PasswordReader.readPassword();
827        tool.getOriginalOut().println();
828      }
829      else
830      {
831        throw new LDAPException(ResultCode.PARAM_ERROR,
832             ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
833                  CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
834      }
835    }
836    else
837    {
838      pw = password;
839    }
840
841    // The authID option is required.
842    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
843    if (authID == null)
844    {
845      throw new LDAPException(ResultCode.PARAM_ERROR,
846           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
847                CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
848    }
849
850    final DIGESTMD5BindRequestProperties properties =
851         new DIGESTMD5BindRequestProperties(authID, pw);
852
853    // The authzID option is optional.
854    properties.setAuthorizationID(
855         options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID)));
856
857    // The realm option is optional.
858    properties.setRealm(options.remove(toLowerCase(SASL_OPTION_REALM)));
859
860    // The QoP option is optional, and may contain multiple values that need to
861    // be parsed.
862    final String qopString = options.remove(toLowerCase(SASL_OPTION_QOP));
863    if (qopString != null)
864    {
865      properties.setAllowedQoP(
866           SASLQualityOfProtection.decodeQoPList(qopString));
867    }
868
869    // Ensure no unsupported options were provided.
870    ensureNoUnsupportedOptions(options,
871         DIGESTMD5BindRequest.DIGESTMD5_MECHANISM_NAME);
872
873    return new DIGESTMD5BindRequest(properties, controls);
874  }
875
876
877
878  /**
879   * Creates a SASL EXTERNAL bind request using the provided set of options.
880   *
881   * @param  password  The password to use for the bind request.
882   * @param  options   The set of SASL options for the bind request.
883   * @param  controls  The set of controls to include in the request.
884   *
885   * @return  The SASL EXTERNAL bind request that was created.
886   *
887   * @throws  LDAPException  If a problem is encountered while trying to create
888   *                         the SASL bind request.
889   */
890  private static EXTERNALBindRequest createEXTERNALBindRequest(
891                                          final byte[] password,
892                                          final Map<String,String> options,
893                                          final Control[] controls)
894          throws LDAPException
895  {
896    if (password != null)
897    {
898      throw new LDAPException(ResultCode.PARAM_ERROR,
899           ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
900                EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME));
901    }
902
903    // Ensure no unsupported options were provided.
904    ensureNoUnsupportedOptions(options,
905         EXTERNALBindRequest.EXTERNAL_MECHANISM_NAME);
906
907    return new EXTERNALBindRequest(controls);
908  }
909
910
911
912  /**
913   * Creates a SASL GSSAPI bind request using the provided password and set of
914   * options.
915   *
916   * @param  password           The password to use for the bind request.
917   * @param  promptForPassword  Indicates whether to interactively prompt for
918   *                            the password if one is needed but none was
919   *                            provided.
920   * @param  tool               The command-line tool whose input and output
921   *                            streams should be used when prompting for the
922   *                            bind password.  It may be {@code null} if
923   *                            {@code promptForPassword} is {@code false}.
924   * @param  options            The set of SASL options for the bind request.
925   * @param  controls           The set of controls to include in the request.
926   *
927   * @return  The SASL GSSAPI bind request that was created.
928   *
929   * @throws  LDAPException  If a problem is encountered while trying to create
930   *                         the SASL bind request.
931   */
932  private static GSSAPIBindRequest createGSSAPIBindRequest(
933                                        final byte[] password,
934                                        final boolean promptForPassword,
935                                        final CommandLineTool tool,
936                                        final Map<String,String> options,
937                                        final Control[] controls)
938          throws LDAPException
939  {
940    // The authID option is required.
941    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
942    if (authID == null)
943    {
944      throw new LDAPException(ResultCode.PARAM_ERROR,
945           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
946                GSSAPIBindRequest.GSSAPI_MECHANISM_NAME));
947    }
948    final GSSAPIBindRequestProperties gssapiProperties =
949         new GSSAPIBindRequestProperties(authID, password);
950
951    // The authzID option is optional.
952    gssapiProperties.setAuthorizationID(
953         options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID)));
954
955    // The configFile option is optional.
956    gssapiProperties.setConfigFilePath(options.remove(toLowerCase(
957         SASL_OPTION_CONFIG_FILE)));
958
959    // The debug option is optional.
960    gssapiProperties.setEnableGSSAPIDebugging(getBooleanValue(options,
961         SASL_OPTION_DEBUG, false));
962
963    // The kdcAddress option is optional.
964    gssapiProperties.setKDCAddress(options.remove(
965         toLowerCase(SASL_OPTION_KDC_ADDRESS)));
966
967    // The protocol option is optional.
968    final String protocol = options.remove(toLowerCase(SASL_OPTION_PROTOCOL));
969    if (protocol != null)
970    {
971      gssapiProperties.setServicePrincipalProtocol(protocol);
972    }
973
974    // The realm option is optional.
975    gssapiProperties.setRealm(options.remove(toLowerCase(SASL_OPTION_REALM)));
976
977    // The QoP option is optional, and may contain multiple values that need to
978    // be parsed.
979    final String qopString = options.remove(toLowerCase(SASL_OPTION_QOP));
980    if (qopString != null)
981    {
982      gssapiProperties.setAllowedQoP(
983           SASLQualityOfProtection.decodeQoPList(qopString));
984    }
985
986    // The renewTGT option is optional.
987    gssapiProperties.setRenewTGT(getBooleanValue(options, SASL_OPTION_RENEW_TGT,
988         false));
989
990    // The requireCache option is optional.
991    gssapiProperties.setRequireCachedCredentials(getBooleanValue(options,
992         SASL_OPTION_REQUIRE_CACHE, false));
993
994    // The ticketCache option is optional.
995    gssapiProperties.setTicketCachePath(options.remove(toLowerCase(
996         SASL_OPTION_TICKET_CACHE_PATH)));
997
998    // The useTicketCache option is optional.
999    gssapiProperties.setUseTicketCache(getBooleanValue(options,
1000         SASL_OPTION_USE_TICKET_CACHE, true));
1001
1002    // Ensure no unsupported options were provided.
1003    ensureNoUnsupportedOptions(options,
1004         GSSAPIBindRequest.GSSAPI_MECHANISM_NAME);
1005
1006    // A password must have been provided unless useTicketCache=true and
1007    // requireTicketCache=true.
1008    if (password == null)
1009    {
1010      if (! (gssapiProperties.useTicketCache() &&
1011           gssapiProperties.requireCachedCredentials()))
1012      {
1013        if (promptForPassword)
1014        {
1015          tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
1016          gssapiProperties.setPassword(PasswordReader.readPassword());
1017          tool.getOriginalOut().println();
1018        }
1019        else
1020        {
1021          throw new LDAPException(ResultCode.PARAM_ERROR,
1022               ERR_SASL_OPTION_GSSAPI_PASSWORD_REQUIRED.get());
1023        }
1024      }
1025    }
1026
1027    return new GSSAPIBindRequest(gssapiProperties, controls);
1028  }
1029
1030
1031
1032  /**
1033   * Creates a SASL PLAIN bind request using the provided password and set of
1034   * options.
1035   *
1036   * @param  password           The password to use for the bind request.
1037   * @param  promptForPassword  Indicates whether to interactively prompt for
1038   *                            the password if one is needed but none was
1039   *                            provided.
1040   * @param  tool               The command-line tool whose input and output
1041   *                            streams should be used when prompting for the
1042   *                            bind password.  It may be {@code null} if
1043   *                            {@code promptForPassword} is {@code false}.
1044   * @param  options            The set of SASL options for the bind request.
1045   * @param  controls           The set of controls to include in the request.
1046   *
1047   * @return  The SASL PLAIN bind request that was created.
1048   *
1049   * @throws  LDAPException  If a problem is encountered while trying to create
1050   *                         the SASL bind request.
1051   */
1052  private static PLAINBindRequest createPLAINBindRequest(
1053                                        final byte[] password,
1054                                        final boolean promptForPassword,
1055                                        final CommandLineTool tool,
1056                                        final Map<String,String> options,
1057                                        final Control[] controls)
1058          throws LDAPException
1059  {
1060    final byte[] pw;
1061    if (password == null)
1062    {
1063      if (promptForPassword)
1064      {
1065        tool.getOriginalOut().print(INFO_LDAP_TOOL_ENTER_BIND_PASSWORD.get());
1066        pw = PasswordReader.readPassword();
1067        tool.getOriginalOut().println();
1068      }
1069      else
1070      {
1071        throw new LDAPException(ResultCode.PARAM_ERROR,
1072             ERR_SASL_OPTION_MECH_REQUIRES_PASSWORD.get(
1073                  CRAMMD5BindRequest.CRAMMD5_MECHANISM_NAME));
1074      }
1075    }
1076    else
1077    {
1078      pw = password;
1079    }
1080
1081    // The authID option is required.
1082    final String authID = options.remove(toLowerCase(SASL_OPTION_AUTH_ID));
1083    if (authID == null)
1084    {
1085      throw new LDAPException(ResultCode.PARAM_ERROR,
1086           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_AUTH_ID,
1087                PLAINBindRequest.PLAIN_MECHANISM_NAME));
1088    }
1089
1090    // The authzID option is optional.
1091    final String authzID = options.remove(toLowerCase(SASL_OPTION_AUTHZ_ID));
1092
1093    // Ensure no unsupported options were provided.
1094    ensureNoUnsupportedOptions(options,
1095         PLAINBindRequest.PLAIN_MECHANISM_NAME);
1096
1097    return new PLAINBindRequest(authID, authzID, pw, controls);
1098  }
1099
1100
1101
1102  /**
1103   * Creates a SASL UNBOUNDID-DELIVERED-OTP bind request using the provided
1104   * password and set of options.
1105   *
1106   * @param  password  The password to use for the bind request.
1107   * @param  options   The set of SASL options for the bind request.
1108   * @param  controls  The set of controls to include in the request.
1109   *
1110   * @return  The SASL UNBOUNDID-DELIVERED-OTP bind request that was created.
1111   *
1112   * @throws  LDAPException  If a problem is encountered while trying to create
1113   *                         the SASL bind request.
1114   */
1115  private static UnboundIDDeliveredOTPBindRequest
1116                      createUNBOUNDIDDeliveredOTPBindRequest(
1117                           final byte[] password,
1118                           final Map<String,String> options,
1119                           final Control... controls)
1120          throws LDAPException
1121  {
1122    if (password != null)
1123    {
1124      throw new LDAPException(ResultCode.PARAM_ERROR,
1125           ERR_SASL_OPTION_MECH_DOESNT_ACCEPT_PASSWORD.get(
1126                UnboundIDDeliveredOTPBindRequest.
1127                     UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME));
1128    }
1129
1130    // The authID option is required.
1131    final String authID =
1132         options.remove(StaticUtils.toLowerCase(SASLUtils.SASL_OPTION_AUTH_ID));
1133    if (authID == null)
1134    {
1135      throw new LDAPException(ResultCode.PARAM_ERROR,
1136           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASLUtils.SASL_OPTION_AUTH_ID,
1137                UnboundIDDeliveredOTPBindRequest.
1138                     UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME));
1139    }
1140
1141    // The OTP option is required.
1142    final String otp = options.remove(StaticUtils.toLowerCase(SASL_OPTION_OTP));
1143    if (otp == null)
1144    {
1145      throw new LDAPException(ResultCode.PARAM_ERROR,
1146           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_OTP,
1147                UnboundIDDeliveredOTPBindRequest.
1148                     UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME));
1149    }
1150
1151    // The authzID option is optional.
1152    final String authzID = options.remove(StaticUtils.toLowerCase(
1153         SASLUtils.SASL_OPTION_AUTHZ_ID));
1154
1155    // Ensure no unsupported options were provided.
1156    SASLUtils.ensureNoUnsupportedOptions(options,
1157         UnboundIDDeliveredOTPBindRequest.
1158              UNBOUNDID_DELIVERED_OTP_MECHANISM_NAME);
1159
1160    return new UnboundIDDeliveredOTPBindRequest(authID, authzID, otp, controls);
1161  }
1162
1163
1164
1165  /**
1166   * Creates a SASL UNBOUNDID-TOTP bind request using the provided password and
1167   * set of options.
1168   *
1169   * @param  password  The password to use for the bind request.
1170   * @param  tool      The command-line tool whose input and output streams
1171   *                   should be used when prompting for the bind password.  It
1172   *                   may be {@code null} if {@code promptForPassword} is
1173   *                   {@code false}.
1174   * @param  options   The set of SASL options for the bind request.
1175   * @param  controls  The set of controls to include in the request.
1176   *
1177   * @return  The SASL UNBOUNDID-TOTP bind request that was created.
1178   *
1179   * @throws  LDAPException  If a problem is encountered while trying to create
1180   *                         the SASL bind request.
1181   */
1182  private static SingleUseTOTPBindRequest createUNBOUNDIDTOTPBindRequest(
1183                                               final byte[] password,
1184                                               final CommandLineTool tool,
1185                                               final Map<String,String> options,
1186                                               final Control... controls)
1187          throws LDAPException
1188  {
1189    // The authID option is required.
1190    final String authID =
1191         options.remove(StaticUtils.toLowerCase(SASLUtils.SASL_OPTION_AUTH_ID));
1192    if (authID == null)
1193    {
1194      throw new LDAPException(ResultCode.PARAM_ERROR,
1195           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASLUtils.SASL_OPTION_AUTH_ID,
1196                UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME));
1197    }
1198
1199    // The TOTP password option is required.
1200    final String totpPassword =
1201         options.remove(StaticUtils.toLowerCase(SASL_OPTION_TOTP_PASSWORD));
1202    if (totpPassword == null)
1203    {
1204      throw new LDAPException(ResultCode.PARAM_ERROR,
1205           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_TOTP_PASSWORD,
1206                UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME));
1207    }
1208
1209    // The authzID option is optional.
1210    byte[] pwBytes = password;
1211    final String authzID = options.remove(StaticUtils.toLowerCase(
1212         SASLUtils.SASL_OPTION_AUTHZ_ID));
1213
1214    // The promptForStaticPassword option is optional.
1215    final String promptStr = options.remove(StaticUtils.toLowerCase(
1216         SASL_OPTION_PROMPT_FOR_STATIC_PW));
1217    if (promptStr != null)
1218    {
1219      if (promptStr.equalsIgnoreCase("true"))
1220      {
1221        if (pwBytes == null)
1222        {
1223          tool.getOriginalOut().print(INFO_SASL_ENTER_STATIC_PW.get());
1224          pwBytes = PasswordReader.readPassword();
1225          tool.getOriginalOut().println();
1226        }
1227        else
1228        {
1229          throw new LDAPException(ResultCode.PARAM_ERROR,
1230               ERR_SASL_PROMPT_FOR_PROVIDED_PW.get(
1231                    SASL_OPTION_PROMPT_FOR_STATIC_PW));
1232        }
1233      }
1234      else if (! promptStr.equalsIgnoreCase("false"))
1235      {
1236        throw new LDAPException(ResultCode.PARAM_ERROR,
1237             ERR_SASL_PROMPT_FOR_STATIC_PW_BAD_VALUE.get(
1238                  SASL_OPTION_PROMPT_FOR_STATIC_PW));
1239      }
1240    }
1241
1242    // Ensure no unsupported options were provided.
1243    SASLUtils.ensureNoUnsupportedOptions(options,
1244         UnboundIDTOTPBindRequest.UNBOUNDID_TOTP_MECHANISM_NAME);
1245
1246    return new SingleUseTOTPBindRequest(authID, authzID, totpPassword, pwBytes,
1247         controls);
1248  }
1249
1250
1251
1252  /**
1253   * Creates a SASL UNBOUNDID-YUBIKEY-OTP bind request using the provided
1254   * password and set of options.
1255   *
1256   * @param  password  The password to use for the bind request.
1257   * @param  tool      The command-line tool whose input and output streams
1258   *                   should be used when prompting for the bind password.  It
1259   *                   may be {@code null} if {@code promptForPassword} is
1260   *                   {@code false}.
1261   * @param  options   The set of SASL options for the bind request.
1262   * @param  controls  The set of controls to include in the request.
1263   *
1264   * @return  The SASL UNBOUNDID-YUBIKEY-OTP bind request that was created.
1265   *
1266   * @throws  LDAPException  If a problem is encountered while trying to create
1267   *                         the SASL bind request.
1268   */
1269  private static UnboundIDYubiKeyOTPBindRequest
1270                      createUNBOUNDIDYUBIKEYOTPBindRequest(
1271                           final byte[] password, final CommandLineTool tool,
1272                           final Map<String,String> options,
1273                           final Control... controls)
1274          throws LDAPException
1275  {
1276    // The authID option is required.
1277    final String authID =
1278         options.remove(StaticUtils.toLowerCase(SASLUtils.SASL_OPTION_AUTH_ID));
1279    if (authID == null)
1280    {
1281      throw new LDAPException(ResultCode.PARAM_ERROR,
1282           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASLUtils.SASL_OPTION_AUTH_ID,
1283                UnboundIDYubiKeyOTPBindRequest.
1284                     UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME));
1285    }
1286
1287    // The otp option is required.
1288    final String otp = options.remove(StaticUtils.toLowerCase(SASL_OPTION_OTP));
1289    if (otp == null)
1290    {
1291      throw new LDAPException(ResultCode.PARAM_ERROR,
1292           ERR_SASL_MISSING_REQUIRED_OPTION.get(SASL_OPTION_OTP,
1293                UnboundIDYubiKeyOTPBindRequest.
1294                     UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME));
1295    }
1296
1297    // The authzID option is optional.
1298    final String authzID = options.remove(StaticUtils.toLowerCase(
1299         SASLUtils.SASL_OPTION_AUTHZ_ID));
1300
1301    // The promptForStaticPassword option is optional.
1302    byte[] pwBytes = password;
1303    final String promptStr = options.remove(StaticUtils.toLowerCase(
1304         SASL_OPTION_PROMPT_FOR_STATIC_PW));
1305    if (promptStr != null)
1306    {
1307      if (promptStr.equalsIgnoreCase("true"))
1308      {
1309        if (pwBytes == null)
1310        {
1311          tool.getOriginalOut().print(INFO_SASL_ENTER_STATIC_PW.get());
1312          pwBytes = PasswordReader.readPassword();
1313          tool.getOriginalOut().println();
1314        }
1315        else
1316        {
1317          throw new LDAPException(ResultCode.PARAM_ERROR,
1318               ERR_SASL_PROMPT_FOR_PROVIDED_PW.get(
1319                    SASL_OPTION_PROMPT_FOR_STATIC_PW));
1320        }
1321      }
1322      else if (! promptStr.equalsIgnoreCase("false"))
1323      {
1324        throw new LDAPException(ResultCode.PARAM_ERROR,
1325             ERR_SASL_PROMPT_FOR_STATIC_PW_BAD_VALUE.get(
1326                  SASL_OPTION_PROMPT_FOR_STATIC_PW));
1327      }
1328    }
1329
1330    // Ensure no unsupported options were provided.
1331    SASLUtils.ensureNoUnsupportedOptions(options,
1332         UnboundIDYubiKeyOTPBindRequest.UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME);
1333
1334    return new UnboundIDYubiKeyOTPBindRequest(authID, authzID, pwBytes, otp,
1335         controls);
1336  }
1337
1338
1339
1340  /**
1341   * Parses the provided list of SASL options.
1342   *
1343   * @param  options  The list of options to be parsed.
1344   *
1345   * @return  A map with the parsed set of options.
1346   *
1347   * @throws  LDAPException  If a problem is encountered while parsing options.
1348   */
1349  private static Map<String,String>
1350                      parseOptions(final List<String> options)
1351          throws LDAPException
1352  {
1353    if (options == null)
1354    {
1355      return new HashMap<String,String>(0);
1356    }
1357
1358    final HashMap<String,String> m = new HashMap<String,String>(options.size());
1359    for (final String s : options)
1360    {
1361      final int equalPos = s.indexOf('=');
1362      if (equalPos < 0)
1363      {
1364        throw new LDAPException(ResultCode.PARAM_ERROR,
1365             ERR_SASL_OPTION_MISSING_EQUAL.get(s));
1366      }
1367      else if (equalPos == 0)
1368      {
1369        throw new LDAPException(ResultCode.PARAM_ERROR,
1370             ERR_SASL_OPTION_STARTS_WITH_EQUAL.get(s));
1371      }
1372
1373      final String name = s.substring(0, equalPos);
1374      final String value = s.substring(equalPos + 1);
1375      if (m.put(toLowerCase(name), value) != null)
1376      {
1377        throw new LDAPException(ResultCode.PARAM_ERROR,
1378             ERR_SASL_OPTION_NOT_MULTI_VALUED.get(name));
1379      }
1380    }
1381
1382    return m;
1383  }
1384
1385
1386
1387  /**
1388   * Ensures that the provided map is empty, and will throw an exception if it
1389   * isn't.  This method is intended for internal use only.
1390   *
1391   * @param  options    The map of options to ensure is empty.
1392   * @param  mechanism  The associated SASL mechanism.
1393   *
1394   * @throws  LDAPException  If the map of SASL options is not empty.
1395   */
1396  @InternalUseOnly()
1397  public static void ensureNoUnsupportedOptions(
1398                          final Map<String,String> options,
1399                          final String mechanism)
1400          throws LDAPException
1401  {
1402    if (! options.isEmpty())
1403    {
1404      for (final String s : options.keySet())
1405      {
1406        throw new LDAPException(ResultCode.PARAM_ERROR,
1407             ERR_SASL_OPTION_UNSUPPORTED_FOR_MECH.get(s,mechanism));
1408      }
1409    }
1410  }
1411
1412
1413
1414  /**
1415   * Retrieves the value of the specified option and parses it as a boolean.
1416   * Values of "true", "t", "yes", "y", "on", and "1" will be treated as
1417   * {@code true}.  Values of "false", "f", "no", "n", "off", and "0" will be
1418   * treated as {@code false}.
1419   *
1420   * @param  m  The map from which to retrieve the option.  It must not be
1421   *            {@code null}.
1422   * @param  o  The name of the option to examine.
1423   * @param  d  The default value to use if the given option was not provided.
1424   *
1425   * @return  The parsed boolean value.
1426   *
1427   * @throws  LDAPException  If the option value cannot be parsed as a boolean.
1428   */
1429  static boolean getBooleanValue(final Map<String,String> m, final String o,
1430                                 final boolean d)
1431         throws LDAPException
1432  {
1433    final String s = toLowerCase(m.remove(toLowerCase(o)));
1434    if (s == null)
1435    {
1436      return d;
1437    }
1438    else if (s.equals("true") ||
1439             s.equals("t") ||
1440             s.equals("yes") ||
1441             s.equals("y") ||
1442             s.equals("on") ||
1443             s.equals("1"))
1444    {
1445      return true;
1446    }
1447    else if (s.equals("false") ||
1448             s.equals("f") ||
1449             s.equals("no") ||
1450             s.equals("n") ||
1451             s.equals("off") ||
1452             s.equals("0"))
1453    {
1454      return false;
1455    }
1456    else
1457    {
1458      throw new LDAPException(ResultCode.PARAM_ERROR,
1459           ERR_SASL_OPTION_MALFORMED_BOOLEAN_VALUE.get(o));
1460    }
1461  }
1462
1463
1464
1465  /**
1466   * Retrieves a string representation of the SASL usage information.  This will
1467   * include the supported SASL mechanisms and the properties that may be used
1468   * with each.
1469   *
1470   * @param  maxWidth  The maximum line width to use for the output.  If this is
1471   *                   less than or equal to zero, then no wrapping will be
1472   *                   performed.
1473   *
1474   * @return  A string representation of the usage information
1475   */
1476  public static String getUsageString(final int maxWidth)
1477  {
1478    final StringBuilder buffer = new StringBuilder();
1479
1480    for (final String line : getUsage(maxWidth))
1481    {
1482      buffer.append(line);
1483      buffer.append(EOL);
1484    }
1485
1486    return buffer.toString();
1487  }
1488
1489
1490
1491  /**
1492   * Retrieves lines that make up the SASL usage information, optionally
1493   * wrapping long lines.
1494   *
1495   * @param  maxWidth  The maximum line width to use for the output.  If this is
1496   *                   less than or equal to zero, then no wrapping will be
1497   *                   performed.
1498   *
1499   * @return  The lines that make up the SASL usage information.
1500   */
1501  public static List<String> getUsage(final int maxWidth)
1502  {
1503    final ArrayList<String> lines = new ArrayList<String>(100);
1504
1505    boolean first = true;
1506    for (final SASLMechanismInfo i : getSupportedSASLMechanisms())
1507    {
1508      if (first)
1509      {
1510        first = false;
1511      }
1512      else
1513      {
1514        lines.add("");
1515        lines.add("");
1516      }
1517
1518      lines.addAll(
1519           wrapLine(INFO_SASL_HELP_MECHANISM.get(i.getName()), maxWidth));
1520      lines.add("");
1521
1522      for (final String line : wrapLine(i.getDescription(), maxWidth - 4))
1523      {
1524        lines.add("  " + line);
1525      }
1526      lines.add("");
1527
1528      for (final String line :
1529           wrapLine(INFO_SASL_HELP_MECHANISM_OPTIONS.get(i.getName()),
1530                maxWidth - 4))
1531      {
1532        lines.add("  " + line);
1533      }
1534
1535      if (i.acceptsPassword())
1536      {
1537        lines.add("");
1538        if (i.requiresPassword())
1539        {
1540          for (final String line :
1541               wrapLine(INFO_SASL_HELP_PASSWORD_REQUIRED.get(i.getName()),
1542                    maxWidth - 4))
1543          {
1544            lines.add("  " + line);
1545          }
1546        }
1547        else
1548        {
1549          for (final String line :
1550               wrapLine(INFO_SASL_HELP_PASSWORD_OPTIONAL.get(i.getName()),
1551                    maxWidth - 4))
1552          {
1553            lines.add("  " + line);
1554          }
1555        }
1556      }
1557
1558      for (final SASLOption o : i.getOptions())
1559      {
1560        lines.add("");
1561        lines.add("  * " + o.getName());
1562        for (final String line : wrapLine(o.getDescription(), maxWidth - 14))
1563        {
1564          lines.add("       " + line);
1565        }
1566      }
1567    }
1568
1569    return lines;
1570  }
1571}