Initial commit

This commit is contained in:
2025-07-30 21:40:09 +02:00
commit e3e6d8cb12
149 changed files with 21207 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
/*******************************************************************************
* 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.builder;
import zeroecho.data.DataContent;
import zeroecho.data.processing.AesDecryptor;
import zeroecho.data.processing.AesEncryptor;
/**
* Builder interface for constructing AES encryption or decryption
* {@link DataContent} instances.
* <p>
* The builder can be configured to build either an AES encryptor or decryptor.
* The constructed {@code DataContent} will perform the corresponding
* cryptographic operation.
*
* <p>
* Usage example:
*
* <pre>{@code
* DataContent encryptor = AesBuilder.builder().build(true);
*
* DataContent decryptor = AesBuilder.builder().build(false);
* }</pre>
*/
public interface AesBuilder extends DataContentBuilder<DataContent> { // NOPMD
/**
* Creates a new instance of the default AES builder implementation.
*
* @return a new {@code AesBuilder}
*/
static AesBuilder builder() {
return new DefaultAesContentBuilder();
}
/**
* Default implementation of the {@link AesBuilder} interface.
* <p>
* Builds an AES encryption or decryption content instance depending on the
* {@code encrypt} parameter passed to {@link #build(boolean)}.
* </p>
*/
final class DefaultAesContentBuilder implements AesBuilder {
private DefaultAesContentBuilder() {
}
/**
* Builds and returns an AES encryptor or decryptor.
*
* @param encrypt {@code true} to build an {@link AesEncryptor}, {@code false}
* to build an {@link AesDecryptor}
* @return a {@link DataContent} instance configured for encryption or
* decryption
*/
@Override
public DataContent build(final boolean encrypt) {
return encrypt ? new AesEncryptor(null) : new AesDecryptor(null);
}
}
}

View File

