001/* 002 * Copyright 2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 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.BufferedInputStream; 026import java.io.BufferedReader; 027import java.io.ByteArrayInputStream; 028import java.io.File; 029import java.io.FileInputStream; 030import java.io.FileReader; 031import java.io.IOException; 032import java.io.InputStream; 033import java.io.PrintStream; 034import java.lang.reflect.Method; 035import java.security.GeneralSecurityException; 036import java.security.InvalidKeyException; 037import java.util.ArrayList; 038import java.util.Arrays; 039import java.util.Iterator; 040import java.util.List; 041import java.util.logging.Level; 042import java.util.zip.GZIPInputStream; 043 044import com.unboundid.ldap.sdk.LDAPException; 045import com.unboundid.ldap.sdk.ResultCode; 046import com.unboundid.util.AggregateInputStream; 047import com.unboundid.util.ByteStringBuffer; 048import com.unboundid.util.Debug; 049import com.unboundid.util.ObjectPair; 050import com.unboundid.util.PassphraseEncryptedInputStream; 051import com.unboundid.util.PassphraseEncryptedOutputStream; 052import com.unboundid.util.PassphraseEncryptedStreamHeader; 053import com.unboundid.util.PasswordReader; 054import com.unboundid.util.StaticUtils; 055import com.unboundid.util.ThreadSafety; 056import com.unboundid.util.ThreadSafetyLevel; 057import com.unboundid.util.Validator; 058 059import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*; 060 061 062 063/** 064 * This class provides a number of utility methods primarily intended for use 065 * with command-line tools. 066 * <BR> 067 * <BLOCKQUOTE> 068 * <B>NOTE:</B> This class, and other classes within the 069 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 070 * supported for use against Ping Identity, UnboundID, and 071 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 072 * for proprietary functionality or for external specifications that are not 073 * considered stable or mature enough to be guaranteed to work in an 074 * interoperable way with other types of LDAP servers. 075 * </BLOCKQUOTE> 076 */ 077@ThreadSafety(level= ThreadSafetyLevel.NOT_THREADSAFE) 078public final class ToolUtils 079{ 080 /** 081 * The column at which long lines should be wrapped. 082 */ 083 private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; 084 085 086 087 /** 088 * A handle to a method that can be used to get the passphrase for an 089 * encryption settings definition ID if the server code is available. We have 090 * to call this via reflection because the server code may not be available. 091 */ 092 private static final Method GET_PASSPHRASE_FOR_ENCRYPTION_SETTINGS_ID_METHOD; 093 static 094 { 095 Method m = null; 096 097 try 098 { 099 final Class<?> serverStaticUtilsClass = Class.forName( 100 "com.unboundid.directory.server.util.StaticUtils"); 101 m = serverStaticUtilsClass.getMethod( 102 "getPassphraseForEncryptionSettingsID", String.class, 103 PrintStream.class, PrintStream.class); 104 } 105 catch (final Exception e) 106 { 107 // This is fine. It probably just means that the server code isn't 108 // available. 109 Debug.debugException(Level.FINEST, e); 110 } 111 112 GET_PASSPHRASE_FOR_ENCRYPTION_SETTINGS_ID_METHOD = m; 113 } 114 115 116 117 /** 118 * Prevent this utility class from being instantiated. 119 */ 120 private ToolUtils() 121 { 122 // No implementation is required. 123 } 124 125 126 127 /** 128 * Reads an encryption passphrase from the specified file. The file must 129 * contain exactly one line, which must not be empty, and must be comprised 130 * entirely of the encryption passphrase. 131 * 132 * @param f The file from which the passphrase should be read. It must not 133 * be {@code null}. 134 * 135 * @return The encryption passphrase read from the specified file. 136 * 137 * @throws LDAPException If a problem occurs while attempting to read the 138 * encryption passphrase. 139 */ 140 public static String readEncryptionPassphraseFromFile(final File f) 141 throws LDAPException 142 { 143 Validator.ensureTrue((f != null), 144 "ToolUtils.readEncryptionPassphraseFromFile.f must not be null."); 145 146 if (! f.exists()) 147 { 148 throw new LDAPException(ResultCode.PARAM_ERROR, 149 ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_MISSING.get(f.getAbsolutePath())); 150 } 151 152 if (! f.isFile()) 153 { 154 throw new LDAPException(ResultCode.PARAM_ERROR, 155 ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_NOT_FILE.get(f.getAbsolutePath())); 156 } 157 158 try (final FileReader fileReader = new FileReader(f); 159 final BufferedReader bufferedReader = new BufferedReader(fileReader)) 160 { 161 final String encryptionPassphrase = bufferedReader.readLine(); 162 if (encryptionPassphrase == null) 163 { 164 throw new LDAPException(ResultCode.PARAM_ERROR, 165 ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_EMPTY.get(f.getAbsolutePath())); 166 } 167 else if (bufferedReader.readLine() != null) 168 { 169 throw new LDAPException(ResultCode.PARAM_ERROR, 170 ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_MULTIPLE_LINES.get( 171 f.getAbsolutePath())); 172 } 173 else if (encryptionPassphrase.isEmpty()) 174 { 175 throw new LDAPException(ResultCode.PARAM_ERROR, 176 ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_EMPTY.get(f.getAbsolutePath())); 177 } 178 179 return encryptionPassphrase; 180 } 181 catch (final LDAPException e) 182 { 183 Debug.debugException(e); 184 throw e; 185 } 186 catch (final Exception e) 187 { 188 Debug.debugException(e); 189 throw new LDAPException(ResultCode.LOCAL_ERROR, 190 ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_READ_ERROR.get( 191 f.getAbsolutePath(), StaticUtils.getExceptionMessage(e))); 192 } 193 } 194 195 196 197 /** 198 * Interactively prompts the user for an encryption passphrase. 199 * 200 * @param allowEmpty Indicates whether the encryption passphrase is allowed 201 * to be empty. If this is {@code false}, then the user 202 * will be re-prompted for the passphrase if the value 203 * they enter is empty. 204 * @param confirm Indicates whether the user will asked to confirm the 205 * passphrase. If this is {@code true}, then the user 206 * will have to enter the same passphrase twice. If this 207 * is {@code false}, then the user will only be prompted 208 * once. 209 * @param out The {@code PrintStream} that will be used for standard 210 * output. It must not be {@code null}. 211 * @param err The {@code PrintStream} that will be used for standard 212 * error. It must not be {@code null}. 213 * 214 * @return The encryption passphrase provided by the user. 215 * 216 * @throws LDAPException If a problem is encountered while trying to obtain 217 * the passphrase from the user. 218 */ 219 public static String promptForEncryptionPassphrase(final boolean allowEmpty, 220 final boolean confirm, 221 final PrintStream out, 222 final PrintStream err) 223 throws LDAPException 224 { 225 return promptForEncryptionPassphrase(allowEmpty, confirm, 226 INFO_TOOL_UTILS_ENCRYPTION_PW_PROMPT.get(), 227 INFO_TOOL_UTILS_ENCRYPTION_PW_CONFIRM.get(), out, err); 228 } 229 230 231 232 /** 233 * Interactively prompts the user for an encryption passphrase. 234 * 235 * @param allowEmpty Indicates whether the encryption passphrase is 236 * allowed to be empty. If this is {@code false}, then 237 * the user will be re-prompted for the passphrase if 238 * the value they enter is empty. 239 * @param confirm Indicates whether the user will asked to confirm the 240 * passphrase. If this is {@code true}, then the user 241 * will have to enter the same passphrase twice. If 242 * this is {@code false}, then the user will only be 243 * prompted once. 244 * @param initialPrompt The initial prompt that will be presented to the 245 * user. It must not be {@code null} or empty. 246 * @param confirmPrompt The prompt that will be presented to the user when 247 * asked to confirm the passphrase. It may be 248 * {@code null} only if {@code confirm} is 249 * {@code false}. 250 * @param out The {@code PrintStream} that will be used for 251 * standard output. It must not be {@code null}. 252 * @param err The {@code PrintStream} that will be used for 253 * standard error. It must not be {@code null}. 254 * 255 * @return The encryption passphrase provided by the user. 256 * 257 * @throws LDAPException If a problem is encountered while trying to obtain 258 * the passphrase from the user. 259 */ 260 public static String promptForEncryptionPassphrase(final boolean allowEmpty, 261 final boolean confirm, 262 final CharSequence initialPrompt, 263 final CharSequence confirmPrompt, 264 final PrintStream out, final PrintStream err) 265 throws LDAPException 266 { 267 Validator.ensureTrue( 268 ((initialPrompt != null) && (initialPrompt.length() > 0)), 269 "TestUtils.promptForEncryptionPassphrase.initialPrompt must not be " + 270 "null or empty."); 271 Validator.ensureTrue( 272 ((! confirm) || 273 ((confirmPrompt != null) && (confirmPrompt.length() > 0))), 274 "TestUtils.promptForEncryptionPassphrase.confirmPrompt must not be " + 275 "null or empty when confirm is true."); 276 Validator.ensureTrue((out != null), 277 "ToolUtils.promptForEncryptionPassphrase.out must not be null"); 278 Validator.ensureTrue((err != null), 279 "ToolUtils.promptForEncryptionPassphrase.err must not be null"); 280 281 while (true) 282 { 283 char[] passphraseChars = null; 284 char[] confirmChars = null; 285 286 try 287 { 288 wrapPrompt(initialPrompt, true, out); 289 290 passphraseChars = PasswordReader.readPasswordChars(); 291 if ((passphraseChars == null) || (passphraseChars.length == 0)) 292 { 293 if (allowEmpty) 294 { 295 passphraseChars = StaticUtils.NO_CHARS; 296 } 297 else 298 { 299 wrap(ERR_TOOL_UTILS_ENCRYPTION_PW_EMPTY.get(), err); 300 err.println(); 301 continue; 302 } 303 } 304 305 if (confirm) 306 { 307 wrapPrompt(confirmPrompt, true, out); 308 309 confirmChars = PasswordReader.readPasswordChars(); 310 if ((confirmChars == null) || 311 (! Arrays.equals(passphraseChars, confirmChars))) 312 { 313 wrap(ERR_TOOL_UTILS_ENCRYPTION_PW_MISMATCH.get(), err); 314 err.println(); 315 continue; 316 } 317 } 318 319 return new String(passphraseChars); 320 } 321 finally 322 { 323 if (passphraseChars != null) 324 { 325 Arrays.fill(passphraseChars, '\u0000'); 326 } 327 328 if (confirmChars != null) 329 { 330 Arrays.fill(confirmChars, '\u0000'); 331 } 332 } 333 } 334 } 335 336 337 338 /** 339 * Writes a wrapped version of the provided message to the given stream. 340 * 341 * @param message The message to be written. If it is {@code null} or 342 * empty, then an empty line will be printed. 343 * @param out The {@code PrintStream} that should be used to write the 344 * provided message. 345 */ 346 public static void wrap(final CharSequence message, final PrintStream out) 347 { 348 Validator.ensureTrue((out != null), "ToolUtils.wrap.out must not be null."); 349 350 if ((message == null) || (message.length() == 0)) 351 { 352 out.println(); 353 return; 354 } 355 356 for (final String line : 357 StaticUtils.wrapLine(message.toString(), WRAP_COLUMN)) 358 { 359 out.println(line); 360 } 361 } 362 363 364 365 /** 366 * Wraps the provided prompt such that every line except the last will be 367 * followed by a newline, but the last line will not be followed by a newline. 368 * 369 * @param prompt The prompt to be wrapped. It must not be 370 * {@code null} or empty. 371 * @param ensureTrailingSpace Indicates whether to ensure that there is a 372 * trailing space after the end of the prompt. 373 * @param out The {@code PrintStream} to which the prompt 374 * should be written. It must not be 375 * {@code null}. 376 */ 377 public static void wrapPrompt(final CharSequence prompt, 378 final boolean ensureTrailingSpace, 379 final PrintStream out) 380 { 381 Validator.ensureTrue(((prompt != null) && (prompt.length() > 0)), 382 "ToolUtils.wrapPrompt.prompt must not be null or empty."); 383 Validator.ensureTrue((out != null), 384 "ToolUtils.wrapPrompt.out must not be null."); 385 386 String promptString = prompt.toString(); 387 if (ensureTrailingSpace && (! promptString.endsWith(" "))) 388 { 389 promptString += ' '; 390 } 391 392 final List<String> lines = StaticUtils.wrapLine(promptString, WRAP_COLUMN); 393 final Iterator<String> iterator = lines.iterator(); 394 while (iterator.hasNext()) 395 { 396 final String line = iterator.next(); 397 if (iterator.hasNext()) 398 { 399 out.println(line); 400 } 401 else 402 { 403 out.print(line); 404 } 405 } 406 } 407 408 409 410 /** 411 * Retrieves an input stream that can be used to read data from the specified 412 * list of files. It will handle the possibility that any or all of the LDIF 413 * files are encrypted and/or compressed. 414 * 415 * @param ldifFiles The list of LDIF files from which the data 416 * is to be read. It must not be {@code null} 417 * or empty. 418 * @param encryptionPassphrase The passphrase that should be used to access 419 * encrypted LDIF files. It may be {@code null} 420 * if the user should be interactively prompted 421 * for the passphrase if any of the files is 422 * encrypted. 423 * @param out The print stream to use for standard output. 424 * It must not be {@code null}. 425 * @param err The print stream to use for standard error. 426 * It must not be {@code null}. 427 * 428 * @return An {@code ObjectPair} whose first element is an input stream that 429 * can be used to read data from the specified list of files, and 430 * whose second element is a possibly-{@code null} passphrase that 431 * is used to encrypt the input data. 432 * 433 * @throws IOException If a problem is encountered while attempting to get 434 * the input stream for reading the data. 435 */ 436 public static ObjectPair<InputStream,String> getInputStreamForLDIFFiles( 437 final List<File> ldifFiles, 438 final String encryptionPassphrase, final PrintStream out, 439 final PrintStream err) 440 throws IOException 441 { 442 Validator.ensureTrue(((ldifFiles != null) && (! ldifFiles.isEmpty())), 443 "ToolUtils.getInputStreamForLDIFFiles.ldifFiles must not be null or " + 444 "empty."); 445 Validator.ensureTrue((out != null), 446 "ToolUtils.getInputStreamForLDIFFiles.out must not be null"); 447 Validator.ensureTrue((err != null), 448 "ToolUtils.getInputStreamForLDIFFiles.err must not be null"); 449 450 451 boolean createdSuccessfully = false; 452 final ArrayList<InputStream> inputStreams = 453 new ArrayList<>(ldifFiles.size() * 2); 454 455 try 456 { 457 byte[] twoEOLs = null; 458 String passphrase = encryptionPassphrase; 459 for (final File f : ldifFiles) 460 { 461 if (! inputStreams.isEmpty()) 462 { 463 if (twoEOLs == null) 464 { 465 final ByteStringBuffer buffer = new ByteStringBuffer(4); 466 buffer.append(StaticUtils.EOL_BYTES); 467 buffer.append(StaticUtils.EOL_BYTES); 468 twoEOLs = buffer.toByteArray(); 469 } 470 471 inputStreams.add(new ByteArrayInputStream(twoEOLs)); 472 } 473 474 InputStream inputStream = new FileInputStream(f); 475 try 476 { 477 final ObjectPair<InputStream,String> p = 478 getPossiblyPassphraseEncryptedInputStream( 479 inputStream, passphrase, (encryptionPassphrase == null), 480 INFO_TOOL_UTILS_ENCRYPTED_LDIF_FILE_PW_PROMPT.get( 481 f.getPath()), 482 ERR_TOOL_UTILS_ENCRYPTED_LDIF_FILE_WRONG_PW.get(), out, 483 err); 484 inputStream = p.getFirst(); 485 if ((p.getSecond() != null) && (passphrase == null)) 486 { 487 passphrase = p.getSecond(); 488 } 489 } 490 catch (final GeneralSecurityException e) 491 { 492 Debug.debugException(e); 493 inputStream.close(); 494 throw new IOException( 495 ERR_TOOL_UTILS_ENCRYPTED_LDIF_FILE_CANNOT_DECRYPT.get( 496 f.getPath(), StaticUtils.getExceptionMessage(e)), 497 e); 498 } 499 500 inputStream = getPossiblyGZIPCompressedInputStream(inputStream); 501 inputStreams.add(inputStream); 502 } 503 504 createdSuccessfully = true; 505 if (inputStreams.size() == 1) 506 { 507 return new ObjectPair<>(inputStreams.get(0), passphrase); 508 } 509 else 510 { 511 return new ObjectPair<InputStream,String>( 512 new AggregateInputStream(inputStreams), passphrase); 513 } 514 } 515 finally 516 { 517 if (! createdSuccessfully) 518 { 519 for (final InputStream inputStream : inputStreams) 520 { 521 try 522 { 523 inputStream.close(); 524 } 525 catch (final IOException e) 526 { 527 Debug.debugException(e); 528 } 529 } 530 } 531 } 532 } 533 534 535 536 /** 537 * Retrieves an {@code InputStream} that can be used to read data from the 538 * provided input stream that may have potentially been GZIP-compressed. If 539 * the provided input stream does not appear to contain GZIP-compressed data, 540 * then the returned stream will permit reading the data from the provided 541 * stream without any alteration. 542 * <BR><BR> 543 * The determination will be made by looking to see if the first two bytes 544 * read from the provided input stream are 0x1F and 0x8B, respectively (which 545 * is the GZIP magic header). To avoid false positives, this method should 546 * only be used if it is known that if the input stream does not contain 547 * compressed data, then it will not start with that two-byte sequence. This 548 * method should always be safe to use if the data to be read is text. If the 549 * data may be binary and that binary data may happen to start with 0x1F 0x8B, 550 * then this method should not be used. 551 * <BR><BR> 552 * The input stream's {@code mark} and {@code reset} methods will be used to 553 * permit peeking at the data at the head of the input stream. If the 554 * provided stream does not support the use of those methods, then it will be 555 * wrapped in a {@code BufferedInputStream}, which does support them. 556 * 557 * @param inputStream The input stream from which the data is to be read. 558 * 559 * @return A {@code GZIPInputStream} that wraps the provided input stream if 560 * the stream appears to contain GZIP-compressed data, or the 561 * provided input stream (potentially wrapped in a 562 * {@code BufferedInputStream}) if the provided stream does not 563 * appear to contain GZIP-compressed data. 564 * 565 * @throws IOException If a problem is encountered while attempting to 566 * determine whether the stream contains GZIP-compressed 567 * data. 568 */ 569 public static InputStream getPossiblyGZIPCompressedInputStream( 570 final InputStream inputStream) 571 throws IOException 572 { 573 Validator.ensureTrue((inputStream != null), 574 "StaticUtils.getPossiblyGZIPCompressedInputStream.inputStream must " + 575 "not be null."); 576 577 578 // Mark the input stream so that we can peek at data from the beginning of 579 // the stream. 580 final InputStream markableInputStream; 581 if (inputStream.markSupported()) 582 { 583 markableInputStream = inputStream; 584 } 585 else 586 { 587 markableInputStream = new BufferedInputStream(inputStream); 588 } 589 590 markableInputStream.mark(2); 591 592 593 // Check to see if the file starts with the GZIP magic header. Whether it 594 // does or not, reset the stream so that we can read it from the beginning. 595 final boolean isCompressed; 596 try 597 { 598 isCompressed = ((markableInputStream.read() == 0x1F) && 599 (markableInputStream.read() == 0x8B)); 600 } 601 finally 602 { 603 markableInputStream.reset(); 604 } 605 606 607 // If the stream starts with the GZIP magic header, then assume it's 608 // GZIP-compressed. Otherwise, assume it's not. 609 if (isCompressed) 610 { 611 return new GZIPInputStream(markableInputStream); 612 } 613 else 614 { 615 return markableInputStream; 616 } 617 } 618 619 620 621 /** 622 * Retrieves an {@code InputStream} that can be used to read data from the 623 * provided input stream that may have potentially been encrypted with a 624 * {@link PassphraseEncryptedOutputStream}. If the provided input stream does 625 * not appear to contain passphrase-encrypted data, then the returned stream 626 * will permit reading the data from the provided stream without any 627 * alteration. 628 * <BR><BR> 629 * The determination will be made by looking to see if the input stream starts 630 * with a valid {@link PassphraseEncryptedStreamHeader}. Because of the 631 * complex nature of that header, it is highly unlikely that the input stream 632 * will just happen to start with a valid header if the stream does not 633 * actually contain encrypted data. 634 * <BR><BR> 635 * The input stream's {@code mark} and {@code reset} methods will be used to 636 * permit peeking at the data at the head of the input stream. If the 637 * provided stream does not support the use of those methods, then it will be 638 * wrapped in a {@code BufferedInputStream}, which does support them. 639 * 640 * @param inputStream The input stream from which the data 641 * is to be read. It must not be 642 * {@code null}. 643 * @param passphrase The passphrase to use to generate the 644 * encryption key. It may be 645 * {@code null} if the passphrase should 646 * only be obtained via interactive 647 * prompting. If the passphrase is not 648 * {@code null} but is incorrect, then 649 * the user may be interactively prompted 650 * for the correct passphrase. 651 * @param promptOnIncorrectPassphrase Indicates whether the user should be 652 * interactively prompted for the correct 653 * passphrase if the provided passphrase 654 * is non-{@code null} and is also 655 * incorrect. 656 * @param passphrasePrompt The prompt that will be presented to 657 * the user if the input stream does 658 * contain encrypted data and the 659 * passphrase needs to be interactively 660 * requested from the user. It must not 661 * be {@code null} or empty. 662 * @param incorrectPassphraseError The error message that will be 663 * presented to the user if the entered 664 * passphrase is not correct. It must 665 * not be {@code null} or empty. 666 * @param standardOutput The {@code PrintStream} to use to 667 * write to standard output while 668 * interactively prompting for the 669 * passphrase. It must not be 670 * {@code null}. 671 * @param standardError The {@code PrintStream} to use to 672 * write to standard error while 673 * interactively prompting for the 674 * passphrase. It must not be 675 * {@code null}. 676 * 677 * @return An {@code ObjectPair} that combines the resulting input stream 678 * with the associated encryption passphrase. If the provided input 679 * stream is encrypted, then the returned input stream element will 680 * be a {@code PassphraseEncryptedInputStream} and the returned 681 * passphrase element will be non-{@code null}. If the provided 682 * input stream is not encrypted, then the returned input stream 683 * element will be the provided input stream (potentially wrapped in 684 * a {@code BufferedInputStream}), and the returned passphrase 685 * element will be {@code null}. 686 * 687 * @throws IOException If a problem is encountered while attempting to 688 * determine whether the stream contains 689 * passphrase-encrypted data. 690 * 691 * @throws InvalidKeyException If the provided passphrase is incorrect and 692 * the user should not be interactively prompted 693 * for the correct passphrase. 694 * 695 * @throws GeneralSecurityException If a problem is encountered while 696 * attempting to prepare to decrypt data 697 * read from the input stream. 698 */ 699 public static ObjectPair<InputStream,String> 700 getPossiblyPassphraseEncryptedInputStream( 701 final InputStream inputStream, 702 final String passphrase, 703 final boolean promptOnIncorrectPassphrase, 704 final CharSequence passphrasePrompt, 705 final CharSequence incorrectPassphraseError, 706 final PrintStream standardOutput, 707 final PrintStream standardError) 708 throws IOException, InvalidKeyException, GeneralSecurityException 709 { 710 final ObjectPair<InputStream, char[]> p = 711 getPossiblyPassphraseEncryptedInputStream(inputStream, 712 (passphrase == null) 713 ? null 714 : passphrase.toCharArray(), 715 promptOnIncorrectPassphrase, passphrasePrompt, 716 incorrectPassphraseError, standardOutput, standardError); 717 718 if (p.getSecond() == null) 719 { 720 return new ObjectPair<>(p.getFirst(), null); 721 } 722 else 723 { 724 return new ObjectPair<>(p.getFirst(), new String(p.getSecond())); 725 } 726 } 727 728 729 730 /** 731 * Retrieves an {@code InputStream} that can be used to read data from the 732 * provided input stream that may have potentially been encrypted with a 733 * {@link PassphraseEncryptedOutputStream}. If the provided input stream does 734 * not appear to contain passphrase-encrypted data, then the returned stream 735 * will permit reading the data from the provided stream without any 736 * alteration. 737 * <BR><BR> 738 * The determination will be made by looking to see if the input stream starts 739 * with a valid {@link PassphraseEncryptedStreamHeader}. Because of the 740 * complex nature of that header, it is highly unlikely that the input stream 741 * will just happen to start with a valid header if the stream does not 742 * actually contain encrypted data. 743 * <BR><BR> 744 * The input stream's {@code mark} and {@code reset} methods will be used to 745 * permit peeking at the data at the head of the input stream. If the 746 * provided stream does not support the use of those methods, then it will be 747 * wrapped in a {@code BufferedInputStream}, which does support them. 748 * 749 * @param inputStream The input stream from which the data 750 * is to be read. It must not be 751 * {@code null}. 752 * @param passphrase The passphrase to use to generate the 753 * encryption key. It may be 754 * {@code null} if the passphrase should 755 * only be obtained via interactive 756 * prompting. If the passphrase is not 757 * {@code null} but is incorrect, then 758 * the user may be interactively prompted 759 * for the correct passphrase. 760 * @param promptOnIncorrectPassphrase Indicates whether the user should be 761 * interactively prompted for the correct 762 * passphrase if the provided passphrase 763 * is non-{@code null} and is also 764 * incorrect. 765 * @param passphrasePrompt The prompt that will be presented to 766 * the user if the input stream does 767 * contain encrypted data and the 768 * passphrase needs to be interactively 769 * requested from the user. It must not 770 * be {@code null} or empty. 771 * @param incorrectPassphraseError The error message that will be 772 * presented to the user if the entered 773 * passphrase is not correct. It must 774 * not be {@code null} or empty. 775 * @param out The {@code PrintStream} to use to 776 * write to standard output while 777 * interactively prompting for the 778 * passphrase. It must not be 779 * {@code null}. 780 * @param err The {@code PrintStream} to use to 781 * write to standard error while 782 * interactively prompting for the 783 * passphrase. It must not be 784 * {@code null}. 785 * 786 * @return An {@code ObjectPair} that combines the resulting input stream 787 * with the associated encryption passphrase. If the provided input 788 * stream is encrypted, then the returned input stream element will 789 * be a {@code PassphraseEncryptedInputStream} and the returned 790 * passphrase element will be non-{@code null}. If the provided 791 * input stream is not encrypted, then the returned input stream 792 * element will be the provided input stream (potentially wrapped in 793 * a {@code BufferedInputStream}), and the returned passphrase 794 * element will be {@code null}. 795 * 796 * @throws IOException If a problem is encountered while attempting to 797 * determine whether the stream contains 798 * passphrase-encrypted data. 799 * 800 * @throws InvalidKeyException If the provided passphrase is incorrect and 801 * the user should not be interactively prompted 802 * for the correct passphrase. 803 * 804 * @throws GeneralSecurityException If a problem is encountered while 805 * attempting to prepare to decrypt data 806 * read from the input stream. 807 */ 808 public static ObjectPair<InputStream,char[]> 809 getPossiblyPassphraseEncryptedInputStream( 810 final InputStream inputStream, 811 final char[] passphrase, 812 final boolean promptOnIncorrectPassphrase, 813 final CharSequence passphrasePrompt, 814 final CharSequence incorrectPassphraseError, 815 final PrintStream out, final PrintStream err) 816 throws IOException, InvalidKeyException, GeneralSecurityException 817 { 818 Validator.ensureTrue((inputStream != null), 819 "StaticUtils.getPossiblyPassphraseEncryptedInputStream.inputStream " + 820 "must not be null."); 821 Validator.ensureTrue( 822 ((passphrasePrompt != null) && (passphrasePrompt.length() > 0)), 823 "StaticUtils.getPossiblyPassphraseEncryptedInputStream." + 824 "passphrasePrompt must not be null or empty."); 825 Validator.ensureTrue( 826 ((incorrectPassphraseError != null) && 827 (incorrectPassphraseError.length() > 0)), 828 "StaticUtils.getPossiblyPassphraseEncryptedInputStream." + 829 "incorrectPassphraseError must not be null or empty."); 830 Validator.ensureTrue((out != null), 831 "StaticUtils.getPossiblyPassphraseEncryptedInputStream." + 832 "standardOutput must not be null."); 833 Validator.ensureTrue((err != null), 834 "StaticUtils.getPossiblyPassphraseEncryptedInputStream." + 835 "standardError must not be null."); 836 837 838 // Mark the input stream so that we can peek at data from the beginning of 839 // the stream. 840 final InputStream markableInputStream; 841 if (inputStream.markSupported()) 842 { 843 markableInputStream = inputStream; 844 } 845 else 846 { 847 markableInputStream = new BufferedInputStream(inputStream); 848 } 849 850 markableInputStream.mark(1024); 851 852 853 // Try to read a passphrase-encrypted stream header from the beginning of 854 // the stream. Just decode the header, but don't attempt to make it usable 855 // for encryption or decryption. 856 final PassphraseEncryptedStreamHeader streamHeaderShell; 857 try 858 { 859 streamHeaderShell = PassphraseEncryptedStreamHeader.readFrom( 860 markableInputStream, null); 861 } 862 catch (final LDAPException e) 863 { 864 // This is fine. It just means that the stream doesn't contain encrypted 865 // data. In that case, reset the stream and return it so that the 866 // unencrypted data can be read. 867 Debug.debugException(Level.FINEST, e); 868 markableInputStream.reset(); 869 return new ObjectPair<>(markableInputStream, null); 870 } 871 872 873 // If a passphrase was provided, then see if it is correct. 874 if (passphrase != null) 875 { 876 try 877 { 878 final PassphraseEncryptedStreamHeader validStreamHeader = 879 PassphraseEncryptedStreamHeader.decode( 880 streamHeaderShell.getEncodedHeader(), 881 passphrase); 882 return new ObjectPair<InputStream,char[]>( 883 new PassphraseEncryptedInputStream(markableInputStream, 884 validStreamHeader), 885 passphrase); 886 } 887 catch (final InvalidKeyException e) 888 { 889 // The provided passphrase is not correct. That's fine. We'll just 890 // prompt for the correct one. 891 Debug.debugException(e); 892 if (! promptOnIncorrectPassphrase) 893 { 894 throw e; 895 } 896 } 897 catch (final GeneralSecurityException e) 898 { 899 Debug.debugException(e); 900 throw e; 901 } 902 catch (final LDAPException e) 903 { 904 // This should never happen, since we were previously able to decode the 905 // header. Just treat it like a GeneralSecurityException. 906 Debug.debugException(e); 907 throw new GeneralSecurityException(e.getMessage(), e); 908 } 909 } 910 911 912 // If the header includes a key identifier, and if the server code is 913 // available, then see if we can get a passphrase for the corresponding 914 // encryption settings definition ID. 915 if ((streamHeaderShell.getKeyIdentifier() != null) && 916 (GET_PASSPHRASE_FOR_ENCRYPTION_SETTINGS_ID_METHOD != null)) 917 { 918 try 919 { 920 final Object passphraseObject = 921 GET_PASSPHRASE_FOR_ENCRYPTION_SETTINGS_ID_METHOD.invoke(null, 922 streamHeaderShell.getKeyIdentifier(), out, err); 923 if ((passphraseObject != null) && (passphraseObject instanceof String)) 924 { 925 final char[] passphraseChars = 926 ((String) passphraseObject).toCharArray(); 927 final PassphraseEncryptedStreamHeader validStreamHeader = 928 PassphraseEncryptedStreamHeader.decode( 929 streamHeaderShell.getEncodedHeader(), 930 passphraseChars); 931 return new ObjectPair<InputStream,char[]>( 932 new PassphraseEncryptedInputStream(markableInputStream, 933 validStreamHeader), 934 passphraseChars); 935 } 936 } 937 catch (final Exception e) 938 { 939 // This means that either an error occurred while trying to get the 940 // passphrase, or the passphrase we got was incorrect. That's fine. 941 // We'll just continue on to prompt for the passphrase. 942 Debug.debugException(e); 943 } 944 } 945 946 947 // If we've gotten here, then we need to interactively prompt for the 948 // passphrase. 949 while (true) 950 { 951 // Read the passphrase from the user. 952 final String promptedPassphrase; 953 try 954 { 955 promptedPassphrase = 956 promptForEncryptionPassphrase(false, false, passphrasePrompt, null, 957 out, err); 958 } 959 catch (final LDAPException e) 960 { 961 Debug.debugException(e); 962 throw new IOException(e.getMessage(), e); 963 } 964 965 966 // Check to see if the passphrase was correct. If so, then use it. 967 // Otherwise, show an error and prompt again. 968 try 969 { 970 final char[] passphraseChars = promptedPassphrase.toCharArray(); 971 final PassphraseEncryptedStreamHeader validStreamHeader = 972 PassphraseEncryptedStreamHeader.decode( 973 streamHeaderShell.getEncodedHeader(), passphraseChars); 974 return new ObjectPair<InputStream,char[]>( 975 new PassphraseEncryptedInputStream(markableInputStream, 976 validStreamHeader), 977 passphraseChars); 978 } 979 catch (final InvalidKeyException e) 980 { 981 Debug.debugException(e); 982 983 // The passphrase was incorrect. Display a wrapped error message and 984 // re-prompt. 985 wrap(incorrectPassphraseError, err); 986 err.println(); 987 } 988 catch (final GeneralSecurityException e) 989 { 990 Debug.debugException(e); 991 throw e; 992 } 993 catch (final LDAPException e) 994 { 995 // This should never happen, since we were previously able to decode the 996 // header. Just treat it like a GeneralSecurityException. 997 Debug.debugException(e); 998 throw new GeneralSecurityException(e.getMessage(), e); 999 } 1000 } 1001 } 1002}