001/* 002 * Copyright 2016-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2016-2023 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-2023 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; 037 038 039 040import java.io.OutputStream; 041import java.io.Serializable; 042import java.util.LinkedHashMap; 043 044import com.unboundid.ldap.sdk.ExtendedResult; 045import com.unboundid.ldap.sdk.LDAPConnection; 046import com.unboundid.ldap.sdk.LDAPException; 047import com.unboundid.ldap.sdk.ResultCode; 048import com.unboundid.ldap.sdk.Version; 049import com.unboundid.ldap.sdk.unboundidds.extensions. 050 DeregisterYubiKeyOTPDeviceExtendedRequest; 051import com.unboundid.ldap.sdk.unboundidds.extensions. 052 RegisterYubiKeyOTPDeviceExtendedRequest; 053import com.unboundid.util.Debug; 054import com.unboundid.util.LDAPCommandLineTool; 055import com.unboundid.util.NotNull; 056import com.unboundid.util.Nullable; 057import com.unboundid.util.PasswordReader; 058import com.unboundid.util.StaticUtils; 059import com.unboundid.util.ThreadSafety; 060import com.unboundid.util.ThreadSafetyLevel; 061import com.unboundid.util.args.ArgumentException; 062import com.unboundid.util.args.ArgumentParser; 063import com.unboundid.util.args.BooleanArgument; 064import com.unboundid.util.args.FileArgument; 065import com.unboundid.util.args.StringArgument; 066 067import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*; 068 069 070 071/** 072 * This class provides a utility that may be used to register a YubiKey OTP 073 * device for a specified user so that it may be used to authenticate that user. 074 * Alternately, it may be used to deregister one or all of the YubiKey OTP 075 * devices that have been registered for the user. 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 RegisterYubiKeyOTPDevice 089 extends LDAPCommandLineTool 090 implements Serializable 091{ 092 /** 093 * The serial version UID for this serializable class. 094 */ 095 private static final long serialVersionUID = 5705120716566064832L; 096 097 098 099 // Indicates that the tool should deregister one or all of the YubiKey OTP 100 // devices for the user rather than registering a new device. 101 @Nullable private BooleanArgument deregister; 102 103 // Indicates that the tool should interactively prompt for the static password 104 // for the user for whom the YubiKey OTP device is to be registered or 105 // deregistered. 106 @Nullable private BooleanArgument promptForUserPassword; 107 108 // The path to a file containing the static password for the user for whom the 109 // YubiKey OTP device is to be registered or deregistered. 110 @Nullable private FileArgument userPasswordFile; 111 112 // The username for the user for whom the YubiKey OTP device is to be 113 // registered or deregistered. 114 @Nullable private StringArgument authenticationID; 115 116 // The static password for the user for whom the YubiKey OTP device is to be 117 // registered or deregistered. 118 @Nullable private StringArgument userPassword; 119 120 // A one-time password generated by the YubiKey OTP device to be registered 121 // or deregistered. 122 @Nullable private StringArgument otp; 123 124 125 126 /** 127 * Parse the provided command line arguments and perform the appropriate 128 * processing. 129 * 130 * @param args The command line arguments provided to this program. 131 */ 132 public static void main(@NotNull final String... args) 133 { 134 final ResultCode resultCode = main(args, System.out, System.err); 135 if (resultCode != ResultCode.SUCCESS) 136 { 137 System.exit(resultCode.intValue()); 138 } 139 } 140 141 142 143 /** 144 * Parse the provided command line arguments and perform the appropriate 145 * processing. 146 * 147 * @param args The command line arguments provided to this program. 148 * @param outStream The output stream to which standard out should be 149 * written. It may be {@code null} if output should be 150 * suppressed. 151 * @param errStream The output stream to which standard error should be 152 * written. It may be {@code null} if error messages 153 * should be suppressed. 154 * 155 * @return A result code indicating whether the processing was successful. 156 */ 157 @NotNull() 158 public static ResultCode main(@NotNull final String[] args, 159 @Nullable final OutputStream outStream, 160 @Nullable final OutputStream errStream) 161 { 162 final RegisterYubiKeyOTPDevice tool = 163 new RegisterYubiKeyOTPDevice(outStream, errStream); 164 return tool.runTool(args); 165 } 166 167 168 169 /** 170 * Creates a new instance of this tool. 171 * 172 * @param outStream The output stream to which standard out should be 173 * written. It may be {@code null} if output should be 174 * suppressed. 175 * @param errStream The output stream to which standard error should be 176 * written. It may be {@code null} if error messages 177 * should be suppressed. 178 */ 179 public RegisterYubiKeyOTPDevice(@Nullable final OutputStream outStream, 180 @Nullable final OutputStream errStream) 181 { 182 super(outStream, errStream); 183 184 deregister = null; 185 otp = null; 186 promptForUserPassword = null; 187 userPasswordFile = null; 188 authenticationID = null; 189 userPassword = null; 190 } 191 192 193 194 /** 195 * {@inheritDoc} 196 */ 197 @Override() 198 @NotNull() 199 public String getToolName() 200 { 201 return "register-yubikey-otp-device"; 202 } 203 204 205 206 /** 207 * {@inheritDoc} 208 */ 209 @Override() 210 @NotNull() 211 public String getToolDescription() 212 { 213 return INFO_REGISTER_YUBIKEY_OTP_DEVICE_TOOL_DESCRIPTION.get( 214 UnboundIDYubiKeyOTPBindRequest.UNBOUNDID_YUBIKEY_OTP_MECHANISM_NAME); 215 } 216 217 218 219 /** 220 * {@inheritDoc} 221 */ 222 @Override() 223 @NotNull() 224 public String getToolVersion() 225 { 226 return Version.NUMERIC_VERSION_STRING; 227 } 228 229 230 231 /** 232 * {@inheritDoc} 233 */ 234 @Override() 235 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 236 throws ArgumentException 237 { 238 deregister = new BooleanArgument(null, "deregister", 1, 239 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_DEREGISTER.get("--otp")); 240 deregister.addLongIdentifier("de-register", true); 241 parser.addArgument(deregister); 242 243 otp = new StringArgument(null, "otp", false, 1, 244 INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_OTP.get(), 245 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_OTP.get()); 246 parser.addArgument(otp); 247 248 authenticationID = new StringArgument(null, "authID", false, 1, 249 INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_AUTHID.get(), 250 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_AUTHID.get()); 251 authenticationID.addLongIdentifier("authenticationID", true); 252 authenticationID.addLongIdentifier("auth-id", true); 253 authenticationID.addLongIdentifier("authentication-id", true); 254 parser.addArgument(authenticationID); 255 256 userPassword = new StringArgument(null, "userPassword", false, 1, 257 INFO_REGISTER_YUBIKEY_OTP_DEVICE_PLACEHOLDER_USER_PW.get(), 258 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_USER_PW.get( 259 authenticationID.getIdentifierString())); 260 userPassword.setSensitive(true); 261 userPassword.addLongIdentifier("user-password", true); 262 parser.addArgument(userPassword); 263 264 userPasswordFile = new FileArgument(null, "userPasswordFile", false, 1, 265 null, 266 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_USER_PW_FILE.get( 267 authenticationID.getIdentifierString()), 268 true, true, true, false); 269 userPasswordFile.addLongIdentifier("user-password-file", true); 270 parser.addArgument(userPasswordFile); 271 272 promptForUserPassword = new BooleanArgument(null, "promptForUserPassword", 273 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DESCRIPTION_PROMPT_FOR_USER_PW.get( 274 authenticationID.getIdentifierString())); 275 promptForUserPassword.addLongIdentifier("prompt-for-user-password", true); 276 parser.addArgument(promptForUserPassword); 277 278 279 // At most one of the userPassword, userPasswordFile, and 280 // promptForUserPassword arguments must be present. 281 parser.addExclusiveArgumentSet(userPassword, userPasswordFile, 282 promptForUserPassword); 283 284 // If any of the userPassword, userPasswordFile, or promptForUserPassword 285 // arguments is present, then the authenticationID argument must also be 286 // present. 287 parser.addDependentArgumentSet(userPassword, authenticationID); 288 parser.addDependentArgumentSet(userPasswordFile, authenticationID); 289 parser.addDependentArgumentSet(promptForUserPassword, authenticationID); 290 } 291 292 293 294 /** 295 * {@inheritDoc} 296 */ 297 @Override() 298 public void doExtendedNonLDAPArgumentValidation() 299 throws ArgumentException 300 { 301 // If the deregister argument was not provided, then the otp argument must 302 // have been given. 303 if ((! deregister.isPresent()) && (! otp.isPresent())) 304 { 305 throw new ArgumentException( 306 ERR_REGISTER_YUBIKEY_OTP_DEVICE_NO_OTP_TO_REGISTER.get( 307 otp.getIdentifierString())); 308 } 309 } 310 311 312 313 /** 314 * {@inheritDoc} 315 */ 316 @Override() 317 public boolean supportsInteractiveMode() 318 { 319 return true; 320 } 321 322 323 324 /** 325 * {@inheritDoc} 326 */ 327 @Override() 328 public boolean defaultsToInteractiveMode() 329 { 330 return true; 331 } 332 333 334 335 /** 336 * {@inheritDoc} 337 */ 338 @Override() 339 protected boolean supportsOutputFile() 340 { 341 return true; 342 } 343 344 345 346 /** 347 * {@inheritDoc} 348 */ 349 @Override() 350 protected boolean defaultToPromptForBindPassword() 351 { 352 return true; 353 } 354 355 356 357 /** 358 * Indicates whether this tool supports the use of a properties file for 359 * specifying default values for arguments that aren't specified on the 360 * command line. 361 * 362 * @return {@code true} if this tool supports the use of a properties file 363 * for specifying default values for arguments that aren't specified 364 * on the command line, or {@code false} if not. 365 */ 366 @Override() 367 public boolean supportsPropertiesFile() 368 { 369 return true; 370 } 371 372 373 374 /** 375 * Indicates whether the LDAP-specific arguments should include alternate 376 * versions of all long identifiers that consist of multiple words so that 377 * they are available in both camelCase and dash-separated versions. 378 * 379 * @return {@code true} if this tool should provide multiple versions of 380 * long identifiers for LDAP-specific arguments, or {@code false} if 381 * not. 382 */ 383 @Override() 384 protected boolean includeAlternateLongIdentifiers() 385 { 386 return true; 387 } 388 389 390 391 /** 392 * Indicates whether this tool should provide a command-line argument that 393 * allows for low-level SSL debugging. If this returns {@code true}, then an 394 * "--enableSSLDebugging}" argument will be added that sets the 395 * "javax.net.debug" system property to "all" before attempting any 396 * communication. 397 * 398 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 399 * argument, or {@code false} if not. 400 */ 401 @Override() 402 protected boolean supportsSSLDebugging() 403 { 404 return true; 405 } 406 407 408 409 /** 410 * {@inheritDoc} 411 */ 412 @Override() 413 protected boolean logToolInvocationByDefault() 414 { 415 return true; 416 } 417 418 419 420 /** 421 * {@inheritDoc} 422 */ 423 @Override() 424 @NotNull() 425 public ResultCode doToolProcessing() 426 { 427 // Establish a connection to the Directory Server. 428 final LDAPConnection conn; 429 try 430 { 431 conn = getConnection(); 432 } 433 catch (final LDAPException le) 434 { 435 Debug.debugException(le); 436 wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 437 ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_CONNECT.get( 438 StaticUtils.getExceptionMessage(le))); 439 return le.getResultCode(); 440 } 441 442 try 443 { 444 // Get the authentication ID and static password to include in the 445 // request. 446 final String authID = authenticationID.getValue(); 447 448 final byte[] staticPassword; 449 if (userPassword.isPresent()) 450 { 451 staticPassword = StaticUtils.getBytes(userPassword.getValue()); 452 } 453 else if (userPasswordFile.isPresent()) 454 { 455 try 456 { 457 final char[] pwChars = getPasswordFileReader().readPassword( 458 userPasswordFile.getValue()); 459 staticPassword = StaticUtils.getBytes(new String(pwChars)); 460 } 461 catch (final Exception e) 462 { 463 Debug.debugException(e); 464 wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 465 ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get( 466 StaticUtils.getExceptionMessage(e))); 467 return ResultCode.LOCAL_ERROR; 468 } 469 } 470 else if (promptForUserPassword.isPresent()) 471 { 472 try 473 { 474 getOut().print(INFO_REGISTER_YUBIKEY_OTP_DEVICE_ENTER_PW.get(authID)); 475 staticPassword = PasswordReader.readPassword(); 476 } 477 catch (final Exception e) 478 { 479 Debug.debugException(e); 480 wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 481 ERR_REGISTER_YUBIKEY_OTP_DEVICE_CANNOT_READ_PW.get( 482 StaticUtils.getExceptionMessage(e))); 483 return ResultCode.LOCAL_ERROR; 484 } 485 } 486 else 487 { 488 staticPassword = null; 489 } 490 491 492 // Construct and process the appropriate register or deregister request. 493 if (deregister.isPresent()) 494 { 495 final DeregisterYubiKeyOTPDeviceExtendedRequest r = 496 new DeregisterYubiKeyOTPDeviceExtendedRequest(authID, 497 staticPassword, otp.getValue()); 498 499 ExtendedResult deregisterResult; 500 try 501 { 502 deregisterResult = conn.processExtendedOperation(r); 503 } 504 catch (final LDAPException le) 505 { 506 deregisterResult = new ExtendedResult(le); 507 } 508 509 if (deregisterResult.getResultCode() == ResultCode.SUCCESS) 510 { 511 if (otp.isPresent()) 512 { 513 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 514 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ONE.get( 515 authID)); 516 } 517 else 518 { 519 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 520 INFO_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_SUCCESS_ALL.get( 521 authID)); 522 } 523 return ResultCode.SUCCESS; 524 } 525 else 526 { 527 wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 528 ERR_REGISTER_YUBIKEY_OTP_DEVICE_DEREGISTER_FAILED.get(authID, 529 String.valueOf(deregisterResult))); 530 return deregisterResult.getResultCode(); 531 } 532 } 533 else 534 { 535 final RegisterYubiKeyOTPDeviceExtendedRequest r = 536 new RegisterYubiKeyOTPDeviceExtendedRequest(authID, staticPassword, 537 otp.getValue()); 538 539 ExtendedResult registerResult; 540 try 541 { 542 registerResult = conn.processExtendedOperation(r); 543 } 544 catch (final LDAPException le) 545 { 546 registerResult = new ExtendedResult(le); 547 } 548 549 if (registerResult.getResultCode() == ResultCode.SUCCESS) 550 { 551 wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 552 INFO_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_SUCCESS.get(authID)); 553 return ResultCode.SUCCESS; 554 } 555 else 556 { 557 wrapErr(0, StaticUtils.TERMINAL_WIDTH_COLUMNS, 558 ERR_REGISTER_YUBIKEY_OTP_DEVICE_REGISTER_FAILED.get(authID, 559 String.valueOf(registerResult))); 560 return registerResult.getResultCode(); 561 } 562 } 563 } 564 finally 565 { 566 conn.close(); 567 } 568 } 569 570 571 572 /** 573 * {@inheritDoc} 574 */ 575 @Override() 576 @NotNull() 577 public LinkedHashMap<String[],String> getExampleUsages() 578 { 579 final LinkedHashMap<String[],String> exampleMap = 580 new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); 581 582 String[] args = 583 { 584 "--hostname", "server.example.com", 585 "--port", "389", 586 "--bindDN", "uid=admin,dc=example,dc=com", 587 "--bindPassword", "adminPassword", 588 "--authenticationID", "u:test.user", 589 "--userPassword", "testUserPassword", 590 "--otp", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqr" 591 }; 592 exampleMap.put(args, 593 INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_REGISTER.get()); 594 595 args = new String[] 596 { 597 "--hostname", "server.example.com", 598 "--port", "389", 599 "--bindDN", "uid=admin,dc=example,dc=com", 600 "--bindPassword", "adminPassword", 601 "--deregister", 602 "--authenticationID", "dn:uid=test.user,ou=People,dc=example,dc=com" 603 }; 604 exampleMap.put(args, 605 INFO_REGISTER_YUBIKEY_OTP_DEVICE_EXAMPLE_DEREGISTER.get()); 606 607 return exampleMap; 608 } 609}