@@ -0,0 +1,197 @@
/**
* 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.builder;
import java.util.Objects;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
import zeroecho.data.processing.SecretAesRandom;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
/**
* A builder interface for constructing {@link SecretAesRandom} instances, which
* produce AES-based pseudorandom data wrapped as {@link PlainContent}.
*
* <p>
* This builder allows configuring:
* <ul>
* <li>The AES mode (which determines key length: 128, 192, or 256 bits)</li>
* <li>The AES cipher type (e.g., CBC, GCM)</li>
* <li>An optional input source that feeds into the random generator</li>
* <li>Additional Authenticated Data (AAD) to be associated with AES cipher
* operations</li>
* </ul>
*
* <p>
* The {@link #build(boolean)} method produces the configured instance and
* accepts a boolean flag indicating whether the content should operate in
* encryption or decryption mode. When an input is provided via
* {@link #input(DataContent)}, the meaning of this flag becomes relevant:
* </p>
* <ul>
* <li>If {@code encrypt} is {@code true}, the builder produces a randomizer
* suitable for encryption.</li>
* <li>If {@code encrypt} is {@code false}, the builder configures the content
* for decryption based on the input.</li>
* <li>If no input is provided, the {@code encrypt} flag may be ignored.</li>
* </ul>
*
* <p>
* Usage example:
* </p>
* <pre>{@code
* PlainContent random = AesRandomBuilder.builder()
* .mode(AesMode.AES_256)
* .cipherType(AesCipherType.CBC)
* .withAad(new byte[] { ... })
* .build(true);
* }</pre>
*/
public interface AesRandomBuilder extends DataContentBuilder<PlainContent> {
/**
* Sets the AES mode, which determines the key length (128, 192, or 256 bits).
*
* @param mode the AES mode; must not be null
* @return this builder instance for method chaining
* @throws NullPointerException if {@code mode} is null
*/
AesRandomBuilder mode(AesMode mode);
/**
* Sets the AES cipher type (e.g., CBC, GCM).
*
* @param cipherType the cipher type; must not be null
* @return this builder instance for method chaining
* @throws NullPointerException if {@code cipherType} is null
*/
AesRandomBuilder cipherType(AesCipherType cipherType);
/**
* Sets the input {@link DataContent} to be used as source.
*
* @param input the data content to wrap; must not be null
* @return this builder instance for method chaining
* @throws NullPointerException if {@code input} is null
*/
AesRandomBuilder input(DataContent input);
/**
* Sets the Additional Authenticated Data (AAD) to be associated with the AES
* cipher.
*
* @param aad the additional authenticated data to use; may be {@code null} if
* none
* @return this builder instance for method chaining
*/
AesRandomBuilder withAad(byte[] aad);
/**
* Creates a new instance of the default builder implementation.
*
* @return a new {@code AesRandomBuilder}
*/
static AesRandomBuilder builder() {
return new DefaultAesRandomBuilder();
}
/**
* Default implementation of the {@link AesRandomBuilder} interface.
* <p>
* Builds a {@link SecretAesRandom} instance with the configured AES mode,
* cipher type, and AAD.
* </p>
*/
final class DefaultAesRandomBuilder implements AesRandomBuilder {
private AesMode modeField;
private AesCipherType cipherTypeField;
private DataContent source;
private byte[] aadField;
private DefaultAesRandomBuilder() {
}
@Override
public AesRandomBuilder mode(final AesMode mode) {
this.modeField = Objects.requireNonNull(mode, "mode must not be null");
return this;
}
@Override
public AesRandomBuilder cipherType(final AesCipherType cipherType) {
this.cipherTypeField = Objects.requireNonNull(cipherType, "cipherType must not be null");
return this;
}
@Override
public AesRandomBuilder input(final DataContent input) {
this.source = Objects.requireNonNull(input, "input must not be null");
return this;
}
@Override
public AesRandomBuilder withAad(final byte[] aad) {
this.aadField = aad; // NOPMD
return this;
}
/**
* Builds and returns the {@link SecretAesRandom} instance configured by this
* builder.
*
* @param encrypt {@code true} to configure for encryption mode, {@code false}
* for decryption mode
* @return a configured {@link PlainContent} instance representing the AES
* random content
* @throws IllegalStateException if AES mode or cipher type has not been set
*/
@Override
public PlainContent build(final boolean encrypt) {
if (modeField == null) {
throw new IllegalStateException("AES mode must be set before building");
}
if (cipherTypeField == null) {
throw new IllegalStateException("AES cipher type must be set before building");
}
final SecretAesRandom secretAesRandom = new SecretAesRandom(modeField, cipherTypeField, aadField);
if (source != null) {
secretAesRandom.setInput(source);
}
return secretAesRandom;
}
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* 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.builder;
import zeroecho.data.DataContent;
/**
* A builder interface for constructing instances of {@link DataContent}.
* <p>
* This interface enables a uniform and extensible way to create various
* {@code DataContent} implementations in a fluent, builder-based style. It is
* intended to be used with builder classes that encapsulate construction
* parameters and logic specific to each {@code DataContent} subtype.
* <p>
* The {@link #build(boolean)} method configures the resulting
* {@code DataContent} instance for either encryption or decryption mode. When
* encryption is selected, additional metadata (such as headers) may be added to
* the output stream; this metadata must be preserved and reused during
* decryption.
* <p>
* Typical usage example:
*
* <pre>{@code
* DataContent content = SomeDataContentBuilder.builder()
* .parameterX(...)
* .parameterY(...)
* .build(true); // true for encryption mode
* }</pre>
*
* @param <T> the type of {@link DataContent} produced by this builder
*
* @author Leo Galambos
*/
public interface DataContentBuilder<T extends DataContent> { // NOPMD
/**
* Constructs and returns a new {@link DataContent} instance based on the
* builder's current configuration.
*
* @param encrypt whether the resulting {@code DataContent} should be configured
* for encryption ({@code true}) or decryption ({@code false}).
* In encryption mode, additional headers or metadata may be
* inserted into the stream which must be retained for successful
* decryption.
* @return the constructed {@code DataContent} instance
*/
T build(boolean encrypt);
}

View File

@@ -0,0 +1,158 @@
/*******************************************************************************
* 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.builder;
import zeroecho.data.DataContent;
/**
* A builder interface for constructing a chain of {@link DataContent}
* components, where each content unit passes its output as the input to the
* next one.
*
* <p>
* This builder simplifies the composition of processing pipelines such as:
* <ul>
* <li>Encryption with multiple layers (e.g., compression → encryption →
* signing)</li>
* <li>Decryption chains (e.g., verification → decryption → decompression)</li>
* </ul>
*
* <p>
* The builder supports both encryption and decryption modes, which are applied
* consistently across all {@link DataContentBuilder} instances in the chain.
* </p>
*
* <p>
* Usage example: <pre>{@code
* DataContent chain = DataContentChainBuilder.encrypt()
* .add(PlainStringBuilder.builder().value("hello"))
* .add(DerivedAesParametersBuilder.builder().password("secret"))
* .build();
* }</pre>
*/
public interface DataContentChainBuilder {
/**
* Adds a {@link DataContentBuilder} to the chain and links its output to the
* input of the previously added {@code DataContent}, if any.
*
* <p>
* The {@link DataContent} produced by this builder becomes the new tail of the
* chain.
* </p>
*
* @param builder the builder producing the next {@code DataContent}; must not
* be null
* @return this chain builder instance, allowing for fluent chaining
* @throws NullPointerException if {@code builder} is null
*/
DataContentChainBuilder add(DataContentBuilder<? extends DataContent> builder);
/**
* Finalizes the chain and returns the tail {@link DataContent} instance.
*
* <p>
* The returned content may internally reference earlier content via
* {@link DataContent#setInput(DataContent)} chaining.
* </p>
*
* @return the last {@code DataContent} in the chain, or {@code null} if no
* builders were added
*/
DataContent build();
/**
* Creates a new {@code DataContentChainBuilder} configured for encryption mode.
*
* <p>
* All added {@link DataContentBuilder} instances will receive {@code true} for
* their {@code build(boolean encrypt)} method, indicating encryption behavior.
* </p>
*
* @return a new chain builder in encryption mode
*/
static DataContentChainBuilder encrypt() {
return new DefaultDataContentChainBuilder(true);
}
/**
* Creates a new {@code DataContentChainBuilder} configured for decryption mode.
*
* <p>
* All added {@link DataContentBuilder} instances will receive {@code false} for
* their {@code build(boolean encrypt)} method, indicating decryption behavior.
* </p>
*
* @return a new chain builder in decryption mode
*/
static DataContentChainBuilder decrypt() {
return new DefaultDataContentChainBuilder(false);
}
/**
* Default implementation of {@link DataContentChainBuilder}.
*
* <p>
* Maintains a reference to the tail of the content chain. Each added builder is
* built in the specified encryption/decryption mode and connected via
* {@link DataContent#setInput(DataContent)} to the previously built content.
* </p>
*/
final class DefaultDataContentChainBuilder implements DataContentChainBuilder {
private DataContent tail;
private final boolean encrypt;
private DefaultDataContentChainBuilder(final boolean encrypt) {
this.encrypt = encrypt;
}
@Override
public DataContentChainBuilder add(final DataContentBuilder<? extends DataContent> builder) {
final DataContent previous = tail;
tail = builder.build(encrypt);
if (previous != null) {
tail.setInput(previous);
}
return this;
}
@Override
public DataContent build() {
return tail;
}
}
}

View File

@@ -0,0 +1,222 @@
/*******************************************************************************
* 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.builder;
import java.security.spec.InvalidKeySpecException;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.SecretContent;
import zeroecho.data.processing.SecretDerivedAesParameters;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
/**
* A builder interface for constructing {@link SecretDerivedAesParameters}
* instances that encapsulate AES encryption parameters derived from a password.
*
* <p>
* This builder enables configuration of password-based encryption using PBKDF2
* (or a similar KDF), including selection of key strength, cipher type,
* iteration count, and optional Additional Authenticated Data (AAD) for AEAD
* cipher modes.
* </p>
*
* <p>
* Typical usage:
* </p>
*
* <pre>{@code
* SecretContent secret = DerivedAesParametersBuilder.builder()
* .password("myStrongPassword")
* .iterations(100_000)
* .mode(AesMode.AES_256)
* .cipherType(AesCipherType.GCM)
* .withAad(aadBytes)
* .build(true); // true for encryption mode
* }</pre>
*
* <p>
* If invalid parameters are provided (e.g., nulls or unsupported key specs),
* {@link IllegalArgumentException} will be thrown during the build process.
* </p>
*/
public interface DerivedAesParametersBuilder extends DataContentBuilder<SecretContent> {
/**
* Sets the password used to derive the AES key.
* <p>
* This password must be non-null and should have sufficient entropy for secure
* encryption.
* </p>
*
* @param password the password string; must not be {@code null} or empty
* @return this builder instance
* @throws IllegalArgumentException if password is null or empty
*/
DerivedAesParametersBuilder password(String password);
/**
* Specifies the number of iterations for the key derivation function.
* <p>
* Higher iteration counts slow down brute-force attacks, but may increase
* processing time.
* </p>
*
* @param iterations number of iterations; must be a positive integer
* @return this builder instance
* @throws IllegalArgumentException if iterations is not positive
*/
DerivedAesParametersBuilder iterations(int iterations);
/**
* Defines the AES mode to use, which determines the derived key length (e.g.,
* 128-bit, 192-bit, or 256-bit).
*
* @param mode the AES key size mode; must not be {@code null}
* @return this builder instance
* @throws NullPointerException if {@code mode} is null
*/
DerivedAesParametersBuilder mode(AesMode mode);
/**
* Specifies the AES cipher type (e.g., CBC or GCM) to be used for encryption or
* decryption.
*
* @param cipherType the cipher mode; must not be {@code null}
* @return this builder instance
* @throws NullPointerException if {@code cipherType} is null
*/
DerivedAesParametersBuilder cipherType(AesCipherType cipherType);
/**
* Sets Additional Authenticated Data (AAD) for the AES cipher.
* <p>
* This data will be included in the authentication tag for AEAD cipher modes
* such as GCM. It may be {@code null} if no AAD is used.
* </p>
*
* @param aad the additional authenticated data bytes, or {@code null}
* @return this builder instance
*/
DerivedAesParametersBuilder withAad(byte[] aad);
/**
* Creates a new builder instance with default values:
* <ul>
* <li>{@link AesMode#AES_256}</li>
* <li>{@link AesCipherType#CBC}</li>
* <li>{@code iterations = AesSupport.KEY_ITERATIONS}</li>
* <li>{@code aad = null}</li>
* </ul>
*
* @return a new {@code DerivedAesParametersBuilder}
*/
static DerivedAesParametersBuilder builder() {
return new DefaultDerivedAesParametersBuilder();
}
/**
* Default implementation of the {@link DerivedAesParametersBuilder} interface.
* <p>
* Builds a {@link SecretDerivedAesParameters} instance configured with the
* supplied parameters.
* <p>
* On failure to create the secret content due to invalid parameters or key
* specification errors, this builder logs the error and rethrows as an
* {@link IllegalArgumentException}.
*/
final class DefaultDerivedAesParametersBuilder implements DerivedAesParametersBuilder {
private static final Logger LOG = Logger
.getLogger(DerivedAesParametersBuilder.DefaultDerivedAesParametersBuilder.class.getName());
private String passwordField;
private int iterationsField = AesSupport.KEY_ITERATIONS;
private AesMode modeField = AesMode.AES_256;
private AesCipherType cipherTypeField = AesCipherType.CBC;
private byte[] aadField;
private DefaultDerivedAesParametersBuilder() {
}
@Override
public DerivedAesParametersBuilder password(final String password) {
if (password == null || password.isEmpty()) {
throw new IllegalArgumentException("password must not be null or empty");
}
this.passwordField = password;
return this;
}
@Override
public DerivedAesParametersBuilder iterations(final int iterations) {
if (iterations <= 0) {
throw new IllegalArgumentException("iterations must be positive");
}
this.iterationsField = iterations;
return this;
}
@Override
public DerivedAesParametersBuilder mode(final AesMode mode) {
this.modeField = Objects.requireNonNull(mode, "mode must not be null");
return this;
}
@Override
public DerivedAesParametersBuilder cipherType(final AesCipherType cipherType) {
this.cipherTypeField = Objects.requireNonNull(cipherType, "cipherType must not be null");
return this;
}
@Override
public DerivedAesParametersBuilder withAad(final byte[] aad) {
this.aadField = aad; // NOPMD
return this;
}
@Override
public SecretContent build(final boolean encrypt) {
try {
return new SecretDerivedAesParameters(passwordField, iterationsField, aadField, modeField,
cipherTypeField, encrypt);
} catch (IllegalArgumentException | InvalidKeySpecException e) {
LOG.log(Level.WARNING, "Exception during build", e);
throw new IllegalArgumentException(e);
}
}
}
}

View File

@@ -0,0 +1,257 @@
/**
* 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.builder;
import java.io.IOException;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Objects;
import zeroecho.data.processing.SecretKEMAesParameters;
import zeroecho.util.KeySupport;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.asymmetric.AsymmetricContext;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
/**
* Builder for {@link SecretKEMAesParameters}, allowing construction from either
* a {@link PublicKey} (for encryption) or a {@link PrivateKey} (for
* decryption).
*
* <p>
* This builder ensures that the provided key corresponds to a
* {@link KEMAsymmetricContext}. If the key type is not supported by the KEM
* infrastructure, the {@link #build(boolean)} method will fail.
* </p>
*
* <p>
* Validation rules:
* </p>
* <ul>
* <li>For encryption mode ({@code encrypt = true}), the resulting
* {@link KEMAsymmetricContext} must have a {@code null} extractor (meaning it
* can only generate encapsulated keys, not decapsulate).</li>
* <li>For decryption mode ({@code encrypt = false}), the resulting
* {@link KEMAsymmetricContext} must have a {@code null} generator (meaning it
* can only extract secrets, not generate them).</li>
* </ul>
*/
public interface KEMAesParametersBuilder extends DataContentBuilder<SecretKEMAesParameters> {
/**
* Sets the asymmetric key used to build the KEM context.
*
* <p>
* The provided key determines the operational mode:
* </p>
* <ul>
* <li>Public key → encryption mode.</li>
* <li>Private key → decryption mode.</li>
* </ul>
*
* @param key the public or private key; must not be {@code null}
* @return this builder instance for method chaining
* @throws NullPointerException if {@code key} is {@code null}
*/
KEMAesParametersBuilder withKey(Key key);
/**
* Sets the AES mode.
*
* @param aesMode the AES mode (e.g., {@link AesMode#AES_128},
* {@link AesMode#AES_256}); must not be {@code null}
* @return this builder instance for method chaining
* @throws NullPointerException if {@code aesMode} is {@code null}
*/
KEMAesParametersBuilder withAesMode(AesMode aesMode);
/**
* Sets the AES cipher type.
*
* @param cipherType the cipher type (e.g., {@link AesCipherType#CBC},
* {@link AesCipherType#GCM}); must not be {@code null}
* @return this builder instance for method chaining
* @throws NullPointerException if {@code cipherType} is {@code null}
*/
KEMAesParametersBuilder withCipherType(AesCipherType cipherType);
/**
* Sets optional Additional Authenticated Data (AAD) for authenticated modes.
*
* @param aad the AAD bytes; may be {@code null}
* @return this builder instance for method chaining
*/
KEMAesParametersBuilder withAAD(byte[] aad);
/**
* Creates a new {@code KEMAesParametersBuilder} instance with default settings:
* <ul>
* <li>{@link AesMode#AES_256}</li>
* <li>{@link AesCipherType#GCM}</li>
* <li>empty AAD ({@code new byte[0]})</li>
* </ul>
*
* @return a new builder instance
*/
static KEMAesParametersBuilder builder() {
return new DefaultKEMAesParametersBuilder();
}
/**
* Default implementation of {@link KEMAesParametersBuilder}.
*
* <p>
* Performs parameter validation and constructs a {@link SecretKEMAesParameters}
* instance based on the configured key, AES mode, cipher type, and optional
* AAD.
* </p>
*/
final class DefaultKEMAesParametersBuilder implements KEMAesParametersBuilder {
private Key key; // May be PublicKey or PrivateKey
private AesMode aesMode = AesMode.AES_256;
private AesCipherType cipherType = AesCipherType.GCM;
private byte[] aad = {};
private DefaultKEMAesParametersBuilder() {
}
@Override
public KEMAesParametersBuilder withKey(final Key key) {
this.key = Objects.requireNonNull(key, "key must not be null");
return this;
}
@Override
public KEMAesParametersBuilder withAesMode(final AesMode aesMode) {
this.aesMode = Objects.requireNonNull(aesMode, "aesMode must not be null");
return this;
}
@Override
public KEMAesParametersBuilder withCipherType(final AesCipherType cipherType) {
this.cipherType = Objects.requireNonNull(cipherType, "cipherType must not be null");
return this;
}
@Override
public KEMAesParametersBuilder withAAD(final byte[] aad) {
this.aad = aad; // NOPMD
return this;
}
/**
* Builds a {@link SecretKEMAesParameters} instance.
*
* <p>
* The method determines whether encryption or decryption mode should be used
* based on the provided key type:
* </p>
* <ul>
* <li>{@link java.security.PublicKey} → encryption</li>
* <li>{@link java.security.PrivateKey} → decryption</li>
* </ul>
*
* <p>
* Validation includes:
* </p>
* <ul>
* <li>Ensuring the key corresponds to a KEM-capable context.</li>
* <li>Verifying generator/extractor presence depending on the mode.</li>
* </ul>
*
* @param encrypt {@code true} for encryption mode; {@code false} for decryption
* mode
* @return a fully configured {@link SecretKEMAesParameters} instance
* @throws IllegalArgumentException if the key type is invalid, if mode and key
* mismatch, or if the underlying context
* cannot be created
* @throws NullPointerException if required fields are not set
*/
@Override
public SecretKEMAesParameters build(final boolean encrypt) { // NOPMD
Objects.requireNonNull(key, "Key must be set before building");
Objects.requireNonNull(aesMode, "AES mode must be set before building");
Objects.requireNonNull(cipherType, "Cipher type must be set before building");
try {
AsymmetricContext ctx = switch (key) { // NOPMD
case PublicKey pubk -> {
if (!encrypt) {
throw new IllegalArgumentException("Decrypt needs a private key");
}
yield KeySupport.fromKey(pubk);
}
case PrivateKey privk -> {
if (encrypt) {
throw new IllegalArgumentException("Encrypt needs a public key");
}
yield KeySupport.fromKey(privk);
}
default -> {
throw new IllegalArgumentException("Provided key does not correspond to a KEM-capable context: "
+ key.getClass().getName());
}
};
if (!(ctx instanceof KEMAsymmetricContext kemContext)) {
throw new IllegalArgumentException(
"Provided key does not correspond to a KEM-capable context: " + key.getClass().getName());
}
// Validate context capability - just to be sure - if it fails here,
// KeySupport.fromKey is buggy
if (encrypt) {
if (kemContext.generator() == null) {
throw new IllegalArgumentException(
"KEM generator is not defined; this key cannot be used for encryption.");
}
} else {
if (kemContext.extractor() == null) {
throw new IllegalArgumentException(
"KEM extractor is not defined; this key cannot be used for decryption.");
}
}
return new SecretKEMAesParameters(kemContext, aesMode, cipherType, aad);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}
}

View File

@@ -0,0 +1,247 @@
/**
* 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.builder;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import zeroecho.data.EncryptedContent;
import zeroecho.data.processing.SecretMultiRecipientCryptor;
/**
* A builder interface for creating instances of
* {@link SecretMultiRecipientCryptor}, supporting either encryption with
* multiple public keys or decryption with a private key.
* <p>
* This builder enables fluent and type-safe construction of multi-recipient
* cryptographic content handlers. It enforces mutual exclusivity between
* encryption and decryption modes: you may either add one or more recipients
* (and optionally decoys) for encryption, or specify a private key for
* decryption, but not both.
*
* <h2>Usage Examples</h2>
*
* <h3>Encryption with recipients and decoys:</h3> <pre>{@code
* EncryptedContent cryptor = MultiRecipientCryptorBuilder.builder()
* .addRecipient(publicKey1)
* .addRecipient(publicKey2)
* .addDecoy(decoyKey1)
* .addDecoy(decoyKey2)
* .build(true); // true = encryption mode
* }</pre>
*
* <h3>Decryption with private key:</h3> <pre>{@code
* EncryptedContent cryptor = MultiRecipientCryptorBuilder.builder()
* .privateKey(myPrivateKey)
* .build(false); // false = decryption mode
* }</pre>
*
* <p>
* Attempting to mix public recipients/decoys and a private key within the same
* builder instance will result in an {@link IllegalStateException}.
*
* @see SecretMultiRecipientCryptor
* @see EncryptedContent
*/
public interface MultiRecipientCryptorBuilder extends DataContentBuilder<EncryptedContent> {
/**
* Adds a collection of public key recipients for encryption mode. If a private
* key has already been configured, this call will throw an
* {@link IllegalStateException}.
*
* @param recipients a non-null collection of public keys
* @return this builder instance
* @throws NullPointerException if {@code recipients} is null
* @throws IllegalStateException if the builder is already configured with a
* private key
*/
MultiRecipientCryptorBuilder addRecipients(Collection<PublicKey> recipients);
/**
* Adds a single public key recipient for encryption mode. Multiple calls are
* cumulative. If a private key has already been configured, this call will
* throw an {@link IllegalStateException}.
*
* @param recipient a non-null public key
* @return this builder instance
* @throws NullPointerException if {@code recipient} is null
* @throws IllegalStateException if the builder is already configured with a
* private key
*/
MultiRecipientCryptorBuilder addRecipient(PublicKey recipient);
/**
* Configures the builder for decryption using the specified private key. Once
* set, any previously added public key recipients will be ignored. This method
* cannot be used if recipients were already added.
*
* @param privateKey a non-null private key
* @return this builder instance
* @throws NullPointerException if {@code privateKey} is null
* @throws IllegalStateException if recipients have already been configured
*/
MultiRecipientCryptorBuilder privateKey(PrivateKey privateKey);
/**
* Adds a collection of decoy public keys for encryption mode. These keys will
* be indistinguishable from actual recipients but are not able to decrypt the
* message. Useful for obfuscating true recipients.
*
* @param decoys a non-null collection of public keys
* @return this builder instance
* @throws NullPointerException if {@code decoys} is null
* @throws IllegalStateException if the builder is already configured with a
* private key
*/
MultiRecipientCryptorBuilder addDecoys(Collection<PublicKey> decoys);
/**
* Adds a single decoy public key for encryption mode. Multiple calls are
* cumulative. Decoys do not participate in decryption.
*
* @param decoy a non-null public key
* @return this builder instance
* @throws NullPointerException if {@code decoy} is null
* @throws IllegalStateException if the builder is already configured with a
* private key
*/
MultiRecipientCryptorBuilder addDecoy(PublicKey decoy);
/**
* Creates a new builder instance.
*
* @return a fresh {@code MultiRecipientCryptorBuilder}
*/
static MultiRecipientCryptorBuilder builder() {
return new DefaultMultiRecipientCryptorBuilder();
}
/**
* Default implementation of {@link MultiRecipientCryptorBuilder}.
*/
final class DefaultMultiRecipientCryptorBuilder implements MultiRecipientCryptorBuilder {
private final Set<PublicKey> allRecipients = new HashSet<>();
private final Set<PublicKey> allDecoys = new HashSet<>();
private PrivateKey privateKeyField;
private DefaultMultiRecipientCryptorBuilder() {
}
@Override
public MultiRecipientCryptorBuilder addRecipients(final Collection<PublicKey> recipients) {
if (privateKeyField == null) {
Objects.requireNonNull(recipients, "recipients must not be null");
} else {
throw new IllegalStateException(
"Both public keys and the private key cannot be configured for the cryptor.");
}
allRecipients.addAll(recipients);
return this;
}
@Override
public MultiRecipientCryptorBuilder addRecipient(final PublicKey recipient) {
if (privateKeyField == null) {
Objects.requireNonNull(recipient, "recipient must not be null");
} else {
throw new IllegalStateException(
"Both public keys and the private key cannot be configured for the cryptor.");
}
allRecipients.add(recipient);
return this;
}
@Override
public MultiRecipientCryptorBuilder privateKey(final PrivateKey privateKey) {
if (!allRecipients.isEmpty() || !allDecoys.isEmpty()) {
throw new IllegalStateException(
"Both public keys and the private key cannot be configured for the cryptor.");
}
Objects.requireNonNull(privateKey, "privateKey must not be null");
this.privateKeyField = privateKey;
return this;
}
@Override
public MultiRecipientCryptorBuilder addDecoys(final Collection<PublicKey> decoys) {
if (privateKeyField != null) {
throw new IllegalStateException("Cannot add decoys when a private key is configured.");
}
Objects.requireNonNull(decoys, "decoys must not be null");
allDecoys.addAll(decoys);
return this;
}
@Override
public MultiRecipientCryptorBuilder addDecoy(final PublicKey decoy) {
if (privateKeyField != null) {
throw new IllegalStateException("Cannot add decoy when a private key is configured.");
}
Objects.requireNonNull(decoy, "decoy must not be null");
allDecoys.add(decoy);
return this;
}
@Override
public EncryptedContent build(final boolean encrypt) {
if (privateKeyField != null) {
if (encrypt) {
throw new IllegalStateException("You requested encryption with a private key.");
}
return new SecretMultiRecipientCryptor(privateKeyField);
}
if (allRecipients.isEmpty() && allDecoys.isEmpty()) {
throw new IllegalStateException("No recipients or decoys configured for encryption.");
}
if (!encrypt) {
throw new IllegalStateException("You requested decryption with public keys.");
}
PublicKey[] recipients = allRecipients.toArray(new PublicKey[0]);
PublicKey[] decoys = allDecoys.toArray(new PublicKey[0]);
return new SecretMultiRecipientCryptor(recipients, decoys);
}
}
}

View File

@@ -0,0 +1,110 @@
/**
* 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.builder;
import java.util.Objects;
import zeroecho.data.PlainContent;
import zeroecho.data.processing.PlainBytes;
/**
* Builder interface for constructing {@link PlainBytes} instances that
* encapsulate unencrypted byte array content.
* <p>
* This builder allows specifying a raw byte array to be wrapped as
* {@code PlainContent}, which can be used as input for cryptographic operations
* or as standalone plaintext data.
* <p>
* The {@link #build(boolean)} method accepts an {@code encrypt} flag for API
* consistency, but the flag has no effect for plain byte content.
*
* <p>
* <strong>Usage example:</strong>
*
* <pre>{@code
* PlainContent content = PlainBytesBuilder.builder()
* .bytes(new byte[] { 1, 2, 3, 4 })
* .build(false); // encrypt flag is ignored for PlainBytes
* }</pre>
*
* @see PlainBytes
*/
public interface PlainBytesBuilder extends DataContentBuilder<PlainContent> {
/**
* Sets the byte array content to be wrapped by {@link PlainBytes}.
*
* @param bytes the byte array; must not be null
* @return this builder instance for method chaining
* @throws NullPointerException if {@code bytes} is null
*/
PlainBytesBuilder bytes(byte[] bytes);
/**
* Creates a new instance of the default builder implementation.
*
* @return a new {@code PlainBytesBuilder}
*/
static PlainBytesBuilder builder() {
return new DefaultPlainBytesBuilder();
}
/**
* Default implementation of the {@link PlainBytesBuilder} interface.
* <p>
* Builds a {@link PlainBytes} instance wrapping the specified byte array.
* </p>
*/
final class DefaultPlainBytesBuilder implements PlainBytesBuilder {
private byte[] bytesField;
private DefaultPlainBytesBuilder() {
}
@Override
public PlainBytesBuilder bytes(final byte[] bytes) {
this.bytesField = Objects.requireNonNull(bytes, "bytes must not be null");
return this;
}
@Override
public PlainContent build(final boolean encrypt) {
if (bytesField == null) {
throw new IllegalStateException("bytes must be set before building");
}
return new PlainBytes(bytesField);
}
}
}

View File

@@ -0,0 +1,105 @@
/*******************************************************************************
* 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.builder;
import java.net.URL;
import java.util.Objects;
import zeroecho.data.PlainContent;
import zeroecho.data.processing.PlainFile;
/**
* Builder interface for constructing {@link PlainContent} instances that
* represent unencrypted file-based content sourced from a {@link URL}.
* <p>
* This builder allows specifying the source URL of a file to be used as plain
* (non-encrypted) content. It is typically used as input for cryptographic
* operations or for representing raw file data.
* <p>
* The {@link #build(boolean)} method accepts an {@code encrypt} flag to comply
* with the {@link DataContentBuilder} interface; however, this flag is ignored
* for plain content types.
* <p>
* <strong>Usage example:</strong>
*
* <pre>{@code
* PlainContent plainFile = PlainFileBuilder.builder()
* .url(new URL("file:///path/to/file.txt"))
* .build(true); // encrypt flag is ignored for PlainFile
* }</pre>
*
* @see PlainFile
*/
public interface PlainFileBuilder extends DataContentBuilder<PlainContent> {
/**
* Sets the URL of the file to be used as the plain content source.
*
* @param url the URL of the file; must not be {@code null}
* @return this builder instance
*/
PlainFileBuilder url(URL url);
/**
* Creates a new instance of the default builder implementation.
*
* @return a new {@code PlainFileBuilder}
*/
static PlainFileBuilder builder() {
return new DefaultPlainFileBuilder();
}
/**
* Default implementation of the {@link PlainFileBuilder} interface.
* <p>
* Builds a {@link PlainFile} instance using the specified URL.
*/
final class DefaultPlainFileBuilder implements PlainFileBuilder {
private URL urlField;
private DefaultPlainFileBuilder() {
}
@Override
public PlainFileBuilder url(final URL url) {
this.urlField = Objects.requireNonNull(url);
return this;
}
@Override
public PlainContent build(final boolean encrypt) {
return new PlainFile(urlField);
}
}
}

View File

@@ -0,0 +1,101 @@
/*******************************************************************************
* 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.builder;
import zeroecho.data.processing.PlainString;
/**
* Builder interface for constructing {@link PlainString} instances, which
* represent unencrypted textual content.
* <p>
* This builder supports specifying a string value to be encapsulated and
* optionally used in cryptographic processing pipelines, although the value
* itself remains unencrypted.
* <p>
* The {@link #build(boolean)} method accepts an {@code encrypt} flag to comply
* with the {@link DataContentBuilder} interface. However, this flag is ignored
* because {@code PlainString} represents unencrypted data.
* <p>
* Usage example:
*
* <pre>{@code
* PlainString plainText = PlainStringBuilder.builder()
* .value("Hello, World!")
* .build(true); // encrypt flag is ignored for PlainString
* }</pre>
*/
public interface PlainStringBuilder extends DataContentBuilder<PlainString> {
/**
* Creates a new instance of the default builder implementation.
*
* @return a new {@code PlainStringBuilder}
*/
static PlainStringBuilder builder() {
return new DefaultPlainStringBuilder();
}
/**
* Sets the string value that the {@link PlainString} instance will contain.
*
* @param value the string value; may be {@code null} or empty depending on
* usage
* @return this builder instance
*/
PlainStringBuilder value(String value);
/**
* Default implementation of the {@link PlainStringBuilder} interface.
* <p>
* Builds a {@link PlainString} instance using the specified string value.
*/
final class DefaultPlainStringBuilder implements PlainStringBuilder {
private String valueField;
private DefaultPlainStringBuilder() {
}
@Override
public PlainStringBuilder value(final String value) {
this.valueField = value;
return this;
}
@Override
public PlainString build(final boolean encrypt) {
return new PlainString(valueField);
}
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* 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.builder;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
import zeroecho.data.processing.ReclassifiedPlain;
/**
* Builder interface for constructing {@link ReclassifiedPlain} instances, which
* wrap an existing {@link DataContent} stream and reclassify it as plain
* content without modifying the underlying data.
* <p>
* Typically used to repurpose an encrypted or transformed stream back into a
* plain data stream for further processing.
* <p>
* Usage example:
*
* <pre>{@code
* PlainContent reclassified = ReclassifiedPlainBuilder.builder().build();
* }</pre>
*/
@Deprecated
public interface ReclassifiedPlainBuilder extends DataContentBuilder<PlainContent> { // NOPMD
/**
* Creates a new instance of the default {@code ReclassifiedPlainBuilder}.
*
* @return a new builder instance
*/
static ReclassifiedPlainBuilder builder() {
return new DefaultReclassifiedPlainBuilder();
}
/**
* Default implementation of {@link ReclassifiedPlainBuilder}.
*/
final class DefaultReclassifiedPlainBuilder implements ReclassifiedPlainBuilder {
private DefaultReclassifiedPlainBuilder() {
}
@Override
public PlainContent build(final boolean encrypt) {
return new ReclassifiedPlain();
}
}
}

View File

@@ -0,0 +1,89 @@
/*******************************************************************************
* 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.
******************************************************************************/
/**
* Provides builder interfaces and implementations for constructing various
* {@link zeroecho.data.DataContent} types and assembling content processing
* pipelines, including encryption and decryption workflows.
* <p>
* This package offers a modular and extensible approach to build
* {@code DataContent} chains in a fluent style. It currently supports:
* <ul>
* <li>Plain content builders such as {@link PlainStringBuilder} and
* {@link PlainFileBuilder}</li>
* <li>Symmetric encryption parameter builders, including
* {@link DerivedAesParametersBuilder} and {@link AesBuilder}</li>
* <li>Utility builders like {@link ReclassifiedPlainBuilder} for stream
* reclassification</li>
* </ul>
* <p>
* The builders can be combined to form flexible data processing pipelines. Each
* builder produces an instance of {@code DataContent} or its subtype, which can
* be chained by setting the input stream of the next stage.
* <p>
* <b>Example usage:</b>
*
* <pre>{@code
* DataContent output = DataContentChainBuilder.builder()
* // Start from a plain string input
* .add(PlainStringBuilder.builder().value("Example plaintext"))
* // Encrypt the input
* .add(AesBuilder.builder().encrypt())
* // Apply derived AES parameters with a password
* .add(DerivedAesParametersBuilder.builder().password("secretPassword").iterations(10000).mode(AesMode.AES_256)
* .cipherType(AesCipherType.CBC))
* // Reclassify encrypted stream as plain for further processing
* .add(ReclassifiedPlainBuilder.builder())
* // Decrypt using the same password-derived parameters
* .add(DerivedAesParametersBuilder.builder().password("secretPassword").iterations(10000).mode(AesMode.AES_256)
* .cipherType(AesCipherType.CBC))
* .add(AesBuilder.builder().decrypt())
* // Build the final data content chain
* .build();
* String decrypted = output.toText();
* }</pre>
* <p>
* Future extensions will include additional cryptographic parameter builders
* (e.g., for asymmetric algorithms), and support for other data content
* transformations.
*
* @author Leo Galambos
* @see zeroecho.data.DataContent
* @see PlainStringBuilder
* @see PlainFileBuilder
* @see DerivedAesParametersBuilder
* @see AesBuilder
* @see ReclassifiedPlainBuilder
*/
package zeroecho.builder;

View File

@@ -0,0 +1,178 @@
/**
* 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.covert;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Queue;
import java.util.TreeMap;
import zeroecho.util.RandomSupport;
/**
* A utility class for generating pseudo-random textual content based on
* predefined character frequency distributions.
* <p>
* The {@code TextualCodec} class contains a nested {@link Generator} class that
* can be configured with a set of character frequencies (e.g., English letter
* frequencies) to produce text that mimics the statistical distribution of the
* given language or character set.
*/
public class TextualCodec { // NOPMD
/**
* Private constructor to prevent instantiation of {@code TextualCodec}.
* <p>
* This class is intended to be used as a utility container for static members
* and should not be instantiated.
*/
private TextualCodec() {
}
/**
* Generates characters or strings using a frequency-based distribution.
* <p>
* This generator uses a cumulative frequency table internally to map random
* numbers to characters, enabling the creation of realistic-looking text that
* follows the given character frequency distribution. The generator also avoids
* consecutive duplicate characters by employing a simple queuing mechanism.
*/
public static class Generator {
/**
* Internal map representing the cumulative frequency ranges mapped to
* characters.
*/
private final NavigableMap<Double, Character> ranges = new TreeMap<>();
/**
* The maximum value of the cumulative frequency range.
*/
private final double maxRange;
/**
* A predefined English character frequency distribution including the space
* character, based on typical usage in English text.
*/
public final static Map<Character, Double> ENGLISH = Map.ofEntries(Map.entry('a', 8.2), Map.entry('b', 1.5),
Map.entry('c', 2.8), Map.entry('d', 4.3), Map.entry('e', 12.7), Map.entry('f', 2.2),
Map.entry('g', 2.0), Map.entry('h', 6.1), Map.entry('i', 7.0), Map.entry('j', 0.15),
Map.entry('k', 0.77), Map.entry('l', 4.0), Map.entry('m', 2.4), Map.entry('n', 6.7),
Map.entry('o', 7.5), Map.entry('p', 1.9), Map.entry('q', 0.095), Map.entry('r', 6.0),
Map.entry('s', 6.3), Map.entry('t', 9.1), Map.entry('u', 2.8), Map.entry('v', 0.98),
Map.entry('w', 2.4), Map.entry('x', 0.15), Map.entry('y', 2.0), Map.entry('z', 0.074),
Map.entry(' ', 25.4));
/**
* A default generator using the {@link #ENGLISH} frequency distribution.
*/
public final static Generator EN = new Generator(ENGLISH);
private Character lastChar = '~';
private final Queue<Character> backlog = new ArrayDeque<>();
/**
* Constructs a new {@code Generator} with the specified character frequency
* distribution.
*
* @param frequencies a map of characters to their relative frequencies (must be
* non-negative)
*/
public Generator(Map<Character, Double> frequencies) {
double cumulative = 0.0;
for (Map.Entry<Character, Double> entry : frequencies.entrySet()) {
double freq = entry.getValue();
if (freq <= 0) {
continue;
}
ranges.put(cumulative, entry.getKey());
cumulative = cumulative + freq;
}
maxRange = cumulative;
}
/**
* Generates a string of the specified length using the configured character
* frequency distribution. Consecutive duplicate characters are avoided when
* possible.
*
* @param length the number of characters to generate
* @return a randomly generated string
*/
public String getText(int length) {
StringBuffer sb = new StringBuffer();
while (length-- > 0) { // NOPMD
sb.append(getChar());
}
return sb.toString();
}
/**
* Returns the next randomly generated character, avoiding consecutive
* duplicates when possible.
*
* @return the next character in the generated sequence
*/
public char getChar() {
if (backlog.isEmpty() || lastChar.equals(backlog.peek())) {
Character next = getChar(RandomSupport.getRandom().nextDouble(maxRange));
while (lastChar.equals(next)) {
backlog.add(next);
next = getChar(RandomSupport.getRandom().nextDouble(maxRange));
}
lastChar = next;
return lastChar;
}
return lastChar = backlog.poll();
}
/**
* Returns a character based on the provided value in the frequency range.
*
* @param value a value between 0 (inclusive) and {@code maxRange} (exclusive)
* @return the corresponding character for the specified value
* @throws IllegalArgumentException if the value is out of range
*/
public char getChar(double value) {
if (value < 0.0 || value >= maxRange) {
throw new IllegalArgumentException("Value must be in [0.0, " + maxRange + ")");
}
Map.Entry<Double, Character> entry = ranges.floorEntry(value);
return (entry == null) ? ranges.firstEntry().getValue() : entry.getValue();
}
}
}

View File

@@ -0,0 +1,166 @@
/**
* 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.covert.jpeg;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter;
import org.apache.commons.imaging.formats.tiff.fieldtypes.AbstractFieldType;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputField;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
/**
* Embeds binary payloads into the EXIF metadata of a JPEG file using predefined
* {@link SlotType}s.
* <p>
* This class enables lossless modification of JPEG images by injecting custom
* data into specific EXIF fields. These fields are typically unused or
* repurposable for application-specific steganography or metadata tagging.
* </p>
*
* <p>
* The embedding process respects total capacity constraints and allows per-slot
* overrides to fine-tune the number of bytes stored in each {@link SlotType}.
* </p>
*/
public class JpegExifEmbedder {
/**
* Optional overrides for the default capacity of each {@link SlotType}. This
* allows finer control over how the payload is partitioned and embedded.
*/
private final Map<SlotType, Integer> capacityOverrides = new EnumMap<>(SlotType.class); // NOPMD
/**
* Constructs a new instance of {@code JpegExifEmbedder}.
* <p>
* This default constructor initializes the embedder without any specific
* configuration. Further setup may be required before using it to embed EXIF
* metadata into JPEG images.
*/
public JpegExifEmbedder() { // NOPMD
// empty
}
/**
* Overrides the default capacity for a specific EXIF {@link SlotType}.
*
* @param slot the EXIF slot whose capacity should be overridden
* @param bytes the new capacity in bytes for this slot
*/
public void overrideCapacity(SlotType slot, int bytes) {
capacityOverrides.put(slot, bytes);
}
/**
* Embeds the given payload into the EXIF metadata of a JPEG file and writes the
* result to the provided output stream.
* <p>
* The payload is split across the available EXIF {@link SlotType}s. The
* embedding respects the total capacity defined either by defaults or the
* overridden values.
* </p>
*
* @param jpegPath the path to the source JPEG file
* @param payloadInput the input stream containing the binary payload to embed
* @param jpegOutput the output stream to which the modified JPEG is written
* @throws IOException if file access or modification fails
* @throws IllegalArgumentException if the payload exceeds the total EXIF
* capacity
*/
public void embed(Path jpegPath, InputStream payloadInput, OutputStream jpegOutput) throws IOException {
byte[] jpegBytes = Files.readAllBytes(jpegPath);
byte[] payload = payloadInput.readAllBytes();
int totalCapacity = 0;
for (SlotType slot : SlotType.values()) {
totalCapacity += capacityOverrides.getOrDefault(slot, slot.defaultCapacity);
}
if (payload.length > totalCapacity) {
throw new IllegalArgumentException("Payload too large. Max capacity: " + totalCapacity + " bytes.");
}
Map<SlotType, byte[]> slotMap = splitPayload(payload);
JpegImageMetadata jpegMetadata = (JpegImageMetadata) Imaging.getMetadata(jpegBytes);
TiffOutputSet outputSet = (jpegMetadata != null && jpegMetadata.getExif() != null)
? jpegMetadata.getExif().getOutputSet()
: new TiffOutputSet();
TiffOutputDirectory exifDirectory = outputSet.getOrCreateExifDirectory();
for (Map.Entry<SlotType, byte[]> entry : slotMap.entrySet()) {
SlotType slot = entry.getKey();
byte[] data = entry.getValue();
exifDirectory.removeField(slot.tagInfo);
exifDirectory.add(new TiffOutputField(slot.tagInfo, AbstractFieldType.BYTE, data.length, data)); // NOPMD
}
try (ByteArrayInputStream jpegInputStream = new ByteArrayInputStream(jpegBytes)) {
new ExifRewriter().updateExifMetadataLossless(jpegInputStream, jpegOutput, outputSet);
}
}
private Map<SlotType, byte[]> splitPayload(byte[] payload) {
Map<SlotType, byte[]> result = new LinkedHashMap<>(); // NOPMD
int offset = 0;
for (SlotType slot : SlotType.values()) {
int capacity = capacityOverrides.getOrDefault(slot, slot.defaultCapacity);
if (offset > payload.length) {
break;
}
int chunkSize = Math.min(capacity, payload.length - offset);
byte[] chunk = Arrays.copyOfRange(payload, offset, offset + chunkSize);
result.put(slot, chunk);
offset += chunkSize;
}
return result;
}
}

View File

@@ -0,0 +1,140 @@
/**
* 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.covert.jpeg;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumMap;
import java.util.Map;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
import org.apache.commons.imaging.formats.tiff.TiffField;
/**
* Extracts embedded binary payloads from the EXIF metadata of a JPEG image.
* <p>
* This class is designed to reverse the embedding process performed by
* {@code JpegExifEmbedder}. It reads a JPEG file, locates specific EXIF fields
* designated for data storage (as defined by {@link SlotType}), and
* reconstructs the original payload into a binary stream.
* </p>
*
* <p>
* The extraction respects custom per-slot capacity overrides, ensuring that
* embedded payloads that don't fully utilize the maximum slot size can be
* detected by an early termination during read.
* </p>
*
* <p>
* This class assumes that the EXIF fields were populated sequentially and that
* the payload ends when a field is smaller than its declared or default
* capacity.
* </p>
*/
public class JpegExifExtractor {
/**
* Optional overrides for the capacity of each {@link SlotType}. These can be
* used to control how much data is expected from each slot during extraction.
*/
private final Map<SlotType, Integer> capacityOverrides = new EnumMap<>(SlotType.class); // NOPMD
/**
* Constructs a new instance of {@code JpegExifExtractor}.
* <p>
* This default constructor creates an extractor that can be used to read and
* retrieve EXIF metadata from JPEG image files.
*/
public JpegExifExtractor() { // NOPMD
// empty
}
/**
* Sets a custom capacity for the given slot. This allows the extractor to
* determine how much data to expect from each EXIF field.
*
* @param slot the slot whose capacity is being overridden
* @param bytes the maximum number of bytes expected from this slot
*/
public void overrideCapacity(SlotType slot, int bytes) {
capacityOverrides.put(slot, bytes);
}
/**
* Extracts an embedded binary payload from the EXIF fields of the specified
* JPEG image.
* <p>
* This method reads the JPEG file, parses its EXIF metadata, and collects
* binary data from all configured {@link SlotType} entries in the order they
* are defined. If a field contains fewer bytes than its capacity (default or
* overridden), extraction is stopped early, assuming the end of the payload has
* been reached.
* </p>
*
* @param jpegPath the path to the JPEG file containing embedded data
* @param payloadOutput the output stream to which the extracted binary data
* will be written
* @throws IOException if an I/O error occurs during file reading
* or writing
* @throws IllegalArgumentException if the JPEG file does not contain valid EXIF
* metadata
*/
public void extract(Path jpegPath, OutputStream payloadOutput) throws IOException {
byte[] jpegBytes = Files.readAllBytes(jpegPath);
JpegImageMetadata jpegMetadata = (JpegImageMetadata) Imaging.getMetadata(jpegBytes);
if (jpegMetadata == null || jpegMetadata.getExif() == null) {
throw new IllegalArgumentException("No EXIF metadata found.");
}
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
for (SlotType slot : SlotType.values()) {
TiffField field = jpegMetadata.findExifValue(slot.tagInfo);
if (field != null && field.getByteArrayValue() != null) {
final byte[] data = field.getByteArrayValue();
buffer.write(data);
if (data.length < capacityOverrides.getOrDefault(slot, slot.defaultCapacity)) {
break;
}
}
}
payloadOutput.write(buffer.toByteArray());
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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.covert.jpeg;
import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo;
/**
* Represents predefined EXIF metadata slots that can be used for embedding or
* extracting textual data within a JPEG image's metadata.
* <p>
* Each slot is associated with a specific {@link TagInfo} EXIF tag and a
* default capacity (in bytes), which can be used to determine how much data can
* be stored in that slot.
*/
public enum SlotType {
/**
* EXIF tag used for user comments. Commonly used for storing textual
* annotations.
*/
USER_COMMENT(ExifTagConstants.EXIF_TAG_USER_COMMENT, 4096),
/**
* EXIF tag typically used by camera manufacturers to store proprietary data.
*/
MAKER_NOTE(ExifTagConstants.EXIF_TAG_MAKER_NOTE, 4096),
/**
* EXIF tag indicating the version of the EXIF specification used.
*/
EXIF_VERSION(ExifTagConstants.EXIF_TAG_EXIF_VERSION, 1024),
/**
* EXIF tag representing the software used to process or generate the image.
*/
SOFTWARE(ExifTagConstants.EXIF_TAG_SOFTWARE, 2048);
/**
* The EXIF {@link TagInfo} associated with this slot.
*/
public final TagInfo tagInfo; // NOPMD
/**
* The default storage capacity (in bytes) for this EXIF slot.
*/
public final int defaultCapacity;
/**
* Constructs a {@code SlotType} with the specified EXIF tag and default
* capacity.
*
* @param tagInfo the {@link TagInfo} representing the EXIF tag
* @param defaultCapacity the default capacity in bytes for this slot
*/
SlotType(TagInfo tagInfo, int defaultCapacity) {
this.tagInfo = tagInfo;
this.defaultCapacity = defaultCapacity;
}
}

View File

@@ -0,0 +1,38 @@
/**
* 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.covert.jpeg;

View File

@@ -0,0 +1,61 @@
/**
* 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.
*/
/**
* Provides implementations of covert data embedding and extraction techniques.
* <p>
* The {@code zeroecho.covert} package includes utilities for hiding binary
* payloads within standard media formats, particularly images, using
* metadata-based steganography and other non-invasive channels. These
* techniques are designed to conceal the presence of data without altering the
* visible or functional aspects of the host content.
* <p>
* Current capabilities focus on:
* <ul>
* <li>Embedding encrypted or unstructured data into JPEG EXIF metadata
* fields</li>
* <li>Configurable allocation across multiple EXIF fields with capacity
* enforcement</li>
* <li>Reversible extraction pipelines for recovery of embedded payloads</li>
* <li>Modular design suitable for extending to new carrier formats and
* methods</li>
* </ul>
* Future support may include techniques based on file structure abuse, protocol
* headers, or steganographic encoding in binary or multimedia content.
* <p>
* This package is intended for research, secure communication, and
* privacy-preserving applications where the existence of a payload must be
* obfuscated or made non-obvious.
*/
package zeroecho.covert;

View File

@@ -0,0 +1,131 @@
/*******************************************************************************
* 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.data;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
/**
* Represents a generic unit of data content. This may include plain, secret, or
* encrypted forms of data.
*/
public interface DataContent { // NOPMD
/**
* Default maximum number of bytes that {@link #toBytes()} will read. This
* prevents accidental memory overload when reading large content into RAM.
*/
int DEFAULT_MAX_READ_SIZE = 32 * 1024 * 1024; // 32 MB
/**
* Returns an {@link InputStream} representing the content's data.
* <p>
* The stream may be raw or transformed depending on the content type.
*
* @return input stream of the content; never {@code null}
* @throws IOException if an I/O error occurs opening the stream
*/
InputStream getStream() throws IOException;
/**
* Returns the content's data as a byte array, with a size limit of 16MB by
* default.
* <p>
* This method is intended for small to medium-sized content. If the content
* exceeds {@link #DEFAULT_MAX_READ_SIZE}, a {@link IllegalStateException} is
* thrown.
*
* @return the content data as {@code byte[]}
* @throws RuntimeException if reading from the stream fails
* @throws IllegalStateException if content exceeds allowed memory limit
*/
default byte[] toBytes() {
try (InputStream in = getStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
final byte[] buffer = new byte[4096];
int total = 0;
int n;
while ((n = in.read(buffer)) != -1) { // NOPMD
total += n;
if (total > DEFAULT_MAX_READ_SIZE) {
throw new IllegalStateException("Content too large to buffer (" + total + " bytes)");
}
out.write(buffer, 0, n);
}
return out.toByteArray();
} catch (IOException e) {
throw new UncheckedIOException("Failed to read content stream", e);
}
}
/**
* Returns the content's data as a UTF-8 encoded string.
* <p>
* The default implementation converts {@code toBytes()} using UTF-8.
* <strong>Warning:</strong> This method is only safe if the content represents
* valid UTF-8 text. For arbitrary binary data (e.g., encrypted content), this
* may produce invalid or garbled output.
*
* @return the content data as {@code String}
* @throws RuntimeException if reading from the stream fails
* @throws IllegalArgumentException if the bytes are not valid UTF-8 (optional)
*/
default String toText() {
return new String(toBytes(), StandardCharsets.UTF_8);
}
/**
* Connects this content to the output of a previous stage in the pipeline.
* <p>
* This method is used to supply the input data that this content will process
* (e.g., a {@code PlainContent} being encrypted, or an encrypted content being
* decrypted).
* </p>
*
* @param input the {@code DataContent} instance providing upstream data; may be
* {@code null} if this is the start of the pipeline
* @throws UnsupportedOperationException if not overridden by a subclass
* @throws IllegalArgumentException if the input is not allowed by the
* implementation
*
* @implSpec The default implementation of this method throws
* {@link UnsupportedOperationException}. Subclasses that support
* chaining must override this method.
*/
default void setInput(DataContent input) {
throw new UnsupportedOperationException("This content type does not accept input chaining.");
}
}

View File

@@ -0,0 +1,45 @@
/*******************************************************************************
* 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.data;
/**
* Represents encrypted content, which is the result of an encryption process
* and can be safely deployed to a public space without security concerns.
* Deployment methods may include saving to a file, writing to standard output,
* or using steganography for enhanced secrecy.
*/
public interface EncryptedContent extends DataContent { // NOPMD
}

View File

@@ -0,0 +1,99 @@
/**
* 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.data;
/**
* An extension of {@link DataContent} that provides export capabilities in
* different modes.
* <p>
* This interface is intended for data sources that support not only raw binary
* streaming but also platform-specific script export, such as Bash or Windows
* CMD. The export mode determines how the underlying content is transformed or
* wrapped.
*
* <p>
* Typical use cases include:
* <ul>
* <li>Uploading images or data directly to a remote server (RAW mode)</li>
* <li>Generating self-contained Bash scripts that embed the encoded data
* (BASH_SCRIPT mode)</li>
* <li>Generating CMD scripts for use on Windows systems (CMD_SCRIPT mode)</li>
* </ul>
*
* <p>
* Implementations are responsible for adapting the content to the selected
* {@code ExportMode}.
*
* @see ExportableDataContent.ExportMode
* @see DataContent
*/
public interface ExportableDataContent extends DataContent {
/**
* Enumeration of supported export modes.
*/
enum ExportMode {
/**
* Raw mode returns the content as a direct InputStream, without any
* transformation or encoding.
*/
RAW,
/**
* Bash script mode returns a shell script with embedded Base64-encoded content,
* suitable for execution on Unix-like systems.
*/
BASH_SCRIPT,
/**
* CMD script mode returns a Windows batch file containing encoded content that
* can be decoded and used locally.
*/
CMD_SCRIPT
}
/**
* Returns the current export mode.
*
* @return the export mode that determines how the content will be formatted or
* transformed
*/
ExportMode getExportMode();
/**
* Sets the desired export mode.
*
* @param mode the export mode to use; must not be {@code null}
* @throws NullPointerException if the mode is {@code null}
*/
void setExportMode(ExportMode mode);
}

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* 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.data;
/**
* Represents a plain content that includes some original content, such as data
* from interactive input, a file, or the result of decrypting encrypted
* content.
*/
public interface PlainContent extends DataContent { // NOPMD
}

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* 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.data;
/**
* Represents secret content, which is a specific type of plain content that
* stores a secret phrase. This phrase may be obtained from input, read from a
* file, or generated dynamically.
*/
public interface SecretContent extends PlainContent { // NOPMD
}

View File

@@ -0,0 +1,104 @@
/**
* 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.data.output;
import java.util.Objects;
import zeroecho.data.DataContent;
import zeroecho.data.ExportableDataContent;
/**
* An abstract base class for exportable data content, providing default
* implementations for managing input data and export mode.
*
* <p>
* This class is intended to be extended by concrete implementations that
* generate various forms of export output, such as raw binary streams, shell
* scripts, or platform-specific wrappers.
* </p>
*
* <p>
* It manages:
* <ul>
* <li>The {@link DataContent} input that is to be exported</li>
* <li>The {@link ExportMode} that controls how the export is formatted</li>
* </ul>
*
* <p>
* Implementing classes must override {@link ExportableDataContent#getStream()}
* to provide the actual export logic based on the configured mode.
* </p>
*/
abstract class AbstractExportableDataContent implements ExportableDataContent {
/**
* The export mode (RAW, BASH_SCRIPT, CMD_SCRIPT, etc.). Defaults to RAW.
*/
protected ExportMode mode = ExportMode.RAW;
/**
* The input data to be exported. Must be non-null before use.
*/
protected DataContent input;
/**
* Sets the input {@link DataContent} for export.
*
* @param input the input data to be exported
* @throws NullPointerException if {@code input} is {@code null}
*/
@Override
public void setInput(DataContent input) {
this.input = Objects.requireNonNull(input, "Input content cannot be null.");
}
/**
* Returns the currently selected export mode.
*
* @return the {@link ExportMode} in use
*/
@Override
public ExportMode getExportMode() {
return mode;
}
/**
* Sets the export mode to control how the data is output.
*
* @param mode the desired {@link ExportMode}
*/
@Override
public void setExportMode(ExportMode mode) {
this.mode = mode;
}
}

View File

@@ -0,0 +1,194 @@
/**
* 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.data.output;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.Base64.Encoder;
/**
* A streaming {@link InputStream} that encodes binary input data into Base64
* format with optional line prefixes and suffixes for each encoded line.
*
* <p>
* This class is designed for script-friendly output formatting, such as
* generating platform-specific batch or shell script lines where each line
* might begin with a command (e.g., {@code echo }) and end with a
* platform-specific line terminator (e.g., {@code \n} for UNIX-like systems or
* {@code \r\n} for Windows).
* </p>
*
* <p>
* It supports chunked streaming and avoids holding the entire Base64-encoded
* content in memory, making it suitable for large binary inputs. Lines are
* split according to the specified maximum line length and are composed as
* follows:
* </p>
*
* <pre>
* [prefix][base64-encoded data][suffix]
* </pre>
*
* <p>
* If a suffix is defined, the actual Base64 data per line will be truncated to
* {@code lineLength - suffix.length} characters to ensure total line length
* does not exceed {@code lineLength}.
* </p>
*
* <p>
* This stream does not automatically insert newlines between lines unless
* explicitly provided via the {@code suffix} argument (e.g.,
* {@code "\n".getBytes()}).
* </p>
*
* <p>
* <strong>Usage example:</strong>
* </p>
*
* <pre>{@code
* InputStream source = new FileInputStream("input.bin");
* InputStream encoded = new Base64Stream(
* source,
* "echo ".getBytes(StandardCharsets.UTF_8),
* 76,
* "\n".getBytes(StandardCharsets.UTF_8)
* );
* encoded.transferTo(System.out);
* }</pre>
*
* @author Leo Galambos
*/
public class Base64Stream extends InputStream {
InputStream source;
InputStream in, prefix, suffix;
Encoder base64;
int pos = 0;
final int lineLength;
int lineBreak;
/**
* Constructs a new {@code Base64Stream}.
*
* @param source the raw binary input stream to be Base64 encoded
* @param linePrefix optional prefix to prepend at the beginning of each line
* (e.g., {@code "echo "}); can be {@code null} for no prefix
* @param lineLength the total maximum length of each output line, including any
* prefix and suffix
* @param lineSuffix optional suffix to append at the end of each line (e.g.,
* newline); can be {@code null}
*/
public Base64Stream(InputStream source, byte[] linePrefix, int lineLength, byte[] lineSuffix) {
this.source = source;
this.lineLength = lineLength;
in = InputStream.nullInputStream();
base64 = Base64.getEncoder();
if (linePrefix != null) {
prefix = new ByteArrayInputStream(linePrefix);
}
if (lineSuffix != null) {
suffix = new ByteArrayInputStream(lineSuffix);
}
lineBreak = lineLength - ((lineSuffix == null) ? 0 : lineSuffix.length);
}
@Override
public int read() throws IOException {
byte[] result = { 0 };
int count = read(result, 0, 1);
return (count == 0) ? -1 : result[0] & 0xff;
}
InputStream is = InputStream.nullInputStream();
int breakPos;
boolean closed = false;
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (pos == lineLength) {
pos = 0;
}
if (pos == 0 && prefix != null) {
prefix.reset();
breakPos = Integer.MAX_VALUE;
is = prefix;
} else {
if (pos == lineBreak && suffix != null && !closed) {
suffix.reset();
breakPos = Integer.MAX_VALUE;
is = suffix;
} else {
if (in.available() == 0) {
ensureData();
}
breakPos = lineBreak;
is = in;
}
}
int l = Math.min(Math.min(is.available(), len), breakPos - pos);
pos += l;
return is.read(b, off, l);
}
final static int TRIPLES_INPUT = 1000;
/**
* Reads the next block of raw bytes from the source and encodes them into
* Base64. Handles stream exhaustion and final line suffix if applicable.
*/
private void ensureData() throws IOException {
byte[] buf = source.readNBytes(3 * TRIPLES_INPUT);
if (buf.length == 0 && suffix != null && !closed) {
System.out.println("suffix");
closed = true;
suffix.reset();
breakPos = lineBreak = Integer.MAX_VALUE;
in = suffix;
return;
}
if (buf.length != 3 * TRIPLES_INPUT) {
in = new ByteArrayInputStream(base64.encode(buf));
} else {
in = new ByteArrayInputStream(base64.withoutPadding().encode(buf));
}
}
}

View File

@@ -0,0 +1,231 @@
/**
* 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.data.output;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.SequenceInputStream;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* {@code PiwigoExportDataContent} is a specialized exportable content class
* that supports uploading an image file to a Piwigo photo gallery server,
* either directly via HTTP POST or by generating platform-specific scripts for
* deferred uploading.
*
* <p>
* Depending on the export mode (RAW, BASH_SCRIPT, CMD_SCRIPT), it can:
* <ul>
* <li>Upload the image to a Piwigo server directly using HTTP
* multipart/form-data</li>
* <li>Generate a Bash script that decodes the base64-encoded image and uploads
* it using curl</li>
* <li>Generate a CMD (Windows batch) script that reconstructs the image using
* certutil and uploads it</li>
* </ul>
*
* <p>
* This class integrates with {@code AbstractExportableDataContent} and expects
* its {@code input} field to be set before invoking {@link #getStream()}.
* </p>
*
* <p>
* The image is uploaded using the {@code pwg.images.add} method of the Piwigo
* API.
* </p>
*/
class PiwigoExportDataContent extends AbstractExportableDataContent {
private final String imageFileName;
private final String piwigoUrl;
private final String username;
private final String password;
private final String albumId;
/**
* Constructs a new exportable Piwigo upload object.
*
* @param imageFileName the name of the image file to assign during upload or
* script output
* @param piwigoUrl the URL of the Piwigo API endpoint
* @param username the Piwigo username for authentication
* @param password the Piwigo password
* @param albumId the ID of the Piwigo album to which the image will be
* uploaded
*/
public PiwigoExportDataContent(String imageFileName, String piwigoUrl, String username, String password,
String albumId) {
this.imageFileName = imageFileName;
this.piwigoUrl = piwigoUrl;
this.username = username;
this.password = password;
this.albumId = albumId;
}
/**
* Returns an {@code InputStream} that provides either the raw upload stream, or
* a platform-specific script depending on the export mode.
*
* @return the resulting {@code InputStream}
* @throws IOException if reading the input or creating the stream fails
*/
@Override
public InputStream getStream() throws IOException {
if (input == null) {
throw new IllegalStateException("Input not set.");
}
return switch (mode) {
case RAW -> performDirectUpload(input.getStream());
case BASH_SCRIPT -> generateBashScript(input.getStream());
case CMD_SCRIPT -> generateCmdScript(input.getStream());
};
}
/**
* Performs a direct upload of the image data to the Piwigo server using a
* multipart/form-data HTTP POST request.
*
* @param dataStream the input stream of the binary image data
* @return the server's response stream
* @throws IOException if the upload fails
*/
private InputStream performDirectUpload(InputStream dataStream) throws IOException {
String boundary = "----Boundary" + System.currentTimeMillis();
URL url = URI.create(piwigoUrl).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
try (OutputStream out = conn.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
writeFormField(writer, boundary, "method", "pwg.images.add");
writeFormField(writer, boundary, "username", username);
writeFormField(writer, boundary, "password", password);
writeFormField(writer, boundary, "category", albumId);
writer.write("--" + boundary + "\r\n");
writer.write("Content-Disposition: form-data; name=\"image\"; filename=\"" + imageFileName + "\"\r\n");
writer.write("Content-Type: image/jpeg\r\n\r\n");
writer.flush();
dataStream.transferTo(out);
out.flush();
writer.write("\r\n--" + boundary + "--\r\n");
writer.flush();
}
InputStream responseStream;
try {
responseStream = conn.getInputStream();
} catch (IOException e) {
InputStream errorStream = conn.getErrorStream();
if (errorStream != null) {
return errorStream;
}
return new ByteArrayInputStream(("Error: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
}
return responseStream;
}
/**
* Writes a single form field as part of a multipart/form-data HTTP request.
*
* @param writer the writer to output the field to
* @param boundary the multipart boundary
* @param name the name of the form field
* @param value the value of the form field
* @throws IOException if writing fails
*/
private void writeFormField(Writer writer, String boundary, String name, String value) throws IOException {
writer.write("--" + boundary + "\r\n");
writer.write("Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n");
writer.write(value + "\r\n");
}
/**
* Generates a Bash script that reconstructs the image using a Base64 heredoc
* block and uploads it using {@code curl}.
*
* @param originalStream the original binary stream of the image
* @return a stream containing the complete shell script
*/
private InputStream generateBashScript(InputStream originalStream) {
InputStream header = new ByteArrayInputStream(("#!/bin/bash\nset -e\n\ncurl -X POST \"" + piwigoUrl + "\" \\\n"
+ " -F method=\"pwg.images.add\" \\\n" + " -F username=\"" + username + "\" \\\n" + " -F password=\""
+ password + "\" \\\n" + " -F category=\"" + albumId + "\" \\\n" + " -F image=@<(base64 -d <<'EOF'\n")
.getBytes(StandardCharsets.UTF_8));
@SuppressWarnings("resource")
InputStream body = new Base64Stream(originalStream, null, 76, new byte[] { 10 });
InputStream footer = new ByteArrayInputStream("EOF\n)\n".getBytes(StandardCharsets.UTF_8));
return new SequenceInputStream(new SequenceInputStream(header, body), footer);
}
/**
* Generates a CMD batch script that reconstructs the image using certutil and
* uploads it using {@code curl}.
*
* @param originalStream the original binary stream of the image
* @return a stream containing the complete Windows batch script
*/
private InputStream generateCmdScript(InputStream originalStream) {
InputStream header = new ByteArrayInputStream(
("@echo off\nsetlocal\necho -----BEGIN BASE64----- > tmp.b64\n").getBytes(StandardCharsets.UTF_8));
@SuppressWarnings("resource")
InputStream body = new Base64Stream(originalStream, "echo ".getBytes(), 76, " >> tmp.b64\r\n".getBytes());
InputStream footer = new ByteArrayInputStream(
("echo -----END BASE64----- >> tmp.b64\n" + "certutil -decode tmp.b64 \"" + imageFileName + "\" >nul\n"
+ "del tmp.b64\n" + "curl -X POST \"" + piwigoUrl + "\" ^\n" + " -F method=pwg.images.add ^\n"
+ " -F username=" + username + " ^\n" + " -F password=" + password + " ^\n" + " -F category="
+ albumId + " ^\n" + " -F image=@" + imageFileName + "\n" + "del \"" + imageFileName + "\"\n")
.getBytes(StandardCharsets.UTF_8));
return new SequenceInputStream(new SequenceInputStream(header, body), footer);
}
}

View File

@@ -0,0 +1,62 @@
/**
* 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.
*/
/**
* Provides classes for exporting binary data in various formats including:
* <ul>
* <li>Raw binary stream uploads</li>
* <li>Platform-specific shell script encodings (e.g., Bash or Windows CMD)</li>
* <li>Base64 transformation and line-wrapping</li>
* </ul>
*
* <p>
* The core components of this package include:
* </p>
* <ul>
* <li>{@link zeroecho.data.output.Base64Stream} a stream that applies Base64
* encoding and formats output per-line with optional prefixes and
* suffixes.</li>
* <li>{@link zeroecho.data.output.AbstractExportableDataContent} a reusable
* base class for any exportable content, managing input and export mode.</li>
* <li>{@link zeroecho.data.output.PiwigoExportDataContent} an implementation
* that exports images to a Piwigo gallery, supporting direct upload or
* script-based methods.</li>
* </ul>
*
* <p>
* These tools allow platform-neutral data packaging and transport, and support
* automation scenarios.
* </p>
*/
package zeroecho.data.output;

View File

@@ -0,0 +1,53 @@
/*******************************************************************************
* 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.
******************************************************************************/
/**
* Provides abstractions for representing different types of content used in
* secure data handling.
* <p>
* The package defines the following interfaces:
* <ul>
* <li>{@link PlainContent} Represents unprocessed or original content
* obtained from input, files, or after decryption.</li>
* <li>{@link SecretContent} Represents sensitive plain content, typically a
* secret phrase that may be input manually, read from a file, or generated
* dynamically.</li>
* <li>{@link EncryptedContent} Represents the result of encrypting content,
* which can be safely shared or stored publicly. It may also support
* steganographic methods for additional secrecy.</li>
* </ul>
* These interfaces form the foundation for building secure data exchange
* mechanisms and cryptographic workflows.
*/
package zeroecho.data;

View File

@@ -0,0 +1,139 @@
/**
* 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.data.processing;
import java.util.Objects;
import conflux.Ctx;
import conflux.Key;
import zeroecho.data.DataContent;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
/**
* A base class for AES encryption and decryption utilities.
*
* <p>
* This class manages AES key material, initialization vectors (IVs), optional
* Additional Authenticated Data (AAD), cipher type selection, and block size
* configuration. It is designed to be extended by more specific encryption or
* decryption implementations that rely on a consistent context for AES
* parameter management.
* </p>
*
* <p>
* Instances automatically populate the shared {@link Ctx} context with
* AES-related parameters, ensuring they are available for use by cipher
* initialization routines.
* </p>
*
* @author Leo Galambos
*/
public class AesCommon {
/**
* Parameter key for AES block size.
*/
public static final Key<Integer> BLOCK_SIZE = Key.of("aes.block.size", Integer.class);
/**
* Parameter key for AES encryption key bytes.
*/
public static final Key<byte[]> KEY = Key.of("aes.key", byte[].class);
/**
* Parameter key for AES initialization vector (IV) bytes.
*/
public static final Key<byte[]> IV = Key.of("aes.iv", byte[].class);
/**
* Parameter key for AES mode.
*/
public static final Key<AesMode> MODE = Key.of("aes.mode", AesMode.class);
/**
* Parameter key for AES cipher type.
*/
public static final Key<AesCipherType> CIPHER_TYPE = Key.of("aes.cipher.type", AesCipherType.class);
/**
* Parameter key for Additional Authenticated Data (AAD) used in AEAD modes such
* as AES-GCM. This value must be identical for both encryption and decryption
* to ensure authentication succeeds.
*/
public static final Key<byte[]> AAD = Key.of("aes.aad", byte[].class);
/**
* The source data content to be encrypted or decrypted.
*/
protected DataContent source;
/**
* Constructs an {@code AesCommon} instance and initializes the encryption
* context with AES-specific parameters.
*
* <p>
* This constructor sets the required AES block size in the shared {@link Ctx}
* context, and, if {@code params} is non-null, it stores the provided AES mode,
* key material, IV, cipher type, and any AAD values. The {@link Ctx} singleton
* acts as a storage mechanism to hold the encryption-related state needed for
* downstream operations.
* </p>
*
* @param params the {@link AesParameters} to be saved into the encryption
* context; may be {@code null} if no user parameters are to be
* configured
*/
public AesCommon(final AesParameters params) {
Ctx.INSTANCE.put(BLOCK_SIZE, AesSupport.BLOCK_SIZE);
if (params != null) {
params.save(Ctx.INSTANCE);
}
}
/**
* Sets the input data content to be encrypted or decrypted.
*
* @param input the {@link DataContent} to set as source
* @throws NullPointerException if {@code input} is {@code null}
*/
public void setInput(final DataContent input) {
Objects.requireNonNull(input, "input must not be null");
source = input;
}
}

View File

@@ -0,0 +1,109 @@
/*******************************************************************************
* 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.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.data.PlainContent;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
/**
* Decrypts AES-encrypted content from an input stream.
*
* This class extends {@link AesCommon} and implements {@link PlainContent},
* providing functionality to retrieve a decrypted {@link InputStream} from an
* encrypted source using AES parameters such as key, IV, and mode.
*
* This class itself does not perform password-based key derivation; subclasses
* should implement such logic if needed.
*
* @author Leo Galambos
*/
public class AesDecryptor extends AesCommon implements PlainContent {
/** Logger for internal messages and error reporting. */
private static final Logger LOG = Logger.getLogger(AesDecryptor.class.getName());
/**
* Constructs an {@code AesDecryptor} initialized with the specified AES
* parameters.
*
* @param params the AES parameters including mode, key, and IV
*/
public AesDecryptor(final AesParameters params) {
super(params);
}
/**
* Returns a decrypted {@link InputStream} by applying AES decryption on the
* input stream provided by the underlying source.
*
* This method retrieves cryptographic parameters including the encryption key,
* initialization vector (IV), and cipher type from a contextual configuration
* (via {@link Ctx}). It then delegates decryption to
* {@link AesSupport#decrypt(byte[], byte[], byte[], AesCipherType, InputStream)}.
*
* If the source stream is {@code null}, a {@link NullPointerException} is
* thrown. If any decryption error occurs (e.g., invalid parameters or corrupted
* input), it is logged and rethrown as an {@link IOException}.
*
* @return the decrypted {@code InputStream}
* @throws IOException if decryption fails or an I/O error occurs
* during stream processing
* @throws NullPointerException if the input stream retrieved from the source is
* {@code null}
*/
@Override
public InputStream getStream() throws IOException {
final InputStream previousInput = source.getStream();
Objects.requireNonNull(previousInput, "input stream must not be null");
try {
return AesSupport.decrypt(Ctx.INSTANCE.get(KEY), Ctx.INSTANCE.get(IV), Ctx.INSTANCE.get(AAD),
Ctx.INSTANCE.get(CIPHER_TYPE), previousInput);
} catch (IllegalArgumentException e) {
LOG.logp(Level.WARNING, "AesDecryptor", "getStream", "Exception during decryption", e);
throw new IOException(e);
}
}
}

View File

@@ -0,0 +1,111 @@
/*******************************************************************************
* 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.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.data.EncryptedContent;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
/**
* Encrypts the provided content using AES and exposes the result as an
* {@link InputStream}.
*
* The encryption key, IV, and mode are specified by the configured AES
* parameters. The source content is streamed and encrypted on-the-fly using
* {@link AesSupport#encrypt}.
*
* This implementation logs encryption errors and wraps them in
* {@link IOException}s to ensure compatibility with streaming APIs.
*
* @author Leo Galambos
*/
public class AesEncryptor extends AesCommon implements EncryptedContent {
/** Logger for internal messages and error reporting. */
private static final Logger LOG = Logger.getLogger(AesEncryptor.class.getName());
/**
* Constructs an {@code AesEncryptor} initialized with the specified AES
* parameters.
*
* @param params the AES parameters including mode, key, and IV
* @throws IllegalArgumentException if {@code params} is {@code null}
*/
public AesEncryptor(final AesParameters params) {
super(params);
}
/**
* Returns an {@link InputStream} that applies AES encryption to the underlying
* input data.
*
* This method retrieves the original {@code InputStream} from the configured
* {@code source}, verifies its presence, and then wraps it in an AES-encrypting
* stream using the encryption context provided by {@link Ctx}. The encryption
* parameters used include the AES key, initialization vector (IV), and cipher
* type, all of which must be pre-configured in the context.
*
* If the original stream is {@code null}, or if encryption setup fails due to
* invalid or missing parameters, this method throws an {@link IOException}. Any
* internal {@link IllegalArgumentException} is logged and rethrown as an
* {@code IOException} to ensure consistent error handling.
*
* @return an {@code InputStream} that provides AES-encrypted data as it is read
* @throws IOException if the input stream is missing or encryption
* setup fails
* @throws NullPointerException if the source stream is {@code null}
*/
@Override
public InputStream getStream() throws IOException {
final InputStream previousInput = source.getStream();
Objects.requireNonNull(previousInput, "input stream must not be null");
try {
return AesSupport.encrypt(Ctx.INSTANCE.get(KEY), Ctx.INSTANCE.get(IV), Ctx.INSTANCE.get(AAD),
Ctx.INSTANCE.get(CIPHER_TYPE), previousInput);
} catch (IllegalArgumentException e) {
LOG.logp(Level.WARNING, "AesEncryptor", "getStream", "Exception", e);
throw new IOException(e);
}
}
}

View File

@@ -0,0 +1,144 @@
/**
* 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.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
/**
* A KEM-based AES decryptor implementation of {@link PlainContent}.
* <p>
* This class uses a {@link KEMAsymmetricContext} configured with a key
* extractor to perform key decapsulation, combined with AES decryption for the
* payload.
* </p>
* <p>
* The decrypted content can be accessed as a stream after setting the encrypted
* input content.
* </p>
*/
public final class KEMDecryptor implements PlainContent {
private static final Logger LOG = Logger.getLogger(KEMDecryptor.class.getName());
/**
* The KEM context configured for decapsulation (extractor must be non-null).
*/
private final KEMAsymmetricContext kemContext;
/**
* AES cipher variant used for symmetric decryption.
*/
private final AesCipherType cipherType;
/**
* Optional Additional Authenticated Data used during decryption; may be
* {@code null}.
*/
private final byte[] aad;
/**
* The encrypted content input to be decrypted.
* <p>
* Must be set before calling {@link #getStream()}.
* </p>
*/
private DataContent encryptedContent;
/**
* Constructs a KEM-based AES decryptor.
*
* @param kemContext the KEM context configured for decapsulation mode; must
* have a non-null extractor for key decapsulation
* @param cipherType the AES cipher type used during encryption (must match)
* @param aad optional Additional Authenticated Data for AEAD ciphers;
* may be {@code null}
* @throws IllegalArgumentException if {@code kemContext} has no extractor or if
* {@code cipherType} is {@code null}
*/
public KEMDecryptor(KEMAsymmetricContext kemContext, AesCipherType cipherType, byte[] aad) {
if (kemContext.extractor() == null) {
throw new IllegalArgumentException(
"KEMDecryptor requires a KEMAsymmetricContext in decapsulation mode (extractor must not be null)");
}
this.kemContext = kemContext;
this.cipherType = Objects.requireNonNull(cipherType, "cipherType must not be null");
this.aad = aad; // NOPMD
}
/**
* Sets the encrypted input content to be decrypted.
*
* @param input the encrypted {@link DataContent} input; must not be
* {@code null}
* @throws IllegalArgumentException if {@code input} is {@code null}
*/
@Override
public void setInput(DataContent input) {
if (input == null) {
throw new IllegalArgumentException("Input DataContent cannot be null");
}
this.encryptedContent = input;
}
/**
* Returns an {@link InputStream} providing the decrypted plaintext.
* <p>
* This method requires that the encrypted input content has been set via
* {@link #setInput(DataContent)}.
* </p>
*
* @return an input stream yielding the decrypted plaintext; never {@code null}
* @throws IllegalStateException if no encrypted content has been set prior to
* invocation
* @throws IOException if an I/O error occurs during stream retrieval
* or decryption
*/
@Override
public InputStream getStream() throws IOException {
if (encryptedContent == null) {
throw new IllegalStateException("No encrypted content set for decryption");
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Starting KEM decryption using cipher type: " + cipherType);
}
return kemContext.getDecryptedStream(encryptedContent.getStream(), cipherType, aad);
}
}

View File

@@ -0,0 +1,174 @@
/**
* 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.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.DataContent;
import zeroecho.data.EncryptedContent;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
/**
* Implements encryption of content using a Key Encapsulation Mechanism (KEM)
* combined with AES symmetric encryption.
* <p>
* This class wraps a {@link KEMAsymmetricContext} configured with a KEM key
* generator and applies AES encryption (specified by {@link AesCipherType}) to
* the input data. The encryption output stream consists of the concatenation of
* the KEM encapsulated key, the AES initialization vector (IV), and the
* AES-encrypted payload.
* <p>
* Optionally supports Additional Authenticated Data (AAD) to be included in
* authenticated cipher modes (e.g., AES-GCM).
* <p>
* Usage:
* <ul>
* <li>Create an instance with a properly initialized
* {@code KEMAsymmetricContext} in encapsulation mode.</li>
* <li>Set the input content using {@link #setInput(DataContent)}.</li>
* <li>Obtain the encrypted output stream via {@link #getStream()}.</li>
* </ul>
* <p>
* Note: The {@code KEMAsymmetricContext} must have a non-null generator to
* perform encryption.
*
* @implSpec The {@link #getStream()} method produces an InputStream that yields
* the concatenation of:
* <ol>
* <li>KEM encapsulated key bytes</li>
* <li>AES initialization vector bytes</li>
* <li>Encrypted payload bytes</li>
* </ol>
* This stream never returns null and throws IOException on failure.
*/
public class KEMEncryptor implements EncryptedContent {
private static final Logger LOG = Logger.getLogger(KEMEncryptor.class.getName());
/**
* The KEM context configured for encapsulation (generator must be non-null).
*/
private final KEMAsymmetricContext kemContext;
/**
* AES cipher variant used for symmetric decryption.
*/
private final AesCipherType cipherType;
/**
* Optional Additional Authenticated Data used during decryption; may be
* {@code null}.
*/
private final byte[] aad;
/**
* The input content to be encrypted.
* <p>
* Must be set before {@link #getStream()} is called. It represents the
* plaintext data that will be encrypted using the KEM + AES process.
*/
private DataContent inputContent;
/**
* Constructs a new KEM-based AES encryptor.
*
* @param kemContext the KEM context configured for encapsulation mode; must
* have a non-null generator for key encapsulation
* @param cipherType the AES cipher variant to use for symmetric encryption
* (e.g., CBC, GCM)
* @param aad optional Additional Authenticated Data bytes for AEAD
* ciphers; may be {@code null} if not applicable
* @throws IllegalArgumentException if the {@code kemContext} does not have a
* generator or if {@code cipherType} is
* {@code null}
*/
public KEMEncryptor(KEMAsymmetricContext kemContext, AesCipherType cipherType, byte[] aad) {
if (kemContext.generator() == null) {
throw new IllegalArgumentException(
"KEMEncryptor requires a KEMAsymmetricContext in encapsulation mode (generator must not be null)");
}
this.kemContext = kemContext;
this.cipherType = Objects.requireNonNull(cipherType, "cipherType must not be null");
this.aad = aad; // NOPMD
}
/**
* Sets the input content that will be encrypted.
*
* @param input the {@link DataContent} providing the plaintext data; must not
* be {@code null}
* @throws IllegalArgumentException if {@code input} is {@code null}
*/
@Override
public void setInput(DataContent input) {
if (input == null) {
throw new IllegalArgumentException("Input DataContent cannot be null");
}
this.inputContent = input;
}
/**
* Returns an {@link InputStream} that yields the encrypted data stream.
* <p>
* The stream consists of the concatenation of:
* <ul>
* <li>KEM encapsulated key bytes</li>
* <li>AES initialization vector (IV)</li>
* <li>AES-encrypted payload</li>
* </ul>
* <p>
* This method requires that the input content has been set via
* {@link #setInput(DataContent)}.
*
* @return an input stream providing the encrypted content; never {@code null}
* @throws IllegalStateException if no input content has been set prior to
* invocation
* @throws IOException if an I/O error occurs during stream retrieval
* or encryption
*/
@Override
public InputStream getStream() throws IOException {
if (inputContent == null) {
throw new IllegalStateException("No input content set for encryption");
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Starting KEM encryption using cipher type: " + cipherType);
}
return kemContext.getEncryptedStream(inputContent.getStream(), cipherType, aad);
}
}

View File

@@ -0,0 +1,193 @@
/**
* 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.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.util.IOUtil;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
import zeroecho.util.aes.DerivedAesParameters;
/**
* A password-based AES decryption utility that reads key derivation parameters
* (salt and iteration count) from the encrypted stream header and performs
* on-the-fly decryption using the derived cryptographic key and IV.
*
* This class is designed to complement {@link PasswordBasedAesEncryptor}, which
* prepends the salt and iteration count to the encrypted stream. The decryptor
* reads these values to securely re-derive the same AES key and IV using the
* configured {@link AesMode} and {@link AesCipherType}.
*
* Internally, it uses PBKDF2 (Password-Based Key Derivation Function 2) to
* derive the key material and delegates decryption to the {@link AesDecryptor}
* superclass.
*
* The shared {@link Ctx} context is used to temporarily store cryptographic
* configuration and derived parameters during decryption. The password is held
* in memory only long enough to perform re-derivation and is not retained
* beyond that.
*
* If the encrypted stream header is malformed, or if the key derivation fails,
* an exception is raised to indicate potential tampering or incompatibility.
*
* @see PasswordBasedAesEncryptor
* @see AesSupport#rederiveKeyAndIv(String, byte[], int, AesMode, byte[],
* AesCipherType)
*
* @author Leo Galambos
*/
public class PasswordBasedAesDecryptor extends AesDecryptor {
private static final Logger LOG = Logger.getLogger(PasswordBasedAesDecryptor.class.getName());
private final String password;
/**
* Constructs a PasswordBasedAesDecryptor with password, AES mode, and default
* cipher type (CBC).
*
* @param password the password for key derivation
* @param mode the AES mode (128, 192, or 256-bit)
* @throws IllegalArgumentException if arguments are invalid
* @throws InvalidKeySpecException if key derivation fails
*/
public PasswordBasedAesDecryptor(final String password, final AesMode mode) throws InvalidKeySpecException {
this(password, null, mode, AesCipherType.CBC);
}
/**
* Constructs a password-based AES decryptor with specified AES mode, cipher
* type, and Additional Authenticated Data (AAD).
*
* <p>
* This decryptor uses the provided password to derive the AES key for
* decryption. The AES mode and cipher type define the algorithm parameters
* (e.g., key size and block mode). The optional AAD is used for authenticated
* encryption modes like GCM to ensure integrity and authenticity of additional
* associated data.
* </p>
*
* <p>
* The provided AAD bytes will be stored in the global context for use during
* decryption. If the cipher mode does not support AAD or if no AAD is needed,
* this parameter can be {@code null}.
* </p>
*
* @param password the password used for AES key derivation; must not be
* {@code null}
* @param aad additional authenticated data bytes; may be {@code null} if
* not used
* @param mode the AES mode indicating key length (e.g., AES-128,
* AES-256); must not be {@code null}
* @param cipherType the AES cipher type (e.g., CBC, GCM); must not be
* {@code null}
* @throws InvalidKeySpecException if key derivation or initialization fails
* @throws NullPointerException if {@code password}, {@code mode}, or
* {@code cipherType} is {@code null}
*/
public PasswordBasedAesDecryptor(final String password, final byte[] aad, final AesMode mode,
final AesCipherType cipherType) throws InvalidKeySpecException {
super(null);
Ctx.INSTANCE.put(MODE, mode);
Ctx.INSTANCE.put(CIPHER_TYPE, cipherType);
if (aad != null) {
Ctx.INSTANCE.put(AAD, aad);
}
this.password = password;
}
/**
* Returns an {@link InputStream} that decrypts AES-encrypted data using a key
* and IV derived from the password.
* <p>
* This method expects the encrypted input stream to begin with:
* <ul>
* <li>A packed 7-bit encoded salt length</li>
* <li>The salt bytes</li>
* <li>A packed 7-bit encoded PBKDF iteration count</li>
* </ul>
* These parameters are used to re-derive the decryption key and IV using PBKDF,
* which are then used to decrypt the rest of the stream.
*
* @return a decrypted {@link InputStream}
* @throws IOException if header parsing or stream decryption fails
* @throws IllegalStateException if key derivation fails due to an invalid or
* corrupt state
*/
@Override
public InputStream getStream() throws IOException {
final InputStream in = source.getStream();
// Read salt and iteration count from input stream header
final int saltLength = IOUtil.readPack7I(in);
if (saltLength > 4 * AesSupport.BLOCK_SIZE) {
throw new IOException("Salt length " + saltLength + " is weird");
}
final byte[] salt = in.readNBytes(saltLength);
final int iterations = IOUtil.readPack7I(in);
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "processing AES (iterations={0} salt={1})",
new Object[] { iterations, Arrays.toString(salt) });
}
final DerivedAesParameters params;
try {
params = AesSupport.rederiveKeyAndIv(password, salt, iterations, Ctx.INSTANCE.get(MODE),
Ctx.INSTANCE.get(AAD), Ctx.INSTANCE.get(CIPHER_TYPE));
params.save(Ctx.INSTANCE);
} catch (InvalidKeySpecException e) {
LOG.logp(Level.WARNING, "PasswordBasedAesDecryptor", "getStream", "Exception", e);
throw new IllegalStateException("Failed to generate key: invalid state", e);
}
try {
return AesSupport.decrypt(Ctx.INSTANCE.get(KEY), Ctx.INSTANCE.get(IV), Ctx.INSTANCE.get(AAD),
Ctx.INSTANCE.get(CIPHER_TYPE), in);
} catch (IllegalArgumentException e) {
LOG.logp(Level.WARNING, "PasswordBasedAesDecryptor", "getStream", "Exception during decryption", e);
throw new IOException(e);
}
}
}

View File

@@ -0,0 +1,180 @@
/**
* 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.data.processing;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.util.IOUtil;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
import zeroecho.util.aes.DerivedAesParameters;
/**
* A password-based AES encryption utility that derives a cryptographic key and
* IV using PBKDF2 (Password-Based Key Derivation Function 2) and performs AES
* encryption on a given input stream.
* <p>
* This class encapsulates the logic required to securely derive AES parameters
* from a user-supplied password and to stream-encrypt data using the derived
* values.
* <p>
* A header containing the salt and iteration count is prepended to the
* encrypted stream, allowing the corresponding decryption mechanism to
* re-derive the key for symmetric operations. The underlying encryption is
* performed using the {@link AesEncryptor} superclass.
* <p>
* For security and immutability reasons, critical cryptographic parameters
* (iterations, mode, cipher type) are made read-only after construction.
* Attempts to modify them will result in an {@link IllegalStateException}.
*
* @see AesSupport#deriveKeyAndIv(String, int, AesMode, byte[], AesCipherType)
* @see PasswordBasedAesDecryptor
*
* @author Leo Galambos
*/
public class PasswordBasedAesEncryptor extends AesEncryptor {
private static final Logger LOG = Logger.getLogger(PasswordBasedAesEncryptor.class.getName());
/**
* Constructs a PasswordBasedAesEncryptor using a password, iteration count, AES
* mode, and default cipher type (CBC).
*
* @param password the password for key derivation
* @param iterations the PBKDF2 iteration count
* @param mode the AES mode (128, 192, or 256-bit)
* @throws IllegalArgumentException if parameters are invalid
* @throws InvalidKeySpecException if key derivation fails
*/
public PasswordBasedAesEncryptor(final String password, final int iterations, final AesMode mode)
throws InvalidKeySpecException {
this(password, iterations, null, mode, AesCipherType.CBC);
}
/**
* Constructs a {@code PasswordBasedAesEncryptor} instance that derives a
* cryptographic key and initialization vector (IV) from a given password using
* the specified number of iterations, AES mode, associated data (AAD), and
* cipher type.
*
* <p>
* This constructor uses a password-based key derivation function to generate
* the necessary encryption parameters, which are then passed to the parent
* {@link AesCommon} class.
* </p>
*
* <p>
* To ensure the integrity of derived cryptographic material, listeners are
* registered on sensitive context parameters (iterations, mode, and cipher
* type). Any attempt to modify these parameters after construction will result
* in an {@link IllegalStateException}.
* </p>
*
* @param password the password used to derive the encryption key and IV
* @param iterations the number of iterations for key derivation
* @param mode the AES mode to use (e.g., 128-bit, 192-bit, 256-bit)
* @param aad additional authenticated data (AAD) to be included in the
* derived parameters; may be {@code null} if unused
* @param cipherType the cipher type to use (e.g., CBC, GCM, CTR)
*
* @throws IllegalArgumentException if any parameter is invalid
* @throws InvalidKeySpecException if the key derivation fails due to an
* invalid specification
*
* @see AesSupport#deriveKeyAndIv(String, int, AesMode, byte[], AesCipherType)
*/
public PasswordBasedAesEncryptor(final String password, final int iterations, final byte[] aad, final AesMode mode,
final AesCipherType cipherType) throws InvalidKeySpecException {
super(AesSupport.deriveKeyAndIv(password, iterations, mode, aad, cipherType));
Ctx.INSTANCE.addListener(DerivedAesParameters.ITERATIONS, newValue -> {
throw new IllegalStateException(DerivedAesParameters.ITERATIONS
+ " value cannot be modified because it would affect previously computed crypto material");
});
Ctx.INSTANCE.addListener(MODE, newValue -> {
throw new IllegalStateException(
MODE + " value cannot be modified because it would affect previously computed crypto material");
});
Ctx.INSTANCE.addListener(CIPHER_TYPE, newValue -> {
throw new IllegalStateException(CIPHER_TYPE
+ " value cannot be modified because it would affect previously computed crypto material");
});
}
/**
* Returns an {@link InputStream} that provides AES-encrypted data derived from
* the configured password-based key and IV.
*
* This method prepends a header to the encrypted stream containing the salt and
* iteration count used for key derivation, which is necessary for corresponding
* decryption processes to re-derive the correct key.
*
* The encryption itself is delegated to the superclass's
* {@link AesEncryptor#getStream()}, which performs on-the-fly encryption of the
* underlying source data.
*
* @return an input stream of AES-encrypted content including a header with salt
* and iterations
* @throws IOException if encryption or stream access fails
*/
@Override
public InputStream getStream() throws IOException {
// Compose header: salt + iterations
final byte[] salt = Ctx.INSTANCE.get(DerivedAesParameters.SALT);
final int iterations = Ctx.INSTANCE.get(DerivedAesParameters.ITERATIONS);
final ByteArrayOutputStream header = new ByteArrayOutputStream();
IOUtil.writePack7I(header, salt.length);
header.write(salt);
IOUtil.writePack7I(header, iterations);
// salt is not a secret, so we can log it
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "processing AES (iterations={0} salt={1}",
new Object[] { iterations, Arrays.toString(salt) });
}
final InputStream encryptedStream = super.getStream();
return new SequenceInputStream(new ByteArrayInputStream(header.toByteArray()), encryptedStream);
}
}

View File

@@ -0,0 +1,135 @@
/*******************************************************************************
* 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.data.processing;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
/**
* An implementation of {@link PlainContent} that encapsulates a byte array.
*
* Provides read-only access to the content through an {@link InputStream}
* backed by the internal byte buffer.
*
* This class represents the start of a {@link DataContent} processing chain,
* thus it does not accept input from preceding content.
*
* @author Leo Galambos
*/
public class PlainBytes implements PlainContent {
/**
* Logger instance for the {@code PlainBytes} class used to log runtime
* information, warnings, or errors. It is statically initialized with the class
* name to ensure consistent logging context throughout the class.
*/
private static final Logger LOG = Logger.getLogger(PlainBytes.class.getName());
/**
* Byte array buffer that holds the raw data managed by this instance.
*/
protected final byte[] buffer;
/**
* Constructs a new {@code PlainBytes} instance by copying the provided byte
* array.
*
* @param buffer the byte array to wrap
*/
public PlainBytes(final byte[] buffer) {
Objects.requireNonNull(buffer, "PlainBytes cannot operate with null buffer");
this.buffer = Arrays.copyOf(buffer, buffer.length);
}
/**
* Constructs a new {@code PlainBytes} instance with a buffer of the specified
* length.
*
* @param length the size of the internal byte buffer to allocate
* @throws NegativeArraySizeException if {@code length} is negative
*/
protected PlainBytes(final int length) {
this.buffer = new byte[length];
}
/**
* {@inheritDoc}
*
* {@code PlainBytes} represents the start of a {@link DataContent} chain and
* therefore must not have any input. Calling this method with a
* non-{@code null} argument will result in an exception.
*
*
* @param input the preceding {@link DataContent}, which must be {@code null}
* @throws IllegalArgumentException if {@code input} is not {@code null}
*/
@Override
public void setInput(final DataContent input) {
if (input != null) {
throw new IllegalArgumentException(
getClass().getName() + " must be the first element in a DataContent chain; it cannot accept input");
}
}
/**
* Returns an {@link InputStream} for reading the byte array.
*
* @return a new {@link ByteArrayInputStream}
* @throws IOException if an I/O error occurs
*/
@Override
public InputStream getStream() throws IOException {
LOG.log(Level.INFO, "opening byte array for read, length={0}", buffer.length);
return new ByteArrayInputStream(buffer);
}
/**
* Returns a copy of the internal byte buffer.
*
* @return a new byte array containing the data from the internal buffer
*/
@Override
public byte[] toBytes() {
return Arrays.copyOf(buffer, buffer.length);
}
}

View File

@@ -0,0 +1,110 @@
/*******************************************************************************
* 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.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
/**
* A {@link PlainContent} implementation that reads content from a file-like
* URL.
*/
public final class PlainFile implements PlainContent {
/**
* Logger instance for the {@code PlainFile} class used for logging debug,
* informational, and error messages. Initialized with the class name to provide
* context-specific logging output.
*/
private static final Logger LOG = Logger.getLogger(PlainFile.class.getName());
/**
* URL representing the location of the file associated with this instance.
*
* This URL may point to a file on the local file system, a remote resource, or
* any other location accessible via the {@link java.net.URL} protocol. It is
* final and set during construction.
*/
private final URL location;
/**
* Constructs a PlainFile from the specified URL.
*
* @param location the file URL; must not be null
*/
public PlainFile(final URL location) {
Objects.requireNonNull(location, "URL must not be null");
this.location = location;
}
/**
* {@inheritDoc}
*
* {@code PlainFile} represents the start of a {@link DataContent} chain and
* therefore must not have any input. Calling this method with a
* non-{@code null} argument will result in an exception.
*
* @param input the preceding {@link DataContent}, which must be {@code null}
* @throws IllegalArgumentException if {@code input} is not {@code null}
*/
@Override
public void setInput(final DataContent input) {
if (input != null) {
throw new IllegalArgumentException(
getClass().getName() + " must be the first element in a DataContent chain; it cannot accept input");
}
}
/**
* Returns an {@link InputStream} for reading from the file.
*
* @return an {@link InputStream} from the URL
* @throws IOException if an I/O error occurs opening the stream
*/
@Override
public InputStream getStream() throws IOException {
LOG.log(Level.INFO, "opening {0}", location);
return location.openStream();
}
}

View File

@@ -0,0 +1,126 @@
/*******************************************************************************
* 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.data.processing;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
/**
* A {@link PlainContent} implementation that wraps a UTF-8 string.
*
* @author Leo Galambos
*/
public class PlainString implements PlainContent {
/**
* Logger instance for the {@code PlainString} class used to log informational,
* debug, or error messages related to the classs operations.
*
* The logger is initialized with the name of the {@code PlainString} class,
* enabling targeted and organized logging output.
*/
private static final Logger LOG = Logger.getLogger(PlainString.class.getName());
/**
* The plain text content represented by this instance.
*/
protected String str;
/**
* Constructs a PlainString with the given string.
*
* @param str the plain text content; must not be null
*/
public PlainString(final String str) {
Objects.requireNonNull(str, "plain string must not be null");
this.str = str;
}
/**
* Returns the original string content.
*
* @return the string
*/
@Override
public String toText() {
return str;
}
/**
* {@inheritDoc}
*
* {@code PlainString} represents the start of a {@link DataContent} chain and
* therefore must not have any input. Calling this method with a
* non-{@code null} argument will result in an exception.
*
* @param input the preceding {@link DataContent}, which must be {@code null}
* @throws IllegalArgumentException if {@code input} is not {@code null}
*/
@Override
public void setInput(final DataContent input) {
if (input != null) {
throw new IllegalArgumentException(
getClass().getName() + " must be the first element in a DataContent chain; it cannot accept input");
}
}
/**
* Returns an {@link InputStream} of the UTF-8 encoded string.
*
* @return a {@link ByteArrayInputStream}
*/
@Override
public InputStream getStream() {
LOG.log(Level.FINE, "opening \"{0}\"", str);
return new ByteArrayInputStream(toBytes());
}
/**
* Returns the UTF-8 encoded byte array of the string.
*
* @return a byte array representation
*/
@Override
public byte[] toBytes() {
return str.getBytes(StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,99 @@
/*******************************************************************************
* 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.data.processing;
import java.io.IOException;
import java.io.InputStream;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
/**
* A {@link PlainContent} implementation that reclassifies an existing
* {@link DataContent} instance as plain (unencrypted) without modifying the
* underlying data stream.
*
* This is useful in cryptographic pipelines where the actual content remains
* unchanged, but its classification as "plain" is semantically important for
* subsequent processing.
*/
@Deprecated
public class ReclassifiedPlain implements PlainContent {
private DataContent previous;
/**
* Creates a new {@code ReclassifiedPlain} instance without any initial input.
*
* The input must be later provided using {@link #setInput(DataContent)}.
*/
public ReclassifiedPlain() {
this(null);
}
/**
* Constructs a {@code ReclassifiedPlain} with the specified upstream content.
*
* @param previous the upstream {@link DataContent} to be reclassified as plain
*/
public ReclassifiedPlain(final DataContent previous) {
super();
this.previous = previous;
}
/**
* Sets the upstream {@link DataContent} that this plain content reclassifies.
*
* @param input the content to treat as plain
*/
@Override
public void setInput(final DataContent input) {
previous = input;
}
/**
* Returns the input stream from the upstream content without any
* transformation.
*
* @return the raw input stream from the upstream content
* @throws IOException if the underlying content fails to provide a stream
*/
@Override
public InputStream getStream() throws IOException {
if (previous == null) {
throw new IllegalStateException("Input content is not set for ReclassifiedPlain");
}
return previous.getStream();
}
}

View File

@@ -0,0 +1,165 @@
/*******************************************************************************
* 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.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.data.DataContent;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
/**
* A {@link SecretRandom} implementation that generates a random AES key and IV
* using the specified {@link AesMode} and {@link AesCipherType}.
*
* <p>
* The combined key and IV are stored in the internal buffer and propagated to
* the shared {@link Ctx} for use by other components that rely on AES
* encryption or decryption.
*
* <p>
* When the {@code SECRET} context value is updated, this class listens and
* splits the secret value into separate key and IV portions, updating both the
* internal buffer and the relevant context entries.
*
* <p>
* Likewise, changes to the {@code KEY} context value will update the buffer
* accordingly.
*
* <p>
* This class does not perform encryption or decryption itself. Instead, it
* makes the key/IV accessible to other components and exposes the raw input
* stream.
*
* @author Leo Galambos
*/
public class SecretAesRandom extends SecretRandom {
private static final Logger LOG = Logger.getLogger(SecretAesRandom.class.getName());
private DataContent source;
/**
* Creates a new {@code SecretAesRandom} instance by generating a random AES
* key, IV, and optionally incorporating Additional Authenticated Data (AAD).
*
* <p>
* The generated values are saved to the {@link Ctx} and stored internally in a
* buffer. Listeners are registered to propagate updates between the
* {@code SECRET}, {@code KEY}, and {@code IV}.
* </p>
*
* @param mode the AES mode determining the key length (e.g., AES-128 or
* AES-256)
* @param cipherType the AES cipher type (e.g., CBC, GCM)
* @param aad optional Additional Authenticated Data (AAD) associated
* with the generated parameters; may be {@code null} if
* unused
*
* @throws IllegalArgumentException if key or IV generation fails
*
* @see AesSupport#generateKeyAndIV(AesMode, AesCipherType, byte[])
*/
public SecretAesRandom(final AesMode mode, final AesCipherType cipherType, final byte[] aad) {
super(mode.getKeyLengthBytes() + cipherType.getIVLengthBytes(), false);
final AesParameters params;
try {
params = AesSupport.generateKeyAndIV(mode, cipherType, aad);
} catch (IllegalArgumentException | NoSuchAlgorithmException | NoSuchProviderException e) {
LOG.logp(Level.WARNING, "SecretAesRandom", "SecretAesRandom", "Exception", e);
throw new IllegalArgumentException(e);
}
System.arraycopy(params.key().getKey(), 0, buffer, 0, mode.getKeyLengthBytes());
System.arraycopy(params.iv(), 0, buffer, mode.getKeyLengthBytes(), cipherType.getIVLengthBytes());
params.save(Ctx.INSTANCE);
Ctx.INSTANCE.addListener(SECRET, newValue -> {
Ctx.INSTANCE.put(AesCommon.KEY, Arrays.copyOf(newValue, mode.getKeyLengthBytes()));
Ctx.INSTANCE.put(AesCommon.IV, Arrays.copyOfRange(newValue, mode.getKeyLengthBytes(),
mode.getKeyLengthBytes() + cipherType.getIVLengthBytes()));
});
Ctx.INSTANCE.addListener(AesCommon.KEY, newValue -> {
System.arraycopy(newValue, 0, buffer, 0, mode.getKeyLengthBytes());
});
Ctx.INSTANCE.addListener(SECRET, newValue -> {
System.arraycopy(newValue, 0, buffer, mode.getKeyLengthBytes(), cipherType.getIVLengthBytes());
});
}
/**
* Sets the source {@link DataContent} for this instance.
* <p>
* This class does not modify or encrypt the input data—it only passes through
* the stream as-is via {@link #getStream()}.
*
* @param input the data source to use (must not be null)
* @throws NullPointerException if {@code input} is null
*/
@Override
public void setInput(final DataContent input) {
Objects.requireNonNull(input, "input must not be null");
source = input;
}
/**
* Returns the raw input stream from the previously set {@link DataContent}.
* <p>
* This stream is not encrypted or modified. Consumers are expected to use the
* AES key and IV made available through the {@link Ctx} for encryption or
* decryption operations.
*
* @return the raw input stream
* @throws IOException if the underlying stream cannot be opened
*/
@Override
public InputStream getStream() throws IOException {
return source.getStream();
}
}

View File

@@ -0,0 +1,266 @@
/*******************************************************************************
* 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.data.processing;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.data.DataContent;
import zeroecho.data.SecretContent;
import zeroecho.util.IOUtil;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
import zeroecho.util.aes.DerivedAesParameters;
/**
* A {@link SecretContent} implementation that uses a password-based key
* derivation function (PBKDF) to produce AES encryption parameters (key and
* IV).
*
* <p>
* This class supports both encryption and decryption:
* <ul>
* <li><strong>Encryption mode</strong>:
* <ul>
* <li>Derives the AES key and IV immediately upon construction.</li>
* <li>Stores the derived parameters, salt, and iteration count in a shared
* {@link Ctx}.</li>
* <li>The output stream prepends salt and iteration count as a header.</li>
* </ul>
* </li>
* <li><strong>Decryption mode</strong>:
* <ul>
* <li>Expects the input stream to start with a salt and iteration count
* header.</li>
* <li>Key and IV are re-derived dynamically in {@link #getStream()} using the
* extracted values.</li>
* <li>Mode and cipher type must be known in advance and are stored in
* {@link Ctx} at construction.</li>
* </ul>
* </li>
* </ul>
*
* <p>
* Derived parameters and configuration values are stored in {@link Ctx} using
* standard keys like {@code KEY}, {@code IV}, {@code SALT}, {@code ITERATIONS},
* {@code MODE}, {@code CIPHER_TYPE}, and {@code BLOCK_SIZE}.
*
* @author Leo Galambos
*/
public class SecretDerivedAesParameters implements SecretContent {
private static final Logger LOG = Logger.getLogger(SecretDerivedAesParameters.class.getName());
private DataContent source;
private final String password;
private final boolean encrypt;
/**
* Constructs an AES key derivation context with default cipher type
* ({@code CBC}).
* <p>
* This constructor performs full setup for encryption or preps configuration
* for decryption.
* </p>
*
* @param password the password used for PBKDF key derivation (must not be
* null)
* @param iterations the number of PBKDF iterations
* @param mode the AES mode (e.g., {@code AES-128}, {@code AES-256})
* @param encrypt true for encryption (derive key now), false for decryption
* (key derived later)
* @throws IllegalArgumentException if arguments are invalid
* @throws InvalidKeySpecException if key derivation fails (only in encryption
* mode)
*/
public SecretDerivedAesParameters(final String password, final int iterations, final AesMode mode,
final boolean encrypt) throws InvalidKeySpecException {
this(password, iterations, null, mode, AesCipherType.CBC, encrypt);
}
/**
* Constructs an AES key derivation context with explicit cipher type and
* optional Additional Authenticated Data (AAD).
*
* <p>
* In encryption mode, key derivation is performed immediately using PBKDF2, and
* the derived parameters (including AAD) are saved in {@link Ctx}. In
* decryption mode, the AES mode and cipher type are stored in {@link Ctx}, but
* key derivation is deferred until {@link #getStream()} reads the salt and
* iteration count from the input stream.
* </p>
*
* @param password the password used for PBKDF2 key derivation; must not be
* {@code null}
* @param iterations the PBKDF2 iteration count; should be at least 100,000 for
* adequate security
* @param aad optional Additional Authenticated Data (AAD); may be
* {@code null} if unused
* @param mode the AES mode (e.g., {@code AES-128}, {@code AES-256}); must
* not be {@code null}
* @param cipherType the AES cipher type (e.g., {@code CBC}, {@code GCM}); must
* not be {@code null}
* @param encrypt {@code true} for encryption mode (immediate derivation),
* {@code false} for decryption mode
*
* @throws IllegalArgumentException if any argument is invalid
* @throws InvalidKeySpecException if key derivation fails (only in encryption
* mode)
*
* @see AesSupport#deriveKeyAndIv(String, int, AesMode, byte[], AesCipherType)
*/
public SecretDerivedAesParameters(final String password, final int iterations, final byte[] aad, final AesMode mode,
final AesCipherType cipherType, final boolean encrypt) throws InvalidKeySpecException {
this.password = password;
this.encrypt = encrypt;
if (encrypt) {
final DerivedAesParameters params = AesSupport.deriveKeyAndIv(password, iterations, mode, aad, cipherType);
params.save(Ctx.INSTANCE);
} else {
Ctx.INSTANCE.put(AesCommon.CIPHER_TYPE, cipherType);
Ctx.INSTANCE.put(AesCommon.MODE, mode);
if (aad != null) {
Ctx.INSTANCE.put(AesCommon.AAD, aad);
}
}
Ctx.INSTANCE.put(AesCommon.BLOCK_SIZE, AesSupport.BLOCK_SIZE);
}
/**
* Sets the data source for encryption or decryption.
* <p>
* The actual processing mode depends on the {@code encrypt} flag provided
* during construction:
* <ul>
* <li><b>Encryption</b>: the stream will prepend salt and iteration header
* before actual data.</li>
* <li><b>Decryption</b>: the stream will read salt and iteration header to
* re-derive the key and IV.</li>
* </ul>
*
* @param input the input content to wrap (must not be null)
* @throws NullPointerException if {@code input} is null
*/
@Override
public void setInput(final DataContent input) {
Objects.requireNonNull(input, "input must not be null");
source = input;
}
/**
* Returns a stream that wraps the input content, handling AES encryption or
* decryption.
*
* <ul>
* <li><b>Encryption mode</b>:
* <ul>
* <li>Prepends a header (salt length, salt bytes, and iteration count).</li>
* <li>Returns a concatenated stream: header + encrypted content.</li>
* </ul>
* </li>
* <li><b>Decryption mode</b>:
* <ul>
* <li>Reads the header (salt and iterations) from the input stream.</li>
* <li>Re-derives the AES key and IV using stored password, mode, and cipher
* type.</li>
* <li>Returns the input stream positioned after the header.</li>
* </ul>
* </li>
* </ul>
*
* @return the wrapped input stream with header handling
* @throws IOException if stream reading or header parsing fails
* @throws IllegalStateException if key derivation fails during decryption
*/
@Override
public InputStream getStream() throws IOException {
if (encrypt) {
// encryption
final byte[] salt = Ctx.INSTANCE.get(DerivedAesParameters.SALT);
final int iterations = Ctx.INSTANCE.get(DerivedAesParameters.ITERATIONS);
final ByteArrayOutputStream header = new ByteArrayOutputStream();
IOUtil.write(header, salt);
IOUtil.writePack7I(header, iterations);
// salt is not a secret, so we can log it
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "processing AES (iterations={0} salt={1}",
new Object[] { iterations, Arrays.toString(salt) });
}
final InputStream encryptedStream = source.getStream();
return new SequenceInputStream(new ByteArrayInputStream(header.toByteArray()), encryptedStream);
} else {
// decryption
// Read salt and iteration count from input stream header
final InputStream in = source.getStream();
final byte[] salt = IOUtil.read(in, 4 * AesSupport.BLOCK_SIZE);
final int iterations = IOUtil.readPack7I(in);
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "processing AES (iterations={0} salt={1})",
new Object[] { iterations, Arrays.toString(salt) });
}
try {
final AesMode mode = Ctx.INSTANCE.get(AesCommon.MODE);
final AesCipherType cipherType = Ctx.INSTANCE.get(AesCommon.CIPHER_TYPE);
final byte[] aad = Ctx.INSTANCE.get(AesCommon.AAD);
final DerivedAesParameters params = AesSupport.rederiveKeyAndIv(password, salt, iterations, mode, aad,
cipherType);
Ctx.INSTANCE.put(AesCommon.KEY, params.key().getKey());
Ctx.INSTANCE.put(AesCommon.IV, params.iv());
Ctx.INSTANCE.put(DerivedAesParameters.SALT, params.salt());
Ctx.INSTANCE.put(DerivedAesParameters.ITERATIONS, iterations);
} catch (InvalidKeySpecException e) {
LOG.logp(Level.WARNING, "PasswordBasedAesDecryptor", "getStream", "Exception", e);
throw new IllegalStateException("Failed to generate key: invalid state", e);
}
return in;
}
}
}

View File

@@ -0,0 +1,253 @@
/**
* 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.data.processing;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.data.DataContent;
import zeroecho.data.SecretContent;
import zeroecho.util.IOUtil;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
import zeroecho.util.aes.BasicAesParameters;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
/**
* Provides AES encryption and decryption parameter management using a Key
* Encapsulation Mechanism (KEM).
* <p>
* This class integrates a {@link KEMAsymmetricContext} to securely derive AES
* keys and initialization vectors, supporting both encryption and decryption
* workflows:
* </p>
* <ul>
* <li><strong>Encryption mode:</strong> Uses the KEM generator to derive AES
* parameters, prepends the encapsulated key and IV to the data stream, and
* stores them in {@link Ctx} for downstream processing.</li>
* <li><strong>Decryption mode:</strong> Reads the encapsulated key and IV from
* the input stream, performs KEM decapsulation to reconstruct the AES key, and
* populates {@link Ctx} for subsequent AES operations.</li>
* </ul>
*
* <p>
* The class implements {@link SecretContent} and can be chained into
* {@code DataContent} pipelines for streaming encryption/decryption.
* </p>
*
* <h2>Usage</h2> <pre>{@code
* SecretKEMAesParameters params =
* new SecretKEMAesParameters(kemContext, aesMode, cipherType, aad);
* params.setInput(dataContent);
* InputStream stream = params.getStream();
* }</pre>
*
* <p>
* Thread-safety: Instances are not thread-safe. Each encryption or decryption
* pipeline must use its own {@code SecretKEMAesParameters}.
* </p>
*/
public class SecretKEMAesParameters implements SecretContent {
private static final Logger LOG = Logger.getLogger(SecretKEMAesParameters.class.getName());
/** KEM context used for encapsulation/decapsulation. */
private final KEMAsymmetricContext kemContext;
/** Indicates encryption (true) or decryption (false). */
private final boolean encrypt;
/** Data source for processing. */
private DataContent source;
/**
* Creates a new KEM-based AES parameter provider.
*
* <p>
* Depending on whether the provided {@link KEMAsymmetricContext} is configured
* for encryption or decryption, this constructor initializes the internal
* context and populates {@link Ctx} with the required AES parameters.
* </p>
*
* <ul>
* <li>In encryption mode, derives AES key material and IV using KEM and stores
* them in {@link Ctx}.</li>
* <li>In decryption mode, records the cipher type, AES mode, and optional
* Additional Authenticated Data (AAD) into {@link Ctx}.</li>
* </ul>
*
* @param kemContext the KEM context used for encapsulation/decapsulation; must
* not be {@code null}
* @param mode the AES mode specifying key length; must not be
* {@code null}
* @param cipherType the AES cipher type (CBC, GCM, etc.); must not be
* {@code null}
* @param aad optional Additional Authenticated Data (AAD); may be
* {@code null}
* @throws NullPointerException if any required parameter is {@code null}
*/
public SecretKEMAesParameters(final KEMAsymmetricContext kemContext, final AesMode mode,
final AesCipherType cipherType, final byte[] aad) {
this.kemContext = Objects.requireNonNull(kemContext, "kemContext must not be null");
this.encrypt = kemContext.extractor() == null;
if (encrypt) {
final BasicAesParameters params = AesSupport.deriveFromKEM(kemContext, mode, cipherType, aad);
params.save(Ctx.INSTANCE);
} else {
Ctx.INSTANCE.put(AesCommon.CIPHER_TYPE, Objects.requireNonNull(cipherType, "cipherType must not be null"));
Ctx.INSTANCE.put(AesCommon.MODE, Objects.requireNonNull(mode, "mode must not be null"));
if (aad != null) {
Ctx.INSTANCE.put(AesCommon.AAD, aad);
}
}
Ctx.INSTANCE.put(AesCommon.BLOCK_SIZE, AesSupport.BLOCK_SIZE);
}
/**
* Sets the input data content for subsequent processing.
* <p>
* This must be called prior to invoking {@link #getStream()}.
* </p>
*
* @param input the non-null input content to be processed
* @throws NullPointerException if {@code input} is {@code null}
*/
@Override
public void setInput(final DataContent input) {
this.source = Objects.requireNonNull(input, "input must not be null");
}
/**
* Returns an {@link InputStream} configured for either encryption or
* decryption.
*
* <ul>
* <li>In encryption mode, the stream begins with a header containing the
* encapsulated KEM data and IV, followed by the original content.</li>
* <li>In decryption mode, the method reads the encapsulated KEM data and IV,
* derives the AES key, updates {@link Ctx}, and returns the remaining
* stream.</li>
* </ul>
*
* @return a prepared {@link InputStream} suitable for AES processing
* @throws IOException if an error occurs while preparing the stream
*/
@Override
public InputStream getStream() throws IOException {
return encrypt ? buildEncryptionStream() : buildDecryptionStream();
}
/**
* Constructs an encryption stream by generating and prepending a KEM header.
* <p>
* The header contains:
* </p>
* <ul>
* <li>Encapsulated KEM data (used for key reconstruction during
* decryption).</li>
* <li>Initialization Vector (IV) required by the AES cipher.</li>
* </ul>
*
* @return an {@link InputStream} combining the header and original data content
* @throws IOException if writing to the header fails
*/
private InputStream buildEncryptionStream() throws IOException {
ByteArrayOutputStream header = new ByteArrayOutputStream();
byte[] encapsulated = kemContext.getEncapsulation();
byte[] iv = Ctx.INSTANCE.get(AesCommon.IV);
IOUtil.write(header, encapsulated);
IOUtil.write(header, iv);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Encryption header: encap=" + encapsulated.length + " iv=" + iv.length);
}
// --- Combine header + original stream ---
return new SequenceInputStream(new ByteArrayInputStream(header.toByteArray()), source.getStream());
}
/**
* Constructs a decryption stream by consuming the KEM header and restoring AES
* parameters.
*
* <p>
* Steps performed:
* </p>
* <ul>
* <li>Reads encapsulated KEM data and the IV from the input stream.</li>
* <li>Performs KEM decapsulation to rederive the AES key.</li>
* <li>Stores the derived key and IV into {@link Ctx} for downstream AES
* operations.</li>
* </ul>
*
* @return the remaining {@link InputStream} after header consumption
* @throws IOException if reading from the stream fails
* @throws IllegalArgumentException if the IV length does not match the expected
* size
*/
private InputStream buildDecryptionStream() throws IOException {
InputStream in = source.getStream();
AesCipherType cipherType = Ctx.INSTANCE.get(AesCommon.CIPHER_TYPE);
// --- Read encapsulated & IV ---
byte[] encapsulated = IOUtil.read(in, 30_000);
byte[] iv = IOUtil.read(in, cipherType.getIVLengthBytes());
if (iv.length != cipherType.getIVLengthBytes()) {
throw new IllegalArgumentException(cipherType + " requires IV length " + cipherType.getIVLengthBytes()
+ ", but only IV with " + iv.length + "available");
}
// --- KEM decapsulation ---
byte[] key = AesSupport.rederiveFromKEM(kemContext, encapsulated, Ctx.INSTANCE.get(AesCommon.MODE));
Ctx.INSTANCE.put(AesCommon.KEY, key);
Ctx.INSTANCE.put(AesCommon.IV, iv);
return in;
}
}

View File

@@ -0,0 +1,338 @@
/**
* 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.data.processing;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.auth.DestroyFailedException;
import org.bouncycastle.crypto.DataLengthException;
import conflux.Ctx;
import conflux.Key;
import zeroecho.data.DataContent;
import zeroecho.data.EncryptedContent;
import zeroecho.util.IOUtil;
import zeroecho.util.KeySupport;
import zeroecho.util.RandomSupport;
import zeroecho.util.asymmetric.AsymmetricContext;
import zeroecho.util.asymmetric.AsymmetricStreamBuilder;
import zeroecho.util.asymmetric.ClassicAsymmetricContext;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
import zeroecho.util.asymmetric.SignatureAsymmetricContext;
/**
* A cryptographic content wrapper that supports multi-recipient encryption and
* single-recipient decryption using asymmetric keys.
* <p>
* {@code SecretMultiRecipientCryptor} operates on {@link DataContent} instances
* and provides an {@link InputStream} that transparently performs encryption or
* decryption depending on the configured keys and input content type:
*
* <ul>
* <li>When constructed with public keys, the cryptor operates in <b>encryption
* mode</b>: a secret is encrypted once for each recipient and prepended as a
* header before the actual content stream.</li>
* <li>When constructed with a private key, the cryptor operates in
* <b>decryption mode</b>: the encrypted secret is extracted and decrypted from
* the header, and made available in the runtime context.</li>
* </ul>
*
* <p>
* This class uses a shared key reference ({@code SECRET}) to store the
* recovered or generated secret in the global context ({@link Ctx}) for
* downstream use.
*
* <p>
* Logging is kept at an operational level and avoids exposing sensitive data.
*
* @author Leo Galambos
*/
public class SecretMultiRecipientCryptor implements EncryptedContent {
private static final Logger LOG = Logger.getLogger(SecretMultiRecipientCryptor.class.getName());
private final PublicKey recipient[];
private final PublicKey decoy[];
private final PrivateKey privKey;
private DataContent source;
/**
* A typed key used for storing or retrieving secret data as a byte array.
* <p>
* The key identifier is "secret.data" and it expects values of type
* {@code byte[]}.
* </p>
*/
protected static final Key<byte[]> SECRET = Key.of("secret.data", byte[].class);
private record TaggedPublicKey(boolean decoy, PublicKey publicKey) {
}
/**
* Constructs a new {@code SecretMultiRecipientCryptor} configured for
* encryption using the specified recipients' public keys and optional decoy
* public keys.
* <p>
* The recipient public keys are used to encrypt the secret key for legitimate
* recipients. The decoy public keys are included to provide plausible
* deniability or to obfuscate the true set of recipients, but do not correspond
* to actual decryption capabilities.
* </p>
*
* @param recipient an array of {@link PublicKey} objects representing the
* legitimate recipients of the encrypted data
* @param decoy an array of {@link PublicKey} objects representing decoy
* recipients that cannot decrypt the data; may be empty or
* {@code null} if no decoys are desired
*/
public SecretMultiRecipientCryptor(final PublicKey[] recipient, final PublicKey[] decoy) { // NOPMD
super();
this.recipient = (recipient == null) ? new PublicKey[0] : recipient;
this.decoy = (decoy == null) ? new PublicKey[0] : decoy;
this.privKey = null;
}
/**
* Constructs a new {@code SecretMultiRecipientCryptor} configured for
* decryption with the provided private key.
*
* @param privKey the private key for decrypting the secret
*/
public SecretMultiRecipientCryptor(final PrivateKey privKey) {
super();
this.recipient = null;
this.decoy = null;
this.privKey = privKey;
}
/**
* Sets the input {@link DataContent} that this cryptor will wrap. This must be
* called before invoking {@link #getStream()}.
*
* @param input the data content to be encrypted or decrypted; must not be
* {@code null}
* @throws NullPointerException if {@code input} is {@code null}
*/
@Override
public void setInput(final DataContent input) {
Objects.requireNonNull(input, "input must not be null");
source = input;
}
/**
* Returns an {@link InputStream} representing either encrypted or decrypted
* data based on the configured mode:
*
* <ul>
* <li><b>Decryption Mode</b> (if a private key is set): Attempts to extract and
* decrypt the shared secret from the stream header. The decrypted secret is
* validated by hash and stored in the context under {@link #SECRET}. The
* remaining stream is returned for use.</li>
* <li><b>Encryption Mode</b> (if public keys are set): Encrypts the shared
* secret separately for each recipient, constructs a header from these
* encrypted blocks, appends a zero-length marker, and returns a stream
* combining the header and the original content.</li>
* </ul>
*
* @return an input stream for the processed content
* @throws IOException if the secret cannot be found or decrypted in
* decryption mode, or if an I/O error occurs
* @throws IllegalStateException if {@link #setInput(DataContent)} was not
* called before invocation
*/
@Override
public InputStream getStream() throws IOException {
if (privKey != null) {
return decrypt();
} else {
return encrypt();
}
}
private InputStream encrypt() throws IOException {
final ByteArrayOutputStream pubChunk = new ByteArrayOutputStream();
final ByteArrayOutputStream header = new ByteArrayOutputStream();
// valid secret
final ByteArrayOutputStream secretItem = new ByteArrayOutputStream();
final byte[] secret = Ctx.INSTANCE.get(SECRET);
IOUtil.writePack7I(secretItem, Arrays.hashCode(secret));
secretItem.write(secret);
final byte hashAndSecret[] = secretItem.toByteArray();
final ByteArrayInputStream secretStream = new ByteArrayInputStream(hashAndSecret);
// false secret - decoy
final ByteArrayOutputStream decoyItem = new ByteArrayOutputStream();
final byte[] decoysecret = RandomSupport.generateRandom(secret.length);
IOUtil.writePack7I(decoyItem, Arrays.hashCode(decoysecret));
decoyItem.write(decoysecret);
final byte hashAndDecoy[] = decoyItem.toByteArray();
final ByteArrayInputStream decoyStream = new ByteArrayInputStream(hashAndDecoy);
// prepare
final List<TaggedPublicKey> items = new ArrayList<>();
for (PublicKey item : recipient) {
items.add(new TaggedPublicKey(false, item));
}
for (PublicKey item : decoy) {
items.add(new TaggedPublicKey(true, item));
}
Collections.shuffle(items, RandomSupport.getRandom());
final byte[] aad = new byte[4];
int aadCounter = 0;
for (TaggedPublicKey item : items) {
aad[0] = (byte) (aadCounter >>> 24);
aad[1] = (byte) (aadCounter >>> 16);
aad[2] = (byte) (aadCounter >>> 8);
aad[3] = (byte) aadCounter;
aadCounter++;
try (AsymmetricContext ctx = KeySupport.fromKey(item.publicKey())) {
constructEncryptor(ctx).withInputStream(item.decoy() ? decoyStream : secretStream).withKEMCipherAad(aad)
.buildEncryptingStream().transferTo(pubChunk);
IOUtil.write(header, pubChunk.toByteArray());
pubChunk.reset();
if (item.decoy()) {
decoyStream.reset();
} else {
secretStream.reset();
}
} catch (DestroyFailedException e) {
throw new IOException(e);
}
}
IOUtil.writePack7I(header, 0);
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "header for {0} recipients, {1} decoys, size {2} bytes",
new Object[] { recipient.length, decoy.length, header.size() });
}
final InputStream encryptedStream = source.getStream();
return new SequenceInputStream(new ByteArrayInputStream(header.toByteArray()), encryptedStream);
}
private AsymmetricStreamBuilder constructEncryptor(AsymmetricContext context) {
return switch (context) {
case ClassicAsymmetricContext ctx -> // NOPMD
AsymmetricStreamBuilder.newBuilder().withCipherEngine(ctx.cipher()).withKey(ctx.key());
case KEMAsymmetricContext ctx -> // NOPMD
AsymmetricStreamBuilder.newBuilder().withKEMGenerator(ctx.generator()).withKey(ctx.key());
case SignatureAsymmetricContext ctx -> // NOPMD
throw new IllegalArgumentException(ctx.toString() + " cannot be used for encryption");
};
}
private InputStream decrypt() throws IOException { // NOPMD
final InputStream in = source.getStream();
try (AsymmetricContext ctx = KeySupport.fromKey(privKey)) {
final AsymmetricStreamBuilder asb = constructDecryptor(ctx);
final ByteArrayOutputStream decrypted = new ByteArrayOutputStream();
final byte[] aad = new byte[4];
int aadCounter = 0;
for (byte pubChunkByte[] = IOUtil.read(in, 20 * 1024); pubChunkByte.length > 0; pubChunkByte = IOUtil
.read(in, 20 * 1024)) {
if (decrypted.size() == 0) {
// the secret was not yet found
aad[0] = (byte) (aadCounter >>> 24);
aad[1] = (byte) (aadCounter >>> 16);
aad[2] = (byte) (aadCounter >>> 8);
aad[3] = (byte) aadCounter;
aadCounter++;
try {
final InputStream is = asb.withInputStream(new ByteArrayInputStream(pubChunkByte)) // NOPMD
.withKEMCipherAad(aad).buildDecryptingStream();
final int hash = IOUtil.readPack7I(is);
is.transferTo(decrypted);
final byte[] candidate = decrypted.toByteArray();
if (hash != Arrays.hashCode(candidate)) {
decrypted.reset();
}
} catch (IOException | DataLengthException | DestroyFailedException e) {
if (LOG.isLoggable(Level.INFO)) {
LOG.info(e.toString());
}
decrypted.reset();
} catch (IllegalArgumentException e) {
// thrown by PQ algos when something is wrong
if (LOG.isLoggable(Level.INFO)) {
LOG.info("Exception from PQ " + e);
}
decrypted.reset();
}
}
}
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "header with {0} recipients, secret size {1} bytes",
new Object[] { aadCounter, decrypted.size() });
}
if (decrypted.size() == 0) {
throw new IOException("secret was not found in the header");
}
Ctx.INSTANCE.put(SECRET, decrypted.toByteArray());
return in;
}
}
private AsymmetricStreamBuilder constructDecryptor(AsymmetricContext context) {
return switch (context) {
case ClassicAsymmetricContext ctx -> // NOPMD
AsymmetricStreamBuilder.newBuilder().withKey(ctx.key()).withCipherEngine(ctx.cipher());
case KEMAsymmetricContext ctx -> // NOPMD
AsymmetricStreamBuilder.newBuilder().withKEMExtractor(ctx.extractor()).withKey(ctx.key());
case SignatureAsymmetricContext ctx -> // NOPMD
throw new IllegalArgumentException(ctx.toString() + " cannot be used for decryption");
};
}
}

