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}