001/*
002 * Copyright 2016-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2016-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2016-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.unboundidds;
037
038
039
040import java.io.OutputStream;
041import java.io.Serializable;
042import java.util.LinkedHashMap;
043
044import com.unboundid.ldap.sdk.ExtendedResult;
045import com.unboundid.ldap.sdk.LDAPConnection;
046import com.unboundid.ldap.sdk.LDAPException;
047import com.unboundid.ldap.sdk.ResultCode;
048import com.unboundid.ldap.sdk.Version;
049import com.unboundid.ldap.sdk.unboundidds.extensions.
050            DeregisterYubiKeyOTPDeviceExtendedRequest;
051import com.unboundid.ldap.sdk.unboundidds.extensions.
052            RegisterYubiKeyOTPDeviceExtendedRequest;
053import com.unboundid.util.Debug;
054import com.unboundid.util.LDAPCommandLineTool;
055import com.unboundid.util.NotNull;
056import com.unboundid.util.Nullable;
057import com.unboundid.util.PasswordReader;
058import com.unboundid.util.StaticUtils;
059import com.unboundid.util.ThreadSafety;
060import com.unboundid.util.ThreadSafetyLevel;
061import com.unboundid.util.args.ArgumentException;
062import com.unboundid.util.args.ArgumentParser;
063import com.unboundid.util.args.BooleanArgument;
064import com.unboundid.util.args.FileArgument;
065import com.unboundid.util.args.StringArgument;
066
067import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*;
068
069
070
071/**
072 * This class provides a utility that may be used to register a YubiKey OTP
073 * device for a specified user so that it may be used to authenticate that user.
074 * Alternately, it may be used to deregister one or all of the YubiKey OTP
075 * devices that have been registered for the user.
076 * <BR>
077 * <BLOCKQUOTE>
078 *   <B>NOTE:</B>  This class, and other classes within the
079 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
080 *   supported for use against Ping Identity, UnboundID, and
081 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
082 *   for proprietary functionality or for external specifications that are not
083 *   considered stable or mature enough to be guaranteed to work in an
084 *   interoperable way with other types of LDAP servers.
085 * </BLOCKQUOTE>
086 */
087@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
088public final class RegisterYubiKeyOTPDevice
089       extends LDAPCommandLineTool
090       implements Serializable
091{
092  /**
093   * The serial version UID for this serializable class.
094   */
095  private static final long serialVersionUID = 5705120716566064832L;
096
097
098
099  // Indicates that the tool should deregister one or all of the YubiKey OTP
100  // devices for the user rather than registering a new device.
101  @Nullable private BooleanArgument deregister;
102
103  // Indicates that the tool should interactively prompt for the static password
104  // for the user for whom the YubiKey OTP device is to be registered or
105  // deregistered.
106  @Nullable private BooleanArgument promptForUserPassword;
107
108  // The path to a file containing the static password for the user for whom the
109  // YubiKey OTP device is to be registered or deregistered.
110  @Nullable private FileArgument userPasswordFile;
111
112  // The username for the user for whom the YubiKey OTP device is to be
113  // registered or deregistered.
114  @Nullable private StringArgument authenticationID;
115
116  // The static password for the user for whom the YubiKey OTP device is to be
117  // registered or deregistered.
118  @Nullable private StringArgument userPassword;
119
120  // A one-time password generated by the YubiKey OTP device to be registered
121  // or deregistered.
122  @Nullable private StringArgument otp;
123
124
125
126  /**
127   * Parse the provided command line arguments and perform the appropriate
128   * processing.
129   *
130   * @param  args  The command line arguments provided to this program.
131   */
132  public static void main(@NotNull final String... args)
133  {
134    final ResultCode resultCode = main(args, System.out, System.err);
135    if (resultCode != ResultCode.SUCCESS)
136    {
137      System.exit(resultCode.intValue());
138    }
139  }
140
141
142
143  /**
144   * Parse the provided command line arguments and perform the appropriate
145   * processing.
146   *
147   * @param  args       The command line arguments provided to this program.
148   * @param  outStream  The output stream to which standard out should be
149   *                    written.  It may be {@code null} if output should be
150   *                    suppressed.
151   * @param  errStream  The output stream to which standard error should be
152   *                    written.  It may be {@code null} if error messages
153   *                    should be suppressed.
154   *
155   * @return  A result code indicating whether the processing was successful.
156   */
157  @NotNull()
158  public static ResultCode main(@NotNull final String[] args,
159                                @Nullable final OutputStream outStream,
160                                @Nullable final OutputStream errStream)
161  {
162    final RegisterYubiKeyOTPDevice tool =
163         new RegisterYubiKeyOTPDevice(outStream, errStream);
164    return tool.runTool(args);
165  }
166
167
168
169  /**
170   * Creates a new instance of this tool.
171   *
172   * @param  outStream  The output stream to which standard out should be
173   *                    written.  It may be {@code null} if output should be
174   *                    suppressed.
175   * @param  errStream  The output stream to which standard error should be
176   *                    written.  It may be {@code null} if error messages
177   *                    should be suppressed.
178   */
179  public RegisterYubiKeyOTPDevice(@Nullable final OutputStream outStream,
180                                  @Nullable final OutputStream errStream)
181  {
182    super(outStream, errStream);
183
184    deregister            = null;
185    otp                   = null;
186    promptForUserPassword = null;
187    userPasswordFile      = null;
188    authenticationID      = null;
189    userPassword          = null;
190  }
191
192
193
194  /**
195   * {@inheritDoc}
196   */
197  @Override()
198  @NotNull()
199  public String getToolName()
200  {
201    return "register-yubikey-otp-device";
202  }
203
204
205
206  /**
207   * {@inheritDoc}
208   */
209  @Override()
210  @NotNull()
211  public String getToolDescription()
212  {
213    return INFO_REGISTER_YUBIKEY_OTP_DEVICE_TOOL_DESCRIPTION.get(
214         UnboundIDYubiKeyOTPBindRequest.UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME);
215  }
216
217
218
219  /**
220   * {@inheritDoc}
221   */
222  @Override()
223  @NotNull()
224  public String getToolVersion()
225  {
226    return Version.NUMERIC_VERSION_STRING;
227  }
228
229
230
231  /**
232   * {@inheritDoc}
233   */
234  @Override()
235  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
236         throws ArgumentException
237  {
238    deregister = new BooleanArgument(null, "deregister", 1,
239         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_DEREGISTER.get("--otp"));
240    deregister.addLongIdentifier("de-register", true);
241    parser.addArgument(deregister);
242
243    otp = new StringArgument(null, "otp", false, 1,
244         INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_OTP.get(),
245         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_OTP.get());
246    parser.addArgument(otp);
247
248    authenticationID = new StringArgument(null, "authID", false, 1,
249         INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_AUTHID.get(),
250         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_AUTHID.get());
251    authenticationID.addLongIdentifier("authenticationID", true);
252    authenticationID.addLongIdentifier("auth-id", true);
253    authenticationID.addLongIdentifier("authentication-id", true);
254    parser.addArgument(authenticationID);
255
256    userPassword = new StringArgument(null, "userPassword", false, 1,
257         INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_USER_PW.get(),
258         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_USER_PW.get(
259              authenticationID.getIdentifierString()));
260    userPassword.setSensitive(true);
261    userPassword.addLongIdentifier("user-password", true);
262    parser.addArgument(userPassword);
263
264    userPasswordFile = new FileArgument(null, "userPasswordFile", false, 1,
265         null,
266         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_USER_PW_FILE.get(
267              authenticationID.getIdentifierString()),
268         true, true, true, false);
269    userPasswordFile.addLongIdentifier("user-password-file", true);
270    parser.addArgument(userPasswordFile);
271
272    promptForUserPassword = new BooleanArgument(null, "promptForUserPassword",
273         INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_PROMPT_FOR_USER_PW.get(
274              authenticationID.getIdentifierString()));
275    promptForUserPassword.addLongIdentifier("prompt-for-user-password", true);
276    parser.addArgument(promptForUserPassword);
277
278
279    // At most one of the userPassword, userPasswordFile, and
280    // promptForUserPassword arguments must be present.
281    parser.addExclusiveArgumentSet(userPassword, userPasswordFile,
282         promptForUserPassword);
283
284    // If any of the userPassword, userPasswordFile, or promptForUserPassword
285    // arguments is present, then the authenticationID argument must also be
286    // present.
287    parser.addDependentArgumentSet(userPassword, authenticationID);
288    parser.addDependentArgumentSet(userPasswordFile, authenticationID);
289    parser.addDependentArgumentSet(promptForUserPassword, authenticationID);
290  }
291
292
293
294  /**
295   * {@inheritDoc}
296   */
297  @Override()
298  public void doExtendedNonLDAPArgumentValidation()
299         throws ArgumentException
300  {
301    // If the deregister argument was not provided, then the otp argument must
302    // have been given.
303    if ((! deregister.isPresent()) && (! otp.isPresent()))
304    {
305      throw new ArgumentException(
306           ERR_REGISTER_YUBIKEY_OTP_DEVICE_NO_OTP_TO_REGISTER.get(
307                otp.getIdentifierString()));
308    }
309  }
310
311
312
313  /**
314   * {@inheritDoc}
315   */
316  @Override()
317  public boolean supportsInteractiveMode()
318  {
319    return true;
320  }
321
322
323
324  /**
325   * {@inheritDoc}
326   */
327  @Override()
328  public boolean defaultsToInteractiveMode()
329  {
330    return true;
331  }
332
333
334
335  /**
336   * {@inheritDoc}
337   */
338  @Override()
339  protected boolean supportsOutputFile()
340  {
341    return true;
342  }
343
344
345
346  /**
347   * {@inheritDoc}
348   */
349  @Override()
350  protected boolean defaultToPromptForBindPassword()
351  {
352    return true;
353  }
354
355
356
357  /**
358   * Indicates whether this tool supports the use of a properties file for
359   * specifying default values for arguments that aren't specified on the
360   * command line.
361   *
362   * @return  {@code true} if this tool supports the use of a properties file
363   *          for specifying default values for arguments that aren't specified
364   *          on the command line, or {@code false} if not.
365   */
366  @Override()
367  public boolean supportsPropertiesFile()
368  {
369    return true;
370  }
371
372
373
374  /**
375   * Indicates whether the LDAP-specific arguments should include alternate
376   * versions of all long identifiers that consist of multiple words so that
377   * they are available in both camelCase and dash-separated versions.
378   *
379   * @return  {@code true} if this tool should provide multiple versions of
380   *          long identifiers for LDAP-specific arguments, or {@code false} if
381   *          not.
382   */
383  @Override()
384  protected boolean includeAlternateLongIdentifiers()
385  {
386    return true;
387  }
388
389
390
391  /**
392   * Indicates whether this tool should provide a command-line argument that
393   * allows for low-level SSL debugging.  If this returns {@code true}, then an
394   * "--enableSSLDebugging}" argument will be added that sets the
395   * "javax.net.debug" system property to "all" before attempting any
396   * communication.
397   *
398   * @return  {@code true} if this tool should offer an "--enableSSLDebugging"
399   *          argument, or {@code false} if not.
400   */
401  @Override()
402  protected boolean supportsSSLDebugging()
403  {
404    return true;
405  }
406
407
408
409  /**
410   * {@inheritDoc}
411   */
412  @Override()
413  protected boolean logToolInvocationByDefault()
414  {
415    return true;
416  }
417
418
419
420  /**
421   * {@inheritDoc}
422   */
423  @Override()
424  @NotNull()
425  public ResultCode doToolProcessing()
426  {
427    // Establish a connection to the Directory Server.
428    final LDAPConnection conn;
429    try
430    {
431      conn = getConnection();
432    }
433    catch (final LDAPException le)
434    {
435      Debug.debugException(le);
436      wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
437           ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_CONNECT.get(
438                StaticUtils.getExceptionMessage(le)));
439      return le.getResultCode();
440    }
441
442    try
443    {
444      // Get the authentication ID and static password to include in the
445      // request.
446      final String authID = authenticationID.getValue();
447
448      final byte[] staticPassword;
449      if (userPassword.isPresent())
450      {
451        staticPassword = StaticUtils.getBytes(userPassword.getValue());
452      }
453      else if (userPasswordFile.isPresent())
454      {
455        try
456        {
457          final char[] pwChars = getPasswordFileReader().readPassword(
458               userPasswordFile.getValue());
459          staticPassword = StaticUtils.getBytes(new String(pwChars));
460        }
461        catch (final Exception e)
462        {
463          Debug.debugException(e);
464          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
465               ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get(
466                    StaticUtils.getExceptionMessage(e)));
467          return ResultCode.LOCAL_ERROR;
468        }
469      }
470      else if (promptForUserPassword.isPresent())
471      {
472        try
473        {
474          getOut().print(INFO_REGISTER_YUBIKEY_OTP_DEVICE_ENTER_PW.get(authID));
475          staticPassword = PasswordReader.readPassword();
476        }
477        catch (final Exception e)
478        {
479          Debug.debugException(e);
480          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
481               ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get(
482                    StaticUtils.getExceptionMessage(e)));
483          return ResultCode.LOCAL_ERROR;
484        }
485      }
486      else
487      {
488        staticPassword = null;
489      }
490
491
492      // Construct and process the appropriate register or deregister request.
493      if (deregister.isPresent())
494      {
495        final DeregisterYubiKeyOTPDeviceExtendedRequest r =
496             new DeregisterYubiKeyOTPDeviceExtendedRequest(authID,
497                  staticPassword, otp.getValue());
498
499        ExtendedResult deregisterResult;
500        try
501        {
502          deregisterResult = conn.processExtendedOperation(r);
503        }
504        catch (final LDAPException le)
505        {
506          deregisterResult = new ExtendedResult(le);
507        }
508
509        if (deregisterResult.getResultCode() == ResultCode.SUCCESS)
510        {
511          if (otp.isPresent())
512          {
513            wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
514                 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ONE.get(
515                      authID));
516          }
517          else
518          {
519            wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
520                 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ALL.get(
521                      authID));
522          }
523          return ResultCode.SUCCESS;
524        }
525        else
526        {
527          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
528               ERR_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_FAILED.get(authID,
529                    String.valueOf(deregisterResult)));
530          return deregisterResult.getResultCode();
531        }
532      }
533      else
534      {
535        final RegisterYubiKeyOTPDeviceExtendedRequest r =
536             new RegisterYubiKeyOTPDeviceExtendedRequest(authID, staticPassword,
537                  otp.getValue());
538
539        ExtendedResult registerResult;
540        try
541        {
542          registerResult = conn.processExtendedOperation(r);
543        }
544        catch (final LDAPException le)
545        {
546          registerResult = new ExtendedResult(le);
547        }
548
549        if (registerResult.getResultCode() == ResultCode.SUCCESS)
550        {
551          wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
552               INFO_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_SUCCESS.get(authID));
553          return ResultCode.SUCCESS;
554        }
555        else
556        {
557          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
558               ERR_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_FAILED.get(authID,
559                    String.valueOf(registerResult)));
560          return registerResult.getResultCode();
561        }
562      }
563    }
564    finally
565    {
566      conn.close();
567    }
568  }
569
570
571
572  /**
573   * {@inheritDoc}
574   */
575  @Override()
576  @NotNull()
577  public LinkedHashMap<String[],String> getExampleUsages()
578  {
579    final LinkedHashMap<String[],String> exampleMap =
580         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
581
582    String[] args =
583    {
584      "--hostname", "server.example.com",
585      "--port", "389",
586      "--bindDN", "uid=admin,dc=example,dc=com",
587      "--bindPassword", "adminPassword",
588      "--authenticationID", "u:test.user",
589      "--userPassword", "testUserPassword",
590      "--otp", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr"
591    };
592    exampleMap.put(args,
593         INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_REGISTER.get());
594
595    args = new String[]
596    {
597      "--hostname", "server.example.com",
598      "--port", "389",
599      "--bindDN", "uid=admin,dc=example,dc=com",
600      "--bindPassword", "adminPassword",
601      "--deregister",
602      "--authenticationID", "dn:uid=test.user,ou=People,dc=example,dc=com"
603    };
604    exampleMap.put(args,
605         INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_DEREGISTER.get());
606
607    return exampleMap;
608  }
609}