View File

@@ -0,0 +1,81 @@
/*******************************************************************************
* 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.data.processing;
import conflux.Ctx;
import conflux.Key;
import zeroecho.data.SecretContent;
import zeroecho.util.Password;
/**
* A {@link SecretContent} implementation that encapsulates a passwordKey
* string. This class extends {@link PlainString} and enforces immutability of
* the passwordKey after construction.
* <p>
* Passwords can be generated randomly or provided explicitly. Once set,
* attempts to change the passwordKey via parameters will cause an exception.
* <p>
* This class supports applying parameters from a map and collecting its state
* back into a map, using the key {@link #PASSWORD}.
*
* @author Leo Galambos
*/
public class SecretPassword extends PlainString implements SecretContent {
private final Key<String> PASSWORD = Key.of("secret.password", String.class);
/**
* Constructs a {@code SecretPassword} with a randomly generated printable
* passwordKey of the specified length.
*
* @param length the length of the generated passwordKey
* @throws IllegalArgumentException if {@code length} is less than or equal to
* zero
*/
public SecretPassword(final int length) {
super(Password.generatePrintablePassword(length));
Ctx.INSTANCE.put(PASSWORD, str);
}
/**
* Constructs a {@code SecretPassword} wrapping the specified passwordKey
* string. The passwordKey may be {@code null}.
*
* @param password the passwordKey string, or {@code null}
*/
public SecretPassword(final String password) {
super(password);
Ctx.INSTANCE.put(PASSWORD, str);
}
}

