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