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