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