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