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