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 logToolInvocationByDefault()
282  {
283    return true;
284  }
285
286
287
288  /**
289   * {@inheritDoc}
290   */
291  @Override()
292  public void addNonLDAPArguments(final ArgumentParser parser)
293         throws ArgumentException
294  {
295    // Create the authentication ID argument, which will identify the target
296    // user.
297    authenticationID = new StringArgument(null, "authID", true, 1,
298         INFO_GEN_TOTP_SECRET_PLACEHOLDER_AUTH_ID.get(),
299         INFO_GEN_TOTP_SECRET_DESCRIPTION_AUTH_ID.get());
300    authenticationID.addLongIdentifier("authenticationID", true);
301    authenticationID.addLongIdentifier("auth-id", true);
302    authenticationID.addLongIdentifier("authentication-id", true);
303    parser.addArgument(authenticationID);
304
305
306    // Create the arguments that may be used to obtain the static password for
307    // the target user.
308    userPassword = new StringArgument(null, "userPassword", false, 1,
309         INFO_GEN_TOTP_SECRET_PLACEHOLDER_USER_PW.get(),
310         INFO_GEN_TOTP_SECRET_DESCRIPTION_USER_PW.get(
311              authenticationID.getIdentifierString()));
312    userPassword.setSensitive(true);
313    userPassword.addLongIdentifier("user-password", true);
314    parser.addArgument(userPassword);
315
316    userPasswordFile = new FileArgument(null, "userPasswordFile", false, 1,
317         null,
318         INFO_GEN_TOTP_SECRET_DESCRIPTION_USER_PW_FILE.get(
319              authenticationID.getIdentifierString()),
320         true, true, true, false);
321    userPasswordFile.addLongIdentifier("user-password-file", true);
322    parser.addArgument(userPasswordFile);
323
324    promptForUserPassword = new BooleanArgument(null, "promptForUserPassword",
325         INFO_GEN_TOTP_SECRET_DESCRIPTION_PROMPT_FOR_USER_PW.get(
326              authenticationID.getIdentifierString()));
327    promptForUserPassword.addLongIdentifier("prompt-for-user-password", true);
328    parser.addArgument(promptForUserPassword);
329
330
331    // Create the arguments that may be used to revoke shared secrets rather
332    // than generate them.
333    revoke = new StringArgument(null, "revoke", false, 1,
334         INFO_GEN_TOTP_SECRET_PLACEHOLDER_SECRET.get(),
335         INFO_GEN_TOTP_SECRET_DESCRIPTION_REVOKE.get());
336    parser.addArgument(revoke);
337
338    revokeAll = new BooleanArgument(null, "revokeAll", 1,
339         INFO_GEN_TOTP_SECRET_DESCRIPTION_REVOKE_ALL.get());
340    revokeAll.addLongIdentifier("revoke-all", true);
341    parser.addArgument(revokeAll);
342
343
344    // At most one of the userPassword, userPasswordFile, and
345    // promptForUserPassword arguments must be present.
346    parser.addExclusiveArgumentSet(userPassword, userPasswordFile,
347         promptForUserPassword);
348
349
350    // If any of the userPassword, userPasswordFile, or promptForUserPassword
351    // arguments is present, then the authenticationID argument must also be
352    // present.
353    parser.addDependentArgumentSet(userPassword, authenticationID);
354    parser.addDependentArgumentSet(userPasswordFile, authenticationID);
355    parser.addDependentArgumentSet(promptForUserPassword, authenticationID);
356
357
358    // At most one of the revoke and revokeAll arguments may be provided.
359    parser.addExclusiveArgumentSet(revoke, revokeAll);
360  }
361
362
363
364  /**
365   * {@inheritDoc}
366   */
367  @Override()
368  public ResultCode doToolProcessing()
369  {
370    // Establish a connection to the Directory Server.
371    final LDAPConnection conn;
372    try
373    {
374      conn = getConnection();
375    }
376    catch (final LDAPException le)
377    {
378      Debug.debugException(le);
379      wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
380           ERR_GEN_TOTP_SECRET_CANNOT_CONNECT.get(
381                StaticUtils.getExceptionMessage(le)));
382      return le.getResultCode();
383    }
384
385    try
386    {
387      // Get the authentication ID and static password to include in the
388      // request.
389      final String authID = authenticationID.getValue();
390
391      final byte[] staticPassword;
392      if (userPassword.isPresent())
393      {
394        staticPassword = StaticUtils.getBytes(userPassword.getValue());
395      }
396      else if (userPasswordFile.isPresent())
397      {
398        try
399        {
400          final char[] pwChars = getPasswordFileReader().readPassword(
401               userPasswordFile.getValue());
402          staticPassword = StaticUtils.getBytes(new String(pwChars));
403        }
404        catch (final Exception e)
405        {
406          Debug.debugException(e);
407          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
408               ERR_GEN_TOTP_SECRET_CANNOT_READ_PW_FROM_FILE.get(
409                    userPasswordFile.getValue().getAbsolutePath(),
410                    StaticUtils.getExceptionMessage(e)));
411          return ResultCode.LOCAL_ERROR;
412        }
413      }
414      else if (promptForUserPassword.isPresent())
415      {
416        try
417        {
418          getOut().print(INFO_GEN_TOTP_SECRET_ENTER_PW.get(authID));
419          staticPassword = PasswordReader.readPassword();
420        }
421        catch (final Exception e)
422        {
423          Debug.debugException(e);
424          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
425               ERR_GEN_TOTP_SECRET_CANNOT_READ_PW_FROM_STDIN.get(
426                    StaticUtils.getExceptionMessage(e)));
427          return ResultCode.LOCAL_ERROR;
428        }
429      }
430      else
431      {
432        staticPassword = null;
433      }
434
435
436      // Create and send the appropriate request based on whether we should
437      // generate or revoke a TOTP shared secret.
438      ExtendedResult result;
439      if (revoke.isPresent())
440      {
441        final RevokeTOTPSharedSecretExtendedRequest request =
442             new RevokeTOTPSharedSecretExtendedRequest(authID, staticPassword,
443                  revoke.getValue());
444        try
445        {
446          result = conn.processExtendedOperation(request);
447        }
448        catch (final LDAPException le)
449        {
450          Debug.debugException(le);
451          result = new ExtendedResult(le);
452        }
453
454        if (result.getResultCode() == ResultCode.SUCCESS)
455        {
456          wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
457               INFO_GEN_TOTP_SECRET_REVOKE_SUCCESS.get(revoke.getValue()));
458        }
459        else
460        {
461          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
462               ERR_GEN_TOTP_SECRET_REVOKE_FAILURE.get(revoke.getValue()));
463        }
464      }
465      else if (revokeAll.isPresent())
466      {
467        final RevokeTOTPSharedSecretExtendedRequest request =
468             new RevokeTOTPSharedSecretExtendedRequest(authID, staticPassword,
469                  null);
470        try
471        {
472          result = conn.processExtendedOperation(request);
473        }
474        catch (final LDAPException le)
475        {
476          Debug.debugException(le);
477          result = new ExtendedResult(le);
478        }
479
480        if (result.getResultCode() == ResultCode.SUCCESS)
481        {
482          wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
483               INFO_GEN_TOTP_SECRET_REVOKE_ALL_SUCCESS.get());
484        }
485        else
486        {
487          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
488               ERR_GEN_TOTP_SECRET_REVOKE_ALL_FAILURE.get());
489        }
490      }
491      else
492      {
493        final GenerateTOTPSharedSecretExtendedRequest request =
494             new GenerateTOTPSharedSecretExtendedRequest(authID,
495                  staticPassword);
496        try
497        {
498          result = conn.processExtendedOperation(request);
499        }
500        catch (final LDAPException le)
501        {
502          Debug.debugException(le);
503          result = new ExtendedResult(le);
504        }
505
506        if (result.getResultCode() == ResultCode.SUCCESS)
507        {
508          wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
509               INFO_GEN_TOTP_SECRET_GEN_SUCCESS.get(
510                    ((GenerateTOTPSharedSecretExtendedResult) result).
511                         getTOTPSharedSecret()));
512        }
513        else
514        {
515          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
516               ERR_GEN_TOTP_SECRET_GEN_FAILURE.get());
517        }
518      }
519
520
521      // If the result is a failure result, then present any additional details
522      // to the user.
523      if (result.getResultCode() != ResultCode.SUCCESS)
524      {
525        wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
526             ERR_GEN_TOTP_SECRET_RESULT_CODE.get(
527                  String.valueOf(result.getResultCode())));
528
529        final String diagnosticMessage = result.getDiagnosticMessage();
530        if (diagnosticMessage != null)
531        {
532          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
533               ERR_GEN_TOTP_SECRET_DIAGNOSTIC_MESSAGE.get(diagnosticMessage));
534        }
535
536        final String matchedDN = result.getMatchedDN();
537        if (matchedDN != null)
538        {
539          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
540               ERR_GEN_TOTP_SECRET_MATCHED_DN.get(matchedDN));
541        }
542
543        for (final String referralURL : result.getReferralURLs())
544        {
545          wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS,
546               ERR_GEN_TOTP_SECRET_REFERRAL_URL.get(referralURL));
547        }
548      }
549
550      return result.getResultCode();
551    }
552    finally
553    {
554      conn.close();
555    }
556  }
557
558
559
560  /**
561   * {@inheritDoc}
562   */
563  @Override()
564  public LinkedHashMap<String[],String> getExampleUsages()
565  {
566    final LinkedHashMap<String[],String> examples =
567         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
568
569    examples.put(
570         new String[]
571         {
572           "--hostname", "ds.example.com",
573           "--port", "389",
574           "--authID", "u:john.doe",
575           "--promptForUserPassword",
576         },
577         INFO_GEN_TOTP_SECRET_GEN_EXAMPLE.get());
578
579    examples.put(
580         new String[]
581         {
582           "--hostname", "ds.example.com",
583           "--port", "389",
584           "--authID", "u:john.doe",
585           "--userPasswordFile", "password.txt",
586           "--revokeAll"
587         },
588         INFO_GEN_TOTP_SECRET_REVOKE_ALL_EXAMPLE.get());
589
590    return examples;
591  }
592}