/** * 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). *

* 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: *

* 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}. * *

* This method performs two key operations: *

* * *

* 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. *

* * @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. *

* 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. *

* Implementations must detect {@code inputLen == 0} to perform finalization if * the cipher supports it (e.g., for block or AEAD ciphers). *

* 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. *

* 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}. *

* 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. *

* 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}. *

* 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. *

* 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. *

* 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; } }