View File

@@ -0,0 +1,148 @@
/*******************************************************************************
* 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.data.processing;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import conflux.Key;
import zeroecho.data.SecretContent;
import zeroecho.util.Password;
import zeroecho.util.RandomSupport;
/**
* A secure byte container that holds cryptographically random data, intended
* for use in encryption-related operations such as secret generation, key
* material storage, or secure content handling.
* <p>
* {@code SecretRandom} extends {@link PlainBytes} and implements
* {@link SecretContent}, offering additional semantics for managing sensitive
* data. All instances are stored in a shared context via a predefined secret
* key reference to allow later retrieval or secure reuse.
* <p>
* Multiple constructors are provided to support different secure initialization
* modes:
* <ul>
* <li>Randomly filled buffer of a specified size</li>
* <li>Wrapping an existing secure byte array</li>
* <li>Seed-based randomization using a secure hash and salt</li>
* </ul>
* <p>
* Logging is limited to operational messages and avoids revealing any sensitive
* content.
*
* @author Leo Galambos
*/
public class SecretRandom extends PlainBytes implements SecretContent {
/**
* Logger instance scoped to this class, used for operational messages related
* to secure random number generation and management.
*
* Note: Care is taken not to log the sensitive content of the secret bytes.
*/
private static final Logger LOG = Logger.getLogger(SecretRandom.class.getName());
/**
* A typed key for secret data as a byte array, delegated from
* {@link SecretMultiRecipientCryptor#SECRET}.
*/
protected static final Key<byte[]> SECRET = SecretMultiRecipientCryptor.SECRET;
/**
* Constructs a {@code SecretRandom} instance with a buffer of the specified
* length.
* <p>
* If {@code fillWithRandom} is {@code true}, the buffer is filled with
* cryptographically secure random bytes. The resulting buffer is also stored in
* the shared context using a secret key.
* </p>
*
* @param length the length (in bytes) of the buffer to be allocated
* @param fillWithRandom if {@code true}, fills the buffer with secure random
* data
*/
public SecretRandom(final int length, final boolean fillWithRandom) {
super(length);
LOG.log(Level.INFO, "generating random, {0} bytes", length);
if (fillWithRandom) {
RandomSupport.generateRandom(buffer);
}
Ctx.INSTANCE.put(SECRET, buffer);
}
/**
* Creates a new instance by copying a pre-existing cryptographically secure
* byte array.
*
* @param random the secure byte array to use as secret content
*/
public SecretRandom(final byte[] random) {
super(random);
Ctx.INSTANCE.put(SECRET, buffer);
}
/**
* Constructs a new {@code SecretRandom} instance with a securely randomized
* byte buffer.
* <p>
* The buffer is initialized with cryptographically strong random data,
* generated using a combination of a user-provided {@code seed} string and an
* internal random salt. The resulting byte array is non-deterministic and
* unique across multiple invocations, even with the same seed.
* <p>
* The randomness is generated via the
* {@link Password#generateRandom(byte[], String)} method, which uses a secure
* hash (SHA-256) of the seed and salt to initialize a {@link SecureRandom}
* instance. This ensures that the internal buffer is filled with high-entropy,
* unpredictable bytes suitable for secret material or key generation purposes.
*
* @param length the size of the internal byte buffer
* @param seed a user-defined seed string that influences the randomness
* generation; must not be {@code null}
* @throws NoSuchAlgorithmException if the cryptographic algorithm (SHA-256 or
* SecureRandom) is not available
* @throws NegativeArraySizeException if {@code length} is negative
* @throws NullPointerException if {@code seed} is {@code null}
*/
public SecretRandom(final int length, final String seed) throws NoSuchAlgorithmException {
super(length);
Password.generateRandom(buffer, seed);
Ctx.INSTANCE.put(SECRET, buffer);
}
}

View File

@@ -0,0 +1,79 @@
/*******************************************************************************
* 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.
******************************************************************************/
/**
* Provides a comprehensive set of classes for AES encryption and decryption,
* password-based key derivation, and secure content handling.
*
* <p>
* This package contains the core AES cryptographic components:
* <ul>
* <li>{@link AesCommon} — common AES utilities and base class for encryptors
* and decryptors</li>
* <li>{@link AesEncryptor} and {@link AesDecryptor} — stream-based AES
* encryption and decryption handlers</li>
* <li>{@link PasswordBasedAesEncryptor} and {@link PasswordBasedAesDecryptor} —
* AES implementations using password-derived keys (via PBKDF2)</li>
* <li>{@link SecretRandom} — abstract base class for secure byte sequence
* generators</li>
* <li>{@link SecretAesRandom} — deterministic AES-based implementation of
* {@code SecretRandom}</li>
* <li>{@link SecretDerivedAesParameters} — encapsulates derived AES keys and
* IVs with contextual integrity checks</li>
* <li>{@link SecretMultiRecipientCryptor} — supports encrypting data for
* multiple recipients using independent key derivation paths</li>
* <li>{@link SecretPassword} — immutable password container designed for secure
* handling and comparison</li>
* </ul>
*
* <p>
* The package also offers plain content wrappers for handling unencrypted data
* sources:
* <ul>
* <li>{@link PlainBytes} — wraps raw byte arrays as content sources</li>
* <li>{@link PlainString} — wraps UTF-8 strings as content sources</li>
* <li>{@link PlainFile} — treats file-based or URL-based content as input
* sources</li>
* </ul>
*
* <p>
* This package is designed around streaming APIs and centralized context-based
* cryptographic parameter management using the shared {@link conflux.Ctx}
* object. It facilitates secure encryption workflows with support for dynamic
* derivation, deterministic streams, and multi-party encryption use cases.
* </p>
*
* @author Leo Galambos
*/
package zeroecho.data.processing;

View File

@@ -0,0 +1,51 @@
/*******************************************************************************
* 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.operations;
import zeroecho.data.DataContent;
import zeroecho.data.EncryptedContent;
import zeroecho.data.PlainContent;
import zeroecho.data.SecretContent;
/**
* Defines an operation that decrypts {@link EncryptedContent} using a
* {@link SecretContent} and returns a {@link DataContent}. The result is
* typically a {@link PlainContent}, but more complex encryption-decryption
* schemes may yield other types of {@link DataContent}, especially in cases
* involving multiple encryption layers for added complexity or obfuscation.
*/
public interface Decryption {
}

View File

@@ -0,0 +1,56 @@
/*******************************************************************************
* 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.operations;
import zeroecho.data.EncryptedContent;
/**
* Defines an operation for publishing {@link EncryptedContent} to public or
* shared locations.
* <p>
* The deployment process ensures that sensitive content, once encrypted, can be
* safely disseminated in a variety of forms and mediums—without revealing its
* meaning or presence. Common deployment strategies include:
* <ul>
* <li>Saving to files on local or remote systems</li>
* <li>Writing to standard output (e.g., console or logs)</li>
* <li>Embedding in other data structures or media via steganography</li>
* <li>Distributing through network endpoints or URLs</li>
* </ul>
* In advanced use cases, deployment may also aim to obscure the very existence
* of the content, making it resistant to detection or suspicion.
*/
public interface Deployment {
}

View File

@@ -0,0 +1,46 @@
/*******************************************************************************
* 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.operations;
import zeroecho.data.EncryptedContent;
import zeroecho.data.SecretContent;
/**
* Defines an operation that transforms arbitrary content into
* {@link EncryptedContent} using a {@link SecretContent} as the encryption key
* or secret.
*/
public interface Encryption {
}

View File

@@ -0,0 +1,64 @@
/*******************************************************************************
* 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.
******************************************************************************/
/**
* Provides abstractions for securely transforming and publishing sensitive data
* in encrypted form across public or shared environments.
* <p>
* The primary objective of this package is to enable the secure dissemination
* of original {@link zeroecho.data.PlainContent} by converting it into
* {@link zeroecho.data.EncryptedContent}, which can be safely shared—even in
* hostile or public environments—without exposing the underlying message.
* <p>
* This package defines the following interfaces:
* <ul>
* <li>{@link Encryption} Transforms any {@link zeroecho.data.DataContent},
* typically a {@link zeroecho.data.PlainContent}, into
* {@link zeroecho.data.EncryptedContent} using a
* {@link zeroecho.data.SecretContent} (e.g., a passphrase or key).</li>
* <li>{@link Decryption} Uses a {@link zeroecho.data.SecretContent} to
* recover the original {@link zeroecho.data.DataContent} from an
* {@link zeroecho.data.EncryptedContent}. In most cases, this yields a
* {@link zeroecho.data.PlainContent}, though more complex encryption chains are
* supported.</li>
* <li>{@link Deployment} Publishes the {@link zeroecho.data.EncryptedContent}
* to publicly accessible locations such as files, console output, URLs, or
* hidden within other data structures using techniques like steganography,
* making the content optionally hard to detect.</li>
* </ul>
* This framework supports layered encryption, flexible content handling, and
* covert deployment strategies for maximum confidentiality and plausible
* deniability in public communication or archival.
*/
package zeroecho.operations;

View File

@@ -0,0 +1,106 @@
/*******************************************************************************
* 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.security.Security;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
/**
* Provides initialization support for the Bouncy Castle cryptographic provider.
* <p>
* This utility class automatically adds the Bouncy Castle provider to the Java
* Security framework upon class loading. It also offers an explicit
* initialization method for optional use.
* <p>
* The class is declared as {@code final} and has a private constructor to
* prevent instantiation, emphasizing its utility nature.
*
* <p>
* Example usage:
*
* <pre>{@code
* BouncyCastleActivator.init();
* }</pre>
*
* @author Leo Galambos
*/
public final class BouncyCastleActivator {
/**
* Logger instance for the {@code BouncyCastleActivator} class, used to log
* messages related to the initialization and management of the Bouncy Castle
* security provider.
* <p>
* Initialized with the class name to ensure logs are specific and traceable to
* this component. Useful for debugging issues during cryptographic provider
* setup.
* </p>
*/
private static final Logger LOG = Logger.getLogger(BouncyCastleActivator.class.getName());
/**
* Static initializer that registers the Bouncy Castle provider with the Java
* Security framework. Logs the initialization process.
*/
static {
LOG.log(Level.INFO, "BouncyCastle provider initialization");
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
LOG.log(Level.INFO, "BouncyCastle PQC provider initialization");
if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastlePQCProvider());
}
}
/**
* Explicitly logs the activation of the Bouncy Castle provider. This method can
* be called to confirm provider activation.
*/
static public void init() {
LOG.log(Level.INFO, "BrouncyCastle activated");
}
/**
* Private constructor to prevent instantiation of this utility class.
*/
private BouncyCastleActivator() {
// this is a utility class
}
}

View File

@@ -0,0 +1,217 @@
/**
* 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.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
/**
* Enumeration of supported cryptographic algorithm names used in
* {@code KeyPairAlgorithm}.
*
* <p>
* Each name includes a human-readable display name that is used for external
* representation, such as in logs or UIs. The internal enum name (e.g.,
* {@code ML_KEM}) can still be accessed via {@link #name()}, while
* {@link #toString()} and {@link #displayName()} return the readable form.
*
* <p>
* This enum can be used in {@code switch-case} structures for logic branching
* on algorithm families.
*/
public enum CryptoAlgorithmsNames {
/** EdDSA (Edwards-curve Digital Signature Algorithm). */
ED25519("Ed25519"),
/** RSA public-key encryption and signature algorithm. */
RSA("RSA"),
/** DSA (Digital Signature Algorithm). */
DSA("DSA"),
/** EC (Elliptic Curve) public-key cryptography. */
EC("EC"),
/** ElGamal public-key encryption algorithm. */
ELGAMAL("ElGamal"),
/** NaccacheStern public-key cryptosystem. */
/// NACCACHE("NaccacheStern"),
/** McEliece code-based public-key encryption scheme. */
MCELIECE("McEliece"),
/**
* ML-KEM (Kyber), a post-quantum lattice-based KEM (Key Encapsulation
* Mechanism).
*/
KYBER("ML-KEM"),
/** SPHINCS+, a stateless hash-based digital signature scheme. */
SPHINCS_PLUS("SPHINCS+"),
/** NewHope, a post-quantum lattice-based KEM. */
NEWHOPE("NewHope"),
/**
* FrodoKEM, a post-quantum lattice-based KEM based on learning with errors
* (LWE).
*/
FRODO("Frodo");
private final String displayNameField;
private static final Map<String, CryptoAlgorithmsNames> BY_NAME = new HashMap<>(); // NOPMD
static {
for (CryptoAlgorithmsNames n : values()) {
BY_NAME.put(n.displayNameField.toUpperCase(Locale.ROOT), n); // case-insensitive match
}
}
/**
* Constructs an algorithm name enum with a human-readable name.
*
* @param displayName the external string representation of the algorithm name
*/
CryptoAlgorithmsNames(String displayName) {
this.displayNameField = displayName;
}
/**
* Returns the external display name of the algorithm.
*
* @return the human-readable algorithm name
*/
public String displayName() {
return displayNameField;
}
/**
* Returns the string representation of the algorithm, same as
* {@link #displayName()}.
*
* @return the human-readable algorithm name
*/
@Override
public String toString() {
return displayNameField;
}
/**
* Parses a human-readable algorithm name and returns the corresponding
* {@code CryptoAlgorithmsNames} enum constant. Matching is case-insensitive.
* <p>
* The method also normalizes the algorithm name extracted from a public or
* private key's format or metadata.
* <p>
* Some cryptographic providers, like BouncyCastle, use detailed algorithm names
* (e.g., "SPHINCS+-SHA2-256S") to specify variant or parameter information.
* This method maps such names to more general algorithm identifiers (e.g.,
* "SPHINCS+").
* <p>
* This ensures consistent algorithm identification regardless of the variant or
* parameter set.
*
* @param name the name of the algorithm (e.g., "RSA", "ML-KEM",
* "SPHINCS+-SHA2-256S")
* @return the corresponding {@code CryptoAlgorithmsNames} enum constant
* @throws IllegalArgumentException if the name is unknown
*/
public static CryptoAlgorithmsNames fromString(String name) { // NOPMD
if (name == null) {
throw new IllegalArgumentException("Algorithm name must not be null.");
}
if (name.startsWith("ML-KEM")) {
name = "ML-KEM"; // NOPMD
} else {
if (name.startsWith("EdDSA")) {
name = "Ed25519";
} else {
if (name.startsWith("Frodo")) {
name = "Frodo";
} else {
final int i = name.indexOf('-');
if (i != -1) {
name = name.substring(0, i);
}
}
}
}
final CryptoAlgorithmsNames result = BY_NAME.get(name.trim().toUpperCase(Locale.ROOT));
if (result == null) {
throw new IllegalArgumentException("Unknown algorithm name: " + name);
}
return result;
}
/**
* Returns a {@link KeyFactory} instance for the algorithm represented by this
* enum constant.
* <p>
* This method first attempts to obtain the {@code KeyFactory} from the classic
* Bouncy Castle provider ({@link BouncyCastleProvider}). If the algorithm is
* not supported there — typically in the case of post-quantum cryptographic
* (PQC) algorithms — it falls back to the Bouncy Castle PQC provider
* ({@link BouncyCastlePQCProvider}).
* </p>
*
* @return a {@code KeyFactory} for the algorithm associated with this enum
* constant
* @throws NoSuchAlgorithmException if the algorithm is not available from
* either provider
* @throws NoSuchProviderException if the required Bouncy Castle provider is
* not registered
*/
public KeyFactory getFactory() throws NoSuchAlgorithmException, NoSuchProviderException {
try {
// Classic algo ?
return KeyFactory.getInstance(displayNameField, BouncyCastleProvider.PROVIDER_NAME);
} catch (NoSuchAlgorithmException x) {
// PQC algo ?
return KeyFactory.getInstance(displayNameField, BouncyCastlePQCProvider.PROVIDER_NAME);
}
}
}

