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