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   * {@inheritDoc}
372   */
373  @Override()
374  protected boolean logToolInvocationByDefault()
375  {
376    return true;
377  }
378
379
380
381  /**
382   * {@inheritDoc}
383   */
384  @Override()
385  public ResultCode doToolProcessing()
386  {
387    // Establish a connection to the Directory Server.
388    final LDAPConnection conn;
389    try
390    {
391      conn = getConnection();
392    }
393    catch (final LDAPException le)
394    {
395      Debug.debugException(le);
396      wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
397           ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_CONNECT.get(
398                StaticUtils.getExceptionMessage(le)));
399      return le.getResultCode();
400    }
401
402    try
403    {
404      // Get the authentication ID and static password to include in the
405      // request.
406      final String authID = authenticationID.getValue();
407
408      final byte[] staticPassword;
409      if (userPassword.isPresent())
410      {
411        staticPassword = StaticUtils.getBytes(userPassword.getValue());
412      }
413      else if (userPasswordFile.isPresent())
414      {
415        try
416        {
417          final char[] pwChars = getPasswordFileReader().readPassword(
418               userPasswordFile.getValue());
419          staticPassword = StaticUtils.getBytes(new String(pwChars));
420        }
421        catch (final Exception e)
422        {
423          Debug.debugException(e);
424          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
425               ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get(
426                    StaticUtils.getExceptionMessage(e)));
427          return ResultCode.LOCAL_ERROR;
428        }
429      }
430      else if (promptForUserPassword.isPresent())
431      {
432        try
433        {
434          getOut().print(INFO_REGISTER_YUBIKEY_OTP_DEVICE_ENTER_PW.get(authID));
435          staticPassword = PasswordReader.readPassword();
436        }
437        catch (final Exception e)
438        {
439          Debug.debugException(e);
440          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
441               ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get(
442                    StaticUtils.getExceptionMessage(e)));
443          return ResultCode.LOCAL_ERROR;
444        }
445      }
446      else
447      {
448        staticPassword = null;
449      }
450
451
452      // Construct and process the appropriate register or deregister request.
453      if (deregister.isPresent())
454      {
455        final DeregisterYubiKeyOTPDeviceExtendedRequest r =
456             new DeregisterYubiKeyOTPDeviceExtendedRequest(authID,
457                  staticPassword, otp.getValue());
458
459        ExtendedResult deregisterResult;
460        try
461        {
462          deregisterResult = conn.processExtendedOperation(r);
463        }
464        catch (final LDAPException le)
465        {
466          deregisterResult = new ExtendedResult(le);
467        }
468
469        if (deregisterResult.getResultCode() == ResultCode.SUCCESS)
470        {
471          if (otp.isPresent())
472          {
473            wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
474                 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ONE.get(
475                      authID));
476          }
477          else
478          {
479            wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
480                 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ALL.get(
481                      authID));
482          }
483          return ResultCode.SUCCESS;
484        }
485        else
486        {
487          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
488               ERR_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_FAILED.get(authID,
489                    String.valueOf(deregisterResult)));
490          return deregisterResult.getResultCode();
491        }
492      }
493      else
494      {
495        final RegisterYubiKeyOTPDeviceExtendedRequest r =
496             new RegisterYubiKeyOTPDeviceExtendedRequest(authID, staticPassword,
497                  otp.getValue());
498
499        ExtendedResult registerResult;
500        try
501        {
502          registerResult = conn.processExtendedOperation(r);
503        }
504        catch (final LDAPException le)
505        {
506          registerResult = new ExtendedResult(le);
507        }
508
509        if (registerResult.getResultCode() == ResultCode.SUCCESS)
510        {
511          wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
512               INFO_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_SUCCESS.get(authID));
513          return ResultCode.SUCCESS;
514        }
515        else
516        {
517          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
518               ERR_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_FAILED.get(authID,
519                    String.valueOf(registerResult)));
520          return registerResult.getResultCode();
521        }
522      }
523    }
524    finally
525    {
526      conn.close();
527    }
528  }
529
530
531
532  /**
533   * {@inheritDoc}
534   */
535  @Override()
536  public LinkedHashMap<String[],String> getExampleUsages()
537  {
538    final LinkedHashMap<String[],String> exampleMap =
539         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
540
541    String[] args =
542    {
543      "--hostname", "server.example.com",
544      "--port", "389",
545      "--bindDN", "uid=admin,dc=example,dc=com",
546      "--bindPassword", "adminPassword",
547      "--authenticationID", "u:test.user",
548      "--userPassword", "testUserPassword",
549      "--otp", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr"
550    };
551    exampleMap.put(args,
552         INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_REGISTER.get());
553
554    args = new String[]
555    {
556      "--hostname", "server.example.com",
557      "--port", "389",
558      "--bindDN", "uid=admin,dc=example,dc=com",
559      "--bindPassword", "adminPassword",
560      "--deregister",
561      "--authenticationID", "dn:uid=test.user,ou=People,dc=example,dc=com"
562    };
563    exampleMap.put(args,
564         INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_DEREGISTER.get());
565
566    return exampleMap;
567  }
568}