View File

@@ -0,0 +1,286 @@
/*******************************************************************************
* Copyright (C) 2024, 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.EOFException;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* Utility class for performing efficient I/O operations with support for packed
* integers. This class provides static methods to write and read data with a
* compact variable-length encoding ("Pack7") and custom handling of UUIDs and
* UTF-8 strings.
* <p>
* <b>Design rationale:</b> Using static methods allows developers to perform
* encoding and decoding operations directly on existing {@link InputStream} and
* {@link OutputStream} instances without the need for additional stream
* wrappers such as {@link FilterInputStream} or {@link FilterOutputStream}.
* This approach eliminates extra object creation and method call overhead,
* potentially improving performance, especially in high-throughput scenarios.
* </p>
* <p>
* <b>Usage:</b> These methods can be used independently with any I/O stream to
* achieve custom serialization formats, such as length-prefixed UTF-8 strings,
* packed integers, or custom binary structures.
* </p>
*/
public final class IOUtil { // NOPMD by Leo Galambos on 6/1/25, 4:30PM
/**
* Private constructor to prevent instantiation of this utility class.
*/
private IOUtil() {
// this is a utility class
}
/**
* Writes a byte array to the output stream with its length encoded as a packed
* 7-bit integer.
*
* @param out the output stream
* @param buf the byte array to write
* @throws IOException if an I/O error occurs
*/
public static void write(final OutputStream out, final byte[] buf) throws IOException {
writePack7I(out, buf.length);
out.write(buf);
}
/**
* Writes a UUID to the output stream as two big-endian long values.
*
* @param out the output stream
* @param uuid the UUID to write
* @throws IOException if an I/O error occurs
*/
public static void write(final OutputStream out, final UUID uuid) throws IOException {
writeLong(out, uuid.getMostSignificantBits());
writeLong(out, uuid.getLeastSignificantBits());
}
/**
* Writes a UTF-8 encoded string to the output stream with its length as a
* packed 7-bit integer.
*
* @param out the output stream
* @param str the string to write
* @throws IOException if an I/O error occurs
*/
public static void writeUTF8(final OutputStream out, final String str) throws IOException {
final byte[] buf = str.getBytes(StandardCharsets.UTF_8);
writePack7I(out, buf.length);
out.write(buf);
}
/**
* Writes a long value to the output stream in big-endian order (8 bytes).
*
* @param out the output stream
* @param val the long value to write
* @throws IOException if an I/O error occurs
*/
public static void writeLong(final OutputStream out, long val) throws IOException {
final byte[] buf = new byte[8];
for (int i = buf.length; --i >= 0;) { // NOPMD by Leo Galambos on 6/1/25, 4:32PM
buf[i] = (byte) (val & 0xff);
val = val >>> 8; // NOPMD by Leo Galambos on 6/1/25, 4:33PM
}
out.write(buf);
}
/**
* Reads a UUID from the input stream, composed of two big-endian long values.
*
* @param in the input stream
* @return the UUID read from the stream
* @throws IOException if an I/O error occurs or if the stream ends prematurely
*/
public static UUID readUUID(final InputStream in) throws IOException {
final long msb = readLong(in);
final long lsb = readLong(in);
return new UUID(msb, lsb);
}
/**
* Reads a UTF-8 encoded string from the input stream. The string is prefixed
* with a packed 7-bit integer indicating its length. To prevent excessive
* allocation, the maximum allowable length must be specified.
*
* @param in the input stream
* @param maxLength the maximum allowable length of the string in bytes
* @return the string read from the stream
* @throws IOException if an I/O error occurs, the length exceeds maxLength, or
* if the stream ends prematurely
*/
public static String readUTF8(final InputStream in, final int maxLength) throws IOException {
final int len = readPack7I(in);
if (len > maxLength) {
throw new IOException("readUTF8 length " + len + " exceeds maximum allowed length " + maxLength);
}
final byte[] buf = new byte[len];
if (in.readNBytes(buf, 0, buf.length) != buf.length) {
throw new EOFException("readUTF8 EOF");
}
return new String(buf, StandardCharsets.UTF_8);
}
/**
* Reads a long value from the input stream in big-endian order (8 bytes).
*
* @param in the input stream
* @return the long value read
* @throws IOException if an I/O error occurs or if the stream ends prematurely
*/
public static long readLong(final InputStream in) throws IOException {
final byte[] buff = new byte[8];
if (in.readNBytes(buff, 0, buff.length) != buff.length) {
throw new EOFException("readLong EOF");
}
long result = 0;
for (final byte b : buff) {
result = (result << 8) | (b & 0xffL);
}
return result;
}
/**
* Writes an integer to the output stream using packed 7-bit encoding (variable
* length).
*
* @param out the output stream
* @param val the integer value to write
* @throws IOException if an I/O error occurs
*/
public static void writePack7I(final OutputStream out, int val) throws IOException {
final byte[] buff = new byte[5];
int idx = buff.length;
while ((val & ~0x7f) != 0) {
buff[--idx] = (byte) (val & 0x7f);
val = val >>> 7; // NOPMD by Leo Galambos on 6/1/25, 4:35PM
}
buff[--idx] = (byte) val;
buff[buff.length - 1] |= 0x80;
out.write(buff, idx, buff.length - idx);
}
/**
* Writes a long value to the output stream using packed 7-bit encoding
* (variable length).
*
* @param out the output stream
* @param val the long value to write
* @throws IOException if an I/O error occurs
*/
public static void writePack7L(final OutputStream out, long val) throws IOException {
final byte[] buff = new byte[10];
int idx = buff.length;
while ((val & ~0x7fL) != 0) {
buff[--idx] = (byte) (val & 0x7f);
val = val >>> 7; // NOPMD by Leo Galambos on 6/1/25, 4:37PM
}
buff[--idx] = (byte) val;
buff[buff.length - 1] |= 0x80;
out.write(buff, idx, buff.length - idx);
}
/**
* Reads a byte array from the input stream. The length of the array is
* specified as a packed 7-bit integer prefix. To prevent excessive allocation,
* the maximum allowable length must be specified.
*
* @param in the input stream
* @param maxLength the maximum allowable length of the byte array
* @return the byte array read
* @throws IOException if an I/O error occurs, the length exceeds maxLength, or
* if the stream ends prematurely
*/
public static byte[] read(final InputStream in, final int maxLength) throws IOException {
final int len = readPack7I(in);
if (len > maxLength || len < 0) {
throw new IOException("read length " + len + " exceeds maximum allowed length " + maxLength);
}
final byte[] result = new byte[len];
if (in.readNBytes(result, 0, result.length) != result.length) {
throw new EOFException("read EOF");
}
return result;
}
/**
* Reads an integer from the input stream using packed 7-bit encoding (variable
* length).
*
* @param in the input stream
* @return the integer value read
* @throws IOException if an I/O error occurs or if the stream ends prematurely
*/
public static int readPack7I(final InputStream in) throws IOException {
int result = in.read();
if (result > 0x7f) { // NOPMD by Leo Galambos on 6/1/25, 4:38PM
return result & 0x7f;
}
int i;
for (i = in.read(); i < 0x80; i = in.read()) {
result = (result << 7) | i;
}
return (result << 7) | (i & 0x7f);
}
/**
* Reads a long value from the input stream using packed 7-bit encoding
* (variable length).
*
* @param in the input stream
* @return the long value read
* @throws IOException if an I/O error occurs or if the stream ends prematurely
*/
public static long readPack7L(final InputStream in) throws IOException {
long result = in.read();
if (result > 0x7f) { // NOPMD by Leo Galambos on 6/1/25, 4:38PM
return result & 0x7fL;
}
int i;
for (i = in.read(); i < 0x80; i = in.read()) {
result = (result << 7) | i;
}
return (result << 7) | (i & 0x7f);
}
}

View File

@@ -0,0 +1,516 @@
/**
* 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.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.KeyGenerationParameters;
import org.bouncycastle.crypto.generators.ElGamalParametersGenerator;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ElGamalParameters;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ElGamalParameterSpec;
import org.bouncycastle.pqc.crypto.frodo.FrodoKeyGenerationParameters;
import org.bouncycastle.pqc.crypto.frodo.FrodoKeyPairGenerator;
import org.bouncycastle.pqc.crypto.frodo.FrodoParameters;
import org.bouncycastle.pqc.crypto.frodo.FrodoPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.frodo.FrodoPublicKeyParameters;
import org.bouncycastle.pqc.crypto.newhope.NHKeyPairGenerator;
import org.bouncycastle.pqc.crypto.newhope.NHPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.newhope.NHPublicKeyParameters;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
import org.bouncycastle.pqc.jcajce.spec.KyberParameterSpec;
import org.bouncycastle.pqc.jcajce.spec.SPHINCSPlusParameterSpec;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McElieceKeyGenerationParameters;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McElieceKeyPairGenerator;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McElieceParameters;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McEliecePrivateKeyParameters;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McEliecePublicKeyParameters;
import zeroecho.util.bc.FrodoPrivateKey;
import zeroecho.util.bc.FrodoPublicKey;
import zeroecho.util.bc.McEliecePrivateKey;
import zeroecho.util.bc.McEliecePublicKey;
import zeroecho.util.bc.NewHopePrivateKey;
import zeroecho.util.bc.NewHopePublicKey;
/**
* Enumeration representing supported asymmetric key pair algorithms along with
* their key sizes and post-quantum cryptography (PQC) status.
*
* <p>
* This enum provides constants for classical cryptographic algorithms such as
* RSA, DSA, EC, ElGamal, and EdDSA, as well as various post-quantum algorithms
* like Kyber, SPHINCS+, NewHope, FrodoKEM, and McEliece.
* </p>
*
* <p>
* Each enum constant includes the algorithm's name, key size (or security level
* for PQC algorithms), and whether it is a post-quantum cryptographic
* algorithm.
* </p>
*
* <p>
* Functionality includes:
* <ul>
* <li>Generating key pairs using the Bouncy Castle (classic and PQC)
* providers.</li>
* <li>Converting Java {@link PublicKey} and {@link PrivateKey} objects to
* Bouncy Castle {@link org.bouncycastle.crypto.params.AsymmetricKeyParameter}
* instances.</li>
* <li>Parsing string representations of algorithms in the format
* "ALGORITHM:KEYSIZE" (case-insensitive) to corresponding enum constants.</li>
* </ul>
*
* <p>
* Note: The {@link #generateKeyPair()} method automatically adds the necessary
* Bouncy Castle providers if not already present.
* </p>
*
* <p>
* The {@link #generateKeyPair()} method uses the Bouncy Castle provider to
* generate the key pair, and the enum supports parsing from string format
* (e.g., {@code "RSA:2048"}) via {@link #fromString(String)}.
* </p>
*
* <p>
* Example usage: <pre>{@code
* KeyPairAlgorithm alg = KeyPairAlgorithm.RSA_2048;
* KeyPair keyPair = alg.generateKeyPair();
* }</pre>
*
* @author Leo Galambos
*/
public enum KeyPairAlgorithm {
/**
* EdDSA (Edwards-curve Digital Signature Algorithm) with 255-bit key size.
*/
ED25519(CryptoAlgorithmsNames.ED25519, 255),
/**
* RSA algorithm with 1024-bit key size.
*/
RSA_1024(CryptoAlgorithmsNames.RSA, 1024),
/**
* RSA algorithm with 2048-bit key size.
*/
RSA_2048(CryptoAlgorithmsNames.RSA, 2048),
/**
* RSA algorithm with 4096-bit key size.
*/
RSA_4096(CryptoAlgorithmsNames.RSA, 4096),
/**
* DSA algorithm with 2048-bit key size.
*/
DSA_2048(CryptoAlgorithmsNames.DSA, 2048),
/**
* EC algorithm with 256-bit key size (e.g., secp256r1).
*/
EC_P256(CryptoAlgorithmsNames.EC, 256),
/**
* EC algorithm with 384-bit key size (e.g., secp384r1).
*/
EC_P384(CryptoAlgorithmsNames.EC, 384),
/**
* ElGamal algorithm with 512-bit key size.
*/
ELGAMAL_512(CryptoAlgorithmsNames.ELGAMAL, 512),
/**
* ElGamal algorithm with 1024-bit key size.
*/
ELGAMAL_1024(CryptoAlgorithmsNames.ELGAMAL, 1024),
/**
* ElGamal algorithm with 2048-bit key size.
*/
ELGAMAL_2048(CryptoAlgorithmsNames.ELGAMAL, 2048),
/// NaccacheStern algorithm with 2048-bit key size.
/// NACCACHE_2048(CryptoAlgorithmsNames.NACCACHE, 2048),
/**
* McEliece algorithm with 256-bit security level.
*/
MCELIECE_256(CryptoAlgorithmsNames.MCELIECE, 256, true),
/**
* ML-KEM (Kyber) algorithm with 512-bit security level.
*/
KYBER_512(CryptoAlgorithmsNames.KYBER, 512, true),
/**
* ML-KEM (Kyber) algorithm with 768-bit security level.
*/
KYBER_768(CryptoAlgorithmsNames.KYBER, 768, true),
/**
* ML-KEM (Kyber) algorithm with 1024-bit security level.
*/
KYBER_1024(CryptoAlgorithmsNames.KYBER, 1024, true),
/**
* SPHINCS+ algorithm with 128-bit security level.
*/
SPHINCS_PLUS_128S(CryptoAlgorithmsNames.SPHINCS_PLUS, 128, true),
/**
* SPHINCS+ algorithm with 192-bit security level.
*/
SPHINCS_PLUS_192S(CryptoAlgorithmsNames.SPHINCS_PLUS, 192, true),
/**
* SPHINCS+ algorithm with 256-bit security level.
*/
SPHINCS_PLUS_256S(CryptoAlgorithmsNames.SPHINCS_PLUS, 256, true),
/**
* NewHope algorithm with 512-bit parameter set.
*/
NEWHOPE_512(CryptoAlgorithmsNames.NEWHOPE, 512, true),
/**
* NewHope algorithm with 1024-bit parameter set.
*/
NEWHOPE_1024(CryptoAlgorithmsNames.NEWHOPE, 1024, true),
/**
* FrodoKEM algorithm with 640-bit parameter set.
*/
FRODOKEM_640(CryptoAlgorithmsNames.FRODO, 640, true),
/**
* FrodoKEM algorithm with 976-bit parameter set.
*/
FRODOKEM_976(CryptoAlgorithmsNames.FRODO, 976, true),
/**
* FrodoKEM algorithm with 1344-bit parameter set.
*/
FRODOKEM_1344(CryptoAlgorithmsNames.FRODO, 1344, true);
/**
* Algorithms for which key pairs are serializable (i.e., keys return non-null
* from {@code getEncoded()}).
*/
public static final KeyPairAlgorithm[] SERIALIZABLE_ALGORITHMS = {
/// EdDSA (Edwards-curve Digital Signature Algorithm) with 255-bit key size.
ED25519,
/// RSA with 1024-bit key size.
RSA_1024,
/// RSA with 2048-bit key size.
RSA_2048,
/// RSA with 4096-bit key size.
RSA_4096,
/// DSA with 2048-bit key size.
DSA_2048,
/// Elliptic Curve with 256-bit key size (e.g., secp256r1).
EC_P256,
/// Elliptic Curve with 384-bit key size (e.g., secp384r1).
EC_P384,
/// ElGamal with 512-bit key size.
ELGAMAL_512,
/// ElGamal with 1024-bit key size.
ELGAMAL_1024,
/// ElGamal with 2048-bit key size.
ELGAMAL_2048,
/// ML-KEM / Kyber with 512-bit parameter.
KYBER_512,
/// ML-KEM / Kyber with 768-bit parameter.
KYBER_768,
/// ML-KEM / Kyber with 1024-bit parameter.
KYBER_1024,
/// SPHINCS+ with 128-bit security level (SHAKE-256).
SPHINCS_PLUS_128S,
/// SPHINCS+ with 192-bit security level (SHAKE-256).
SPHINCS_PLUS_192S,
/// SPHINCS+ with 256-bit security level (SHAKE-256).
SPHINCS_PLUS_256S,
/// FrodoKEM algorithm with 640-bit parameter set.
FRODOKEM_640,
/// FrodoKEM algorithm with 976-bit parameter set.
FRODOKEM_976,
/// FrodoKEM algorithm with 1344-bit parameter set.
FRODOKEM_1344 };
private static final Map<String, KeyPairAlgorithm> BY_STRING = new HashMap<>(); // NOPMD
static {
for (KeyPairAlgorithm alg : values()) {
BY_STRING.put(alg.toString().toUpperCase(Locale.ROOT), alg);
}
}
private final CryptoAlgorithmsNames algorithmName;
private final int keySize;
private final boolean pqc;
KeyPairAlgorithm(CryptoAlgorithmsNames algorithmName, int keySize) {
this(algorithmName, keySize, false);
}
KeyPairAlgorithm(CryptoAlgorithmsNames algorithmName, int keySize, boolean pqc) {
this.algorithmName = algorithmName;
this.keySize = keySize;
this.pqc = pqc;
}
/**
* Converts a given {@link PublicKey} into the corresponding Bouncy Castle
* {@link org.bouncycastle.crypto.params.AsymmetricKeyParameter}.
*
* @param key the public key to convert
* @return the corresponding BC asymmetric key parameter
* @throws IOException if the key encoding is invalid or conversion fails
*/
public AsymmetricKeyParameter getKeyParameter(PublicKey key) throws IOException {
return pqc ? org.bouncycastle.pqc.crypto.util.PublicKeyFactory.createKey(key.getEncoded())
: org.bouncycastle.crypto.util.PublicKeyFactory.createKey(key.getEncoded());
}
/**
* Converts a given {@link PrivateKey} into the corresponding Bouncy Castle
* {@link org.bouncycastle.crypto.params.AsymmetricKeyParameter}.
*
* @param key the private key to convert
* @return the corresponding BC asymmetric key parameter
* @throws IOException if the key encoding is invalid or conversion fails
*/
public AsymmetricKeyParameter getKeyParameter(PrivateKey key) throws IOException {
return pqc ? org.bouncycastle.pqc.crypto.util.PrivateKeyFactory.createKey(key.getEncoded())
: org.bouncycastle.crypto.util.PrivateKeyFactory.createKey(key.getEncoded());
}
/**
* Returns the standard cryptographic algorithm name (e.g., "RSA", "EC", "DSA")
* associated with this enum constant.
*
* @return the algorithm name as a {@code String}
*/
public String getAlgorithmName() {
return algorithmName.toString();
}
/**
* Returns the key size or security level in bits for this algorithm.
*
* <p>
* For classical algorithms, this corresponds to the actual key size (e.g., 2048
* bits for RSA). For post-quantum algorithms, this corresponds to the security
* level or parameter set size.
* </p>
*
* @return the key size in bits, or a negative number if undefined
*/
public int getKeySize() {
return keySize;
}
/**
* Returns a string representation of this algorithm in the format
* {@code "ALGORITHM:KEYSIZE"}, e.g., {@code "RSA:2048"} or
* {@code "McEliece:256"}. This format is consistent for parsing and display.
*
* @return the formatted string representation of the algorithm and key size
*/
@Override
public String toString() {
return algorithmName + ":" + keySize;
}
/**
* Generates a new {@link KeyPair} for this algorithm using the Bouncy Castle
* providers.
*
* <p>
* This method automatically adds and configures the Bouncy Castle and Bouncy
* Castle PQC providers if not already present in the Java Security environment.
* </p>
*
* @return a newly generated {@link KeyPair} instance for this algorithm
* @throws NoSuchAlgorithmException if the algorithm is not supported
* @throws NoSuchProviderException if the required Bouncy Castle
* provider is not registered or
* available
* @throws InvalidAlgorithmParameterException if algorithm parameters are
* invalid
*/
public KeyPair generateKeyPair() // NOPMD
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
switch (this) {
case ED25519: {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithmName.displayName());
keyPairGenerator.initialize(255, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
case RSA_1024:
case RSA_2048:
case RSA_4096:
case DSA_2048: {
KeyPairGenerator gen = KeyPairGenerator.getInstance(algorithmName.displayName(),
BouncyCastleProvider.PROVIDER_NAME);
gen.initialize(keySize);
return gen.generateKeyPair();
}
case EC_P256:
case EC_P384: {
KeyPairGenerator gen = KeyPairGenerator.getInstance(algorithmName.displayName(),
BouncyCastleProvider.PROVIDER_NAME);
gen.initialize(new ECGenParameterSpec("secp" + keySize + "r1"));
return gen.generateKeyPair();
}
case ELGAMAL_512:
case ELGAMAL_1024:
case ELGAMAL_2048: {
ElGamalParametersGenerator paramGen = new ElGamalParametersGenerator();
paramGen.init(keySize, 20, RandomSupport.getRandom());
ElGamalParameters params = paramGen.generateParameters();
ElGamalParameterSpec spec = new ElGamalParameterSpec(params.getP(), params.getG());
KeyPairGenerator gen = KeyPairGenerator.getInstance(algorithmName.displayName(),
BouncyCastleProvider.PROVIDER_NAME);
gen.initialize(spec);
return gen.generateKeyPair();
}
case MCELIECE_256: {
McElieceKeyPairGenerator generator = new McElieceKeyPairGenerator();
McElieceKeyGenerationParameters params = new McElieceKeyGenerationParameters(RandomSupport.getRandom(),
new McElieceParameters());
generator.init(params);
AsymmetricCipherKeyPair kp = generator.generateKeyPair();
McEliecePublicKeyParameters pubParams = (McEliecePublicKeyParameters) kp.getPublic();
McEliecePrivateKeyParameters privParams = (McEliecePrivateKeyParameters) kp.getPrivate();
PublicKey pub = new McEliecePublicKey(pubParams);
PrivateKey priv = new McEliecePrivateKey(privParams);
return new KeyPair(pub, priv);
}
case KYBER_512:
case KYBER_768:
case KYBER_1024: {
KeyPairGenerator gen = KeyPairGenerator.getInstance("Kyber", BouncyCastlePQCProvider.PROVIDER_NAME);
KyberParameterSpec paramSpec = switch (this) {
case KYBER_512 -> KyberParameterSpec.kyber512;
case KYBER_768 -> KyberParameterSpec.kyber768;
case KYBER_1024 -> KyberParameterSpec.kyber1024;
default -> throw new IllegalStateException("Unexpected Kyber variant");
};
gen.initialize(paramSpec, RandomSupport.getRandom());
return gen.generateKeyPair();
}
case SPHINCS_PLUS_128S:
case SPHINCS_PLUS_192S:
case SPHINCS_PLUS_256S: {
SPHINCSPlusParameterSpec params = switch (this) {
case SPHINCS_PLUS_128S -> SPHINCSPlusParameterSpec.shake_128s;
case SPHINCS_PLUS_192S -> SPHINCSPlusParameterSpec.shake_192s;
case SPHINCS_PLUS_256S -> SPHINCSPlusParameterSpec.shake_256s;
default -> throw new IllegalStateException("Unexpected SPHINCS variant");
};
KeyPairGenerator gen = KeyPairGenerator.getInstance("SPHINCSPlus",
BouncyCastlePQCProvider.PROVIDER_NAME);
gen.initialize(params);
return gen.generateKeyPair();
}
case NEWHOPE_512:
case NEWHOPE_1024: {
NHKeyPairGenerator generator = new NHKeyPairGenerator();
generator.init(new KeyGenerationParameters(RandomSupport.getRandom(), keySize));
AsymmetricCipherKeyPair kp = generator.generateKeyPair();
NHPublicKeyParameters pubParams = (NHPublicKeyParameters) kp.getPublic();
NHPrivateKeyParameters privParams = (NHPrivateKeyParameters) kp.getPrivate();
PublicKey pub = new NewHopePublicKey(pubParams);
PrivateKey priv = new NewHopePrivateKey(privParams);
return new KeyPair(pub, priv);
}
case FRODOKEM_640:
case FRODOKEM_976:
case FRODOKEM_1344: {
FrodoParameters params = switch (this) {
case FRODOKEM_640 -> FrodoParameters.frodokem640aes;
case FRODOKEM_976 -> FrodoParameters.frodokem976aes;
case FRODOKEM_1344 -> FrodoParameters.frodokem1344aes;
default -> throw new IllegalStateException("Unexpected Frodo variant");
};
FrodoKeyPairGenerator generator = new FrodoKeyPairGenerator();
generator.init(new FrodoKeyGenerationParameters(RandomSupport.getRandom(), params));
AsymmetricCipherKeyPair kp = generator.generateKeyPair();
FrodoPublicKeyParameters pubParams = (FrodoPublicKeyParameters) kp.getPublic();
FrodoPrivateKeyParameters privParams = (FrodoPrivateKeyParameters) kp.getPrivate();
PublicKey pub = new FrodoPublicKey(pubParams);
PrivateKey priv = new FrodoPrivateKey(privParams);
return new KeyPair(pub, priv);
}
}
return null;
}
/**
* Parses a string representation of an algorithm in the format
* {@code "ALGORITHM:KEYSIZE"} (case-insensitive) and returns the matching
* {@link KeyPairAlgorithm} enum constant.
*
* @param value the string to parse, e.g., "RSA:2048"
* @return the corresponding {@code KeyPairAlgorithm} constant
* @throws IllegalArgumentException if the input is null, malformed, or not
* recognized
*/
public static KeyPairAlgorithm fromString(String value) {
if (value == null || !value.contains(":")) {
throw new IllegalArgumentException("Expected format: ALGORITHM:KEYSIZE (e.g., RSA:2048)");
}
final KeyPairAlgorithm alg = BY_STRING.get(value.trim().toUpperCase(Locale.ROOT));
if (alg == null) {
throw new IllegalArgumentException("Unsupported key pair algorithm: " + value);
}
return alg;
}
}

View File

@@ -0,0 +1,425 @@
/*******************************************************************************
* 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; // NOPMD
import java.io.IOException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.DSAKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.crypto.interfaces.DHKey;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.engines.ElGamalEngine;
import org.bouncycastle.crypto.engines.RSAEngine;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.DSAParameters;
import org.bouncycastle.crypto.params.DSAPrivateKeyParameters;
import org.bouncycastle.crypto.params.DSAPublicKeyParameters;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.params.ElGamalParameters;
import org.bouncycastle.crypto.params.ElGamalPrivateKeyParameters;
import org.bouncycastle.crypto.params.ElGamalPublicKeyParameters;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.pqc.crypto.frodo.FrodoKEMExtractor;
import org.bouncycastle.pqc.crypto.frodo.FrodoKEMGenerator;
import org.bouncycastle.pqc.crypto.frodo.FrodoPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.frodo.FrodoPublicKeyParameters;
import org.bouncycastle.pqc.crypto.mlkem.MLKEMExtractor;
import org.bouncycastle.pqc.crypto.mlkem.MLKEMGenerator;
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters;
import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory;
import org.bouncycastle.pqc.crypto.util.PublicKeyFactory;
import org.bouncycastle.pqc.jcajce.interfaces.SPHINCSPlusPrivateKey;
import org.bouncycastle.pqc.jcajce.interfaces.SPHINCSPlusPublicKey;
import zeroecho.util.asymmetric.AsymmetricContext;
import zeroecho.util.asymmetric.ClassicAsymmetricContext;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
import zeroecho.util.asymmetric.SignatureAsymmetricContext;
import zeroecho.util.bc.FrodoPrivateKey;
import zeroecho.util.bc.FrodoPublicKey;
/**
* Utility class for converting between standard Java {@link Key} objects and
* BouncyCastle {@link AsymmetricKeyParameter} with optional cipher context.
* <p>
* Supports serialization and deserialization of public and private keys to/from
* a standardized string format, as well as conversion to
* {@link AsymmetricContext} wrappers compatible with BouncyCastle.
* </p>
*
* <h2>Supported Algorithms</h2>
* <ul>
* <li>RSA</li>
* <li>DSA</li>
* <li>EC</li>
* <li>ElGamal</li>
* <li>SPHINCS+</li>
* <li>KEM-based (e.g., Kyber, Frodo)</li>
* </ul>
*
* <h2>String Format</h2> <pre>{@code
* ALGORITHM:BIT_LENGTH:BASE64_ENCODED_KEY
* Example: RSA:2048:MIIBIjANBgkqhkiG9...
* }</pre>
*
* <p>
* Note that algorithms which do not support encryption (e.g., DSA, EC) will
* have a <code>null</code> cipher in their {@link AsymmetricContext}.
* </p>
*/
public final class KeySupport {
private KeySupport() {
// Utility class; do not instantiate
}
/**
* Converts a standard Java {@link PublicKey} into an {@link AsymmetricContext},
* which contains the corresponding BouncyCastle key parameter and an optional
* encryption engine if applicable.
*
* @param pubKey the public key to convert
* @return an {@link AsymmetricContext} wrapping the key and optional cipher
* @throws IOException if key decoding fails
* @throws IllegalArgumentException if the algorithm is unsupported
*/
public static AsymmetricContext fromKey(final PublicKey pubKey) throws IOException { // NOPMD
return switch (CryptoAlgorithmsNames.fromString(pubKey.getAlgorithm())) {
case CryptoAlgorithmsNames.ED25519 -> {
byte[] keyBytes = pubKey.getEncoded(); // full SubjectPublicKeyInfo DER-encoded
SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(keyBytes);
byte[] keyData = spki.getPublicKeyData().getBytes();
Ed25519PublicKeyParameters keyParam = new Ed25519PublicKeyParameters(keyData, 0);
yield new SignatureAsymmetricContext(keyParam); // , null);
}
case CryptoAlgorithmsNames.RSA -> {
final RSAPublicKey rsa = (RSAPublicKey) pubKey;
final RSAKeyParameters keyParam = new RSAKeyParameters(false, rsa.getModulus(),
rsa.getPublicExponent());
yield new ClassicAsymmetricContext(keyParam, new org.bouncycastle.crypto.encodings.OAEPEncoding(
new RSAEngine(), new SHA256Digest(), new byte[0]));
}
case CryptoAlgorithmsNames.DSA -> {
final DSAPublicKey dsa = (DSAPublicKey) pubKey;
final DSAParams p = dsa.getParams();
final DSAParameters dsaParams = new DSAParameters(p.getP(), p.getQ(), p.getG());
final DSAPublicKeyParameters keyParam = new DSAPublicKeyParameters(dsa.getY(), dsaParams);
yield new SignatureAsymmetricContext(keyParam); // , null); // Signing only
}
case CryptoAlgorithmsNames.EC -> {
final ECPublicKey ec = (ECPublicKey) pubKey;
final ECNamedCurveSpec ecSpec = (ECNamedCurveSpec) ec.getParams();
final ECNamedCurveParameterSpec bcSpec = ECNamedCurveTable.getParameterSpec(ecSpec.getName());
final ECPoint point = bcSpec.getCurve().createPoint(ec.getW().getAffineX(), ec.getW().getAffineY());
final ECDomainParameters domainParams = new ECDomainParameters(bcSpec.getCurve(), bcSpec.getG(),
bcSpec.getN(), bcSpec.getH(), bcSpec.getSeed());
final ECPublicKeyParameters keyParam = new ECPublicKeyParameters(point, domainParams);
yield new SignatureAsymmetricContext(keyParam); // , null); // Signing only
}
case CryptoAlgorithmsNames.ELGAMAL -> {
final DHPublicKey elg = (DHPublicKey) pubKey;
final DHParameterSpec elParams = elg.getParams();
final ElGamalParameters params = new ElGamalParameters(elParams.getP(), elParams.getG());
final ElGamalPublicKeyParameters keyParam = new ElGamalPublicKeyParameters(elg.getY(), params);
yield new ClassicAsymmetricContext(keyParam, new ElGamalEngine());
}
case CryptoAlgorithmsNames.SPHINCS_PLUS -> {
final SPHINCSPlusPublicKey sphincs = (SPHINCSPlusPublicKey) pubKey;
final AsymmetricKeyParameter keyParam = PublicKeyFactory.createKey(sphincs.getEncoded());
yield new SignatureAsymmetricContext(keyParam); // , (EncapsulatedSecretGenerator) null);
}
case CryptoAlgorithmsNames.KYBER -> {
// Kyber
final MLKEMPublicKeyParameters keyParam = (MLKEMPublicKeyParameters) PublicKeyFactory
.createKey(pubKey.getEncoded());
yield new KEMAsymmetricContext(keyParam, new MLKEMGenerator(RandomSupport.getRandom()));
}
case CryptoAlgorithmsNames.FRODO -> {
// Frodo
final FrodoPublicKeyParameters keyParam = ((FrodoPublicKey) pubKey).getKeyParameters();
yield new KEMAsymmetricContext(keyParam, new FrodoKEMGenerator(RandomSupport.getRandom()));
}
default -> {
throw new IllegalArgumentException("Unsupported algorithm: " + pubKey.getAlgorithm());
}
};
}
/**
* Converts a standard Java {@link PrivateKey} into an
* {@link AsymmetricContext}, which contains the corresponding BouncyCastle key
* parameter and an optional encryption engine if applicable.
*
* @param privKey the private key to convert
* @return an {@link AsymmetricContext} wrapping the key and optional cipher
* @throws IOException if key decoding fails
* @throws IllegalArgumentException if the algorithm is unsupported
*/
public static AsymmetricContext fromKey(final PrivateKey privKey) throws IOException { // NOPMD
return switch (CryptoAlgorithmsNames.fromString(privKey.getAlgorithm())) {
case CryptoAlgorithmsNames.ED25519 -> {
byte[] keyBytes = privKey.getEncoded();
PrivateKeyInfo pki = PrivateKeyInfo.getInstance(keyBytes);
byte[] keyData = pki.parsePrivateKey().toASN1Primitive().getEncoded();
Ed25519PrivateKeyParameters keyParam = new Ed25519PrivateKeyParameters(keyData, 0);
yield new SignatureAsymmetricContext(keyParam); // , null);
}
case CryptoAlgorithmsNames.RSA -> {
final RSAPrivateKey rsa = (RSAPrivateKey) privKey;
final RSAKeyParameters keyParam = new RSAKeyParameters(true, rsa.getModulus(),
rsa.getPrivateExponent());
yield new ClassicAsymmetricContext(keyParam, new org.bouncycastle.crypto.encodings.OAEPEncoding(
new RSAEngine(), new SHA256Digest(), new byte[0]));
}
case CryptoAlgorithmsNames.DSA -> {
final DSAPrivateKey dsa = (DSAPrivateKey) privKey;
final DSAParams p = dsa.getParams();
final DSAParameters dsaParams = new DSAParameters(p.getP(), p.getQ(), p.getG());
final DSAPrivateKeyParameters keyParam = new DSAPrivateKeyParameters(dsa.getX(), dsaParams);
yield new SignatureAsymmetricContext(keyParam); // , null); // Signing only
}
case CryptoAlgorithmsNames.EC -> {
final ECPrivateKey ecPrivateKey = (ECPrivateKey) privKey;
final ECNamedCurveSpec ecNamedSpec = (ECNamedCurveSpec) ecPrivateKey.getParams();
final ECNamedCurveParameterSpec bcSpec = ECNamedCurveTable.getParameterSpec(ecNamedSpec.getName());
final ECDomainParameters domainParams = new ECDomainParameters(bcSpec.getCurve(), bcSpec.getG(),
bcSpec.getN(), bcSpec.getH(), bcSpec.getSeed());
final ECPrivateKeyParameters keyParam = new ECPrivateKeyParameters(ecPrivateKey.getS(), domainParams);
yield new SignatureAsymmetricContext(keyParam); // , null); // Signing only
}
case CryptoAlgorithmsNames.ELGAMAL -> {
final DHPrivateKey elg = (DHPrivateKey) privKey;
final DHParameterSpec elParams = elg.getParams();
final ElGamalParameters params = new ElGamalParameters(elParams.getP(), elParams.getG());
final ElGamalPrivateKeyParameters keyParam = new ElGamalPrivateKeyParameters(elg.getX(), params);
yield new ClassicAsymmetricContext(keyParam, new ElGamalEngine());
}
case CryptoAlgorithmsNames.SPHINCS_PLUS -> {
final SPHINCSPlusPrivateKey sphincs = (SPHINCSPlusPrivateKey) privKey;
final AsymmetricKeyParameter keyParam = PrivateKeyFactory.createKey(sphincs.getEncoded());
yield new SignatureAsymmetricContext(keyParam); // , (EncapsulatedSecretExtractor) null);
}
case CryptoAlgorithmsNames.KYBER -> {
final MLKEMPrivateKeyParameters keyParam = (MLKEMPrivateKeyParameters) PrivateKeyFactory
.createKey(privKey.getEncoded());
yield new KEMAsymmetricContext(keyParam, new MLKEMExtractor(keyParam));
}
case CryptoAlgorithmsNames.FRODO -> {
// Frodo
final FrodoPrivateKeyParameters keyParam = ((FrodoPrivateKey) privKey).getKeyParameters();
yield new KEMAsymmetricContext(keyParam, new FrodoKEMExtractor(keyParam));
}
default -> {
throw new IllegalArgumentException("Unsupported algorithm: " + privKey.getAlgorithm());
}
};
}
/**
* Serializes a {@link Key} into a string format for storage or transmission.
* <p>
* Format: <code>ALGORITHM:BIT_LENGTH:BASE64_ENCODED_KEY</code>
* </p>
*
* @param key the key to serialize
* @return serialized string representation of the key
*/
public static String serializeKey(final Key key) {
final CryptoAlgorithmsNames algorithm = CryptoAlgorithmsNames.fromString(key.getAlgorithm());
final int keySize;
switch (algorithm) {
case CryptoAlgorithmsNames.RSA -> keySize = ((RSAKey) key).getModulus().bitLength();
case CryptoAlgorithmsNames.DSA -> keySize = ((DSAKey) key).getParams().getP().bitLength();
case CryptoAlgorithmsNames.EC -> keySize = ((ECKey) key).getParams().getCurve().getField().getFieldSize();
case CryptoAlgorithmsNames.ELGAMAL -> keySize = ((DHKey) key).getParams().getP().bitLength(); // ElGamal
// keys often
// use DH
// interface
case CryptoAlgorithmsNames.SPHINCS_PLUS -> {
// Approximate bit length from encoded length (not precisely meaningful for PQC)
keySize = key.getEncoded().length * 8;
}
case CryptoAlgorithmsNames.KYBER -> {
// Kyber: ML-KEM-1024 => KYBER.displayName() + "-" + {keySize}
keySize = Integer
.parseInt(key.getAlgorithm().substring(CryptoAlgorithmsNames.KYBER.displayName().length() + 1));
}
default -> {
// fallback
keySize = key.getEncoded().length * 8; // fallback
}
}
final String base64 = Base64.getEncoder().encodeToString(key.getEncoded());
return algorithm + ":" + keySize + ":" + base64;
}
/**
* Deserializes a public key from a string previously generated using a matching
* serialization format (e.g., by {@link #serializeKey(Key)}).
*
* <p>
* The expected format of the input string is:
* </p>
*
* <pre>{@code
* ALGORITHM:BIT_LENGTH:BASE64_ENCODED_KEY
* }</pre>
*
* <p>
* The {@code BIT_LENGTH} component is ignored during parsing and is only used
* for informational purposes.
* </p>
*
* <p>
* This method reconstructs a {@link PublicKey} instance using the
* {@link java.security.spec.X509EncodedKeySpec} and the {@link KeyFactory} for
* the specified algorithm. The BouncyCastle security provider is required to be
* registered beforehand.
* </p>
*
* @param serialized the serialized string representation of the public key
* @return the deserialized {@link PublicKey} object
* @throws IllegalArgumentException if the serialized string is malformed or
* does not contain exactly three parts
* @throws NoSuchAlgorithmException if the algorithm specified in the serialized
* string is not supported
* @throws NoSuchProviderException if the BouncyCastle provider is not
* available
* @throws InvalidKeySpecException if the key specification is invalid or
* incompatible with the algorithm
*/
public static PublicKey deserializePublicKey(final String serialized)
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
final int PARTS = 3;
final String[] parts = serialized.split(":", PARTS);
if (parts.length != PARTS) {
throw new IllegalArgumentException("Invalid serialized key format");
}
final CryptoAlgorithmsNames algorithm = CryptoAlgorithmsNames.fromString(parts[0]);
final KeyFactory keyFactory = algorithm.getFactory();
final byte[] encoded = Base64.getDecoder().decode(parts[2]);
final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
return keyFactory.generatePublic(keySpec);
}
/**
* Deserializes a private key from a string representation.
*
* <p>
* The expected format of the input string is:
* </p>
*
* <pre>{@code
* ALGORITHM:BIT_LENGTH:BASE64_ENCODED_PKCS8_KEY
* }</pre>
*
* <p>
* Example:
* </p>
*
* <pre>{@code
* RSA:2048:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBK...
* }</pre>
*
* <p>
* This method supports keys for standard algorithms such as RSA, DSA, EC, and
* ElGamal, provided they are properly encoded in PKCS#8 format and recognized
* by the BouncyCastle provider.
* </p>
* <p>
* The bit length field is ignored during deserialization, as the key strength
* is derived from the actual key material.
* </p>
*
* @param serialized the serialized private key string in the format
* {@code ALGORITHM:BIT_LENGTH:BASE64}
* @return the reconstructed {@link PrivateKey} instance
* @throws IllegalArgumentException if the serialized format is invalid
* @throws NoSuchAlgorithmException if the algorithm is not supported
* @throws NoSuchProviderException if the BouncyCastle provider is not
* available
* @throws InvalidKeySpecException if the key specification is invalid or the
* key cannot be reconstructed
*/
public static PrivateKey deserializePrivateKey(final String serialized)
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
final int PARTS = 3;
final String[] parts = serialized.split(":", PARTS);
if (parts.length != PARTS) {
throw new IllegalArgumentException("Invalid serialized key format");
}
final CryptoAlgorithmsNames algorithm = CryptoAlgorithmsNames.fromString(parts[0]);
final KeyFactory keyFactory = algorithm.getFactory();
final byte[] encoded = Base64.getDecoder().decode(parts[2]);
return keyFactory.generatePrivate(new java.security.spec.PKCS8EncodedKeySpec(encoded));
}
}

