Files
ZeroEcho/lib/src/main/java/zeroecho/util/SymmetricStreamBuilder.java
2025-07-30 21:40:09 +02:00

523 lines
20 KiB
Java

/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.util;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidParameterException;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.StreamCipher;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
/**
* A builder for creating {@link InputStream}s that apply symmetric encryption
* or decryption using a variety of cipher types (block, stream, or AEAD).
* <p>
* Supports Bouncy Castle's {@link BufferedBlockCipher}, {@link StreamCipher},
* and {@link AEADBlockCipher} implementations. This builder validates
* configuration and initializes the cipher before wrapping the input stream in
* a cipher-specific processing stream.
*/
public final class SymmetricStreamBuilder {
/** Internal buffer size used during stream processing. */
public static final int BUF_SIZE = 4096;
private InputStream in;
private Object cipher;
private CipherParameters params;
private SymmetricStreamBuilder() {
// private constructor
}
/**
* Creates a new instance of {@code SymmetricStreamBuilder}.
*
* @return a new builder instance
*/
public static SymmetricStreamBuilder newBuilder() {
return new SymmetricStreamBuilder();
}
/**
* Sets the input stream that will be encrypted or decrypted.
*
* @param in the source {@link InputStream}
* @return this builder instance
*/
public SymmetricStreamBuilder withInputStream(final InputStream in) {
this.in = in;
return this;
}
/**
* Configures the cipher to be used for symmetric encryption or decryption.
*
* This method accepts either a predefined {@link AesCipherType}, or a concrete
* cipher instance such as:
* <ul>
* <li>{@link org.bouncycastle.crypto.StreamCipher}</li>
* <li>{@link org.bouncycastle.crypto.BufferedBlockCipher}</li>
* <li>{@link org.bouncycastle.crypto.modes.AEADBlockCipher}</li>
* </ul>
* If an {@link AesCipherType} is provided, it is internally converted into the
* appropriate cipher instance via its {@code createCipher()} method.
*
* Any unsupported cipher type will result in an
* {@link InvalidParameterException}.
*
* @param cipher the cipher type or instance to use for stream processing; must
* be an instance of {@code AesCipherType}, {@code StreamCipher},
* {@code BufferedBlockCipher}, or {@code AEADBlockCipher}
*
* @return this {@code SymmetricStreamBuilder} instance for fluent chaining
*
* @throws InvalidParameterException if the provided cipher is of an unsupported
* type
*
* @see AesCipherType
* @see org.bouncycastle.crypto.StreamCipher
* @see org.bouncycastle.crypto.BufferedBlockCipher
* @see org.bouncycastle.crypto.modes.AEADBlockCipher
*/
public SymmetricStreamBuilder withCipher(final Object cipher) {
this.cipher = switch (cipher) {
case AesCipherType aes -> aes.createCipher();
case BufferedBlockCipher buff -> buff;
case StreamCipher stream -> stream;
case AEADBlockCipher aead -> aead;
default -> throw new InvalidParameterException("Unsupported cipher type: " + cipher.getClass().getName());
};
return this;
}
/**
* Sets the cipher parameters (e.g., key, IV, nonce).
*
* @param params the cipher parameters
* @return this builder instance
*/
public SymmetricStreamBuilder withParameters(final CipherParameters params) {
this.params = params;
return this;
}
/**
* Configures this {@link SymmetricStreamBuilder} with both the AES cipher
* instance and its associated parameters, derived from the specified
* {@link AesParameters}.
*
* <p>
* This method performs two key operations:
* </p>
* <ul>
* <li>Initializes the cipher by invoking {@link AesCipherType#createCipher()}
* on {@code params.cipherType()}.</li>
* <li>Generates the corresponding {@link CipherParameters} using the provided
* secret key, initialization vector (IV), and optional Additional Authenticated
* Data (AAD).</li>
* </ul>
*
* <p>
* If the selected cipher type supports AEAD (e.g., AES-GCM), the
* {@code params.aad()} value will be incorporated into the parameter set. For
* non-AEAD modes, any provided AAD is ignored.
* </p>
*
* @param params the {@link AesParameters} object containing the cipher type,
* secret key, IV, and optional AAD; must not be {@code null}
* @return this {@code SymmetricStreamBuilder} instance for method chaining
* @throws NullPointerException if {@code params} is {@code null}
*/
public SymmetricStreamBuilder withCipherAndParameters(final AesParameters params) {
withCipher(params.cipherType().createCipher());
this.params = AesSupport.getParameters(params.cipherType(), params.key(), params.iv(), params.aad());
return this;
}
/**
* Builds a stream that encrypts data from the configured input stream.
*
* @return an {@link InputStream} that provides encrypted output
* @throws IllegalStateException if the builder is not fully configured
*/
public InputStream buildEncryptingStream() {
return buildStream(true);
}
/**
* Builds a stream that decrypts data from the configured input stream.
*
* @return an {@link InputStream} that provides decrypted output
* @throws IllegalStateException if the builder is not fully configured
*/
public InputStream buildDecryptingStream() {
return buildStream(false);
}
private InputStream buildStream(final boolean forEncryption) {
if (cipher == null || in == null || params == null) {
throw new IllegalStateException("InputStream, cipher, and parameters must be provided.");
}
return switch (cipher) {
case BufferedBlockCipher bbc -> {
bbc.init(forEncryption, params);
yield new BufferedBlockCipherInputStream(in, bbc);
}
case AEADBlockCipher aead -> {
aead.init(forEncryption, params);
yield new AEADBlockCipherInputStream(in, aead);
}
case StreamCipher sc -> {
sc.init(forEncryption, params);
yield new StreamCipherInputStream(in, sc);
}
default -> throw new IllegalStateException("Unsupported cipher type: " + cipher.getClass().getName());
};
}
}
/**
* Abstract base class for cipher-based {@link InputStream} implementations that
* process encrypted or decrypted data using a symmetric cipher.
*
* Manages buffered input/output handling, stream reading, and stream
* finalization logic. Concrete subclasses implement cipher-specific processing
* through the {@link #processCipher} method.
*/
abstract class SymmetricCipherInputStreamBase extends InputStream {
/**
* The underlying input stream supplying raw (encrypted or plaintext) data.
*/
protected final InputStream in;
/**
* Internal buffer used to read data from the underlying input stream.
*/
protected final byte[] inputBuffer = new byte[SymmetricStreamBuilder.BUF_SIZE];
/**
* Buffer holding the output from cipher processing.
*/
protected final byte[] outputBuffer;
/**
* Current read position within the output buffer.
*/
protected int outputPos;
/**
* Total number of valid bytes currently in the output buffer.
*/
protected int outputLen;
/**
* Indicates whether the cipher has been finalized (no more data to process).
*/
protected boolean finalProcessed;
/**
* Constructs a cipher input stream with the specified underlying input and
* output buffer size.
*
* @param in the underlying input stream
* @param outputBufferSize the size of the buffer to hold processed output
*/
protected SymmetricCipherInputStreamBase(final InputStream in, final int outputBufferSize) {
super();
this.in = in;
this.outputBuffer = new byte[outputBufferSize];
}
/**
* Reads a single byte from the encrypted or decrypted stream.
*
* @return the byte read, or {@code -1} if end of stream
* @throws IOException if an I/O or cipher processing error occurs
*/
@Override
public int read() throws IOException {
if (outputPos >= outputLen && !fillBuffer()) {
return -1;
}
return outputBuffer[outputPos++] & 0xFF;
}
/**
* Reads up to {@code len} bytes into the given buffer, starting at {@code off}.
*
* @param b the destination buffer
* @param off the start offset
* @param len the maximum number of bytes to read
* @return the number of bytes read, or {@code -1} if end of stream
* @throws IOException if an I/O or cipher processing error occurs
*/
@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
if (outputPos >= outputLen && !fillBuffer()) {
return -1;
}
final int toCopy = Math.min(len, outputLen - outputPos);
System.arraycopy(outputBuffer, outputPos, b, off, toCopy);
outputPos += toCopy;
return toCopy;
}
/**
* Attempts to refill the output buffer by reading from the input stream and
* processing the data with the cipher.
* <p>
* If the end of input is reached, this method finalizes the cipher (if
* applicable) and marks the stream as complete.
*
* @return {@code true} if new output was generated and is available to read,
* {@code false} if end of stream was reached and no more output is
* available
* @throws IOException if cipher processing fails
*/
protected boolean fillBuffer() throws IOException {
if (finalProcessed) {
return false;
}
int read;
try {
do {
read = in.readNBytes(inputBuffer, 0, inputBuffer.length);
outputLen = processCipher(inputBuffer, read, outputBuffer);
} while (outputLen == 0 && read > 0);
finalProcessed = read == 0;
outputPos = 0;
return outputLen > 0;
} catch (DataLengthException | IllegalStateException | InvalidCipherTextException e) {
throw new IOException("Cipher processing failed", e);
}
}
/**
* Processes a chunk of data using the configured cipher.
* <p>
* Implementations must detect {@code inputLen == 0} to perform finalization if
* the cipher supports it (e.g., for block or AEAD ciphers).
* <p>
* For stream ciphers, this typically means returning {@code 0} as no
* finalization is needed.
*
* @param input the buffer containing input data; contents are undefined if
* {@code inputLen == 0}
* @param inputLen the number of bytes to process, or {@code 0} to finalize the
* cipher
* @param output the buffer into which processed output is written
* @return the number of bytes written to the output buffer
*
* @throws DataLengthException if the input data is too large for the
* cipher
* @throws IllegalStateException if the cipher has not been properly
* initialized
* @throws InvalidCipherTextException if finalization fails due to invalid
* padding or corrupted ciphertext
* @throws Exception if any other unexpected error occurs
* during processing
*/
protected abstract int processCipher(byte[] input, int inputLen, byte[] output) throws InvalidCipherTextException;
/**
* Closes the underlying input stream and releases any associated resources.
*
* @throws IOException if an I/O error occurs during closing
*/
@Override
public void close() throws IOException {
in.close();
}
}
/**
* An {@link InputStream} that applies a {@link BufferedBlockCipher} to
* transform the data during reading.
* <p>
* This stream supports both encryption and decryption depending on how the
* cipher is initialized.
*/
class BufferedBlockCipherInputStream extends SymmetricCipherInputStreamBase {
private final BufferedBlockCipher cipher;
/**
* Creates a new stream that wraps the given input and processes it using a
* {@link BufferedBlockCipher}.
*
* @param in the input stream to wrap
* @param cipher the initialized {@link BufferedBlockCipher}
*/
public BufferedBlockCipherInputStream(final InputStream in, final BufferedBlockCipher cipher) {
super(in, cipher.getOutputSize(SymmetricStreamBuilder.BUF_SIZE));
this.cipher = cipher;
}
/**
* Processes a block of input using the {@link BufferedBlockCipher}.
* <p>
* If {@code inputLen == 0}, the cipher is finalized via
* {@link BufferedBlockCipher#doFinal(byte[], int)}, completing any remaining
* buffered input and writing final output (including padding, if applicable).
* Otherwise, the method delegates to
* {@link BufferedBlockCipher#processBytes(byte[], int, int, byte[], int)}.
*
* @param input the input buffer containing plaintext or ciphertext bytes;
* ignored when {@code inputLen == 0}
* @param inputLen number of bytes to process from the input buffer, or
* {@code 0} to trigger finalization
* @param output the output buffer to write processed data into
* @return the number of bytes written to the output buffer
*
* @throws DataLengthException if the input data is too large for the
* cipher
* @throws IllegalStateException if the cipher has not been properly
* initialized
* @throws InvalidCipherTextException if finalization fails due to invalid
* padding or corrupted ciphertext
*/
@Override
protected int processCipher(final byte[] input, final int inputLen, final byte[] output)
throws InvalidCipherTextException {
if (inputLen == 0) {
return cipher.doFinal(output, 0);
}
return cipher.processBytes(input, 0, inputLen, output, 0);
}
}
/**
* An {@link InputStream} that applies an {@link AEADBlockCipher} to transform
* the data during reading.
* <p>
* AEAD (Authenticated Encryption with Associated Data) ciphers require
* finalization to validate authentication tags.
*/
class AEADBlockCipherInputStream extends SymmetricCipherInputStreamBase {
private final AEADBlockCipher cipher;
/**
* Creates a new stream that wraps the given input and processes it using an
* {@link AEADBlockCipher}.
*
* @param in the input stream to wrap
* @param cipher the initialized {@link AEADBlockCipher}
*/
public AEADBlockCipherInputStream(final InputStream in, final AEADBlockCipher cipher) {
super(in, cipher.getOutputSize(SymmetricStreamBuilder.BUF_SIZE) + AesSupport.BLOCK_SIZE);
this.cipher = cipher;
}
/**
* Processes a block of input using the {@link AEADBlockCipher}.
* <p>
* If {@code inputLen == 0}, the cipher is finalized via
* {@link AEADBlockCipher#doFinal(byte[], int)}, completing encryption or
* decryption and verifying the authentication tag. If authentication fails, an
* {@link InvalidCipherTextException} is thrown.
* <p>
* For normal processing, the method delegates to
* {@link AEADBlockCipher#processBytes(byte[], int, int, byte[], int)} to
* transform the input data into ciphertext or plaintext.
*
* @param input the input buffer containing plaintext or ciphertext data;
* ignored when {@code inputLen == 0}
* @param inputLen number of bytes to process, or {@code 0} to finalize and
* verify authentication
* @param output the output buffer to receive processed data
* @return the number of bytes written to the output buffer
*
* @throws IllegalStateException if the cipher is not properly initialized
* @throws InvalidCipherTextException if finalization fails due to
* authentication tag mismatch
*/
@Override
protected int processCipher(final byte[] input, final int inputLen, final byte[] output)
throws InvalidCipherTextException {
if (inputLen == 0) {
return cipher.doFinal(output, 0);
}
return cipher.processBytes(input, 0, inputLen, output, 0);
}
}
/**
* An {@link InputStream} that applies a {@link StreamCipher} to transform the
* data during reading.
* <p>
* Stream ciphers operate byte-by-byte and do not require finalization.
*/
class StreamCipherInputStream extends SymmetricCipherInputStreamBase {
private final StreamCipher cipher;
/**
* Creates a new stream that wraps the given input and processes it using a
* {@link StreamCipher}.
*
* @param in the input stream to wrap
* @param cipher the initialized {@link StreamCipher}
*/
public StreamCipherInputStream(final InputStream in, final StreamCipher cipher) {
super(in, SymmetricStreamBuilder.BUF_SIZE);
this.cipher = cipher;
}
/**
* Processes a block of input using the stream cipher. Finalization is a no-op
* for stream ciphers (returns 0 bytes).
*
* @param input input buffer
* @param inputLen number of bytes to process, or {@code 0} to signal end of
* stream
* @param output output buffer
* @return number of bytes written to output
*/
@Override
protected int processCipher(final byte[] input, final int inputLen, final byte[] output) {
if (inputLen == 0) {
// No finalization for StreamCipher, just indicate end of stream
return 0;
}
cipher.processBytes(input, 0, inputLen, output, 0);
return inputLen;
}
}