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.tools;
022
023
024
025import java.io.BufferedReader;
026import java.io.FileReader;
027import java.io.OutputStream;
028import java.util.LinkedHashMap;
029
030import com.unboundid.ldap.sdk.ExtendedResult;
031import com.unboundid.ldap.sdk.LDAPConnection;
032import com.unboundid.ldap.sdk.LDAPException;
033import com.unboundid.ldap.sdk.ResultCode;
034import com.unboundid.ldap.sdk.Version;
035import com.unboundid.ldap.sdk.unboundidds.extensions.
036            GenerateTOTPSharedSecretExtendedRequest;
037import com.unboundid.ldap.sdk.unboundidds.extensions.
038            GenerateTOTPSharedSecretExtendedResult;
039import com.unboundid.ldap.sdk.unboundidds.extensions.
040            RevokeTOTPSharedSecretExtendedRequest;
041import com.unboundid.util.Debug;
042import com.unboundid.util.LDAPCommandLineTool;
043import com.unboundid.util.PasswordReader;
044import com.unboundid.util.StaticUtils;
045import com.unboundid.util.ThreadSafety;
046import com.unboundid.util.ThreadSafetyLevel;
047import com.unboundid.util.args.ArgumentException;
048import com.unboundid.util.args.ArgumentParser;
049import com.unboundid.util.args.BooleanArgument;
050import com.unboundid.util.args.FileArgument;
051import com.unboundid.util.args.StringArgument;
052
053import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
054
055
056
057/**
058 * This class provides a tool that can be used to generate a TOTP shared secret
059 * for a user.  That shared secret may be used to generate TOTP authentication
060 * codes for the purpose of authenticating with the UNBOUNDID-TOTP SASL
061 * mechanism, or as a form of step-up authentication for external applications
062 * using the validate TOTP password extended operation.
063 * <BR>
064 * <BLOCKQUOTE>
065 *   <B>NOTE:</B>  This class, and other classes within the
066 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
067 *   supported for use against Ping Identity, UnboundID, and Alcatel-Lucent 8661
068 *   server products.  These classes provide support for proprietary
069 *   functionality or for external specifications that are not considered stable
070 *   or mature enough to be guaranteed to work in an interoperable way with
071 *   other types of LDAP servers.
072 * </BLOCKQUOTE>
073 */
074@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
075public final class GenerateTOTPSharedSecret
076       extends LDAPCommandLineTool
077{
078  // Indicates that the tool should interactively prompt for the static password
079  // for the user for whom the TOTP secret is to be generated.
080  private BooleanArgument promptForUserPassword = null;
081
082  // Indicates that the tool should revoke all existing TOTP shared secrets for
083  // the user.
084  private BooleanArgument revokeAll = null;
085
086  // The path to a file containing the static password for the user for whom the
087  // TOTP secret is to be generated.
088  private FileArgument userPasswordFile = null;
089
090  // The username for the user for whom the TOTP shared secret is to be
091  // generated.
092  private StringArgument authenticationID = null;
093
094  // The TOTP shared secret to revoke.
095  private StringArgument revoke = null;
096
097  // The static password for the user for whom the TOTP shared sec ret is to be
098  // generated.
099  private StringArgument userPassword = null;
100
101
102
103  /**
104   * Invokes the tool with the provided set of arguments.
105   *
106   * @param  args  The command-line arguments provided to this program.
107   */
108  public static void main(final String... args)
109  {
110    final ResultCode resultCode = main(System.out, System.err, args);
111    if (resultCode != ResultCode.SUCCESS)
112    {
113      System.exit(resultCode.intValue());
114    }
115  }
116
117
118
119  /**
120   * Invokes the tool with the provided set of arguments.
121   *
122   * @param  out   The output stream to use for standard out.  It may be
123   *               {@code null} if standard out should be suppressed.
124   * @param  err   The output stream to use for standard error.  It may be
125   *               {@code null} if standard error should be suppressed.
126   * @param  args  The command-line arguments provided to this program.
127   *
128   * @return  A result code with the status of the tool processing.  Any result
129   *          code other than {@link ResultCode#SUCCESS} should be considered a
130   *          failure.
131   */
132  public static ResultCode main(final OutputStream out, final OutputStream err,
133                                final String... args)
134  {
135    final GenerateTOTPSharedSecret tool =
136         new GenerateTOTPSharedSecret(out, err);
137    return tool.runTool(args);
138  }
139
140
141
142  /**
143   * Creates a new instance of this tool with the provided arguments.
144   *
145   * @param  out  The output stream to use for standard out.  It may be
146   *              {@code null} if standard out should be suppressed.
147   * @param  err  The output stream to use for standard error.  It may be
148   *              {@code null} if standard error should be suppressed.
149   */
150  public GenerateTOTPSharedSecret(final OutputStream out,
151                                  final OutputStream err)
152  {
153    super(out, err);
154  }
155
156
157
158  /**
159   * {@inheritDoc}
160   */
161  @Override()
162  public String getToolName()
163  {
164    return "generate-totp-shared-secret";
165  }
166
167
168
169  /**
170   * {@inheritDoc}
171   */
172  @Override()
173  public String getToolDescription()
174  {
175    return INFO_GEN_TOTP_SECRET_TOOL_DESC.get();
176  }
177
178
179
180  /**
181   * {@inheritDoc}
182   */
183  @Override()
184  public String getToolVersion()
185  {
186    return Version.NUMERIC_VERSION_STRING;
187  }
188
189
190
191  /**
192   * {@inheritDoc}
193   */
194  @Override()
195  public boolean supportsInteractiveMode()
196  {
197    return true;
198  }
199
200
201
202  /**
203   * {@inheritDoc}
204   */
205  @Override()
206  public boolean defaultsToInteractiveMode()
207  {
208    return true;
209  }
210
211
212
213  /**
214   * {@inheritDoc}
215   */
216  @Override()
217  public boolean supportsPropertiesFile()
218  {
219    return true;
220  }
221
222
223
224  /**
225   * {@inheritDoc}
226   */
227  @Override()
228  protected boolean supportsOutputFile()
229  {
230    return true;
231  }
232
233
234
235  /**
236   * {@inheritDoc}
237   */
238  @Override()
239  protected boolean supportsAuthentication()
240  {
241    return true;
242  }
243
244
245
246  /**
247   * {@inheritDoc}
248   */
249  @Override()
250  protected boolean defaultToPromptForBindPassword()
251  {
252    return true;
253  }
254
255
256
257  /**
258   * {@inheritDoc}
259   */
260  @Override()
261  protected boolean supportsSASLHelp()
262  {
263    return true;
264  }
265
266
267
268  /**
269   * {@inheritDoc}
270   */
271  @Override()
272  protected boolean includeAlternateLongIdentifiers()
273  {
274    return true;
275  }
276
277
278
279  /**
280   * {@inheritDoc}
281   */
282  @Override()
283  protected boolean logToolInvocationByDefault()
284  {
285    return true;
286  }
287
288
289
290  /**
291   * {@inheritDoc}
292   */
293  @Override()
294  public void addNonLDAPArguments(final ArgumentParser parser)
295         throws ArgumentException
296  {
297    // Create the authentication ID argument, which will identify the target
298    // user.
299    authenticationID = new StringArgument(null, "authID", true, 1,
300         INFO_GEN_TOTP_SECRET_PLACEHOLDER_AUTH_ID.get(),
301         INFO_GEN_TOTP_SECRET_DESCRIPTION_AUTH_ID.get());
302    authenticationID.addLongIdentifier("authenticationID", true);
303    authenticationID.addLongIdentifier("auth-id", true);
304    authenticationID.addLongIdentifier("authentication-id", true);
305    parser.addArgument(authenticationID);
306
307
308    // Create the arguments that may be used to obtain the static password for
309    // the target user.
310    userPassword = new StringArgument(null, "userPassword", false, 1,
311         INFO_GEN_TOTP_SECRET_PLACEHOLDER_USER_PW.get(),
312         INFO_GEN_TOTP_SECRET_DESCRIPTION_USER_PW.get(
313              authenticationID.getIdentifierString()));
314    userPassword.setSensitive(true);
315    userPassword.addLongIdentifier("user-password", true);
316    parser.addArgument(userPassword);
317
318    userPasswordFile = new FileArgument(null, "userPasswordFile", false, 1,
319         null,
320         INFO_GEN_TOTP_SECRET_DESCRIPTION_USER_PW_FILE.get(
321              authenticationID.getIdentifierString()),
322         true, true, true, false);
323    userPasswordFile.addLongIdentifier("user-password-file", true);
324    parser.addArgument(userPasswordFile);
325
326    promptForUserPassword = new BooleanArgument(null, "promptForUserPassword",
327         INFO_GEN_TOTP_SECRET_DESCRIPTION_PROMPT_FOR_USER_PW.get(
328              authenticationID.getIdentifierString()));
329    promptForUserPassword.addLongIdentifier("prompt-for-user-password", true);
330    parser.addArgument(promptForUserPassword);
331
332
333    // Create the arguments that may be used to revoke shared secrets rather
334    // than generate them.
335    revoke = new StringArgument(null, "revoke", false, 1,
336         INFO_GEN_TOTP_SECRET_PLACEHOLDER_SECRET.get(),
337         INFO_GEN_TOTP_SECRET_DESCRIPTION_REVOKE.get());
338    parser.addArgument(revoke);
339
340    revokeAll = new BooleanArgument(null, "revokeAll", 1,
341         INFO_GEN_TOTP_SECRET_DESCRIPTION_REVOKE_ALL.get());
342    revokeAll.addLongIdentifier("revoke-all", true);
343    parser.addArgument(revokeAll);
344
345
346    // At most one of the userPassword, userPasswordFile, and
347    // promptForUserPassword arguments must be present.
348    parser.addExclusiveArgumentSet(userPassword, userPasswordFile,
349         promptForUserPassword);
350
351
352    // If any of the userPassword, userPasswordFile, or promptForUserPassword
353    // arguments is present, then the authenticationID argument must also be
354    // present.
355    parser.addDependentArgumentSet(userPassword, authenticationID);
356    parser.addDependentArgumentSet(userPasswordFile, authenticationID);
357    parser.addDependentArgumentSet(promptForUserPassword, authenticationID);
358
359
360    // At most one of the revoke and revokeAll arguments may be provided.
361    parser.addExclusiveArgumentSet(revoke, revokeAll);
362  }
363
364
365
366  /**
367   * {@inheritDoc}
368   */
369  @Override()
370  public ResultCode doToolProcessing()
371  {
372    // Establish a connection to the Directory Server.
373    final LDAPConnection conn;
374    try
375    {
376      conn = getConnection();
377    }
378    catch (final LDAPException le)
379    {
380      Debug.debugException(le);
381      wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
382           ERR_GEN_TOTP_SECRET_CANNOT_CONNECT.get(
383                StaticUtils.getExceptionMessage(le)));
384      return le.getResultCode();
385    }
386
387    try
388    {
389      // Get the authentication ID and static password to include in the
390      // request.
391      final String authID = authenticationID.getValue();
392
393      final byte[] staticPassword;
394      if (userPassword.isPresent())
395      {
396        staticPassword = StaticUtils.getBytes(userPassword.getValue());
397      }
398      else if (userPasswordFile.isPresent())
399      {
400        BufferedReader reader = null;
401        try
402        {
403          reader =
404               new BufferedReader(new FileReader(userPasswordFile.getValue()));
405          staticPassword = StaticUtils.getBytes(reader.readLine());
406        }
407        catch (final Exception e)
408        {
409          Debug.debugException(e);
410          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
411               ERR_GEN_TOTP_SECRET_CANNOT_READ_PW_FROM_FILE.get(
412                    userPasswordFile.getValue().getAbsolutePath(),
413                    StaticUtils.getExceptionMessage(e)));
414          return ResultCode.LOCAL_ERROR;
415        }
416        finally
417        {
418          if (reader != null)
419          {
420            try
421            {
422              reader.close();
423            }
424            catch (final Exception e)
425            {
426              Debug.debugException(e);
427            }
428          }
429        }
430      }
431      else if (promptForUserPassword.isPresent())
432      {
433        try
434        {
435          getOut().print(INFO_GEN_TOTP_SECRET_ENTER_PW.get(authID));
436          staticPassword = PasswordReader.readPassword();
437        }
438        catch (final Exception e)
439        {
440          Debug.debugException(e);
441          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
442               ERR_GEN_TOTP_SECRET_CANNOT_READ_PW_FROM_STDIN.get(
443                    StaticUtils.getExceptionMessage(e)));
444          return ResultCode.LOCAL_ERROR;
445        }
446      }
447      else
448      {
449        staticPassword = null;
450      }
451
452
453      // Create and send the appropriate request based on whether we should
454      // generate or revoke a TOTP shared secret.
455      ExtendedResult result;
456      if (revoke.isPresent())
457      {
458        final RevokeTOTPSharedSecretExtendedRequest request =
459             new RevokeTOTPSharedSecretExtendedRequest(authID, staticPassword,
460                  revoke.getValue());
461        try
462        {
463          result = conn.processExtendedOperation(request);
464        }
465        catch (final LDAPException le)
466        {
467          Debug.debugException(le);
468          result = new ExtendedResult(le);
469        }
470
471        if (result.getResultCode() == ResultCode.SUCCESS)
472        {
473          wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
474               INFO_GEN_TOTP_SECRET_REVOKE_SUCCESS.get(revoke.getValue()));
475        }
476        else
477        {
478          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
479               ERR_GEN_TOTP_SECRET_REVOKE_FAILURE.get(revoke.getValue()));
480        }
481      }
482      else if (revokeAll.isPresent())
483      {
484        final RevokeTOTPSharedSecretExtendedRequest request =
485             new RevokeTOTPSharedSecretExtendedRequest(authID, staticPassword,
486                  null);
487        try
488        {
489          result = conn.processExtendedOperation(request);
490        }
491        catch (final LDAPException le)
492        {
493          Debug.debugException(le);
494          result = new ExtendedResult(le);
495        }
496
497        if (result.getResultCode() == ResultCode.SUCCESS)
498        {
499          wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
500               INFO_GEN_TOTP_SECRET_REVOKE_ALL_SUCCESS.get());
501        }
502        else
503        {
504          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
505               ERR_GEN_TOTP_SECRET_REVOKE_ALL_FAILURE.get());
506        }
507      }
508      else
509      {
510        final GenerateTOTPSharedSecretExtendedRequest request =
511             new GenerateTOTPSharedSecretExtendedRequest(authID,
512                  staticPassword);
513        try
514        {
515          result = conn.processExtendedOperation(request);
516        }
517        catch (final LDAPException le)
518        {
519          Debug.debugException(le);
520          result = new ExtendedResult(le);
521        }
522
523        if (result.getResultCode() == ResultCode.SUCCESS)
524        {
525          wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
526               INFO_GEN_TOTP_SECRET_GEN_SUCCESS.get(
527                    ((GenerateTOTPSharedSecretExtendedResult) result).
528                         getTOTPSharedSecret()));
529        }
530        else
531        {
532          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
533               ERR_GEN_TOTP_SECRET_GEN_FAILURE.get());
534        }
535      }
536
537
538      // If the result is a failure result, then present any additional details
539      // to the user.
540      if (result.getResultCode() != ResultCode.SUCCESS)
541      {
542        wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
543             ERR_GEN_TOTP_SECRET_RESULT_CODE.get(
544                  String.valueOf(result.getResultCode())));
545
546        final String diagnosticMessage = result.getDiagnosticMessage();
547        if (diagnosticMessage != null)
548        {
549          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
550               ERR_GEN_TOTP_SECRET_DIAGNOSTIC_MESSAGE.get(diagnosticMessage));
551        }
552
553        final String matchedDN = result.getMatchedDN();
554        if (matchedDN != null)
555        {
556          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
557               ERR_GEN_TOTP_SECRET_MATCHED_DN.get(matchedDN));
558        }
559
560        for (final String referralURL : result.getReferralURLs())
561        {
562          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
563               ERR_GEN_TOTP_SECRET_REFERRAL_URL.get(referralURL));
564        }
565      }
566
567      return result.getResultCode();
568    }
569    finally
570    {
571      conn.close();
572    }
573  }
574
575
576
577  /**
578   * {@inheritDoc}
579   */
580  @Override()
581  public LinkedHashMap<String[],String> getExampleUsages()
582  {
583    final LinkedHashMap<String[],String> examples =
584         new LinkedHashMap<String[],String>(2);
585
586    examples.put(
587         new String[]
588         {
589           "--hostname", "ds.example.com",
590           "--port", "389",
591           "--authID", "u:john.doe",
592           "--promptForUserPassword",
593         },
594         INFO_GEN_TOTP_SECRET_GEN_EXAMPLE.get());
595
596    examples.put(
597         new String[]
598         {
599           "--hostname", "ds.example.com",
600           "--port", "389",
601           "--authID", "u:john.doe",
602           "--userPasswordFile", "password.txt",
603           "--revokeAll"
604         },
605         INFO_GEN_TOTP_SECRET_REVOKE_ALL_EXAMPLE.get());
606
607    return examples;
608  }
609}