View File

@@ -0,0 +1,182 @@
/**
* 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
/**
* An abstract adapter that converts an {@link OutputStream}-based
* transformation (such as encryption or compression) into an
* {@link InputStream}-based one. This class reads data from a given
* {@link InputStream} (the {@code previousInput}), processes it through a
* transformation {@link OutputStream} (e.g., encryption, compression), and
* exposes the transformed data via the standard {@link InputStream} API.
* <p>
* Subclasses must initialize the {@code transformationOut} field with a proper
* {@link OutputStream} that writes transformed data into the internal buffer
* ({@code baos}). This initialization must be done <strong>after construction
* and before any read operation</strong>, via a subclass- specific method such
* as an <code>initialize(...)</code> method.
* </p>
*
* <h2>Subclass Contract</h2>
* <ul>
* <li>Set {@code transformationOut} to a valid {@link OutputStream} that writes
* to {@code baos}.</li>
* <li>Do <em>not</em> call virtual methods like initialization from
* constructors.</li>
* <li>Call the initialization method before performing any reads.</li>
* </ul>
*
* @author Leo Galambos
*/
@Deprecated
public abstract class OutputToInputStreamAdapter extends InputStream {
/**
* Default buffer size in bytes. Typically set to 8 KB (8 * 1024 bytes).
*/
protected static final int DEFAULT_BUF_SIZE = 8 * 1024;
/**
* Buffer size in bytes used by this instance. Initialized during object
* construction and remains constant.
*/
protected final int BUF_SIZE;
/**
* The original input stream containing untransformed data.
*/
protected final InputStream previousInput;
/**
* A buffer holding transformed data.
*/
protected ByteArrayOutputStream baos;
/**
* The output stream performing transformation and writing to {@code baos}. Must
* be initialized by subclasses before use.
*/
protected OutputStream transformationOut;
/**
* Input stream initialized to a no-op {@code InputStream} that contains no
* data.
* <p>
* This stream is set to {@link InputStream#nullInputStream()}, which is a
* convenient way to avoid {@code null} values while providing a valid,
* non-functional stream. Useful as a default placeholder until a real stream is
* assigned.
* </p>
*/
private InputStream bais = nullInputStream();
/**
* Constructs a new adapter wrapping the specified input stream with a given
* buffer size for transformation output.
*
* @param previousInput The input stream to read untransformed data from.
* @param bufSize The buffer size for the transformation output buffer.
*/
public OutputToInputStreamAdapter(final InputStream previousInput, final int bufSize) {
super();
Objects.requireNonNull(previousInput, "input stream must not be null");
this.previousInput = previousInput;
this.BUF_SIZE = bufSize;
baos = new ByteArrayOutputStream(BUF_SIZE);
}
/**
* Constructs a new adapter wrapping the specified input stream using the
* default buffer size.
*
* @param previousInput The input stream to read untransformed data from.
*/
public OutputToInputStreamAdapter(final InputStream previousInput) {
this(previousInput, DEFAULT_BUF_SIZE);
}
@Override
public int read() throws IOException {
ensureData();
return bais.read();
}
@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
ensureData();
return bais.read(b, off, len);
}
@Override
public void close() throws IOException {
previousInput.close();
}
/**
* Reads untransformed data from {@code previousInput}, writes it to
* {@code transformationOut} for processing, and buffers the transformed data in
* {@code baos} for reading.
*
* @throws IOException If an I/O error occurs during reading or transformation.
*/
private void ensureData() throws IOException {
if (bais.available() > 0 || baos == null) {
return;
}
bais = null; // NOPMD by Leo Galambos on 6/1/25, 4:09PM
final byte[] chunk = previousInput.readNBytes(BUF_SIZE);
if (chunk.length == 0) {
transformationOut.close();
final byte[] finalBytes = baos.toByteArray();
bais = new ByteArrayInputStream(finalBytes);
baos = null; // NOPMD
return;
} else {
transformationOut.write(chunk);
}
final byte[] chunkBytes = baos.toByteArray();
bais = new ByteArrayInputStream(chunkBytes);
baos.reset();
}
}

View File

@@ -0,0 +1,147 @@
/*******************************************************************************
* 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.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.locks.ReentrantLock;
/**
* Utility class for generating random passwords and secure random byte arrays.
* <p>
* This class provides methods to:
* <ul>
* <li>Generate cryptographically secure random byte arrays of specified
* length.</li>
* <li>Generate random passwords composed of characters derived from random
* bytes.</li>
* </ul>
* <p>
* A single instance of {@link SecureRandom} is used unless
* {@code UNSAFE_SINGLE_SECURE} is set to {@code false}, in which case a new
* {@link SecureRandom} instance is created for each operation.
* <p>
* This class is thread-safe through use of a {@link ReentrantLock}.
*
* @author Leo Galambos
*/
public final class Password {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private Password() {
// this is a utility class
}
/**
* Generates a random password by filling the provided byte array with
* cryptographically strong random bytes. The randomness is influenced by a
* combination of a user-supplied seed string and a randomly generated salt,
* ensuring that the result is both secure and non-deterministic across multiple
* invocations with the same input.
* <p>
* Internally, the method uses the SHA-256 digest of the concatenation of the
* seed and a 16-byte random salt to derive a seed for a {@link SecureRandom}
* instance. This seed is mixed into the internal state of the
* {@code SecureRandom} generator to produce a high-entropy, unpredictable byte
* sequence. As a result, the output differs each time the method is called,
* even with the same seed and password buffer.
* <p>
* Note: The generated salt is not returned or stored. If you require
* reproducible output or need to verify the result later, you must persist the
* salt separately.
*
* @param password the byte array to be filled with random password bytes; must
* not be {@code null}
* @param seed a user-supplied string used to influence the randomness
* generation; must not be {@code null}
* @return the same {@code password} byte array, now filled with
* cryptographically strong random data
* @throws NoSuchAlgorithmException if the SHA-256 digest or strong
* {@code SecureRandom} implementation is not
* available
* @throws NullPointerException if {@code password} or {@code seed} is
* {@code null}
*/
public static byte[] generateRandom(final byte[] password, final String seed) throws NoSuchAlgorithmException {
final byte[] salt = new byte[16];
RandomSupport.getRandom().nextBytes(salt);
// Combine input + salt
final byte[] seedBytes = seed.getBytes(StandardCharsets.UTF_8);
final byte[] combined = new byte[seedBytes.length + salt.length];
System.arraycopy(seedBytes, 0, combined, 0, seedBytes.length);
System.arraycopy(salt, 0, combined, seedBytes.length, salt.length);
// Derive seed using SHA-256
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
final byte[] rndSeed = digest.digest(combined);
// Seed SecureRandom (deterministic per run, but uses a fresh salt)
final SecureRandom seededRandom = SecureRandom.getInstanceStrong();
seededRandom.setSeed(rndSeed); // mixes with internal state
// Generate random output
seededRandom.nextBytes(password);
return password;
}
/**
* Generates a cryptographically secure random password consisting of printable
* ASCII characters.
*
* @param length the desired length of the password; must be positive
* @return a random password string using printable characters in the ASCII
* range 33126
* @throws IllegalArgumentException if {@code length} is less than or equal to
* zero
*/
public static String generatePrintablePassword(final int length) {
if (length <= 0) {
throw new IllegalArgumentException("Password length must be greater than zero");
}
final StringBuilder password = new StringBuilder(length);
for (int i = 0; i < length; i++) {
final int ascii = RandomSupport.getRandom().nextInt('~' - '!' + 1) + '!';
password.append((char) ascii);
}
return password.toString();
}
}

View File

@@ -0,0 +1,131 @@
/**
* 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.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utility class providing support for secure random number generation.
* <p>
* This class encapsulates logic for generating cryptographically secure random
* data using Java's {@link SecureRandom}. It optionally supports a singleton
* instance pattern for the {@code SecureRandom} generator, controlled by the
* {@code UNSAFE_SINGLE_SECURE} flag.
* </p>
*
* <p>
* <strong>Note:</strong> Reusing a single {@code SecureRandom} instance can
* improve performance, but may reduce randomness guarantees in some
* multi-threaded or long-lived contexts. Always assess your threat model and
* performance needs when toggling {@code UNSAFE_SINGLE_SECURE}.
* </p>
*
* @author Leo Galambos
*/
public final class RandomSupport {
private static final Logger LOG = Logger.getLogger(RandomSupport.class.getName());
/** Flag indicating whether a single {@link SecureRandom} instance is reused. */
private final static boolean UNSAFE_SINGLE_SECURE = true;
/** Lock for thread-safe access to the {@link SecureRandom} instance. */
private static Lock instanceLock = new ReentrantLock();
/** Shared {@link SecureRandom} instance. */
private static SecureRandom RANDOM;
static {
try {
RANDOM = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
LOG.logp(Level.WARNING, "Password", "", "NoSuchAlgorithmException", e);
RANDOM = new SecureRandom();
}
}
private RandomSupport() {
// this is a utility class
}
/**
* Retrieves a {@link SecureRandom} instance.
* <p>
* If {@code UNSAFE_SINGLE_SECURE} is true, returns a shared instance;
* otherwise, creates a new {@link SecureRandom} instance.
*
* @return A {@link SecureRandom} instance for random number generation.
*/
public static SecureRandom getRandom() {
instanceLock.lock();
try {
if (UNSAFE_SINGLE_SECURE) {
return RANDOM;
} else {
LOG.log(Level.INFO, "creating a new SecureRandom");
return new SecureRandom();
}
} finally {
instanceLock.unlock();
}
}
/**
* Generates a secure random byte array of the specified size.
*
* @param size The size of the byte array to generate.
* @return A byte array filled with cryptographically secure random bytes.
*/
public static byte[] generateRandom(final int size) {
return generateRandom(new byte[size]);
}
/**
* Fills the given byte array with random bytes and returns it.
* <p>
* This method uses a thread-safe {@code SecureRandom} instance to generate
* cryptographically strong random values.
*
* @param buffer the byte array to fill with random bytes
* @return the same byte array, now containing random data
* @throws NullPointerException if {@code buffer} is {@code null}
*/
public static byte[] generateRandom(final byte[] buffer) {
getRandom().nextBytes(buffer);
return buffer;
}
}

View File

@@ -0,0 +1,523 @@
/**
* 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;
}
}

View File

@@ -0,0 +1,265 @@
/*******************************************************************************
* 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.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermission;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* UniversalKeyStoreFile provides a simple, secure mechanism for storing and
* retrieving cryptographic public and private keys using a plain text file. It
* supports various key algorithms, including asymmetric and post-quantum, and
* ensures minimal file permissions for security.
* <p>
* The keys are stored as key-value pairs where the key is a symbolic name with
* suffix "_pub" or "_priv", and the value is formatted as:
*
* <pre>
* Algorithm:Length:Base64EncodedKey
* </pre>
* <p>
* File permissions are restricted to read and write for the owner (chmod 600).
*/
public class UniversalKeyStoreFile {
/**
* Logger instance for the {@code UniversalKeyStoreFile} class, used to log
* informational, debug, and error messages related to keystore file operations.
* <p>
* Initialized with the class name to enable clear and contextual logging.
* </p>
*/
private static final Logger LOG = Logger.getLogger(UniversalKeyStoreFile.class.getName());
/**
* File system path pointing to the keystore file managed by this instance.
* <p>
* This path is immutable after construction and represents the physical
* location of the keystore file, which may be used for reading or writing
* keystore data.
* </p>
*/
private final Path filePath;
/**
* Constructs a UniversalKeyStoreFile with the given path. If the file does not
* exist, it is created with secure permissions (rw-------).
*
* @param filePath The path of the file to use for storing keys.
*/
public UniversalKeyStoreFile(final Path filePath) {
this.filePath = filePath;
try {
if (!Files.exists(filePath)) {
Files.createFile(filePath);
}
setSecurePermissions(filePath);
} catch (IOException e) {
throw new UncheckedIOException("Error creating or securing key store file", e);
}
}
/**
* Constructs a UniversalKeyStoreFile with the given file name. If the file does
* not exist, it is created with secure permissions (rw-------).
*
* @param fileName The name of the file to use for storing keys.
*/
public UniversalKeyStoreFile(final String fileName) {
this(Path.of(fileName));
}
/**
* Sets the file permissions to rw------- (owner read/write only).
*
* @param path The path of the file to secure.
* @throws IOException If an I/O error occurs.
*/
private void setSecurePermissions(final Path path) throws IOException {
try {
// Set POSIX permissions: rw------- (owner read/write)
final Set<PosixFilePermission> perms = Set.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE);
Files.setPosixFilePermissions(path, perms);
} catch (UnsupportedOperationException e) {
LOG.log(Level.WARNING, "POSIX file permissions not supported on this platform.");
}
}
/**
* Adds a public key to the underlying storage for the specified owner.
* <p>
* The key must support standard encoding, i.e., {@link PublicKey#getEncoded()}
* must return a non-null value. If the key's format is {@code null}, indicating
* it is not serializable (e.g., some post-quantum keys), an
* {@link IllegalArgumentException} will be thrown.
*
* @param owner the identifier for the key owner; must not be {@code null}
* @param pubKey the public key to store; must support encoding
* @throws IllegalArgumentException if the key does not support encoding (i.e.,
* {@code getFormat() == null})
* @throws NullPointerException if either {@code owner} or {@code pubKey} is
* {@code null}
*/
public void addPubKey(final String owner, final PublicKey pubKey) {
if (pubKey.getEncoded() == null) {
throw new IllegalArgumentException(pubKey.toString() + " is not serializable");
}
writeEntry(owner + "_pub", KeySupport.serializeKey(pubKey));
}
/**
* Adds a private key to the underlying storage for the specified owner.
* <p>
* The key must be serializable, i.e., it must support encoding via
* {@link PrivateKey#getEncoded()}. If the key's format is {@code null},
* indicating it does not support standard encoding (e.g., some post-quantum
* keys), an {@link IllegalArgumentException} will be thrown.
*
* @param owner the identifier for the key owner; must not be {@code null}
* @param privKey the private key to store; must support encoding
* @throws IllegalArgumentException if the key does not support encoding (i.e.,
* {@code getFormat() == null})
* @throws NullPointerException if either {@code owner} or {@code privKey}
* is {@code null}
*/
public void addPrivKey(final String owner, final PrivateKey privKey) {
if (privKey.getEncoded() == null) {
throw new IllegalArgumentException(privKey.toString() + " is not serializable");
}
writeEntry(owner + "_priv", KeySupport.serializeKey(privKey));
}
/**
* Writes an entry to the key store file in the format key=value.
*
* @param key The key for the entry (e.g., owner_pub).
* @param value The value for the entry, including metadata and Base64 key.
*/
private void writeEntry(final String key, final String value) {
final String entry = key + "=" + value + System.lineSeparator();
try {
Files.writeString(filePath, entry, StandardOpenOption.APPEND);
} catch (IOException e) {
throw new UncheckedIOException("Error writing to key store file", e);
}
}
/**
* Loads a public key for the specified owner from the key store.
* <p>
* This method retrieves a line from the key store matching the pattern
* {@code owner_pub}, parses it to extract the key algorithm and Base64-encoded
* key data, and reconstructs the {@link PublicKey} using the {@code BC} (Bouncy
* Castle) provider.
* </p>
*
* @param owner the symbolic name of the key owner (must not be {@code null})
* @return the reconstructed {@link PublicKey} instance
* @throws NoSuchProviderException if the Bouncy Castle provider is not
* available
* @throws NoSuchAlgorithmException if the algorithm is not supported
* @throws InvalidKeySpecException if the key specification is invalid
* @throws NoSuchElementException if the key for the given owner is not found
* @throws IOException if an I/O error occurs while accessing the
* key store
*/
public PublicKey loadPublicKey(final String owner)
throws IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
final String line = findLine(owner + "_pub");
if (line == null) {
throw new NoSuchElementException("Public key for " + owner + " not found.");
}
final String[] parts = line.split("=", 2);
return KeySupport.deserializePublicKey(parts[1]);
}
/**
* Loads a private key for the specified owner from the key store.
*
* @param owner The symbolic name of the key owner.
* @return The reconstructed private key.
* @throws NoSuchElementException If the key is not found.
* @throws IOException If the key cannot be reconstructed.
* @throws NoSuchProviderException If the key cannot be reconstructed.
* @throws NoSuchAlgorithmException If the key cannot be reconstructed.
* @throws InvalidKeySpecException If the key cannot be reconstructed.
*/
public PrivateKey loadPrivateKey(final String owner)
throws IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
final String line = findLine(owner + "_priv");
if (line == null) {
throw new NoSuchElementException("Private key for " + owner + " not found.");
}
final String[] parts = line.split("=", 2);
return KeySupport.deserializePrivateKey(parts[1]);
}
/**
* Finds a line in the key store file corresponding to the specified key.
*
* @param key The key to search for.
* @return The line containing the key, or null if not found.
* @throws IOException If an I/O error occurs.
*/
private String findLine(final String key) throws IOException {
return Files.lines(filePath).filter(line -> line.startsWith(key + "=")).findFirst().orElse(null);
}
/**
* Clears all entries from the key store file.
*/
public void clearStore() {
try {
Files.writeString(filePath, "", StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException e) {
throw new UncheckedIOException("Error clearing key store file", e);
}
}
}

View File

@@ -0,0 +1,719 @@
/*******************************************************************************
* Copyright (C) 2024, 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.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.CRLReason;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v2CRLBuilder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.util.encoders.Base64;
/**
* A simple X.509 Certification Authority (CA) implementation for issuing,
* managing, and revoking certificates.
* <p>
* This class manages a CA for an organization, storing keys and certificates in
* files, handling a Certificate Revocation List (CRL), and issuing new
* certificates based on CSRs or cross-certificates. It also supports
* certificate revocation and ensures minimal file access permissions.
* <p>
* Key features:
* <ul>
* <li>Initializes CA with a self-signed root certificate</li>
* <li>Issues certificates based on PKCS#10 requests or existing
* certificates</li>
* <li>Manages a CRL and supports revoking certificates</li>
* <li>Persists data to disk with strong access permissions</li>
* <li>Uses BouncyCastle for cryptography</li>
* </ul>
*
* @author Leo Galambos
*/
public class X509CertificationAuthority {
/**
* Logger for logging internal operations and warnings.
*/
private static final Logger LOG = Logger.getLogger(X509CertificationAuthority.class.getName());
/**
* Default key length for the CA's key pair.
*/
private static final int CA_KEYPAIR_KEY_LENGTH = 2048;
/**
* Algorithm used for the CA key pair (e.g., "RSA").
*/
private static final String CA_KEYPAIR_ALG = "RSA";
/**
* Signature algorithm used for CA operations (e.g., "SHA256withRSA").
*/
protected static final String CA_SIGNATURE_ALG = "SHA256withRSA"; // SHA3-512WITHRSA, ED25519
/**
* CA certificate validity period in years.
*/
private static final int CA_VALIDITY_YEARS = 10;
/**
* Issued end-entity certificate validity period in years.
*/
private static final int VALIDITY_YEARS = 1;
/**
* X500Name representing the CA's subject information.
*/
private final X500Name caSubject;
/**
* The organization name associated with the CA.
*/
final private String organizationName;
/**
* The directory path where the CA's files and data are stored.
*/
final private Path dirPath;
/**
* Path to the CA's database file.
*/
final private Path dbPath;
/**
* Path to the Certificate Revocation List (CRL) file.
*/
final private Path crlPath;
/**
* Path to the keystore file storing CA keys and certificates.
*/
final protected Path keyStorePath;
/**
* Current serial number used for issuing certificates.
*/
protected int serialNum;
/**
* The key pair (private and public keys) of the root CA.
*/
protected KeyPair rootPair;
/**
* The root CA's X509 certificate.
*/
protected X509Certificate rootCert;
/**
* Holder for the current Certificate Revocation List (CRL).
*/
private X509CRLHolder crlHolder;
/**
* The universal keystore managing keys and certificates for the CA.
*/
private UniversalKeyStoreFile keyStore;
/**
* An empty array constant for certificates, to avoid repeated allocations.
*/
private static final Certificate[] EMPTY_CERTIFICATES_ARRAY = new Certificate[0];
/**
* Initializes a new instance of the CA for a given directory and organization.
*
* @param directory the base directory to store CA data
* @param organizationName the name of the organization (alphanumeric only)
*/
public X509CertificationAuthority(final String directory, final String organizationName) {
if (!organizationName.matches("^[a-zA-Z0-9]+$")) {
throw new IllegalArgumentException("organizationName must include alphanum(s) only");
}
dirPath = Paths.get(directory);
dbPath = dirPath.resolve(organizationName + ".db");
crlPath = dirPath.resolve(organizationName + ".crl");
keyStorePath = dirPath.resolve(organizationName + ".keystore");
serialNum = 0;
this.organizationName = organizationName;
this.caSubject = new X500Name("CN=zeroecho,OU=HQ,O=" + organizationName);
}
/**
* Initializes the Certificate Authority (CA) by creating necessary directories,
* loading or storing keys and Certificate Revocation Lists (CRLs). If the CA is
* not yet initialized, this method creates a new one with a self-signed
* certificate.
*
* @return {@code true} if the CA was already initialized; {@code false} if it
* was newly created
* @throws IOException if an I/O error occurs during directory
* creation or CRL handling
* @throws NumberFormatException if number parsing fails during
* initialization
* @throws GeneralSecurityException if a security-related exception occurs
* during initialization
*/
public boolean initialize() throws IOException, GeneralSecurityException {
Files.createDirectories(dirPath,
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
keyStore = new UniversalKeyStoreFile(keyStorePath);
final boolean alreadyInitialized;
try {
alreadyInitialized = initializeCA();
if (Files.notExists(crlPath)) {
createCrl();
}
} catch (OperatorCreationException e) {
LOG.logp(Level.WARNING, "X509CertificationAuthority", "initialize", "OperatorCreationException", e);
throw new GeneralSecurityException("Failed to create cryptographic operator", e);
}
if (Files.isReadable(crlPath)) {
loadCrl();
} else {
throw new IOException("CRL cannot be read: " + crlPath.toAbsolutePath());
}
return alreadyInitialized;
}
/**
* Issues a certificate for a given username based on a Base64-encoded PKCS#10
* CSR.
*
* @param username the user for whom the certificate is issued
* @param csrBase64String the CSR in Base64 encoding
* @return the issued X.509 certificate, or null if failed
*/
public Certificate issueFromCSR(final String username, final String csrBase64String) {
try {
final PKCS10CertificationRequest csr = new PKCS10CertificationRequest(Base64.decode(csrBase64String));
return processCSR(username, csr);
} catch (IOException e) {
LOG.log(Level.WARNING, "certificate issue", e);
return null;
}
}
/**
* Issues an X.509 certificate based on a PKCS#10 Certification Request (CSR)
* loaded from a PEM file.
* <p>
* This method loads the CSR from the specified file, validates it, and
* delegates to the internal {@code issue(String, PKCS10CertificationRequest)}
* method to generate the certificate.
* <p>
* If the CSR file is invalid or an I/O error occurs, the method logs the error
* and returns {@code null}.
*
* @param username the username or identifier for whom the certificate is being
* issued
* @param csrFile the file path to the PEM-encoded CSR file
* @return the issued {@link Certificate}, or {@code null} if an error occurred
* or the CSR is invalid
*/
public Certificate issueFromCSRFile(final String username, final String csrFile) {
try {
final PKCS10CertificationRequest csr = X509Support.loadCSR(csrFile);
if (csr == null) {
LOG.log(Level.SEVERE, "Failed to load CSR from file: {0}", csrFile);
return null;
}
return processCSR(username, csr);
} catch (IOException e) {
LOG.log(Level.WARNING, "certificate issue", e);
return null;
}
}
/**
* A container for a newly issued certificate and its associated key pair.
*
* @param cert the issued {@link Certificate}
* @param key the {@link KeyPair} associated with the certificate
*/
public record NewCertificate(Certificate cert, KeyPair key) {
}
/**
* Issues a new certificate for the given user and subject.
* <p>
* Generates a new {@link KeyPair}, constructs the subject public key info, and
* creates a certificate for the provided username and subject.
*
* @param username the identifier of the user for whom the certificate is issued
* @param subject the X.500 distinguished name for the certificate subject
* @return a {@code NewCertificate} record containing the issued certificate and
* key pair
* @throws NoSuchAlgorithmException if the algorithm used for key
* generation is not available
* @throws NoSuchProviderException if the cryptographic provider is
* not available
* @throws InvalidAlgorithmParameterException if the specified key size of
* EC-algorithms is invalid
*/
public NewCertificate issue(final String username, final String subject)
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
final KeyPair keyPair = KeyPairAlgorithm.RSA_2048.generateKeyPair();
final SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
return new NewCertificate(issue(username, new X500Name(subject), spki), keyPair);
}
/**
* Issues a new X.509 certificate for the specified user based on the provided
* Certification Signing Request (CSR).
*
* <p>
* This method extracts the subject and public key information from the CSR and
* delegates to the underlying certificate issuance method.
* </p>
*
* @param username the name of the user to whom the certificate will be issued
* @param csr the PKCS#10 Certification Signing Request containing the
* subject and public key
* @return the issued {@link Certificate} instance
*/
private Certificate processCSR(final String username, final PKCS10CertificationRequest csr) {
return issue(username, csr.getSubject(), csr.getSubjectPublicKeyInfo());
}
/**
* Issues a cross-certificate for an existing certificate.
*
* @param username the user name
* @param certificate the certificate to cross-sign
* @return the cross-signed certificate, or null if failed
*/
public Certificate issueCrossCertificate(final String username, final Certificate certificate) {
try {
final X509CertificateHolder cHolder = new X509CertificateHolder(certificate.getEncoded());
return issue(username, cHolder.getSubject(), cHolder.getSubjectPublicKeyInfo());
} catch (CertificateEncodingException | IOException e) {
LOG.log(Level.WARNING, "x-certificate issue", e);
return null;
}
}
/**
* Retrieves all certificates issued for a user, excluding revoked ones.
*
* @param username the username
* @return an array of X509Certificates, or empty array if none
*/
public Certificate[] getAll(final String username) { // NOPMD
if (Files.isReadable(dbPath)) {
final List<Certificate> result = new ArrayList<>();
try (LineNumberReader usrLine = new LineNumberReader(
new InputStreamReader(Files.newInputStream(dbPath, StandardOpenOption.READ)))) {
for (String line = usrLine.readLine(); line != null; line = usrLine.readLine()) {
final String[] attr = line.split("\\p{Blank}+", 3); // cert_id username
// certificate_with_pubkey/base64
final int id = Integer.parseInt(attr[0]);
if (id > serialNum) {
serialNum = id;
}
if (username.compareTo(attr[1]) == 0) {
final X509CertificateHolder ch = new X509CertificateHolder(Base64.decode(attr[2])); // NOPMD by
// Leo
// Galambos
// on
// 6/1/25,
// 12:50PM
if (crlHolder.getRevokedCertificate(ch.getSerialNumber()) == null) {
final X509Certificate cert = new JcaX509CertificateConverter() // NOPMD by Leo Galambos on
// 6/25/25, 6:55PM
.setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(ch);
result.add(cert);
} else {
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "Certificate #{0} ({1}) is revoked",
new Object[] { ch.getSerialNumber().toString(), ch.getSubject() }); // NOPMD by
// Leo
// Galambos
// on
// 6/1/25,
// 12:50PM
}
}
}
}
if (!result.isEmpty()) {
final Certificate[] resultArray = new Certificate[result.size()];
return result.toArray(resultArray);
}
} catch (IOException | CertificateException x) {
LOG.log(Level.WARNING, "getAll", x);
}
}
return EMPTY_CERTIFICATES_ARRAY;
}
/**
* Revokes a given certificate for a user and updates the CRL.
*
* @param username the username
* @param certificate the certificate to revoke
* @return true if successfully revoked, false otherwise
*/
public boolean revoke(final String username, final Certificate certificate) {
final Certificate[] candidates = getAll(username);
if (candidates.length == 0) {
return false;
}
try {
if (crlHolder.getNextUpdate().before(new Date())) {
LOG.info("Reloading CRL - we might have an old copy");
loadCrl();
}
for (final Certificate c : candidates) {
if (!(c instanceof X509Certificate x)) {
throw new IllegalStateException(
"X509 CA includes a certificate which is not X509: " + c.toString());
}
// if it is the same certificate, or it has the same public key
if (c.equals(certificate) || c.getPublicKey().equals(certificate.getPublicKey())) {
// found!
final X509v2CRLBuilder crlGen = new X509v2CRLBuilder(crlHolder); // NOPMD by Leo Galambos on 6/1/25,
// 12:55PM
crlGen.setNextUpdate(new Date(System.currentTimeMillis() + 1000 * 24 * 3600)); // NOPMD by Leo
// Galambos on
// 6/1/25, 12:55PM
final ExtensionsGenerator extGen = new ExtensionsGenerator(); // NOPMD by Leo Galambos on 6/1/25,
// 12:56PM
final CRLReason crlReason = CRLReason.lookup(CRLReason.privilegeWithdrawn);
extGen.addExtension(Extension.reasonCode, false, crlReason);
crlGen.addCRLEntry(x.getSerialNumber(), new Date(), extGen.generate()); // NOPMD by Leo Galambos on
// 6/1/25, 12:56PM
final ContentSigner signer = new JcaContentSignerBuilder(CA_SIGNATURE_ALG) // NOPMD by Leo Galambos
// on 6/25/25, 6:55PM
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(rootPair.getPrivate());
crlHolder = crlGen.build(signer);
writeCrl();
return true;
}
}
} catch (IOException | OperatorCreationException x) {
LOG.log(Level.WARNING, "revoke", x);
}
return false;
}
/**
* Initializes or loads the CA, generating key/cert if needed.
*
* @return true if CA was already initialized, false otherwise
* @throws GeneralSecurityException, OperatorCreationException, IOException
*/
private boolean initializeCA() throws GeneralSecurityException, OperatorCreationException, IOException {
final Path certPath = dirPath.resolve(organizationName + "-ca.crt");
final Path keyPath = dirPath.resolve(organizationName + "-ca.key");
final boolean certExists = Files.isReadable(certPath);
if (certExists ^ Files.isReadable(keyPath)) {
// one of the files is missing
throw new IllegalStateException("one of the cert or key files are not available");
}
if (certExists) {
// load our CA: cert and key
try (PEMParser pemParser = new PEMParser(Files.newBufferedReader(certPath))) {
final X509CertificateHolder rootCertHolder = (X509CertificateHolder) pemParser.readObject();
rootCert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME)
.getCertificate(rootCertHolder);
}
try (PEMParser pemParser = new PEMParser(Files.newBufferedReader(keyPath))) {
final PEMKeyPair kp = (PEMKeyPair) pemParser.readObject();
rootPair = new KeyPair(rootCert.getPublicKey(), new JcaPEMKeyConverter()
.setProvider(BouncyCastleProvider.PROVIDER_NAME).getPrivateKey(kp.getPrivateKeyInfo()));
}
serialNum = 1;
try (LineNumberReader usrLine = new LineNumberReader(
new InputStreamReader(Files.newInputStream(dbPath, StandardOpenOption.READ)))) {
for (String line = usrLine.readLine(); line != null; line = usrLine.readLine()) {
final String[] attr = line.split("\\p{Blank}+", 3); // cert_id username
// certificate_with_pubkey/base64
final int id = Integer.parseInt(attr[0]);
if (id > serialNum) {
serialNum = id;
}
}
} catch (NoSuchFileException e) {
// it might not yet exist, it is OK
LOG.log(Level.INFO, "{0} does not exist, will create a new one", dbPath);
} catch (IOException e) {
LOG.log(Level.WARNING, "Certificates cannot be fully read", e);
throw e;
}
return true;
}
// we are starting a new CA
final Calendar cal = Calendar.getInstance();
final Date startDate = cal.getTime();
cal.add(Calendar.YEAR, CA_VALIDITY_YEARS);
final Date endDate = cal.getTime();
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(CA_KEYPAIR_ALG);
keyPairGenerator.initialize(CA_KEYPAIR_KEY_LENGTH, new SecureRandom());
rootPair = keyPairGenerator.generateKeyPair();
final X509v3CertificateBuilder rootCertBuilder = new JcaX509v3CertificateBuilder(/* issuer */ caSubject,
BigInteger.valueOf(1), startDate, endDate, caSubject, rootPair.getPublic());
// BasicContraint - mark cert as CA cert
final JcaX509ExtensionUtils rootCertExtUtils = new JcaX509ExtensionUtils();
rootCertBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
rootCertBuilder.addExtension(Extension.authorityKeyIdentifier, false,
rootCertExtUtils.createAuthorityKeyIdentifier(rootPair.getPublic()));
rootCertBuilder.addExtension(Extension.subjectKeyIdentifier, false,
rootCertExtUtils.createSubjectKeyIdentifier(rootPair.getPublic()));
rootCertBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
final ContentSigner rootCertContentSigner = new JcaContentSignerBuilder(CA_SIGNATURE_ALG)
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(rootPair.getPrivate());
final X509CertificateHolder rootCertHolder = rootCertBuilder.build(rootCertContentSigner);
rootCert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME)
.getCertificate(rootCertHolder);
try (PrintWriter p = new PrintWriter(
new BufferedOutputStream(Files.newOutputStream(certPath, StandardOpenOption.CREATE_NEW)))) {
try (JcaPEMWriter jpw = new JcaPEMWriter(p)) {
jpw.writeObject(rootCert);
}
}
try (PrintWriter p = new PrintWriter(
new BufferedOutputStream(Files.newOutputStream(keyPath, StandardOpenOption.CREATE_NEW)))) {
try (JcaPEMWriter jpw = new JcaPEMWriter(p)) {
jpw.writeObject(rootPair.getPrivate());
}
}
serialNum = 1;
keyStore.addPubKey("*CA*", rootPair.getPublic());
keyStore.addPrivKey("*CA*", rootPair.getPrivate());
return false;
}
/**
* Creates a new Certificate Revocation List (CRL) and saves it.
*
* @throws NoSuchAlgorithmException, CertificateEncodingException,
* OperatorCreationException, IOException
*/
private void createCrl()
throws NoSuchAlgorithmException, CertificateEncodingException, OperatorCreationException, IOException {
final X509v2CRLBuilder crlBase = new JcaX509v2CRLBuilder(rootCert.getSubjectX500Principal(), new Date());
crlBase.setNextUpdate(new Date(System.currentTimeMillis() + 1000 * 24 * 3600));
// add extensions to CRL
final JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
crlBase.addExtension(Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(rootCert));
final ContentSigner signer = new JcaContentSignerBuilder(CA_SIGNATURE_ALG)
.setProvider(BouncyCastleProvider.PROVIDER_NAME).build(rootPair.getPrivate());
crlHolder = crlBase.build(signer);
writeCrl();
}
/**
* Loads the CRL from file if newer than current in-memory CRL.
*
* @throws IOException if CRL cannot be loaded
*/
private void loadCrl() throws IOException {
try (PEMParser pemParser = new PEMParser(Files.newBufferedReader(crlPath))) {
final X509CRLHolder newerCrl = (X509CRLHolder) pemParser.readObject();
if (crlHolder == null || crlHolder.getThisUpdate().before(newerCrl.getThisUpdate())) {
crlHolder = newerCrl;
} else {
LOG.log(Level.INFO, "CRL was not updated, file {0}", crlPath);
}
}
}
/**
* Writes the CRL to file.
*
* @throws IOException if CRL cannot be saved
*/
private void writeCrl() throws IOException {
try (PrintWriter p = new PrintWriter(new BufferedOutputStream(
Files.newOutputStream(crlPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE)))) {
try (JcaPEMWriter jpw = new JcaPEMWriter(p)) {
jpw.writeObject(crlHolder);
}
}
}
/**
* Issues a certificate using the subject name and public key.
*
* @param username the username
* @param subject X500 subject name
* @param publicKeyInfo public key info from CSR or certificate
* @return the issued certificate, or null if failed
*/
public Certificate issue(final String username, final String subject, final SubjectPublicKeyInfo publicKeyInfo) {
return issue(username, new X500Name(subject), publicKeyInfo);
}
/**
* Issues a certificate using the subject name and public key.
*
* @param username the username
* @param subject X500 subject name
* @param publicKeyInfo public key info from CSR or certificate
* @return the issued certificate, or null if failed
*/
public Certificate issue(final String username, final X500Name subject, final SubjectPublicKeyInfo publicKeyInfo) {
try {
final int serialNumCert = serialNum + 1;
final Calendar cal = Calendar.getInstance();
final Date startDate = cal.getTime();
cal.add(Calendar.YEAR, VALIDITY_YEARS);
final Date endDate = cal.getTime();
final X509v3CertificateBuilder myCertificateGenerator = new X509v3CertificateBuilder(caSubject,
BigInteger.valueOf(serialNumCert), startDate, endDate, subject, publicKeyInfo);
final JcaX509ExtensionUtils certExtUtils = new JcaX509ExtensionUtils();
myCertificateGenerator.addExtension(Extension.authorityKeyIdentifier, false,
certExtUtils.createAuthorityKeyIdentifier(rootPair.getPublic()));
myCertificateGenerator.addExtension(Extension.subjectKeyIdentifier, false,
certExtUtils.createSubjectKeyIdentifier(publicKeyInfo));
myCertificateGenerator.addExtension(Extension.keyUsage, true,
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.dataEncipherment));
final AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find(CA_SIGNATURE_ALG);
final AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
final ContentSigner sigGen = new BcRSAContentSignerBuilder(sigAlgId, digAlgId)
.build(PrivateKeyFactory.createKey(rootPair.getPrivate().getEncoded()));
final X509CertificateHolder holder = myCertificateGenerator.build(sigGen);
final org.bouncycastle.asn1.x509.Certificate eeX509CertStruct = holder.toASN1Structure();
final java.security.cert.CertificateFactory factory = java.security.cert.CertificateFactory
.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME);
final X509Certificate cert = (X509Certificate) factory
.generateCertificate(new ByteArrayInputStream(eeX509CertStruct.getEncoded()));
try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(dbPath, StandardCharsets.UTF_8,
StandardOpenOption.APPEND, StandardOpenOption.CREATE))) {
writer.printf("%d %s %s%n", serialNumCert, username, Base64.toBase64String(cert.getEncoded()));
}
serialNum = serialNumCert;
keyStore.addPubKey(username, cert.getPublicKey());
return cert;
} catch (IOException | CertificateException | OperatorCreationException | NoSuchProviderException
| NoSuchAlgorithmException e) {
LOG.log(Level.WARNING, "certificate issue", e);
return null;
}
}
}

