Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright 2018-2022 Ping Identity Corporation
* All Rights Reserved.
*/
/*
* Copyright 2018-2022 Ping Identity Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright (C) 2018-2022 Ping Identity Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (GPLv2 only)
* or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
package com.unboundid.ldap.sdk.unboundidds.tools;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.zip.GZIPInputStream;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.AggregateInputStream;
import com.unboundid.util.ByteStringBuffer;
import com.unboundid.util.Debug;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.ObjectPair;
import com.unboundid.util.PassphraseEncryptedInputStream;
import com.unboundid.util.PassphraseEncryptedOutputStream;
import com.unboundid.util.PassphraseEncryptedStreamHeader;
import com.unboundid.util.PasswordReader;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.Validator;
import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
/**
* This class provides a number of utility methods primarily intended for use
* with command-line tools.
*
*
* NOTE: This class, and other classes within the
* {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
* supported for use against Ping Identity, UnboundID, and
* Nokia/Alcatel-Lucent 8661 server products. These classes provide support
* for proprietary functionality or for external specifications that are not
* considered stable or mature enough to be guaranteed to work in an
* interoperable way with other types of LDAP servers.
*
*/
@ThreadSafety(level= ThreadSafetyLevel.NOT_THREADSAFE)
public final class ToolUtils
{
/**
* The column at which long lines should be wrapped.
*/
private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
/**
* A handle to a method that can be used to get the passphrase for an
* encryption settings definition ID if the server code is available. We have
* to call this via reflection because the server code may not be available.
*/
@Nullable private static final Method
GET_PASSPHRASE_FOR_ENCRYPTION_SETTINGS_ID_METHOD;
static
{
Method m = null;
try
{
final Class> serverStaticUtilsClass = Class.forName(
"com.unboundid.directory.server.util.StaticUtils");
m = serverStaticUtilsClass.getMethod(
"getPassphraseForEncryptionSettingsID", String.class,
PrintStream.class, PrintStream.class);
}
catch (final Exception e)
{
// This is fine. It probably just means that the server code isn't
// available.
Debug.debugException(Level.FINEST, e);
}
GET_PASSPHRASE_FOR_ENCRYPTION_SETTINGS_ID_METHOD = m;
}
/**
* Prevent this utility class from being instantiated.
*/
private ToolUtils()
{
// No implementation is required.
}
/**
* Reads an encryption passphrase from the specified file. The file must
* contain exactly one line, which must not be empty, and must be comprised
* entirely of the encryption passphrase.
*
* @param f The file from which the passphrase should be read. It must not
* be {@code null}.
*
* @return The encryption passphrase read from the specified file.
*
* @throws LDAPException If a problem occurs while attempting to read the
* encryption passphrase.
*/
@NotNull()
public static String readEncryptionPassphraseFromFile(@NotNull final File f)
throws LDAPException
{
Validator.ensureTrue((f != null),
"ToolUtils.readEncryptionPassphraseFromFile.f must not be null.");
if (! f.exists())
{
throw new LDAPException(ResultCode.PARAM_ERROR,
ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_MISSING.get(f.getAbsolutePath()));
}
if (! f.isFile())
{
throw new LDAPException(ResultCode.PARAM_ERROR,
ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_NOT_FILE.get(f.getAbsolutePath()));
}
try (FileReader fileReader = new FileReader(f);
BufferedReader bufferedReader = new BufferedReader(fileReader))
{
final String encryptionPassphrase = bufferedReader.readLine();
if (encryptionPassphrase == null)
{
throw new LDAPException(ResultCode.PARAM_ERROR,
ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_EMPTY.get(f.getAbsolutePath()));
}
else if (bufferedReader.readLine() != null)
{
throw new LDAPException(ResultCode.PARAM_ERROR,
ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_MULTIPLE_LINES.get(
f.getAbsolutePath()));
}
else if (encryptionPassphrase.isEmpty())
{
throw new LDAPException(ResultCode.PARAM_ERROR,
ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_EMPTY.get(f.getAbsolutePath()));
}
return encryptionPassphrase;
}
catch (final LDAPException e)
{
Debug.debugException(e);
throw e;
}
catch (final Exception e)
{
Debug.debugException(e);
throw new LDAPException(ResultCode.LOCAL_ERROR,
ERR_TOOL_UTILS_ENCRYPTION_PW_FILE_READ_ERROR.get(
f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)));
}
}
/**
* Interactively prompts the user for an encryption passphrase.
*
* @param allowEmpty Indicates whether the encryption passphrase is allowed
* to be empty. If this is {@code false}, then the user
* will be re-prompted for the passphrase if the value
* they enter is empty.
* @param confirm Indicates whether the user will asked to confirm the
* passphrase. If this is {@code true}, then the user
* will have to enter the same passphrase twice. If this
* is {@code false}, then the user will only be prompted
* once.
* @param out The {@code PrintStream} that will be used for standard
* output. It must not be {@code null}.
* @param err The {@code PrintStream} that will be used for standard
* error. It must not be {@code null}.
*
* @return The encryption passphrase provided by the user.
*
* @throws LDAPException If a problem is encountered while trying to obtain
* the passphrase from the user.
*/
@NotNull()
public static String promptForEncryptionPassphrase(final boolean allowEmpty,
final boolean confirm,
@NotNull final PrintStream out,
@NotNull final PrintStream err)
throws LDAPException
{
return promptForEncryptionPassphrase(allowEmpty, confirm,
INFO_TOOL_UTILS_ENCRYPTION_PW_PROMPT.get(),
INFO_TOOL_UTILS_ENCRYPTION_PW_CONFIRM.get(), out, err);
}
/**
* Interactively prompts the user for an encryption passphrase.
*
* @param allowEmpty Indicates whether the encryption passphrase is
* allowed to be empty. If this is {@code false}, then
* the user will be re-prompted for the passphrase if
* the value they enter is empty.
* @param confirm Indicates whether the user will asked to confirm the
* passphrase. If this is {@code true}, then the user
* will have to enter the same passphrase twice. If
* this is {@code false}, then the user will only be
* prompted once.
* @param initialPrompt The initial prompt that will be presented to the
* user. It must not be {@code null} or empty.
* @param confirmPrompt The prompt that will be presented to the user when
* asked to confirm the passphrase. It may be
* {@code null} only if {@code confirm} is
* {@code false}.
* @param out The {@code PrintStream} that will be used for
* standard output. It must not be {@code null}.
* @param err The {@code PrintStream} that will be used for
* standard error. It must not be {@code null}.
*
* @return The encryption passphrase provided by the user.
*
* @throws LDAPException If a problem is encountered while trying to obtain
* the passphrase from the user.
*/
@NotNull()
public static String promptForEncryptionPassphrase(final boolean allowEmpty,
final boolean confirm,
@NotNull final CharSequence initialPrompt,
@Nullable final CharSequence confirmPrompt,
@NotNull final PrintStream out,
@NotNull final PrintStream err)
throws LDAPException
{
Validator.ensureTrue(
((initialPrompt != null) && (initialPrompt.length() > 0)),
"TestUtils.promptForEncryptionPassphrase.initialPrompt must not be " +
"null or empty.");
Validator.ensureTrue(
((! confirm) ||
((confirmPrompt != null) && (confirmPrompt.length() > 0))),
"TestUtils.promptForEncryptionPassphrase.confirmPrompt must not be " +
"null or empty when confirm is true.");
Validator.ensureTrue((out != null),
"ToolUtils.promptForEncryptionPassphrase.out must not be null");
Validator.ensureTrue((err != null),
"ToolUtils.promptForEncryptionPassphrase.err must not be null");
while (true)
{
char[] passphraseChars = null;
char[] confirmChars = null;
try
{
wrapPrompt(initialPrompt, true, out);
passphraseChars = PasswordReader.readPasswordChars();
if ((passphraseChars == null) || (passphraseChars.length == 0))
{
if (allowEmpty)
{
passphraseChars = StaticUtils.NO_CHARS;
}
else
{
wrap(ERR_TOOL_UTILS_ENCRYPTION_PW_EMPTY.get(), err);
err.println();
continue;
}
}
if (confirm)
{
wrapPrompt(confirmPrompt, true, out);
confirmChars = PasswordReader.readPasswordChars();
if ((confirmChars == null) ||
(! Arrays.equals(passphraseChars, confirmChars)))
{
wrap(ERR_TOOL_UTILS_ENCRYPTION_PW_MISMATCH.get(), err);
err.println();
continue;
}
}
return new String(passphraseChars);
}
finally
{
if (passphraseChars != null)
{
Arrays.fill(passphraseChars, '\u0000');
}
if (confirmChars != null)
{
Arrays.fill(confirmChars, '\u0000');
}
}
}
}
/**
* Writes a wrapped version of the provided message to the given stream.
*
* @param message The message to be written. If it is {@code null} or
* empty, then an empty line will be printed.
* @param out The {@code PrintStream} that should be used to write the
* provided message.
*/
public static void wrap(@NotNull final CharSequence message,
@NotNull final PrintStream out)
{
Validator.ensureTrue((out != null), "ToolUtils.wrap.out must not be null.");
if ((message == null) || (message.length() == 0))
{
out.println();
return;
}
for (final String line :
StaticUtils.wrapLine(message.toString(), WRAP_COLUMN))
{
out.println(line);
}
}
/**
* Wraps the provided prompt such that every line except the last will be
* followed by a newline, but the last line will not be followed by a newline.
*
* @param prompt The prompt to be wrapped. It must not be
* {@code null} or empty.
* @param ensureTrailingSpace Indicates whether to ensure that there is a
* trailing space after the end of the prompt.
* @param out The {@code PrintStream} to which the prompt
* should be written. It must not be
* {@code null}.
*/
public static void wrapPrompt(@NotNull final CharSequence prompt,
final boolean ensureTrailingSpace,
@NotNull final PrintStream out)
{
Validator.ensureTrue(((prompt != null) && (prompt.length() > 0)),
"ToolUtils.wrapPrompt.prompt must not be null or empty.");
Validator.ensureTrue((out != null),
"ToolUtils.wrapPrompt.out must not be null.");
String promptString = prompt.toString();
if (ensureTrailingSpace && (! promptString.endsWith(" ")))
{
promptString += ' ';
}
final List lines = StaticUtils.wrapLine(promptString, WRAP_COLUMN);
final Iterator iterator = lines.iterator();
while (iterator.hasNext())
{
final String line = iterator.next();
if (iterator.hasNext())
{
out.println(line);
}
else
{
out.print(line);
}
}
}
/**
* Retrieves an input stream that can be used to read data from the specified
* list of files. It will handle the possibility that any or all of the LDIF
* files are encrypted and/or compressed.
*
* @param ldifFiles The list of LDIF files from which the data
* is to be read. It must not be {@code null}
* or empty.
* @param encryptionPassphrase The passphrase that should be used to access
* encrypted LDIF files. It may be {@code null}
* if the user should be interactively prompted
* for the passphrase if any of the files is
* encrypted.
* @param out The print stream to use for standard output.
* It must not be {@code null}.
* @param err The print stream to use for standard error.
* It must not be {@code null}.
*
* @return An {@code ObjectPair} whose first element is an input stream that
* can be used to read data from the specified list of files, and
* whose second element is a possibly-{@code null} passphrase that
* is used to encrypt the input data.
*
* @throws IOException If a problem is encountered while attempting to get
* the input stream for reading the data.
*/
@NotNull()
public static ObjectPair getInputStreamForLDIFFiles(
@NotNull final List ldifFiles,
@Nullable final String encryptionPassphrase,
@NotNull final PrintStream out,
@NotNull final PrintStream err)
throws IOException
{
Validator.ensureTrue(((ldifFiles != null) && (! ldifFiles.isEmpty())),
"ToolUtils.getInputStreamForLDIFFiles.ldifFiles must not be null or " +
"empty.");
Validator.ensureTrue((out != null),
"ToolUtils.getInputStreamForLDIFFiles.out must not be null");
Validator.ensureTrue((err != null),
"ToolUtils.getInputStreamForLDIFFiles.err must not be null");
boolean createdSuccessfully = false;
final ArrayList inputStreams =
new ArrayList<>(ldifFiles.size() * 2);
try
{
byte[] twoEOLs = null;
String passphrase = encryptionPassphrase;
for (final File f : ldifFiles)
{
if (! inputStreams.isEmpty())
{
if (twoEOLs == null)
{
final ByteStringBuffer buffer = new ByteStringBuffer(4);
buffer.append(StaticUtils.EOL_BYTES);
buffer.append(StaticUtils.EOL_BYTES);
twoEOLs = buffer.toByteArray();
}
inputStreams.add(new ByteArrayInputStream(twoEOLs));
}
InputStream inputStream = new FileInputStream(f);
try
{
final ObjectPair p =
getPossiblyPassphraseEncryptedInputStream(
inputStream, passphrase, (encryptionPassphrase == null),
INFO_TOOL_UTILS_ENCRYPTED_LDIF_FILE_PW_PROMPT.get(
f.getPath()),
ERR_TOOL_UTILS_ENCRYPTED_LDIF_FILE_WRONG_PW.get(), out,
err);
inputStream = p.getFirst();
if ((p.getSecond() != null) && (passphrase == null))
{
passphrase = p.getSecond();
}
}
catch (final GeneralSecurityException e)
{
Debug.debugException(e);
inputStream.close();
throw new IOException(
ERR_TOOL_UTILS_ENCRYPTED_LDIF_FILE_CANNOT_DECRYPT.get(
f.getPath(), StaticUtils.getExceptionMessage(e)),
e);
}
inputStream = getPossiblyGZIPCompressedInputStream(inputStream);
inputStreams.add(inputStream);
}
createdSuccessfully = true;
if (inputStreams.size() == 1)
{
return new ObjectPair<>(inputStreams.get(0), passphrase);
}
else
{
return new ObjectPair(
new AggregateInputStream(inputStreams), passphrase);
}
}
finally
{
if (! createdSuccessfully)
{
for (final InputStream inputStream : inputStreams)
{
try
{
inputStream.close();
}
catch (final IOException e)
{
Debug.debugException(e);
}
}
}
}
}
/**
* Retrieves an {@code InputStream} that can be used to read data from the
* provided input stream that may have potentially been GZIP-compressed. If
* the provided input stream does not appear to contain GZIP-compressed data,
* then the returned stream will permit reading the data from the provided
* stream without any alteration.
*
* The determination will be made by looking to see if the first two bytes
* read from the provided input stream are 0x1F and 0x8B, respectively (which
* is the GZIP magic header). To avoid false positives, this method should
* only be used if it is known that if the input stream does not contain
* compressed data, then it will not start with that two-byte sequence. This
* method should always be safe to use if the data to be read is text. If the
* data may be binary and that binary data may happen to start with 0x1F 0x8B,
* then this method should not be used.
*
* The input stream's {@code mark} and {@code reset} methods will be used to
* permit peeking at the data at the head of the input stream. If the
* provided stream does not support the use of those methods, then it will be
* wrapped in a {@code BufferedInputStream}, which does support them.
*
* @param inputStream The input stream from which the data is to be read.
*
* @return A {@code GZIPInputStream} that wraps the provided input stream if
* the stream appears to contain GZIP-compressed data, or the
* provided input stream (potentially wrapped in a
* {@code BufferedInputStream}) if the provided stream does not
* appear to contain GZIP-compressed data.
*
* @throws IOException If a problem is encountered while attempting to
* determine whether the stream contains GZIP-compressed
* data.
*/
@NotNull()
public static InputStream getPossiblyGZIPCompressedInputStream(
@NotNull final InputStream inputStream)
throws IOException
{
Validator.ensureTrue((inputStream != null),
"StaticUtils.getPossiblyGZIPCompressedInputStream.inputStream must " +
"not be null.");
// Mark the input stream so that we can peek at data from the beginning of
// the stream.
final InputStream markableInputStream;
if (inputStream.markSupported())
{
markableInputStream = inputStream;
}
else
{
markableInputStream = new BufferedInputStream(inputStream);
}
markableInputStream.mark(2);
// Check to see if the file starts with the GZIP magic header. Whether it
// does or not, reset the stream so that we can read it from the beginning.
final boolean isCompressed;
try
{
isCompressed = ((markableInputStream.read() == 0x1F) &&
(markableInputStream.read() == 0x8B));
}
finally
{
markableInputStream.reset();
}
// If the stream starts with the GZIP magic header, then assume it's
// GZIP-compressed. Otherwise, assume it's not.
if (isCompressed)
{
return new GZIPInputStream(markableInputStream);
}
else
{
return markableInputStream;
}
}
/**
* Retrieves an {@code InputStream} that can be used to read data from the
* provided input stream that may have potentially been encrypted with a
* {@link PassphraseEncryptedOutputStream} using a key from a Ping Identity
* or Nokia/Alcatel-Lucent 8661 Directory Server's encryption settings
* database. This method will throw an exception rather than interactively
* prompting for a passphrase if the content is encrypted but the encryption
* key is not readily available.
*
* The determination will be made by looking to see if the input stream starts
* with a valid {@link PassphraseEncryptedStreamHeader}. Because of the
* complex nature of that header, it is highly unlikely that the input stream
* will just happen to start with a valid header if the stream does not
* actually contain encrypted data.
*
* The input stream's {@code mark} and {@code reset} methods will be used to
* permit peeking at the data at the head of the input stream. If the
* provided stream does not support the use of those methods, then it will be
* wrapped in a {@code BufferedInputStream}, which does support them.
*
* @param inputStream The input stream from which the data is to be read.
* It must not be {@code null}.
*
* @return The resulting input stream that may be used to read from the
* given input stream. If the data was encrypted, then the returned
* stream will be a {@link PassphraseEncryptedInputStream} that may
* be used to read decrypted data from it. If the data was not
* encrypted, then the original stream (possibly wrapped by a
* {@code BufferedInputStream} will be returned.
*
* @throws IOException If a problem is encountered while attempting to
* determine whether the stream contains
* passphrase-encrypted data.
*
* @throws GeneralSecurityException If a problem is encountered while
* attempting to prepare to decrypt data
* read from the input stream, or if the
* stream is encrypted with a key that is
* not readily available.
*/
@NotNull()
public static InputStream getPossiblyPassphraseEncryptedInputStream(
@NotNull final InputStream inputStream)
throws IOException, GeneralSecurityException
{
final ObjectPair pair =
getPossiblyPassphraseEncryptedInputStream(inputStream,
Collections.emptyList(), false, false, "", "", System.out,
System.err);
return pair.getFirst();
}
/**
* Retrieves an {@code InputStream} that can be used to read data from the
* provided input stream that may have potentially been encrypted with a
* {@link PassphraseEncryptedOutputStream}. If the provided input stream does
* not appear to contain passphrase-encrypted data, then the returned stream
* will permit reading the data from the provided stream without any
* alteration.
*
* The determination will be made by looking to see if the input stream starts
* with a valid {@link PassphraseEncryptedStreamHeader}. Because of the
* complex nature of that header, it is highly unlikely that the input stream
* will just happen to start with a valid header if the stream does not
* actually contain encrypted data.
*
* The input stream's {@code mark} and {@code reset} methods will be used to
* permit peeking at the data at the head of the input stream. If the
* provided stream does not support the use of those methods, then it will be
* wrapped in a {@code BufferedInputStream}, which does support them.
*
* @param inputStream The input stream from which the data
* is to be read. It must not be
* {@code null}.
* @param potentialPassphrase A potential passphrase that may have
* been used to encrypt the data. It
* may be {@code null} if the passphrase
* should only be obtained via
* interactive prompting, or if the
* data was encrypted with a server-side
* encryption settings definition. If
* the passphrase is not {@code null} but
* is incorrect, then the user may be
* interactively prompted for the correct
* passphrase.
* @param promptOnIncorrectPassphrase Indicates whether the user should be
* interactively prompted for the correct
* passphrase if the provided passphrase
* is non-{@code null} and is also
* incorrect.
* @param passphrasePrompt The prompt that will be presented to
* the user if the input stream does
* contain encrypted data and the
* passphrase needs to be interactively
* requested from the user. It must not
* be {@code null} or empty.
* @param incorrectPassphraseError The error message that will be
* presented to the user if the entered
* passphrase is not correct. It must
* not be {@code null} or empty.
* @param standardOutput The {@code PrintStream} to use to
* write to standard output while
* interactively prompting for the
* passphrase. It must not be
* {@code null}.
* @param standardError The {@code PrintStream} to use to
* write to standard error while
* interactively prompting for the
* passphrase. It must not be
* {@code null}.
*
* @return An {@code ObjectPair} that combines the resulting input stream
* with the associated encryption passphrase. If the provided input
* stream is encrypted, then the returned input stream element will
* be a {@code PassphraseEncryptedInputStream} and the returned
* passphrase element will be non-{@code null}. If the provided
* input stream is not encrypted, then the returned input stream
* element will be the provided input stream (potentially wrapped in
* a {@code BufferedInputStream}), and the returned passphrase
* element will be {@code null}.
*
* @throws IOException If a problem is encountered while attempting to
* determine whether the stream contains
* passphrase-encrypted data.
*
* @throws InvalidKeyException If the provided passphrase is incorrect and
* the user should not be interactively prompted
* for the correct passphrase.
*
* @throws GeneralSecurityException If a problem is encountered while
* attempting to prepare to decrypt data
* read from the input stream.
*/
@NotNull()
public static ObjectPair
getPossiblyPassphraseEncryptedInputStream(
@NotNull final InputStream inputStream,
@Nullable final String potentialPassphrase,
final boolean promptOnIncorrectPassphrase,
@NotNull final CharSequence passphrasePrompt,
@NotNull final CharSequence incorrectPassphraseError,
@NotNull final PrintStream standardOutput,
@NotNull final PrintStream standardError)
throws IOException, InvalidKeyException, GeneralSecurityException
{
final Collection potentialPassphrases;
if (potentialPassphrase == null)
{
potentialPassphrases = Collections.emptySet();
}
else
{
potentialPassphrases =
Collections.singleton(potentialPassphrase.toCharArray());
}
final ObjectPair p =
getPossiblyPassphraseEncryptedInputStream(inputStream,
potentialPassphrases, promptOnIncorrectPassphrase,
passphrasePrompt, incorrectPassphraseError, standardOutput,
standardError);
if (p.getSecond() == null)
{
return new ObjectPair<>(p.getFirst(), null);
}
else
{
return new ObjectPair<>(p.getFirst(), new String(p.getSecond()));
}
}
/**
* Retrieves an {@code InputStream} that can be used to read data from the
* provided input stream that may have potentially been encrypted with a
* {@link PassphraseEncryptedOutputStream}. If the provided input stream does
* not appear to contain passphrase-encrypted data, then the returned stream
* will permit reading the data from the provided stream without any
* alteration.
*
* The determination will be made by looking to see if the input stream starts
* with a valid {@link PassphraseEncryptedStreamHeader}. Because of the
* complex nature of that header, it is highly unlikely that the input stream
* will just happen to start with a valid header if the stream does not
* actually contain encrypted data.
*
* The input stream's {@code mark} and {@code reset} methods will be used to
* permit peeking at the data at the head of the input stream. If the
* provided stream does not support the use of those methods, then it will be
* wrapped in a {@code BufferedInputStream}, which does support them.
*
* @param inputStream The input stream from which the data
* is to be read. It must not be
* {@code null}.
* @param potentialPassphrase A potential passphrase that may have
* been used to encrypt the data. It
* may be {@code null} if the passphrase
* should only be obtained via
* interactive prompting, or if the
* data was encrypted with a server-side
* encryption settings definition. If
* the passphrase is not {@code null} but
* is incorrect, then the user may be
* interactively prompted for the correct
* passphrase.
* @param promptOnIncorrectPassphrase Indicates whether the user should be
* interactively prompted for the correct
* passphrase if the provided passphrase
* is non-{@code null} and is also
* incorrect.
* @param passphrasePrompt The prompt that will be presented to
* the user if the input stream does
* contain encrypted data and the
* passphrase needs to be interactively
* requested from the user. It must not
* be {@code null} or empty.
* @param incorrectPassphraseError The error message that will be
* presented to the user if the entered
* passphrase is not correct. It must
* not be {@code null} or empty.
* @param standardOutput The {@code PrintStream} to use to
* write to standard output while
* interactively prompting for the
* passphrase. It must not be
* {@code null}.
* @param standardError The {@code PrintStream} to use to
* write to standard error while
* interactively prompting for the
* passphrase. It must not be
* {@code null}.
*
* @return An {@code ObjectPair} that combines the resulting input stream
* with the associated encryption passphrase. If the provided input
* stream is encrypted, then the returned input stream element will
* be a {@code PassphraseEncryptedInputStream} and the returned
* passphrase element will be non-{@code null}. If the provided
* input stream is not encrypted, then the returned input stream
* element will be the provided input stream (potentially wrapped in
* a {@code BufferedInputStream}), and the returned passphrase
* element will be {@code null}.
*
* @throws IOException If a problem is encountered while attempting to
* determine whether the stream contains
* passphrase-encrypted data.
*
* @throws InvalidKeyException If the provided passphrase is incorrect and
* the user should not be interactively prompted
* for the correct passphrase.
*
* @throws GeneralSecurityException If a problem is encountered while
* attempting to prepare to decrypt data
* read from the input stream.
*/
@NotNull()
public static ObjectPair
getPossiblyPassphraseEncryptedInputStream(
@NotNull final InputStream inputStream,
@Nullable final char[] potentialPassphrase,
final boolean promptOnIncorrectPassphrase,
@NotNull final CharSequence passphrasePrompt,
@NotNull final CharSequence incorrectPassphraseError,
@NotNull final PrintStream standardOutput,
@NotNull final PrintStream standardError)
throws IOException, InvalidKeyException, GeneralSecurityException
{
final Collection potentialPassphrases;
if (potentialPassphrase == null)
{
potentialPassphrases = Collections.emptySet();
}
else
{
potentialPassphrases =
Collections.singleton(potentialPassphrase);
}
final ObjectPair p =
getPossiblyPassphraseEncryptedInputStream(inputStream,
potentialPassphrases, promptOnIncorrectPassphrase,
passphrasePrompt, incorrectPassphraseError, standardOutput,
standardError);
if (p.getSecond() == null)
{
return new ObjectPair<>(p.getFirst(), null);
}
else
{
return new ObjectPair<>(p.getFirst(), p.getSecond());
}
}
/**
* Retrieves an {@code InputStream} that can be used to read data from the
* provided input stream that may have potentially been encrypted with a
* {@link PassphraseEncryptedOutputStream}. If the provided input stream does
* not appear to contain passphrase-encrypted data, then the returned stream
* will permit reading the data from the provided stream without any
* alteration.
*
* The determination will be made by looking to see if the input stream starts
* with a valid {@link PassphraseEncryptedStreamHeader}. Because of the
* complex nature of that header, it is highly unlikely that the input stream
* will just happen to start with a valid header if the stream does not
* actually contain encrypted data.
*
* The input stream's {@code mark} and {@code reset} methods will be used to
* permit peeking at the data at the head of the input stream. If the
* provided stream does not support the use of those methods, then it will be
* wrapped in a {@code BufferedInputStream}, which does support them.
*
* @param inputStream
* The input stream from which the data is to be read. It must
* not be {@code null}.
* @param potentialPassphrases
* A collection of potential passphrases that may have been used
* to encrypt the data. It may be {@code null} or empty if the
* passphrase should only be obtained via interactive prompting,
* or if the data was encrypted with a server-side encryption
* settings definition. If none of the provided passphrases are
* correct, then the user may still be interactively prompted
* for the correct passphrase based on the value of the
* {@code promptOnIncorrectPassphrase} argument.
* @param promptOnIncorrectPassphrase
* Indicates whether the user should be interactively prompted
* for the correct passphrase if the set of potential passphrases
* is non-{@code null} and non-empty, but none of the passphrases
* were correct.
* @param passphrasePrompt
* The prompt that will be presented to the user if the input
* stream does contain encrypted data and the passphrase needs to
* be interactively requested from the user. It must not be
* {@code null} or empty.
* @param incorrectPassphraseError
* The error message that will be presented to the user if the
* entered passphrase is not correct. It must not be
* {@code null} or empty.
* @param standardOutput
* The {@code PrintStream} to use to write to standard output
* while interactively prompting for the passphrase. It must not
* be {@code null}.
* @param standardError
* The {@code PrintStream} to use to write to standard error
* while interactively prompting for the passphrase. It must not
* be {@code null}.
*
* @return An {@code ObjectPair} that combines the resulting input stream
* with the associated encryption passphrase. If the provided input
* stream is encrypted, then the returned input stream element will
* be a {@code PassphraseEncryptedInputStream} and the returned
* passphrase element will be non-{@code null}. If the provided
* input stream is not encrypted, then the returned input stream
* element will be the provided input stream (potentially wrapped in
* a {@code BufferedInputStream}), and the returned passphrase
* element will be {@code null}.
*
* @throws IOException If a problem is encountered while attempting to
* determine whether the stream contains
* passphrase-encrypted data.
*
* @throws InvalidKeyException If the provided passphrase is incorrect and
* the user should not be interactively prompted
* for the correct passphrase.
*
* @throws GeneralSecurityException If a problem is encountered while
* attempting to prepare to decrypt data
* read from the input stream.
*/
@NotNull()
public static ObjectPair
getPossiblyPassphraseEncryptedInputStream(
@NotNull final InputStream inputStream,
@Nullable final Collection potentialPassphrases,
final boolean promptOnIncorrectPassphrase,
@NotNull final CharSequence passphrasePrompt,
@NotNull final CharSequence incorrectPassphraseError,
@NotNull final PrintStream standardOutput,
@NotNull final PrintStream standardError)
throws IOException, InvalidKeyException, GeneralSecurityException
{
return getPossiblyPassphraseEncryptedInputStream(inputStream,
potentialPassphrases, promptOnIncorrectPassphrase, true,
passphrasePrompt, incorrectPassphraseError, standardOutput,
standardError);
}
/**
* Retrieves an {@code InputStream} that can be used to read data from the
* provided input stream that may have potentially been encrypted with a
* {@link PassphraseEncryptedOutputStream}. If the provided input stream does
* not appear to contain passphrase-encrypted data, then the returned stream
* will permit reading the data from the provided stream without any
* alteration.
*
* The determination will be made by looking to see if the input stream starts
* with a valid {@link PassphraseEncryptedStreamHeader}. Because of the
* complex nature of that header, it is highly unlikely that the input stream
* will just happen to start with a valid header if the stream does not
* actually contain encrypted data.
*
* The input stream's {@code mark} and {@code reset} methods will be used to
* permit peeking at the data at the head of the input stream. If the
* provided stream does not support the use of those methods, then it will be
* wrapped in a {@code BufferedInputStream}, which does support them.
*
* @param inputStream
* The input stream from which the data is to be read. It must
* not be {@code null}.
* @param potentialPassphrases
* A collection of potential passphrases that may have been used
* to encrypt the data. It may be {@code null} or empty if the
* passphrase should only be obtained via interactive prompting,
* or if the data was encrypted with a server-side encryption
* settings definition. If none of the provided passphrases are
* correct, then the user may still be interactively prompted
* for the correct passphrase based on the value of the
* {@code promptOnIncorrectPassphrase} argument.
* @param promptOnIncorrectPassphrase
* Indicates whether the user should be interactively prompted
* for the correct passphrase if the set of potential passphrases
* is non-{@code null} and non-empty, but none of the passphrases
* were correct.
* @param promptOnUnavailablePassphrase
* Indicates whether the user should be interactively prompted
* for the correct passphrase if the input stream is encrypted,
* the key cannot be automatically obtained from a server-side
* encryption settings definition, and no potential passphrases
* were provided. If this is {@code false}, then an exception
* will be thrown if the data is encrypted with a key that is
* not available.
* @param passphrasePrompt
* The prompt that will be presented to the user if the input
* stream does contain encrypted data and the passphrase needs to
* be interactively requested from the user. It must not be
* {@code null} or empty.
* @param incorrectPassphraseError
* The error message that will be presented to the user if the
* entered passphrase is not correct. It must not be
* {@code null} or empty.
* @param standardOutput
* The {@code PrintStream} to use to write to standard output
* while interactively prompting for the passphrase. It must not
* be {@code null}.
* @param standardError
* The {@code PrintStream} to use to write to standard error
* while interactively prompting for the passphrase. It must not
* be {@code null}.
*
* @return An {@code ObjectPair} that combines the resulting input stream
* with the associated encryption passphrase. If the provided input
* stream is encrypted, then the returned input stream element will
* be a {@code PassphraseEncryptedInputStream} and the returned
* passphrase element will be non-{@code null}. If the provided
* input stream is not encrypted, then the returned input stream
* element will be the provided input stream (potentially wrapped in
* a {@code BufferedInputStream}), and the returned passphrase
* element will be {@code null}.
*
* @throws IOException If a problem is encountered while attempting to
* determine whether the stream contains
* passphrase-encrypted data.
*
* @throws InvalidKeyException If the provided passphrase is incorrect and
* the user should not be interactively prompted
* for the correct passphrase.
*
* @throws GeneralSecurityException If a problem is encountered while
* attempting to prepare to decrypt data
* read from the input stream.
*/
@NotNull()
private static ObjectPair
getPossiblyPassphraseEncryptedInputStream(
@NotNull final InputStream inputStream,
@Nullable final Collection potentialPassphrases,
final boolean promptOnIncorrectPassphrase,
final boolean promptOnUnavailablePassphrase,
@Nullable final CharSequence passphrasePrompt,
@Nullable final CharSequence incorrectPassphraseError,
@Nullable final PrintStream standardOutput,
@Nullable final PrintStream standardError)
throws IOException, InvalidKeyException, GeneralSecurityException
{
Validator.ensureTrue((inputStream != null),
"StaticUtils.getPossiblyPassphraseEncryptedInputStream.inputStream " +
"must not be null.");
Validator.ensureTrue(
(((passphrasePrompt != null) && (passphrasePrompt.length() > 0)) ||
(! promptOnUnavailablePassphrase)),
"StaticUtils.getPossiblyPassphraseEncryptedInputStream." +
"passphrasePrompt must not be null or empty.");
Validator.ensureTrue(
(((incorrectPassphraseError != null) &&
(incorrectPassphraseError.length() > 0)) ||
(! promptOnUnavailablePassphrase)),
"StaticUtils.getPossiblyPassphraseEncryptedInputStream." +
"incorrectPassphraseError must not be null or empty.");
Validator.ensureTrue((standardOutput!= null),
"StaticUtils.getPossiblyPassphraseEncryptedInputStream." +
"standardOutput must not be null.");
Validator.ensureTrue((standardError!= null),
"StaticUtils.getPossiblyPassphraseEncryptedInputStream." +
"standardError must not be null.");
// Mark the input stream so that we can peek at data from the beginning of
// the stream.
final InputStream markableInputStream;
if (inputStream.markSupported())
{
markableInputStream = inputStream;
}
else
{
markableInputStream = new BufferedInputStream(inputStream);
}
markableInputStream.mark(1024);
// Try to read a passphrase-encrypted stream header from the beginning of
// the stream. Just decode the header, but don't attempt to make it usable
// for encryption or decryption.
final PassphraseEncryptedStreamHeader streamHeaderShell;
try
{
streamHeaderShell = PassphraseEncryptedStreamHeader.readFrom(
markableInputStream, null);
}
catch (final LDAPException e)
{
// This is fine. It just means that the stream doesn't contain encrypted
// data. In that case, reset the stream and return it so that the
// unencrypted data can be read.
Debug.debugException(Level.FINEST, e);
markableInputStream.reset();
return new ObjectPair<>(markableInputStream, null);
}
// If the header includes a key identifier, and if the server code is
// available, then see if we can get a passphrase for the corresponding
// encryption settings definition ID.
if ((streamHeaderShell.getKeyIdentifier() != null) &&
(GET_PASSPHRASE_FOR_ENCRYPTION_SETTINGS_ID_METHOD != null))
{
try
{
final Object passphraseObject =
GET_PASSPHRASE_FOR_ENCRYPTION_SETTINGS_ID_METHOD.invoke(null,
streamHeaderShell.getKeyIdentifier(), standardOutput,
standardError);
if ((passphraseObject != null) && (passphraseObject instanceof String))
{
final char[] passphraseChars =
((String) passphraseObject).toCharArray();
final PassphraseEncryptedStreamHeader validStreamHeader =
PassphraseEncryptedStreamHeader.decode(
streamHeaderShell.getEncodedHeader(),
passphraseChars);
return new ObjectPair(
new PassphraseEncryptedInputStream(markableInputStream,
validStreamHeader),
passphraseChars);
}
}
catch (final Exception e)
{
// This means that either an error occurred while trying to get the
// passphrase, or the passphrase we got was incorrect. That's fine.
// We'll just continue on to prompt for the passphrase.
Debug.debugException(e);
}
}
// If any potential passphrases were provided, then see if any of them is
// correct.
final boolean potentialPassphrasesProvided =
((potentialPassphrases != null) && (! potentialPassphrases.isEmpty()));
if (potentialPassphrasesProvided)
{
final Iterator passphraseIterator =
potentialPassphrases.iterator();
while (passphraseIterator.hasNext())
{
try
{
final char[] passphraseChars = passphraseIterator.next();
final PassphraseEncryptedStreamHeader validStreamHeader =
PassphraseEncryptedStreamHeader.decode(
streamHeaderShell.getEncodedHeader(),
passphraseChars);
return new ObjectPair(
new PassphraseEncryptedInputStream(markableInputStream,
validStreamHeader),
passphraseChars);
}
catch (final InvalidKeyException e)
{
// The provided passphrase is not correct. That's fine. We'll just
// prompt for the correct one.
Debug.debugException(e);
if ((! promptOnIncorrectPassphrase) &&
(! passphraseIterator.hasNext()))
{
throw e;
}
}
catch (final GeneralSecurityException e)
{
Debug.debugException(e);
if (! passphraseIterator.hasNext())
{
throw e;
}
}
catch (final LDAPException e)
{
// This should never happen, since we were previously able to decode
// the header. Just treat it like a GeneralSecurityException.
Debug.debugException(e);
if (! passphraseIterator.hasNext())
{
throw new GeneralSecurityException(e.getMessage(), e);
}
}
}
}
// The correct passphrase wasn't provided, and it isn't available from an
// encryption settings definition, so we need to prompt for it. See if
// that's allowed. Note that if any potential passphrases were provided,
// then we should continue on to prompt, since we've already handled the
// case in which we shouldn't prompt on an incorrect passphrase.
if ((! potentialPassphrasesProvided) && (! promptOnUnavailablePassphrase))
{
throw new GeneralSecurityException(
ERR_TOOL_UTILS_UNAVAILABLE_PASSPHRASE.get());
}
while (true)
{
// Read the passphrase from the user.
final String promptedPassphrase;
try
{
promptedPassphrase =
promptForEncryptionPassphrase(false, false, passphrasePrompt, null,
standardOutput, standardError);
}
catch (final LDAPException e)
{
Debug.debugException(e);
throw new IOException(e.getMessage(), e);
}
// Check to see if the passphrase was correct. If so, then use it.
// Otherwise, show an error and prompt again.
try
{
final char[] passphraseChars = promptedPassphrase.toCharArray();
final PassphraseEncryptedStreamHeader validStreamHeader =
PassphraseEncryptedStreamHeader.decode(
streamHeaderShell.getEncodedHeader(), passphraseChars);
return new ObjectPair(
new PassphraseEncryptedInputStream(markableInputStream,
validStreamHeader),
passphraseChars);
}
catch (final InvalidKeyException e)
{
Debug.debugException(e);
// The passphrase was incorrect. Display a wrapped error message and
// re-prompt.
wrap(incorrectPassphraseError, standardError);
standardError.println();
}
catch (final GeneralSecurityException e)
{
Debug.debugException(e);
throw e;
}
catch (final LDAPException e)
{
// This should never happen, since we were previously able to decode the
// header. Just treat it like a GeneralSecurityException.
Debug.debugException(e);
throw new GeneralSecurityException(e.getMessage(), e);
}
}
}
}