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