View File

@@ -0,0 +1,175 @@
/**
* 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.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
/**
* Utility class providing support for working with X.509 certificates, private
* keys, and certification requests using PEM-encoded files and the Bouncy
* Castle library.
* <p>
* This class includes static helper methods to:
* <ul>
* <li>Load an X.509 certificate from a PEM file</li>
* <li>Load a PKCS#10 certification request (CSR) from a PEM file</li>
* <li>Print a {@link java.security.PrivateKey} in PEM format</li>
* <li>Print a {@link java.security.cert.Certificate}, including its PEM-encoded
* form</li>
* </ul>
* <p>
* All methods are static and the class cannot be instantiated.
*
* <p>
* <b>Note:</b> This class relies on the Bouncy Castle library and assumes the
* Bouncy Castle provider is correctly configured.
*
* @author Leo Galambos
*/
public final class X509Support {
private static final Logger LOG = Logger.getLogger(X509Support.class.getName());
/**
* Private constructor to prevent instantiation of this utility class.
*/
private X509Support() {
// this is a utility class
}
/**
* Loads an X509Certificate from a PEM file.
*
* @param path the file path to the certificate PEM file
* @return the loaded {@link X509Certificate} or null if invalid
* @throws IOException if an I/O error occurs reading the file
*/
public static Certificate loadCertificate(final String path) throws IOException {
try (PEMParser pemParser = new PEMParser(Files.newBufferedReader(Paths.get(path), StandardCharsets.UTF_8))) {
final Object obj = pemParser.readObject();
if (obj instanceof org.bouncycastle.cert.X509CertificateHolder) {
final org.bouncycastle.cert.X509CertificateHolder holder = (org.bouncycastle.cert.X509CertificateHolder) obj;
return new org.bouncycastle.cert.jcajce.JcaX509CertificateConverter()
.setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(holder);
} else {
System.err.println("File does not contain a valid X509 certificate.");
return null;
}
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error reading certificate file: {0}", path);
throw e;
} catch (Exception e) { // NOPMD by Leo Galambos on 6/1/25, 1:14PM
LOG.log(Level.SEVERE, "Error reading certificate file: {0}", path);
return null;
}
}
/**
* Utility method to print a {@link PrivateKey} in PEM format to standard
* output.
* <p>
* This method writes the private key using a {@link JcaPEMWriter} and prints it
* to the console in PEM (Base64-encoded) format. If an error occurs during the
* writing process, it logs a warning.
*
* @param privKey the {@link PrivateKey} to print
*/
public static void printPrivateKey(final PrivateKey privKey) {
try (StringWriter sw = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(sw)) {
pemWriter.writeObject(privKey);
pemWriter.flush();
System.out.println("Private Key (PEM):\n" + sw.toString());
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to print private key", e);
}
}
/**
* Prints the certificate details to standard output, including its PEM encoded
* representation.
*
* @param cert the {@link Certificate} to print
*/
public static void printCertificate(final Certificate cert) {
try {
System.out.println("Certificate:");
System.out.println(cert.toString());
// Print PEM encoded certificate
try (StringWriter sw = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(sw)) {
pemWriter.writeObject(cert);
pemWriter.flush();
System.out.println(sw.toString());
}
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to print certificate", e);
}
}
/**
* Loads a PKCS#10 Certification Request (CSR) from a PEM file.
*
* @param path the file path to the CSR PEM file
* @return the loaded {@link PKCS10CertificationRequest} or null if invalid
* @throws IOException if an I/O error occurs reading the file
*/
public static PKCS10CertificationRequest loadCSR(final String path) throws IOException {
try (PEMParser pemParser = new PEMParser(Files.newBufferedReader(Paths.get(path), StandardCharsets.UTF_8))) {
final Object obj = pemParser.readObject();
if (obj instanceof PKCS10CertificationRequest) {
return (PKCS10CertificationRequest) obj;
} else {
System.err.println("File does not contain a valid PKCS#10 CSR.");
return null;
}
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error reading CSR file: {0}", path);
throw e;
}
}
}

View File

@@ -0,0 +1,227 @@
/**
* 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.aes;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.StreamCipher;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.modes.SICBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import zeroecho.util.RandomSupport;
/**
* Enum representing various AES cipher modes along with their padding schemes,
* specifically using BouncyCastle cryptographic implementations.
* <p>
* Each enum constant defines a cipher configuration via a string identifier
* (e.g., {@code "AES/CBC/PKCS7Padding"}) and provides a factory method to
* create the corresponding cipher instance.
* </p>
* <p>
* The {@link #createCipher()} method utilizes a modern switch expression to
* return a new instance of the appropriate cipher implementation:
* <ul>
* <li>{@link org.bouncycastle.crypto.BufferedBlockCipher} for CBC mode</li>
* <li>{@link org.bouncycastle.crypto.modes.AEADBlockCipher} for GCM mode</li>
* <li>{@link org.bouncycastle.crypto.StreamCipher} for CTR mode</li>
* </ul>
* <p>
* The enum also provides utility methods to retrieve the required IV length and
* generate random IVs for each mode.
* </p>
*
* @see org.bouncycastle.crypto.BufferedBlockCipher
* @see org.bouncycastle.crypto.modes.AEADBlockCipher
* @see org.bouncycastle.crypto.StreamCipher
*/
public enum AesCipherType {
/**
* AES encryption in CBC (Cipher Block Chaining) mode with PKCS7 padding.
* Suitable for general-purpose encryption where authenticated encryption is not
* required.
*/
CBC("AES/CBC/PKCS7Padding"),
/**
* AES encryption in GCM (Galois/Counter Mode) with no padding. Provides
* authenticated encryption with associated data (AEAD).
*/
GCM("AES/GCM/NoPadding"),
/**
* AES encryption in CTR (Counter) mode with no padding. Acts as a stream
* cipher; suitable for environments requiring parallelizable
* encryption/decryption.
*/
CTR("AES/CTR/NoPadding");
private final String id;
/**
* Constructs a new {@code AesCipherType} with the given cipher identifier.
*
* @param id the full cipher transformation string, e.g.,
* {@code "AES/CTR/NoPadding"}
*/
AesCipherType(final String id) {
this.id = id;
}
/**
* Returns the string identifier of the AES cipher mode and padding scheme.
*
* @return the cipher identifier string, e.g. "AES/CBC/PKCS7Padding"
*/
public String getId() {
return id;
}
/**
* Returns the string representation of the AES cipher type.
*
* @return the cipher identifier string
*/
@Override
public String toString() {
return id;
}
/**
* Resolves the given string to an {@code AesCipherType} enum constant.
* <p>
* Performs a case-insensitive match against the transformation string (e.g.,
* {@code "AES/GCM/NoPadding"}).
* </p>
*
* @param name the transformation string to resolve
* @return the corresponding {@code AesCipherType}
* @throws IllegalArgumentException if no matching enum constant is found
*/
public static AesCipherType fromString(final String name) {
if (name == null) {
throw new IllegalArgumentException("Cipher type name must not be null");
}
for (final AesCipherType type : values()) {
if (type.id.equalsIgnoreCase(name)) {
return type;
}
}
throw new IllegalArgumentException("Invalid AES cipher type: " + name);
}
/**
* Creates a new instance of the BouncyCastle cipher implementation
* corresponding to this {@code AesCipherType}.
*
* The returned object is one of the following types depending on the cipher:
* <ul>
* <li>{@link org.bouncycastle.crypto.BufferedBlockCipher} for CBC mode</li>
* <li>{@link org.bouncycastle.crypto.modes.AEADBlockCipher} for GCM mode</li>
* <li>{@link org.bouncycastle.crypto.StreamCipher} for CTR mode</li>
* </ul>
* <p>
* The caller is responsible for initializing the cipher (e.g., with key and IV)
* and setting encryption/decryption mode.
* </p>
*
* @return a new instance of the cipher implementation for this AES mode
*/
public Object createCipher() {
return switch (this) {
case CBC -> createCbcCipher();
case GCM -> createGcmCipher();
case CTR -> createCtrCipher();
};
}
/**
* Generates a random initialization vector (IV) suitable for this AES cipher
* mode.
*
* @return a randomly generated IV with the appropriate byte length
*/
public byte[] generateRandomIV() {
return RandomSupport.generateRandom(getIVLengthBytes());
}
/**
* Returns the required IV length in bytes for this AES cipher mode.
* <ul>
* <li>CBC and CTR modes require a 16-byte IV</li>
* <li>GCM mode uses a 12-byte IV by convention for optimal security</li>
* </ul>
*
* @return the IV length in bytes
*/
public int getIVLengthBytes() {
return switch (this) {
case CBC -> 16;
case CTR -> 16;
case GCM -> 12;
};
}
/**
* Creates a new AES cipher configured for CBC mode with PKCS7 padding.
*
* @return a {@link BufferedBlockCipher} configured for AES/CBC/PKCS7Padding
*/
private static BufferedBlockCipher createCbcCipher() {
return new PaddedBufferedBlockCipher(CBCBlockCipher.newInstance(AESEngine.newInstance()));
}
/**
* Creates a new AES cipher configured for GCM mode with no padding (AEAD).
*
* @return an {@link AEADBlockCipher} configured for AES/GCM/NoPadding
*/
private static AEADBlockCipher createGcmCipher() {
return GCMBlockCipher.newInstance(AESEngine.newInstance());
}
/**
* Creates a new AES cipher configured for CTR mode (stream cipher) with no
* padding.
*
* @return a {@link StreamCipher} configured for AES/CTR/NoPadding
*/
private static StreamCipher createCtrCipher() {
return SICBlockCipher.newInstance(AESEngine.newInstance());
}
}

View File

@@ -0,0 +1,114 @@
/**
* 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.aes;
import java.io.IOException;
import java.io.InputStream;
import java.security.spec.InvalidKeySpecException;
import org.bouncycastle.crypto.params.KeyParameter;
import zeroecho.util.OutputToInputStreamAdapter;
/**
* An {@link InputStream} adapter that encrypts data read from a plaintext input
* stream using AES encryption with a specified Initialization Vector (IV).
* <p>
* The encrypted data is produced as the output of this stream.
* </p>
* <p>
* <strong>Important:</strong> AES encryption must be initialized by calling
* {@link #initialize(KeyParameter, byte[], AesCipherType, byte[])} <em>after
* construction and before any read operations</em>.
* </p>
*
* @deprecated This class is deprecated and may be removed in future releases.
* Consider using updated AES encryption stream utilities.
*/
@Deprecated
public class AesInputStreamAdapter extends OutputToInputStreamAdapter {
/**
* Constructs an AES input stream adapter wrapping the specified input stream,
* using the default buffer size.
*
* @param previousInput the input stream containing plaintext data to be
* encrypted
*/
public AesInputStreamAdapter(final InputStream previousInput) {
super(previousInput);
}
/**
* Constructs an AES input stream adapter wrapping the specified input stream,
* using the specified buffer size for internal buffering.
*
* @param previousInput the input stream containing plaintext data to be
* encrypted
* @param bufSize the size of the buffer used for transformation output
*/
public AesInputStreamAdapter(final InputStream previousInput, final int bufSize) {
super(previousInput, bufSize);
}
/**
* Initializes the AES encryption transformation using the provided secret key,
* initialization vector (IV), cipher type, and optional AAD (Additional
* Authenticated Data).
* <p>
* This method <strong>must</strong> be called before any read operations on
* this stream; otherwise, the stream will not function correctly.
* </p>
*
* @param key the AES encryption key parameter
* @param iv the initialization vector (IV) for AES encryption
* @param cipherType the AES cipher mode/type to use (e.g., CBC, GCM)
* @param aad additional authenticated data for AEAD modes (e.g., GCM);
* may be null or empty if not required. For non-AEAD modes,
* this parameter is ignored.
* @throws IOException if the underlying transformation output
* stream cannot be initialized
* @throws IllegalArgumentException if the key or IV parameters are invalid
* @throws InvalidKeySpecException if the encryption setup fails due to invalid
* key specification
*/
public void initialize(final KeyParameter key, final byte[] iv, final AesCipherType cipherType, final byte[] aad)
throws IOException, InvalidKeySpecException {
this.transformationOut = AesSupport.encrypt(key, baos, iv, cipherType, aad);
if (this.transformationOut == null) {
throw new IllegalStateException("AES transformation OutputStream cannot be null");
}
}
}

View File

@@ -0,0 +1,125 @@
/**
* 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.aes;
import java.util.Locale;
/**
* Enumerates the available AES encryption modes distinguished by key length:
* 128, 192, or 256 bits.
* <p>
* Each constant represents a specific AES key size and provides utility methods
* to retrieve the key length in bits and bytes, convert to a formatted string,
* and parse from a string representation.
* </p>
*/
public enum AesMode {
/**
* AES mode with a 128-bit key (16 bytes).
*/
AES_128(128),
/**
* AES mode with a 192-bit key (24 bytes).
*/
AES_192(192),
/**
* AES mode with a 256-bit key (32 bytes).
*/
AES_256(256);
private final int keyLengthBits;
/**
* Constructs an {@code AesMode} with the given key length in bits.
*
* @param keyLengthBits the AES key length in bits (128, 192, or 256)
*/
AesMode(final int keyLengthBits) {
this.keyLengthBits = keyLengthBits;
}
/**
* Returns the AES key length in bits.
*
* @return the key length in bits (e.g. 128, 192, 256)
*/
public int getKeyLengthBits() {
return keyLengthBits;
}
/**
* Returns the AES key length in bytes.
*
* @return the key length in bytes (e.g. 16, 24, 32)
*/
public int getKeyLengthBytes() {
return keyLengthBits / 8;
}
/**
* Returns a human-readable string representation of the AES mode in the format
* {@code "AES-<bits>"}, for example, {@code "AES-256"}.
*
* @return the formatted AES mode string
*/
@Override
public String toString() {
return "AES-" + keyLengthBits;
}
/**
* Parses a string representation of the AES mode (e.g., {@code "AES-128"}) and
* returns the corresponding {@code AesMode} constant.
*
* @param value the string to parse; must not be null or empty and should match
* one of {@code "AES-128"}, {@code "AES-192"}, or
* {@code "AES-256"}
* @return the corresponding {@code AesMode} constant
* @throws IllegalArgumentException if the input string is null, empty, or does
* not correspond to a valid AES mode
*/
public static AesMode fromString(final String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("AesMode string cannot be null or empty");
}
return switch (value.toUpperCase(Locale.ROOT)) {
case "AES-128" -> AES_128;
case "AES-192" -> AES_192;
case "AES-256" -> AES_256;
default -> throw new IllegalArgumentException("Unsupported AesMode: " + value);
};
}
}

View File

