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