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