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