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   * {@inheritDoc}
376   */
377  @Override()
378  protected boolean supportsDebugLogging()
379  {
380    return true;
381  }
382
383
384
385  /**
386   * Indicates whether the LDAP-specific arguments should include alternate
387   * versions of all long identifiers that consist of multiple words so that
388   * they are available in both camelCase and dash-separated versions.
389   *
390   * @return  {@code true} if this tool should provide multiple versions of
391   *          long identifiers for LDAP-specific arguments, or {@code false} if
392   *          not.
393   */
394  @Override()
395  protected boolean includeAlternateLongIdentifiers()
396  {
397    return true;
398  }
399
400
401
402  /**
403   * Indicates whether this tool should provide a command-line argument that
404   * allows for low-level SSL debugging.  If this returns {@code true}, then an
405   * "--enableSSLDebugging}" argument will be added that sets the
406   * "javax.net.debug" system property to "all" before attempting any
407   * communication.
408   *
409   * @return  {@code true} if this tool should offer an "--enableSSLDebugging"
410   *          argument, or {@code false} if not.
411   */
412  @Override()
413  protected boolean supportsSSLDebugging()
414  {
415    return true;
416  }
417
418
419
420  /**
421   * {@inheritDoc}
422   */
423  @Override()
424  protected boolean logToolInvocationByDefault()
425  {
426    return true;
427  }
428
429
430
431  /**
432   * {@inheritDoc}
433   */
434  @Override()
435  @NotNull()
436  public ResultCode doToolProcessing()
437  {
438    // Establish a connection to the Directory Server.
439    final LDAPConnection conn;
440    try
441    {
442      conn = getConnection();
443    }
444    catch (final LDAPException le)
445    {
446      Debug.debugException(le);
447      wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
448           ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_CONNECT.get(
449                StaticUtils.getExceptionMessage(le)));
450      return le.getResultCode();
451    }
452
453    try
454    {
455      // Get the authentication ID and static password to include in the
456      // request.
457      final String authID = authenticationID.getValue();
458
459      final byte[] staticPassword;
460      if (userPassword.isPresent())
461      {
462        staticPassword = StaticUtils.getBytes(userPassword.getValue());
463      }
464      else if (userPasswordFile.isPresent())
465      {
466        try
467        {
468          final char[] pwChars = getPasswordFileReader().readPassword(
469               userPasswordFile.getValue());
470          staticPassword = StaticUtils.getBytes(new String(pwChars));
471        }
472        catch (final Exception e)
473        {
474          Debug.debugException(e);
475          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
476               ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get(
477                    StaticUtils.getExceptionMessage(e)));
478          return ResultCode.LOCAL_ERROR;
479        }
480      }
481      else if (promptForUserPassword.isPresent())
482      {
483        try
484        {
485          getOut().print(INFO_REGISTER_YUBIKEY_OTP_DEVICE_ENTER_PW.get(authID));
486          staticPassword = PasswordReader.readPassword();
487        }
488        catch (final Exception e)
489        {
490          Debug.debugException(e);
491          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
492               ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get(
493                    StaticUtils.getExceptionMessage(e)));
494          return ResultCode.LOCAL_ERROR;
495        }
496      }
497      else
498      {
499        staticPassword = null;
500      }
501
502
503      // Construct and process the appropriate register or deregister request.
504      if (deregister.isPresent())
505      {
506        final DeregisterYubiKeyOTPDeviceExtendedRequest r =
507             new DeregisterYubiKeyOTPDeviceExtendedRequest(authID,
508                  staticPassword, otp.getValue());
509
510        ExtendedResult deregisterResult;
511        try
512        {
513          deregisterResult = conn.processExtendedOperation(r);
514        }
515        catch (final LDAPException le)
516        {
517          deregisterResult = new ExtendedResult(le);
518        }
519
520        if (deregisterResult.getResultCode() == ResultCode.SUCCESS)
521        {
522          if (otp.isPresent())
523          {
524            wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
525                 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ONE.get(
526                      authID));
527          }
528          else
529          {
530            wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
531                 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ALL.get(
532                      authID));
533          }
534          return ResultCode.SUCCESS;
535        }
536        else
537        {
538          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
539               ERR_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_FAILED.get(authID,
540                    String.valueOf(deregisterResult)));
541          return deregisterResult.getResultCode();
542        }
543      }
544      else
545      {
546        final RegisterYubiKeyOTPDeviceExtendedRequest r =
547             new RegisterYubiKeyOTPDeviceExtendedRequest(authID, staticPassword,
548                  otp.getValue());
549
550        ExtendedResult registerResult;
551        try
552        {
553          registerResult = conn.processExtendedOperation(r);
554        }
555        catch (final LDAPException le)
556        {
557          registerResult = new ExtendedResult(le);
558        }
559
560        if (registerResult.getResultCode() == ResultCode.SUCCESS)
561        {
562          wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
563               INFO_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_SUCCESS.get(authID));
564          return ResultCode.SUCCESS;
565        }
566        else
567        {
568          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
569               ERR_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_FAILED.get(authID,
570                    String.valueOf(registerResult)));
571          return registerResult.getResultCode();
572        }
573      }
574    }
575    finally
576    {
577      conn.close();
578    }
579  }
580
581
582
583  /**
584   * {@inheritDoc}
585   */
586  @Override()
587  @NotNull()
588  public LinkedHashMap<String[],String> getExampleUsages()
589  {
590    final LinkedHashMap<String[],String> exampleMap =
591         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
592
593    String[] args =
594    {
595      "--hostname", "server.example.com",
596      "--port", "389",
597      "--bindDN", "uid=admin,dc=example,dc=com",
598      "--bindPassword", "adminPassword",
599      "--authenticationID", "u:test.user",
600      "--userPassword", "testUserPassword",
601      "--otp", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr"
602    };
603    exampleMap.put(args,
604         INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_REGISTER.get());
605
606    args = new String[]
607    {
608      "--hostname", "server.example.com",
609      "--port", "389",
610      "--bindDN", "uid=admin,dc=example,dc=com",
611      "--bindPassword", "adminPassword",
612      "--deregister",
613      "--authenticationID", "dn:uid=test.user,ou=People,dc=example,dc=com"
614    };
615    exampleMap.put(args,
616         INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_DEREGISTER.get());
617
618    return exampleMap;
619  }
620}