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