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