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}