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