@@ -0,0 +1,124 @@
/**
* 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.aes;
import org.bouncycastle.crypto.params.KeyParameter;
import conflux.Ctx;
import zeroecho.data.processing.AesCommon;
/**
* Defines the essential parameters required for AES cryptographic operations,
* including encryption and decryption. This interface encapsulates the AES mode
* (which determines the key size), the secret key, the initialization vector
* (IV), the cipher type, and optional Additional Authenticated Data (AAD) for
* AEAD-capable modes such as GCM.
* <p>
* Implementations provide the necessary inputs to configure AES-based
* encryption/decryption algorithms securely and flexibly.
* </p>
*/
public interface AesParameters {
/**
* Retrieves the AES mode indicating the key size used for cryptographic
* operations. Typical modes correspond to key lengths of 128, 192, or 256 bits.
*
* @return the AES mode specifying the key size
*/
AesMode mode();
/**
* Returns the secret key wrapped in a {@link KeyParameter} object. This key is
* used for AES encryption or decryption.
*
* @return the AES secret key parameter
*/
KeyParameter key();
/**
* Provides the initialization vector (IV) required by the AES algorithm. The IV
* length must be consistent with the AES block size, typically 16 bytes.
* Implementations should ensure the returned array is immutable or a defensive
* copy to prevent unintended modifications.
*
* @return a byte array representing the IV
*/
byte[] iv(); // NOPMD by Leo Galambos on 6/8/25, 5:31PM
/**
* Returns the specific cipher type to be used with AES encryption or
* decryption. Examples include CBC, GCM, or stream cipher variants.
* <p>
* Implementations may define a default cipher type if not explicitly specified.
* </p>
*
* @return the AES cipher type
*/
AesCipherType cipherType();
/**
* Returns the Additional Authenticated Data (AAD) to be used in AEAD modes such
* as GCM. The same AAD must be provided for both encryption and decryption;
* otherwise, decryption will fail with an authentication error.
* <p>
* For non-AEAD modes, this value may be {@code null} or an empty byte array.
* </p>
*
* @return a byte array representing the AAD, or {@code null} if unused
*/
byte[] aad();
/**
* Persists the AES parameters into the provided {@code context}.
* <p>
* This method stores the AES mode, the raw secret key bytes, the initialization
* vector (IV), the cipher type, and, if present, the AAD into the context using
* predefined keys, facilitating secure parameter management.
* </p>
*
* @param context the non-null context in which to save the AES parameters
* @throws NullPointerException if {@code context} is null
*/
default void save(final Ctx context) {
context.put(AesCommon.MODE, mode());
context.put(AesCommon.KEY, key().getKey());
context.put(AesCommon.IV, iv());
context.put(AesCommon.CIPHER_TYPE, cipherType());
if (aad() != null) {
context.put(AesCommon.AAD, aad());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,141 @@
/**
* 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.aes;
import java.util.Objects;
import org.bouncycastle.crypto.params.KeyParameter;
/**
* A simple, immutable implementation of {@link AesParameters} that encapsulates
* the core cryptographic values required for AES encryption or decryption:
* <ul>
* <li>{@link AesMode} — defines the key size and block handling behavior</li>
* <li>{@link KeyParameter} — the AES key material</li>
* <li>Initialization Vector (IV) — must match the expected length for the
* cipher type</li>
* <li>{@link AesCipherType} — the AES cipher mode (e.g., CBC, GCM)</li>
* <li>Optional Additional Authenticated Data (AAD) — used in AEAD modes such as
* GCM</li>
* </ul>
*
* <p>
* This implementation is typically used when the key and IV are known and
* explicitly provided — such as in deterministic or hardware-based encryption
* systems — rather than derived from a password or key derivation function.
* </p>
*
* @param mode the AES mode, which dictates key size and operational
* semantics
* @param key the symmetric key used for AES encryption or decryption
* @param iv the initialization vector (must not be empty; must match
* block size required by cipher type)
* @param cipherType the cipher mode and padding scheme (e.g., CBC, GCM, CTR)
* @param aad optional Additional Authenticated Data for AEAD cipher
* types (may be {@code null}); ignored for non-AEAD modes
*
* @see DerivedAesParameters
* @see AesSupport
*/
public record BasicAesParameters(AesMode mode, KeyParameter key, byte[] iv, byte[] aad, AesCipherType cipherType)
implements AesParameters {
/**
* Constructs a {@code BasicAesParameters} instance with strict validation of
* its arguments.
*
* <p>
* Validations performed:
* </p>
* <ul>
* <li>All required arguments must be non-null</li>
* <li>The IV must not be empty</li>
* <li>The key length must match the expected length defined by
* {@code mode}</li>
* <li>The IV length must match the expected size for the selected
* {@code cipherType}</li>
* </ul>
*
* <p>
* The {@code aad} parameter is optional and is not validated here. If provided,
* it is assumed to be used only with AEAD-capable cipher types (e.g., GCM).
* </p>
*
* @throws NullPointerException if {@code mode}, {@code key}, {@code iv}, or
* {@code cipherType} is {@code null}
* @throws IllegalArgumentException if the key length or IV length are invalid,
* or if the IV is empty
*/
public BasicAesParameters {
mode = Objects.requireNonNull(mode, "mode must not be null");
key = Objects.requireNonNull(key, "key must not be null");
iv = Objects.requireNonNull(iv, "iv must not be null");
cipherType = Objects.requireNonNull(cipherType, "cipherType must not be null");
if (iv.length == 0) {
throw new IllegalArgumentException("iv must not be empty");
}
if (key.getKeyLength() != mode.getKeyLengthBytes()) {
throw new IllegalArgumentException(String.format("Key length mismatch: expected %d bytes, got %d",
mode.getKeyLengthBytes(), key.getKeyLength()));
}
if (iv.length != cipherType.getIVLengthBytes()) {
throw new IllegalArgumentException(String.format("IV length mismatch: expected %d bytes, got %d",
cipherType.getIVLengthBytes(), iv.length));
}
}
/**
* Constructs a {@code BasicAesParameters} instance using the default cipher
* type {@link AesCipherType#CBC}.
*
* <p>
* Use this constructor when cipher mode selection is fixed to CBC and no AAD is
* needed.
* </p>
*
* @param mode the AES mode (e.g., 128, 192, 256-bit)
* @param key the AES encryption key
* @param iv the initialization vector (must match CBC IV size)
*
* @throws NullPointerException if any parameter is {@code null}
* @throws IllegalArgumentException if key length or IV length are invalid
*/
public BasicAesParameters(final AesMode mode, final KeyParameter key, final byte[] iv) {
this(mode, key, iv, null, AesCipherType.CBC);
}
}

View File

@@ -0,0 +1,201 @@
/**
* 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.aes;
import java.util.Objects;
import org.bouncycastle.crypto.params.KeyParameter;
import conflux.Ctx;
import conflux.Key;
/**
* An implementation of {@link AesParameters} that encapsulates AES
* cryptographic parameters derived from a password using a key derivation
* function (KDF), such as PBKDF2.
* <p>
* In addition to the core parameters required for AES operations (mode, key,
* IV, and cipher type), this implementation includes metadata necessary for
* reconstructing the derived key: the salt and iteration count used during the
* KDF process.
* </p>
* <p>
* This class is intended for deterministic, password-based AES key derivation
* workflows, ensuring reproducibility of keys for decryption given the same
* password, salt, and iteration count.
* </p>
*
* <ul>
* <li>{@link AesMode} — the AES mode (determines key size and behavior)</li>
* <li>{@link KeyParameter} — the derived AES key for encryption or
* decryption</li>
* <li>Initialization Vector (IV) — must match the expected length for the
* cipher type</li>
* <li>Salt — the salt value used in the password-based key derivation
* function</li>
* <li>Iterations — the number of iterations used in the key derivation function
* (must be &gt;= 1)</li>
* <li>{@link AesCipherType} — the AES cipher type (e.g., CBC, GCM, CTR)</li>
* <li>Optional Additional Authenticated Data (AAD) — used for AEAD cipher types
* (may be {@code null})</li>
* </ul>
*
* @param mode the AES mode (determines key size and behavior)
* @param key the derived AES key for encryption or decryption
* @param iv the derived initialization vector (IV), must match the
* expected length for the cipher type
* @param salt the salt value used in the password-based key derivation
* function
* @param iterations the number of iterations used in the key derivation
* function (must be &gt;= 1)
* @param aad optional Additional Authenticated Data for AEAD cipher
* types (may be {@code null}); ignored for non-AEAD modes
* @param cipherType the AES cipher type (e.g., CBC, GCM, CTR)
*
* @see BasicAesParameters
* @see AesSupport
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8018">RFC 8018 -
* PBKDF2</a>
*/
public record DerivedAesParameters(AesMode mode, KeyParameter key, byte[] iv, byte[] salt, int iterations, byte[] aad,
AesCipherType cipherType) implements AesParameters {
/**
* Key used to store the salt in a {@link Ctx} context.
*/
public static final Key<byte[]> SALT = Key.of("secret.salt", byte[].class);
/**
* Key used to store the iteration count in a {@link Ctx} context.
*/
public static final Key<Integer> ITERATIONS = Key.of("secret.iterations", int.class);
/**
* Constructs a new {@code DerivedAesParameters} instance with full validation
* of all arguments.
* <p>
* This constructor performs the following checks:
* <ul>
* <li>None of the required parameters are {@code null}</li>
* <li>The IV is non-empty and matches the expected length for the selected
* cipher type</li>
* <li>The key length matches the expected size for the selected AES mode</li>
* <li>The iteration count is a positive integer (&gt;= 1)</li>
* </ul>
* <p>
* The optional {@code aad} may be {@code null} and is not validated here.
* </p>
*
* @throws NullPointerException if any required parameter is {@code null}
* @throws IllegalArgumentException if the IV is empty or invalid, the key
* length is incorrect, or the iteration count
* is less than 1
*/
public DerivedAesParameters {
mode = Objects.requireNonNull(mode, "mode must not be null");
key = Objects.requireNonNull(key, "key must not be null");
iv = Objects.requireNonNull(iv, "iv must not be null");
salt = Objects.requireNonNull(salt, "salt must not be null");
cipherType = Objects.requireNonNull(cipherType, "cipherType must not be null");
// aad may be null, no validation needed
if (iv.length == 0) {
throw new IllegalArgumentException("iv must not be empty");
}
if (key.getKeyLength() != mode.getKeyLengthBytes()) {
throw new IllegalArgumentException(String.format("Key length mismatch: expected %d bytes, got %d",
mode.getKeyLengthBytes(), key.getKeyLength()));
}
if (iv.length != cipherType.getIVLengthBytes()) {
throw new IllegalArgumentException(String.format("IV length mismatch: expected %d bytes, got %d",
cipherType.getIVLengthBytes(), iv.length));
}
if (iterations < 1) { // NOPMD by Leo Galambos on 6/8/25, 9:14PM
throw new IllegalArgumentException("iterations must be a positive integer");
}
}
/**
* Constructs a {@code DerivedAesParameters} instance with the default cipher
* type {@link AesCipherType#CBC} and no AAD.
*
* @param mode the AES mode used
* @param key the derived AES key
* @param iv the derived initialization vector
* @param salt the salt used in the key derivation function
* @param iterations the number of iterations used in key derivation
*/
public DerivedAesParameters(final AesMode mode, final KeyParameter key, final byte[] iv, final byte[] salt,
final int iterations) {
this(mode, key, iv, salt, iterations, null, AesCipherType.CBC);
}
/**
* Converts this derived parameter set into a {@link BasicAesParameters}
* instance by omitting derivation-specific metadata (salt, iteration count) and
* including the AAD if present.
*
* @return a new {@link BasicAesParameters} object with equivalent AES mode,
* key, IV, cipher type, and optional AAD
*/
public BasicAesParameters toBasicParameters() {
return new BasicAesParameters(mode, key, iv, aad, cipherType);
}
/**
* Saves this parameter set into the provided {@link Ctx} object.
* <p>
* This includes:
* <ul>
* <li>The core AES parameters defined in {@link AesParameters}</li>
* <li>The {@code salt} under the key {@link #SALT}</li>
* <li>The {@code iterations} under the key {@link #ITERATIONS}</li>
* </ul>
* This enables later retrieval or transmission of the full AES derivation
* context.
*
* @param context the context to populate; must not be {@code null}
* @throws NullPointerException if the context is {@code null}
*/
@Override
public void save(final Ctx context) {
AesParameters.super.save(context);
context.put(ITERATIONS, iterations());
context.put(SALT, salt());
}
}

View File

@@ -0,0 +1,81 @@
/**
* 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.
*/
/**
* Provides AES (Advanced Encryption Standard) cryptographic utilities and
* abstractions based on the Bouncy Castle library.
* <p>
* This package includes support for multiple AES cipher modes (e.g., CBC, GCM,
* CTR) and key sizes (128, 192, and 256 bits), along with flexible parameter
* encapsulation and utility methods for encryption and decryption.
* </p>
*
* <h2>Core Components</h2>
* <ul>
* <li>{@link AesCipherType} — Enum representing supported AES cipher modes and
* their padding schemes.</li>
* <li>{@link AesMode} — Enum for AES key sizes: 128, 192, and 256-bit
* modes.</li>
* <li>{@link AesParameters} — Interface defining cryptographic parameters
* required for AES operations.</li>
* <li>{@link BasicAesParameters} — Immutable implementation of
* {@code AesParameters} with explicit key and IV.</li>
* <li>{@link DerivedAesParameters} — Parameter object with AES key and IV
* derived from a password using PBKDF2.</li>
* <li>{@link AesSupport} — Final utility class for AES key generation and
* stream-based encryption/decryption.</li>
* <li>{@link AesInputStreamAdapter} — Deprecated input stream adapter for AES
* encryption of plaintext input.</li>
* </ul>
*
* <h2>Usage Notes</h2>
* <p>
* Most users will interact with this package through {@link AesSupport} for
* high-level operations, or construct {@code AesParameters} objects directly to
* configure encryption workflows. Random IV generation and cipher construction
* are abstracted by the {@code AesCipherType}.
* </p>
*
* <h2>Dependencies</h2>
* <p>
* This package relies on the
* <a href="https://www.bouncycastle.org/java.html">Bouncy Castle</a> library
* for low-level cryptographic primitives. Ensure that the Bouncy Castle
* provider is registered and available on the classpath.
* </p>
*
* @author Leo Galambos
* @since 1.0
*/
package zeroecho.util.aes;

View File

@@ -0,0 +1,57 @@
/**
* 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.asymmetric;
import java.io.Closeable;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
/**
* Represents an asymmetric cryptographic context.
* <p>
* This sealed interface is implemented by specific asymmetric context types,
* such as classic asymmetric contexts or key encapsulation mechanism (KEM)
* contexts.
* </p>
*/
public sealed interface AsymmetricContext extends Closeable permits ClassicAsymmetricContext, KEMAsymmetricContext, SignatureAsymmetricContext {
/**
* Returns the asymmetric key parameter associated with this context.
*
* @return the asymmetric key parameter (public or private key)
*/
AsymmetricKeyParameter key();
}

View File

@@ -0,0 +1,307 @@
/**
* 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.asymmetric;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.logging.Logger;
import javax.security.auth.DestroyFailedException;
import org.bouncycastle.crypto.AsymmetricBlockCipher;
import org.bouncycastle.crypto.EncapsulatedSecretExtractor;
import org.bouncycastle.crypto.EncapsulatedSecretGenerator;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import zeroecho.util.aes.AesCipherType;
/**
* Builder class to construct {@link InputStream} instances that perform
* asymmetric encryption or decryption on data streams.
* <p>
* This builder supports two main modes of asymmetric cryptographic processing:
* <ul>
* <li><b>Classic block cipher mode:</b> Using a configured
* {@link AsymmetricBlockCipher} engine and key for direct
* encryption/decryption.</li>
* <li><b>Key Encapsulation Mechanism (KEM) mode:</b> Using
* {@link EncapsulatedSecretGenerator} or {@link EncapsulatedSecretExtractor}
* for encapsulating or decapsulating symmetric keys combined with AES
* encryption.</li>
* </ul>
*
* <p>
* The builder allows configuring the input source stream, the asymmetric key
* (public or private), the cipher engine or KEM components, and the AES cipher
* type used for symmetric encryption within KEM mode.
* </p>
*
* <p>
* Example usage for classic encryption mode: <pre>
* InputStream encryptedStream = AsymmetricStreamBuilder.newBuilder()
* .withInputStream(dataInput)
* .withCipherEngine(myAsymmetricCipher)
* .withKey(publicKeyParam)
* .buildEncryptingStream();
* </pre>
*
* <p>
* Example usage for KEM-based encryption mode: <pre>
* InputStream encryptedStream = AsymmetricStreamBuilder.newBuilder()
* .withInputStream(dataInput)
* .withKEMGenerator(myKEMGenerator)
* .withKey(publicKeyParam)
* .withKEMCipherType(AesCipherType.GCM)
* .buildEncryptingStream();
* </pre>
*
* <p>
* Instances of this builder are immutable after {@code buildEncryptingStream()}
* or {@code buildDecryptingStream()} is called.
* </p>
*
* @author Leo Galambos
*/
public final class AsymmetricStreamBuilder { // NOPMD
private static final Logger LOG = Logger.getLogger(AsymmetricStreamBuilder.class.getName());
private InputStream input;
private AsymmetricBlockCipher engine;
private AsymmetricKeyParameter key;
private byte[] aad;
private EncapsulatedSecretGenerator generator;
private EncapsulatedSecretExtractor extractor;
private AesCipherType cipherType = AesCipherType.GCM;
private AsymmetricStreamBuilder() {
// Use factory method
}
/**
* Creates a new instance of {@code AsymmetricStreamBuilder}.
*
* @return a new builder instance
*/
public static AsymmetricStreamBuilder newBuilder() {
return new AsymmetricStreamBuilder();
}
/**
* Sets the input stream to be encrypted or decrypted.
*
* @param input the source {@link InputStream}
* @return this builder instance
*/
public AsymmetricStreamBuilder withInputStream(final InputStream input) {
this.input = Objects.requireNonNull(input, "Input stream cannot be null");
return this;
}
/**
* Sets the classic asymmetric cipher engine for block encryption or decryption.
*
* <p>
* Using this method will clear any previously set KEM generator or extractor,
* since classic cipher engine and KEM modes are mutually exclusive.
* </p>
*
* @param engine the {@link AsymmetricBlockCipher} engine to use
* @return this builder instance for method chaining
* @throws NullPointerException if {@code engine} is {@code null}
*/
public AsymmetricStreamBuilder withCipherEngine(final AsymmetricBlockCipher engine) {
this.engine = Objects.requireNonNull(engine, "Cipher engine cannot be null");
this.generator = null; // NOPMD
this.extractor = null; // NOPMD
return this;
}
/**
* Specifies the AES cipher type used internally within KEM-based encryption or
* decryption.
*
* @param cipherType the {@link AesCipherType} to use (e.g., CBC, GCM)
* @return this builder instance for method chaining
* @throws NullPointerException if {@code cipherType} is {@code null}
*/
public AsymmetricStreamBuilder withKEMCipherType(final AesCipherType cipherType) {
this.cipherType = Objects.requireNonNull(cipherType, "KEM cipher type cannot be null");
return this;
}
/**
* Sets the Additional Authenticated Data (AAD) to be associated with the Key
* Encapsulation Mechanism (KEM) cipher operations.
*
* <p>
* The specified AAD will be bound to the authenticated encryption process
* (e.g., when using AES-GCM) to ensure integrity protection of external
* associated data that is not encrypted but must be authenticated. This method
* should be called before initiating encryption or decryption.
* </p>
*
* @param aad the Additional Authenticated Data to associate with the cipher;
* may be {@code null} if no AAD is required
* @return this builder instance for method chaining
*/
public AsymmetricStreamBuilder withKEMCipherAad(final byte[] aad) {
this.aad = aad; // NOPMD
return this;
}
/**
* Sets the key encapsulation mechanism (KEM) secret generator for encryption.
*
* <p>
* Using this method will clear any previously set cipher engine or KEM
* extractor, since these modes are mutually exclusive.
* </p>
*
* @param generator the {@link EncapsulatedSecretGenerator} to use
* @return this builder instance for method chaining
* @throws NullPointerException if {@code generator} is {@code null}
*/
public AsymmetricStreamBuilder withKEMGenerator(final EncapsulatedSecretGenerator generator) {
this.engine = null; // NOPMD
this.generator = Objects.requireNonNull(generator, "KEM generator cannot be null");
this.extractor = null; // NOPMD
return this;
}
/**
* Sets the key encapsulation mechanism (KEM) secret extractor for decryption.
*
* <p>
* Using this method will clear any previously set cipher engine or KEM
* generator, since these modes are mutually exclusive.
* </p>
*
* @param extractor the {@link EncapsulatedSecretExtractor} to use
* @return this builder instance for method chaining
* @throws NullPointerException if {@code extractor} is {@code null}
*/
public AsymmetricStreamBuilder withKEMExtractor(final EncapsulatedSecretExtractor extractor) {
this.engine = null; // NOPMD
this.generator = null; // NOPMD
this.extractor = Objects.requireNonNull(extractor, "KEM extractor cannot be null");
return this;
}
/**
* Sets the asymmetric key (public or private) used for encryption or
* decryption.
*
* @param key the {@link AsymmetricKeyParameter} to use
* @return this builder instance
*/
public AsymmetricStreamBuilder withKey(final AsymmetricKeyParameter key) {
this.key = Objects.requireNonNull(key, "Key parameter cannot be null");
return this;
}
/**
* Constructs an {@link InputStream} that encrypts data from the configured
* input stream using the specified asymmetric cipher engine or KEM generator.
*
* @return an {@link InputStream} producing encrypted data
* @throws IOException if an I/O error occurs during stream creation
* @throws DestroyFailedException if secure destruction of cryptographic
* material fails
* @throws IllegalStateException if required parameters are missing or invalid
*/
public InputStream buildEncryptingStream() throws IOException, DestroyFailedException {
validate();
if (engine != null) {
final ClassicAsymmetricEncryptionStream stream = new ClassicAsymmetricEncryptionStream(input, engine);
stream.initialize(true, key);
return stream;
}
LOG.info("building KEM-encryption");
Objects.requireNonNull(generator, "KEM generator must be set for the decryption process");
try (KEMAsymmetricContext kem = new KEMAsymmetricContext(key, generator)) {
return kem.getEncryptedStream(input, cipherType, aad);
}
}
/**
* Constructs an {@link InputStream} that decrypts data from the configured
* input stream using the specified asymmetric cipher engine or KEM extractor.
*
* @return an {@link InputStream} producing decrypted data
* @throws IOException if an I/O error occurs during stream creation
* @throws DestroyFailedException if secure destruction of cryptographic
* material fails
* @throws IllegalStateException if required parameters are missing or invalid
*/
public InputStream buildDecryptingStream() throws IOException, DestroyFailedException {
validate();
if (engine != null) {
final ClassicAsymmetricEncryptionStream stream = new ClassicAsymmetricEncryptionStream(input, engine);
stream.initialize(false, key);
return stream;
}
LOG.info("building KEM-decryption");
Objects.requireNonNull(extractor, "KEM extractor must be set for the decryption process");
try (KEMAsymmetricContext kem = new KEMAsymmetricContext(key, extractor)) {
return kem.getDecryptedStream(input, cipherType, aad);
}
}
/**
* Validates that all required parameters have been set correctly before
* building encryption or decryption streams.
*
* @throws NullPointerException if any mandatory parameter is missing
* @throws IllegalStateException if none of cipher engine, KEM generator, or KEM
* extractor is set
*/
private void validate() {
Objects.requireNonNull(input, "InputStream must be provided");
Objects.requireNonNull(key, "Key must be provided");
if (engine == null && generator == null && extractor == null) {
throw new NullPointerException("Cipher engine, KEM generator or extractor must be provided"); // NOPMD
}
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.asymmetric;
import java.io.IOException;
import org.bouncycastle.crypto.AsymmetricBlockCipher;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
/**
* Classic asymmetric context implementation that uses an asymmetric key
* parameter and an asymmetric block cipher for encryption/decryption.
*
* @param key the asymmetric key parameter (public/private)
* @param cipher the asymmetric block cipher to use
*/
public record ClassicAsymmetricContext(AsymmetricKeyParameter key, AsymmetricBlockCipher cipher)
implements AsymmetricContext {
@Override
public void close() throws IOException {
// empty intentionally
}
}

View File

@@ -0,0 +1,179 @@
/**
* 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.asymmetric;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bouncycastle.crypto.AsymmetricBlockCipher;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
/**
* A streaming class that performs block-based asymmetric encryption or
* decryption on data read from an underlying {@link InputStream}.
*/
class ClassicAsymmetricEncryptionStream extends InputStream {
private static final Logger LOG = Logger.getLogger(ClassicAsymmetricEncryptionStream.class.getName());
private final static int BLOCKS_IN_BUF = 1_000;
/**
* The input stream from which raw data is read.
*/
final private InputStream input;
/**
* The asymmetric block cipher engine used for encryption or decryption.
*/
final private AsymmetricBlockCipher engine;
/**
* The input block size in bytes, determined by the cipher engine. This value is
* set during initialization.
*/
private int blockSize;
private InputStream bais = nullInputStream();
/**
* Constructs a new {@code ClassicAsymmetricEncryptionStream} with the given
* input stream and cipher engine.
*
* @param input the source input stream
* @param engine the asymmetric block cipher engine
*/
public ClassicAsymmetricEncryptionStream(final InputStream input, final AsymmetricBlockCipher engine) {
super();
this.input = input;
this.engine = engine;
}
/**
* Initializes the cipher engine with encryption or decryption mode.
*
* @param encrypt {@code true} to initialize for encryption; {@code false} for
* decryption
* @param key the asymmetric key parameter (public or private)
*/
protected void initialize(final boolean encrypt, final AsymmetricKeyParameter key) {
engine.init(encrypt, key);
blockSize = engine.getInputBlockSize();
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "block size in: {0}, out: {1}",
new Object[] { engine.getInputBlockSize(), engine.getOutputBlockSize() });
}
}
/**
* Reads a single byte from the encrypted/decrypted stream.
*
* @return the byte read, or {@code -1} if end of stream is reached
* @throws IOException if an I/O error occurs
*/
@Override
public int read() throws IOException {
ensureData();
return bais.read();
}
/**
* Reads up to {@code len} bytes of data from the stream into an array of bytes.
*
* @param b the buffer into which the data is read
* @param off the start offset in array {@code b} at which the data is written
* @param len the maximum number of bytes to read
* @return the total number of bytes read, or {@code -1} if end of stream is
* reached
* @throws IOException if an I/O error occurs
*/
@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
ensureData();
return bais.read(b, off, len);
}
/**
* Closes the underlying input stream.
*
* @throws IOException if an I/O error occurs
*/
@Override
public void close() throws IOException {
input.close();
}
/**
* Ensures that encrypted or decrypted data is available to read.
* <p>
* Reads blocks of data from the input stream, processes them with the cipher,
* and buffers the results.
* </p>
*
* @throws IOException if an I/O error occurs or encryption fails
*/
private void ensureData() throws IOException {
// we still have some data in a buffer?
if (bais.available() > 0) {
return;
}
final byte[] buf = new byte[BLOCKS_IN_BUF * blockSize];
final int len = input.readNBytes(buf, 0, buf.length);
if (len == 0) {
// we have EOF
bais = nullInputStream();
return;
}
// XXX we could use the original "buf" iff getInputBlockSize() >=
// getOutputBlockSize()
final ByteArrayOutputStream encrypted = new ByteArrayOutputStream(buf.length);
for (int i = 0; i < len; i += blockSize) {
try {
encrypted.write(engine.processBlock(buf, i, Math.min(blockSize, len - i)));
} catch (InvalidCipherTextException e) {
LOG.logp(Level.FINE, "AsymmetricEncryptionInputStream", "prepareBuffer", "Exception", e);
throw new IOException(e);
}
}
bais = new ByteArrayInputStream(encrypted.toByteArray());
}
}

View File

@@ -0,0 +1,315 @@
/**
* 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.asymmetric;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;
import org.bouncycastle.crypto.EncapsulatedSecretExtractor;
import org.bouncycastle.crypto.EncapsulatedSecretGenerator;
import org.bouncycastle.crypto.SecretWithEncapsulation;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import zeroecho.util.IOUtil;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesSupport;
/**
* A context object for holding data required in post-quantum Key Encapsulation
* Mechanism (KEM) operations.
* <p>
* This class stores the asymmetric key and either an
* {@link EncapsulatedSecretGenerator} (for encapsulation) or an
* {@link EncapsulatedSecretExtractor} (for decapsulation), depending on how the
* instance is constructed.
* </p>
*
* <p>
* It also supports lazy evaluation and caching of the generated
* {@link SecretWithEncapsulation}, and provides access to the derived secret
* and encapsulated value. Additionally, this class implements
* {@link Destroyable} to allow secure erasure of sensitive material once it is
* no longer needed.
* </p>
*
* <p>
* This class is intended to be a simple holder of cryptographic components for
* use in a KEM workflow.
* </p>
*
* @see EncapsulatedSecretGenerator
* @see EncapsulatedSecretExtractor
* @see SecretWithEncapsulation
* @see AsymmetricContext
*/
public final class KEMAsymmetricContext implements AsymmetricContext, Destroyable {
private static final Logger LOG = Logger.getLogger(KEMAsymmetricContext.class.getName());
/**
* The most recently generated or extracted secret with its encapsulation.
* Cached for reuse until destroyed.
*/
private SecretWithEncapsulation lastEncapsulatedSecret;
/**
* The asymmetric key used for the KEM operation.
*/
final private AsymmetricKeyParameter keyField;
/**
* The generator used for encapsulation (if applicable).
*/
final private EncapsulatedSecretGenerator generatorField;
/**
* The extractor used for decapsulation (if applicable).
*/
final private EncapsulatedSecretExtractor extractorField;
/**
* Constructs a context for KEM encapsulation using the given public key and
* generator.
*
* @param key the public key used for encapsulation
* @param generator the generator responsible for producing the encapsulated
* secret
*/
public KEMAsymmetricContext(AsymmetricKeyParameter key, EncapsulatedSecretGenerator generator) {
this.keyField = key;
this.generatorField = Objects.requireNonNull(generator, "generator cannot be null");
this.extractorField = null;
}
/**
* Constructs a context for KEM decapsulation using the given private key and
* extractor.
*
* @param key the private key used for decapsulation
* @param extractor the extractor responsible for deriving the secret from the
* encapsulated input
*/
public KEMAsymmetricContext(AsymmetricKeyParameter key, EncapsulatedSecretExtractor extractor) {
this.keyField = key;
this.generatorField = null;
this.extractorField = Objects.requireNonNull(extractor, "extractor cannot be null");
}
/**
* Creates an input stream that provides the encrypted version of the original
* stream by generating a secret key and encapsulation, then encrypting the data
* with AES.
*
* <p>
* The resulting stream begins with the encapsulation and IV, followed by the
* AES-encrypted payload. If Additional Authenticated Data (AAD) is provided, it
* is included in the encryption process for modes that support authentication
* (e.g., GCM).
* </p>
*
* @param originalInputStream the original plaintext input stream; must not be
* {@code null}
* @param cipherType the AES cipher type to use for encryption; must
* not be {@code null}
* @param aad optional Additional Authenticated Data (AAD) to
* bind to the encryption operation; may be
* {@code null} if unused
* @return an input stream combining encapsulation, IV, and AES-encrypted data
* @throws IOException if reading from or writing to the streams fails
*/
public InputStream getEncryptedStream(InputStream originalInputStream, AesCipherType cipherType, final byte[] aad)
throws IOException {
// Generate secret and encapsulation
byte[] aesKey = getSecret(); // AES key
byte[] encapsulation = getEncapsulation(); // to be sent along
// Generate IV
byte[] iv = cipherType.generateRandomIV();
// AES encrypt original stream
InputStream encryptedStream = AesSupport.encrypt(aesKey, iv, aad, cipherType, originalInputStream);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IOUtil.write(baos, encapsulation);
IOUtil.write(baos, iv);
// Combine everything into a final stream
return new SequenceInputStream(new ByteArrayInputStream(baos.toByteArray()), encryptedStream);
}
/**
* Creates an input stream that provides the decrypted version of the original
* encrypted stream.
*
* <p>
* The stream reads the encapsulation and IV, reconstructs the AES key, and
* decrypts the payload. If Additional Authenticated Data (AAD) was used during
* encryption, the same AAD must be provided to ensure successful authentication
* and decryption.
* </p>
*
* @param originalInputStream the encrypted input stream starting with
* encapsulation and IV; must not be {@code null}
* @param cipherType the AES cipher type used during encryption; must
* not be {@code null}
* @param aad the same Additional Authenticated Data (AAD) used
* during encryption; may be {@code null} if
* encryption did not use AAD
* @return an input stream yielding the decrypted plaintext data
* @throws IOException if reading the stream fails or if authentication fails
* (for modes like GCM)
*/
public InputStream getDecryptedStream(InputStream originalInputStream, AesCipherType cipherType, final byte[] aad)
throws IOException {
byte[] encapsulation = IOUtil.read(originalInputStream, 24_000);
byte[] iv = IOUtil.read(originalInputStream, cipherType.getIVLengthBytes());
// Recover AES key
byte[] aesKey = extractorField.extractSecret(encapsulation);
// Decrypt
return AesSupport.decrypt(aesKey, iv, aad, cipherType, originalInputStream);
}
/**
* Destroys the cached secret, if it exists.
*
* @throws DestroyFailedException if destruction fails
*/
@Override
public void destroy() throws DestroyFailedException {
if (lastEncapsulatedSecret != null) {
lastEncapsulatedSecret.destroy();
}
}
@Override
public void close() throws IOException {
try {
destroy();
} catch (DestroyFailedException e) {
LOG.logp(Level.WARNING, "KEMAsymmetricContext", "close", "DestroyFailedException", e);
throw new IOException(e);
}
}
/**
* Indicates whether the cached secret has been destroyed or never initialized.
*
* @return {@code true} if the secret is destroyed or not yet generated;
* {@code false} otherwise
*/
@Override
public boolean isDestroyed() {
return lastEncapsulatedSecret == null || lastEncapsulatedSecret.isDestroyed();
}
/**
* Returns the derived shared secret. This is lazily generated and cached if the
* context is in encapsulation mode.
*
* @return the derived secret bytes
* @throws IllegalStateException if the context is not initialized for
* encapsulation
*/
public byte[] getSecret() {
return getSecretWithEncapsulation().getSecret();
}
/**
* Returns the encapsulated data associated with the derived secret. Lazily
* generated if not already cached.
*
* @return the encapsulation bytes
* @throws IllegalStateException if the context is not initialized for
* encapsulation
*/
public byte[] getEncapsulation() {
return getSecretWithEncapsulation().getEncapsulation();
}
/**
* Returns the asymmetric key parameter associated with this context.
*
* @return the asymmetric key parameter (public or private)
*/
@Override
public AsymmetricKeyParameter key() {
return keyField;
}
/**
* Returns the encapsulated secret generator if available.
*
* @return the generator instance, or {@code null} if this context is for
* decapsulation
*/
public EncapsulatedSecretGenerator generator() {
return generatorField;
}
/**
* Returns the encapsulated secret extractor if available.
*
* @return the extractor instance, or {@code null} if this context is for
* encapsulation
*/
public EncapsulatedSecretExtractor extractor() {
return extractorField;
}
/**
* Lazily generates or returns the cached {@link SecretWithEncapsulation} for
* this context.
*
* @return the encapsulated secret and its encapsulation bytes
* @throws IllegalStateException if no generator is present in the context
*/
private SecretWithEncapsulation getSecretWithEncapsulation() {
if (lastEncapsulatedSecret == null) {
lastEncapsulatedSecret = generatorField.generateEncapsulated(keyField);
}
return lastEncapsulatedSecret;
}
}

View File

@@ -0,0 +1,52 @@
/**
* 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.asymmetric;
import java.io.IOException;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
/**
* Classic asymmetric context implementation that uses an asymmetric key
* parameter for signing.
*
* @param key the asymmetric key parameter (public/private)
*/
public record SignatureAsymmetricContext(AsymmetricKeyParameter key) implements AsymmetricContext {
@Override
public void close() throws IOException {
// empty intentionally
}
}

View File

@@ -0,0 +1,82 @@
/**
* 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.
*/
/**
* Provides core classes and interfaces for performing asymmetric encryption and
* decryption on data streams.
* <p>
* This package supports both classical asymmetric cryptography based on block
* ciphers and modern post-quantum cryptography using Key Encapsulation
* Mechanisms (KEM).
* </p>
*
* <h2>Key Components</h2>
* <ul>
* <li>{@link AsymmetricStreamBuilder}: A fluent builder to create
* {@link java.io.InputStream} instances that encrypt or decrypt data using
* either classic asymmetric ciphers or KEM-based approaches.</li>
* <li>{@link AsymmetricContext}: A sealed interface representing asymmetric
* cryptographic contexts, with implementations such as
* {@link ClassicAsymmetricContext} and {@link KEMAsymmetricContext}.</li>
* <li>{@link ClassicAsymmetricEncryptionStream}: An {@code InputStream}
* implementation that applies classic block cipher asymmetric
* encryption/decryption on streamed data.</li>
* <li>{@link KEMAsymmetricContext}: Holds state and cryptographic components
* for performing KEM-based encapsulation and decapsulation, supporting
* post-quantum secure workflows.</li>
* </ul>
*
* <h2>Concepts</h2>
* <p>
* <b>Classic block cipher mode:</b> Direct encryption or decryption using an
* asymmetric block cipher engine combined with a public or private key.
* </p>
* <p>
* <b>Key Encapsulation Mechanism (KEM) mode:</b> A hybrid approach where a
* symmetric key is securely encapsulated or decapsulated using asymmetric
* cryptography, and that symmetric key is then used for AES encryption of the
* data stream.
* </p>
*
* <h2>Usage</h2>
* <p>
* Users typically start with {@link AsymmetricStreamBuilder} to configure the
* input stream, cryptographic parameters, and mode, then build an encrypting or
* decrypting stream to process data.
* </p>
*
* @author Leo Galambos
* @since 1.0
*/
package zeroecho.util.asymmetric;

View File

@@ -0,0 +1,185 @@
/**
* 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.bc;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Arrays;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
/**
* Abstract base class for wrapping asymmetric key parameters from Bouncy
* Castle.
* <p>
* This class provides a common foundation for concrete key wrapper
* implementations that encapsulate Bouncy Castle {@link AsymmetricKeyParameter}
* instances. It implements {@link Serializable} to signal serializability but
* explicitly disables default Java serialization mechanisms by throwing
* {@link NotSerializableException} to prevent insecure or unintended
* serialization of sensitive key material.
* </p>
*
* <p>
* Subclasses must implement {@link #isPrivate()} to indicate whether the
* wrapped key is a private key.
* </p>
*
* <p>
* Equality and hash code operations are based on the encoded key bytes returned
* by {@link #getEncoded()}. By default, {@link #getEncoded()} returns
* {@code null}, so subclasses should override it to provide a meaningful key
* encoding.
* </p>
*
* <h2>Serialization Note</h2>
* <p>
* Although this class implements {@link Serializable}, it explicitly disables
* serialization and deserialization via the {@code writeObject} and
* {@code readObject} methods by throwing {@link NotSerializableException}. This
* design prevents accidental exposure of private key material through Java
* serialization.
* </p>
*
* @param <T> the specific type of asymmetric key parameters wrapped by this
* class, extending {@link AsymmetricKeyParameter}
*
* @author Leo Galambos
*/
public abstract class BcKeyWrapper<T extends AsymmetricKeyParameter> implements Serializable {
private static final long serialVersionUID = 1149322274710689183L;
/**
* The wrapped Bouncy Castle asymmetric key parameters. Marked transient to
* prevent default serialization.
*/
protected transient T params;
/**
* Constructs a new key wrapper instance with the specified key parameters.
*
* @param params the asymmetric key parameters to wrap
*/
protected BcKeyWrapper(T params) {
this.params = params;
}
/**
* Returns the wrapped asymmetric key parameters.
*
* @return the underlying key parameters
*/
public T getKeyParameters() {
return params;
}
/**
* Returns the encoded form of the wrapped key, or {@code null} if encoding is
* not supported or not implemented by the subclass.
*
* @return the encoded key bytes, or {@code null}
*/
public byte[] getEncoded() { // NOPMD
return null; // NOPMD
}
/**
* Returns the name of the encoding format for the wrapped key.
* <p>
* For private keys, this returns {@code "PKCS#8"}, indicating the Private-Key
* Information Syntax Specification. For public keys, this returns
* {@code "X.509"}, indicating the standard for public key certificates.
* </p>
* <p>
* If no encoding format is supported, this method may return {@code null}.
* </p>
*
* @return the name of the encoding format, typically {@code "PKCS#8"} for
* private keys and {@code "X.509"} for public keys, or {@code null} if
* unsupported
*/
public String getFormat() {
return isPrivate() ? "PKCS#8" : "X.509";
}
/**
* Indicates whether this key wrapper represents a private key.
*
* @return {@code true} if this is a private key wrapper; {@code false}
* otherwise
*/
protected abstract boolean isPrivate();
/**
* Prevents default Java serialization by throwing
* {@link NotSerializableException}.
*
* @param out the output stream
* @throws IOException always thrown to prevent serialization
*/
private void writeObject(final ObjectOutputStream out) throws IOException { // NOPMD
throw new NotSerializableException(getClass().getName() + " does not support serialization");
}
/**
* Prevents default Java deserialization by throwing
* {@link NotSerializableException}.
*
* @param in the input stream
* @throws IOException always thrown to prevent deserialization
*/
private void readObject(final ObjectInputStream in) throws IOException { // NOPMD
throw new NotSerializableException(getClass().getName() + " does not support deserialization");
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof BcKeyWrapper)) {
return false;
}
BcKeyWrapper<?> other = (BcKeyWrapper<?>) obj;
return Arrays.equals(this.getEncoded(), other.getEncoded());
}
@Override
public int hashCode() {
return Arrays.hashCode(getEncoded());
}
}

View File

@@ -0,0 +1,110 @@
/**
* 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.bc;
import java.security.PrivateKey;
import org.bouncycastle.pqc.crypto.frodo.FrodoPrivateKeyParameters;
/**
* Represents a Frodo private key, wrapping the underlying
* {@link FrodoPrivateKeyParameters} from the Bouncy Castle library.
* <p>
* This class extends {@link BcKeyWrapper} to provide Frodo-specific key
* handling and implements the standard {@link PrivateKey} interface to
* integrate with Java Cryptography Architecture (JCA) APIs.
* </p>
*
* <p>
* The encoded form of the key is obtained from the underlying
* {@link FrodoPrivateKeyParameters#getEncoded()} method.
* </p>
*
* <p>
* The {@link #getAlgorithm()} method returns the string {@code "Frodo"} as the
* algorithm identifier.
* </p>
*
* <p>
* Serialization is explicitly disabled in the superclass to prevent accidental
* exposure of sensitive key material.
* </p>
*
* @author Leo Galambos
*/
public class FrodoPrivateKey extends BcKeyWrapper<FrodoPrivateKeyParameters> implements PrivateKey {
private static final long serialVersionUID = -7452299173472996509L;
/**
* Constructs a new Frodo private key wrapper with the specified private key
* parameters.
*
* @param params the Frodo private key parameters to wrap
*/
public FrodoPrivateKey(FrodoPrivateKeyParameters params) {
super(params);
}
/**
* Returns the standard algorithm name for this key.
*
* @return the string {@code "Frodo"}
*/
@Override
public String getAlgorithm() {
return "Frodo";
}
/**
* Indicates that this key wrapper represents a private key.
*
* @return {@code true}
*/
@Override
protected boolean isPrivate() {
return true;
}
/**
* Returns the encoded form of this private key.
*
* @return a byte array containing the encoded key
*/
@Override
public byte[] getEncoded() {
return params.getEncoded();
}
}

View File

@@ -0,0 +1,110 @@
/**
* 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.bc;
import java.security.PublicKey;
import org.bouncycastle.pqc.crypto.frodo.FrodoPublicKeyParameters;
/**
* Represents a Frodo public key, wrapping the underlying
* {@link FrodoPublicKeyParameters} from the Bouncy Castle library.
* <p>
* This class extends {@link BcKeyWrapper} to provide Frodo-specific key
* handling and implements the standard {@link PublicKey} interface to integrate
* with Java Cryptography Architecture (JCA) APIs.
* </p>
*
* <p>
* The encoded form of the key is obtained from the underlying
* {@link FrodoPublicKeyParameters#getEncoded()} method.
* </p>
*
* <p>
* The {@link #getAlgorithm()} method returns the string {@code "Frodo"} as the
* algorithm identifier.
* </p>
*
* <p>
* Serialization is explicitly disabled in the superclass to prevent accidental
* exposure of sensitive key material.
* </p>
*
* @author Leo Galambos
*/
public class FrodoPublicKey extends BcKeyWrapper<FrodoPublicKeyParameters> implements PublicKey {
private static final long serialVersionUID = -5312503609298272925L;
/**
* Constructs a new Frodo public key wrapper with the specified public key
* parameters.
*
* @param params the Frodo public key parameters to wrap
*/
public FrodoPublicKey(FrodoPublicKeyParameters params) {
super(params);
}
/**
* Returns the standard algorithm name for this key.
*
* @return the string {@code "Frodo"}
*/
@Override
public String getAlgorithm() {
return "Frodo";
}
/**
* Indicates that this key wrapper represents a public key.
*
* @return {@code false}
*/
@Override
protected boolean isPrivate() {
return false;
}
/**
* Returns the encoded form of this public key.
*
* @return a byte array containing the encoded key
*/
@Override
public byte[] getEncoded() {
return params.getEncoded();
}
}

View File

@@ -0,0 +1,94 @@
/**
* 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.bc;
import java.security.PrivateKey;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McEliecePrivateKeyParameters;
/**
* Represents a McEliece private key, wrapping the underlying
* {@link McEliecePrivateKeyParameters} from the Bouncy Castle library.
* <p>
* This class extends {@link BcKeyWrapper} to provide McEliece-specific key
* handling and implements the standard {@link PrivateKey} interface to
* integrate with Java Cryptography Architecture (JCA) APIs.
* </p>
*
* <p>
* The algorithm name returned by {@link #getAlgorithm()} is {@code "McEliece"}.
* </p>
*
* <p>
* Serialization is explicitly disabled in the superclass to prevent accidental
* exposure of sensitive key material.
* </p>
*
* @author Leo Galambos
*/
public class McEliecePrivateKey extends BcKeyWrapper<McEliecePrivateKeyParameters> implements PrivateKey {
private static final long serialVersionUID = -8079222504948456211L;
/**
* Constructs a new McEliece private key wrapper with the specified private key
* parameters.
*
* @param params the McEliece private key parameters to wrap
*/
public McEliecePrivateKey(McEliecePrivateKeyParameters params) {
super(params);
}
/**
* Returns the standard algorithm name for this key.
*
* @return the string {@code "McEliece"}
*/
@Override
public String getAlgorithm() {
return "McEliece";
}
/**
* Indicates that this key wrapper represents a private key.
*
* @return {@code true}
*/
@Override
protected boolean isPrivate() {
return true;
}
}

View File

@@ -0,0 +1,94 @@
/**
* 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.bc;
import java.security.PublicKey;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McEliecePublicKeyParameters;
/**
* Represents a McEliece public key, wrapping the underlying
* {@link McEliecePublicKeyParameters} from the Bouncy Castle library.
* <p>
* This class extends {@link BcKeyWrapper} to provide McEliece-specific key
* handling and implements the standard {@link PublicKey} interface to integrate
* with Java Cryptography Architecture (JCA) APIs.
* </p>
*
* <p>
* The algorithm name returned by {@link #getAlgorithm()} is {@code "McEliece"}.
* </p>
*
* <p>
* Serialization is explicitly disabled in the superclass to prevent accidental
* exposure of sensitive key material.
* </p>
*
* @author Leo Galambos
*/
public class McEliecePublicKey extends BcKeyWrapper<McEliecePublicKeyParameters> implements PublicKey {
private static final long serialVersionUID = -2122646985989549694L;
/**
* Constructs a new McEliece public key wrapper with the specified public key
* parameters.
*
* @param params the McEliece public key parameters to wrap
*/
public McEliecePublicKey(McEliecePublicKeyParameters params) {
super(params);
}
/**
* Returns the standard algorithm name for this key.
*
* @return the string {@code "McEliece"}
*/
@Override
public String getAlgorithm() {
return "McEliece";
}
/**
* Indicates that this key wrapper represents a public key.
*
* @return {@code false}
*/
@Override
protected boolean isPrivate() {
return false;
}
}

View File

@@ -0,0 +1,94 @@
/**
* 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.bc;
import java.security.PrivateKey;
import org.bouncycastle.pqc.crypto.newhope.NHPrivateKeyParameters;
/**
* Represents a NewHope private key, wrapping the underlying
* {@link NHPrivateKeyParameters} used for post-quantum key operations.
* <p>
* This class extends {@link BcKeyWrapper} to provide NewHope-specific key
* handling and implements the standard {@link PrivateKey} interface for
* compatibility with Java Cryptography Architecture (JCA) APIs.
* </p>
*
* <p>
* The algorithm name returned by {@link #getAlgorithm()} is {@code "NewHope"}.
* </p>
*
* <p>
* Serialization is disabled in the superclass to protect sensitive key material
* from accidental exposure.
* </p>
*
* @author Leo Galambos
*/
public class NewHopePrivateKey extends BcKeyWrapper<NHPrivateKeyParameters> implements PrivateKey {
private static final long serialVersionUID = -7098011326074166903L;
/**
* Constructs a new NewHope private key wrapper with the specified private key
* parameters.
*
* @param params the NewHope private key parameters to wrap
*/
public NewHopePrivateKey(NHPrivateKeyParameters params) {
super(params);
}
/**
* Returns the standard algorithm name for this key.
*
* @return the string {@code "NewHope"}
*/
@Override
public String getAlgorithm() {
return "NewHope";
}
/**
* Indicates that this key wrapper represents a private key.
*
* @return {@code true}
*/
@Override
protected boolean isPrivate() {
return true;
}
}

View File

@@ -0,0 +1,94 @@
/**
* 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.bc;
import java.security.PublicKey;
import org.bouncycastle.pqc.crypto.newhope.NHPublicKeyParameters;
/**
* Represents a NewHope public key, wrapping the underlying
* {@link NHPublicKeyParameters} used for post-quantum key operations.
* <p>
* This class extends {@link BcKeyWrapper} to provide NewHope-specific key
* handling and implements the standard {@link PublicKey} interface for
* compatibility with Java Cryptography Architecture (JCA) APIs.
* </p>
*
* <p>
* The algorithm name returned by {@link #getAlgorithm()} is {@code "NewHope"}.
* </p>
*
* <p>
* Serialization is disabled in the superclass to protect sensitive key material
* from accidental exposure.
* </p>
*
* @author Leo Galambos
*/
public class NewHopePublicKey extends BcKeyWrapper<NHPublicKeyParameters> implements PublicKey {
private static final long serialVersionUID = -7781131066571667557L;
/**
* Constructs a new NewHope public key wrapper with the specified public key
* parameters.
*
* @param params the NewHope public key parameters to wrap
*/
public NewHopePublicKey(NHPublicKeyParameters params) {
super(params);
}
/**
* Returns the standard algorithm name for this key.
*
* @return the string {@code "NewHope"}
*/
@Override
public String getAlgorithm() {
return "NewHope";
}
/**
* Indicates that this key wrapper represents a public key.
*
* @return {@code false}
*/
@Override
protected boolean isPrivate() {
return false;
}
}

View File

@@ -0,0 +1,80 @@
/**
* 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.
*/
/**
* This package provides wrapper implementations for various post-quantum
* cryptographic keys, built on top of Bouncy Castle parameter classes.
* <p>
* The core abstraction is the {@link BcKeyWrapper} class, which encapsulates
* the underlying asymmetric key parameters and implements the standard Java
* {@link java.security.Key} interfaces (such as
* {@link java.security.PrivateKey} and {@link java.security.PublicKey}).
* </p>
*
* <p>
* Specific key types supported include:
* <ul>
* <li><b>Frodo</b> keys: {@link FrodoPrivateKey}, {@link FrodoPublicKey}</li>
* <li><b>McEliece</b> keys: {@link McEliecePrivateKey},
* {@link McEliecePublicKey}</li>
* <li><b>NewHope</b> keys: {@link NewHopePrivateKey},
* {@link NewHopePublicKey}</li>
* </ul>
*
* <p>
* These key wrappers provide methods to retrieve algorithm identifiers, access
* encoded key bytes, and support equality and hashing based on the encoded
* form.
* </p>
*
* <p>
* Note that serialization and deserialization are explicitly disabled to
* prevent accidental exposure of sensitive key material.
* </p>
*
* <p>
* All classes in this package adhere to a consistent pattern of wrapping Bouncy
* Castle asymmetric key parameter objects and exposing them as standard Java
* {@code Key} instances for interoperability with cryptographic frameworks.
* </p>
*
* @see BcKeyWrapper
* @see FrodoPrivateKey
* @see FrodoPublicKey
* @see McEliecePrivateKey
* @see McEliecePublicKey
* @see NewHopePrivateKey
* @see NewHopePublicKey
*/
package zeroecho.util.bc;

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* 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.
******************************************************************************/
/**
* Utility classes providing static helper methods and support functions for
* cryptographic operations, certificate handling, input/output processing,
* secure randomness, and related common tasks.
*
* This package includes utilities for:
* <ul>
* <li>Advanced Encryption Standard (AES) key and cipher parameter generation
* and manipulation.</li>
* <li>X.509 certificate parsing, validation, and encoding.</li>
* <li>General input/output stream and file handling helpers.</li>
* <li>Secure random number generation and entropy management.</li>
* <li>Other miscellaneous static utility methods supporting cryptography and
* data processing workflows.</li>
* </ul>
* <p>
* Note: Unless explicitly stated, these utilities are not guaranteed to be
* thread-safe.
* </p>
*/
package zeroecho.util;