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