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