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