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