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}