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

26
lib/.classpath Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/java">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/resources">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

23
lib/.project Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>lib</name>
<comment>Project lib created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

31
lib/LICENSE Normal file
View File

@@ -0,0 +1,31 @@
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.

13
lib/build.gradle Normal file
View File

@@ -0,0 +1,13 @@
plugins {
id 'buildlogic.java-library-conventions'
id 'com.palantir.git-version' version '4.0.0'
}
group 'org.egothor'
version gitVersion(prefix:'release@')
dependencies {
implementation 'org.bouncycastle:bcpkix-jdk18on'
implementation 'org.egothor:conflux'
implementation 'org.apache.commons:commons-imaging'
}

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;

View File

@@ -0,0 +1,232 @@
package zeroecho.builder;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.UncheckedIOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.spec.ECGenParameterSpec;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import conflux.Ctx;
import zeroecho.data.DataContent;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
class DataContentChainBuilderTest {
@BeforeAll
public static void setupProvider() {
BouncyCastleActivator.init();
}
@BeforeEach
public void freshContext() {
Ctx.INSTANCE.clear();
}
// @Test
void testMultiEncryptionDecryptionPipeline() throws Exception {
System.out.println("testMultiEncryptionDecryptionPipeline");
final String originalText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
""";
KeyPair kp0 = keyPair("RSA", 2048);
KeyPair kp1 = keyPair("RSA", 2048);
DataContent output = DataContentChainBuilder.encrypt()
// input
.add(PlainStringBuilder.builder().value(originalText))
// encrypt
.add(AesBuilder.builder())
// ... and save the encryption header
.add(MultiRecipientCryptorBuilder.builder().addRecipient(kp0.getPublic()).addRecipient(kp1.getPublic()))
.add(AesRandomBuilder.builder().mode(AesMode.AES_256).cipherType(AesCipherType.CBC)).build();
final byte[] result = output.toBytes();
Ctx.INSTANCE.clear();
DataContent output2 = DataContentChainBuilder.decrypt()
// input
.add(PlainBytesBuilder.builder().bytes(result))
// read the encryption header
.add(AesRandomBuilder.builder().mode(AesMode.AES_256).cipherType(AesCipherType.CBC))
.add(MultiRecipientCryptorBuilder.builder().privateKey(kp1.getPrivate()))
// ... for the decryption process
.add(AesBuilder.builder())
// and obtain the final product
.build();
final String result2 = output2.toText();
assertEquals(originalText, result2);
System.out.println("...ok");
}
@Test
void testKEMEncryptionDecryptionPipeline() throws Exception {
System.out.println("testKEMEncryptionDecryptionPipeline");
final String originalText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
""";
KeyPair kp = KeyPairAlgorithm.KYBER_512.generateKeyPair();
DataContent output = DataContentChainBuilder.encrypt()
// input
.add(PlainStringBuilder.builder().value(originalText))
// encrypt
.add(AesBuilder.builder())
// ... and save the encryption header
.add(KEMAesParametersBuilder.builder().withKey(kp.getPublic()))
// build!
.build();
final byte[] result = output.toBytes();
Ctx.INSTANCE.clear();
DataContent output2 = DataContentChainBuilder.decrypt()
// input
.add(PlainBytesBuilder.builder().bytes(result))
// read the encryption header
.add(KEMAesParametersBuilder.builder().withKey(kp.getPrivate()))
// ... for the decryption process
.add(AesBuilder.builder())
// and obtain the final product
.build();
final String result2 = output2.toText();
assertEquals(originalText, result2);
System.out.println("...ok");
}
@Test
void testEncryptionDecryptionPipeline() throws Exception {
System.out.println("testEncryptionDecryptionPipeline");
final String originalText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
""";
final String userPassword = "simple_password";
DataContent output = DataContentChainBuilder.encrypt()
// input
.add(PlainStringBuilder.builder().value(originalText))
// encrypt
.add(AesBuilder.builder())
// ... and save the encryption header
.add(DerivedAesParametersBuilder.builder().password(userPassword)).build();
final byte[] result = output.toBytes();
Ctx.INSTANCE.clear();
DataContent output2 = DataContentChainBuilder.decrypt()
// input
.add(PlainBytesBuilder.builder().bytes(result))
// read the encryption header
.add(DerivedAesParametersBuilder.builder().password(userPassword))
// ... for the decryption process
.add(AesBuilder.builder())
// and obtain the final product
.build();
final String result2 = output2.toText();
assertEquals(originalText, result2);
System.out.println("...ok");
}
@Test
void testEncryptionDecryptionPipelineNegative() throws Exception {
System.out.println("testEncryptionDecryptionPipeline-negative");
final String originalText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
""";
final String userPassword = "simple_password";
DataContent output = DataContentChainBuilder.encrypt()
// input
.add(PlainStringBuilder.builder().value(originalText))
// encrypt
.add(AesBuilder.builder())
// ... and save the encryption header
.add(DerivedAesParametersBuilder.builder().password(userPassword)).build();
final byte[] result = output.toBytes();
Ctx.INSTANCE.clear();
DataContent output2 = DataContentChainBuilder.decrypt()
// input
.add(PlainBytesBuilder.builder().bytes(result))
// read the encryption header
.add(DerivedAesParametersBuilder.builder().password(userPassword + userPassword))
// ... for the decryption process
.add(AesBuilder.builder())
// and obtain the final product
.build();
UncheckedIOException thrown = assertThrowsExactly(UncheckedIOException.class, () -> {
output2.toText();
});
assertTrue(hasCause(thrown, InvalidCipherTextException.class),
"Expected cause chain to include InvalidCipherTextException");
System.out.println("...ok");
}
private boolean hasCause(Throwable throwable, Class<? extends Throwable> causeClass) {
Throwable current = throwable;
while (current != null) {
if (causeClass.isInstance(current)) {
return true;
}
current = current.getCause();
}
return false;
}
private KeyPair keyPair(String algorithm, int size) throws Exception {
String label = algorithm + "-" + size;
System.out.println("Testing " + label);
KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm, BouncyCastleProvider.PROVIDER_NAME);
if (algorithm.equals("EC")) {
generator.initialize(new ECGenParameterSpec("secp" + size + "r1"));
} else {
generator.initialize(size);
}
return generator.generateKeyPair();
}
}

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 static org.junit.jupiter.api.Assertions.assertEquals;
import java.security.KeyPair;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import conflux.Ctx;
import zeroecho.data.DataContent;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.KeyPairAlgorithm;
class KEMAesParametersBuilderTest {
@BeforeAll
public static void setupProvider() {
BouncyCastleActivator.init();
}
@BeforeEach
public void freshContext() {
Ctx.INSTANCE.clear();
}
@ParameterizedTest
@EnumSource(KeyPairAlgorithm.class)
void testKEMEncryptionDecryptionPipeline(final KeyPairAlgorithm algorithm) throws Exception {
System.out.println("Testing algorithm: " + algorithm);
if (algorithm == KeyPairAlgorithm.ELGAMAL_2048) {
System.out.println("...too slow key pair generation, skipping");
return;
}
final String originalText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
""";
// --- Generate key pair for this algorithm ---
KeyPair kp = algorithm.generateKeyPair();
try {
// --- Encryption pipeline ---
DataContent encryptedContent = DataContentChainBuilder.encrypt()
.add(PlainStringBuilder.builder().value(originalText)).add(AesBuilder.builder())
.add(KEMAesParametersBuilder.builder().withKey(kp.getPublic())).build();
byte[] cipherBytes = encryptedContent.toBytes();
// --- Clear context before decryption ---
Ctx.INSTANCE.clear();
// --- Decryption pipeline ---
DataContent decryptedContent = DataContentChainBuilder.decrypt()
.add(PlainBytesBuilder.builder().bytes(cipherBytes))
.add(KEMAesParametersBuilder.builder().withKey(kp.getPrivate())).add(AesBuilder.builder()).build();
final String decryptedText = decryptedContent.toText();
// --- Verify ---
assertEquals(originalText, decryptedText);
System.out.println("...algorithm " + algorithm + " passed.");
} catch (IllegalArgumentException | NullPointerException e) {
System.out.println("...algorithm " + algorithm
+ " cannot be submitted to the current KEMAesParameters impl: " + e.toString());
}
}
}

View File

@@ -0,0 +1,75 @@
/**
* 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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
public class TextualCodecTest {
@Test
void testGenerate100CharacterText() {
System.out.println("testGenerate100CharacterText");
TextualCodec.Generator generator = TextualCodec.Generator.EN;
String result = generator.getText(100);
System.out.println("...generated text: " + result);
assertNotNull(result, "Generated text should not be null");
assertEquals(100, result.length(), "Generated text should have 100 characters");
// Ensure all characters are from the expected alphabet
for (char c : result.toCharArray()) {
assertTrue(TextualCodec.Generator.ENGLISH.containsKey(c),
"Generated character '" + c + "' is not part of the English frequency table");
}
// Optional: Analyze distribution for debugging
Map<Character, Integer> histogram = new HashMap<>();
for (char c : result.toCharArray()) {
histogram.merge(c, 1, Integer::sum);
}
System.out.println("...character distribution: " + histogram);
System.out.println("...ok");
}
}

View File

@@ -0,0 +1,129 @@
/**
* 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 static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Base64;
import org.bouncycastle.crypto.params.KeyParameter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
class JpegExifIntegrationTest {
@TempDir
Path tempDir;
@Test
void testEncryptEmbedExtractDecrypt() throws Exception {
System.out.println("testEncryptEmbedExtractDecrypt");
final String inputText = "Hello, this is a secret message to embed in JPEG.";
final byte[] inputBytes = inputText.getBytes(StandardCharsets.UTF_8);
final byte[] aad = { 1, 2, 3 };
// AES config
AesCipherType cipher = AesCipherType.GCM;
AesMode mode = AesMode.AES_256;
AesParameters params = AesSupport.deriveKeyAndIv("one-two-three", AesSupport.KEY_ITERATIONS, mode, aad, cipher);
KeyParameter key = params.key();
// Encrypt
ByteArrayOutputStream encryptedOut = new ByteArrayOutputStream();
try (OutputStream aesOut = AesSupport.encrypt(key, encryptedOut, params.iv(), cipher, aad)) {
aesOut.write(inputBytes);
}
byte[] encryptedBytes = Base64.getEncoder().encode(encryptedOut.toByteArray());
// Create temporary JPEG
Path jpegOriginal = getResourcePath("test.jpg");
// Embed encrypted data
Path jpegEmbedded = tempDir.resolve("stego.jpg"); // Files.createTempFile("stego", ".jpg");
try (InputStream payloadInput = new ByteArrayInputStream(encryptedBytes);
OutputStream jpegOutput = Files.newOutputStream(jpegEmbedded)) {
JpegExifEmbedder embedder = new JpegExifEmbedder();
embedder.embed(jpegOriginal, payloadInput, jpegOutput);
}
System.out.println("...encrypted length=" + encryptedBytes.length + " " + Arrays.toString(encryptedBytes));
// Extract payload
ByteArrayOutputStream extractedEncrypted = new ByteArrayOutputStream();
new JpegExifExtractor().extract(jpegEmbedded, extractedEncrypted);
byte[] extracted = extractedEncrypted.toByteArray();
System.out.println("...extracted length=" + extracted.length + " " + Arrays.toString(extracted));
byte[] extractedEncryptedBytes = Base64.getDecoder().decode(extracted);
// Decrypt
String decrypted;
try (InputStream aesIn = AesSupport.decrypt(key, new ByteArrayInputStream(extractedEncryptedBytes), params.iv(),
cipher, aad)) {
byte[] decryptedBytes = aesIn.readAllBytes();
decrypted = new String(decryptedBytes, StandardCharsets.UTF_8);
}
assertEquals(inputText, decrypted);
System.out.println("...ok");
}
private Path getResourcePath(String resource) throws URISyntaxException {
URL url = getClass().getClassLoader().getResource(resource);
if (url == null) {
throw new IllegalArgumentException("Missing resource: " + resource);
}
return Paths.get(url.toURI());
}
}

View File

@@ -0,0 +1,113 @@
/**
* 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 static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import org.junit.jupiter.api.Test;
class Base64StreamTest {
@Test
void testEncodedOutputFrom4KBInput() throws Exception {
System.out.println("testEncodedOutputFrom4KBInput");
byte[] inputBytes = new byte[4096];
new SecureRandom().nextBytes(inputBytes); // random 4KB data
byte[] inputEncoded = Base64.getEncoder().encode(inputBytes);
System.out.println("...encoded length should be: " + inputEncoded.length);
int lineLength = 76;
String[] prefixes = { "echo ", null };
byte[][] lineSeparators = { " >> file.tmp\r\n".getBytes(StandardCharsets.US_ASCII),
"\n".getBytes(StandardCharsets.US_ASCII), null };
for (String prefix : prefixes) {
for (byte[] lineSep : lineSeparators) {
System.out.printf("...***** testing with prefix=%s and lineSep=%s%n",
prefix == null ? "null" : "\"" + prefix + "\"",
lineSep == null ? "null" : "\"" + new String(lineSep, StandardCharsets.US_ASCII) + "\"");
try (InputStream base64Stream = new Base64Stream(new ByteArrayInputStream(inputBytes),
prefix == null ? null : prefix.getBytes(StandardCharsets.US_ASCII), lineLength, lineSep)) {
String encodedOutput = new String(base64Stream.readAllBytes(), StandardCharsets.US_ASCII);
System.out.println(encodedOutput);
String[] lines = encodedOutput.split("\n");
for (int i = 0; i < lines.length; i++) {
int space = lines[i].indexOf(' ');
if (space > 0) {
lines[i] = lines[i].split(" ")[prefix == null ? 0 : 1];
}
}
if (lineSep != null) {
for (String line : lines) {
assertTrue(line.length() <= lineLength, "Line exceeds max length: " + line.length());
}
}
String joined = String.join("", lines);
System.out.println("...lines: " + lines.length);
System.out.println("......encoded length: " + joined.length());
if (lineSep == null && prefix != null) {
continue;
}
byte[] decoded = Base64.getDecoder().decode(joined);
System.out.println("...decoded length: " + decoded.length);
assertArrayEquals(inputBytes, decoded, "Decoded content does not match original input");
}
}
}
System.out.println("...ok");
}
}

View File

@@ -0,0 +1,136 @@
/**
* 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 static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.bouncycastle.crypto.params.KeyParameter;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import conflux.Ctx;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.SymmetricStreamBuilder;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
class AesEncryptorTest {
@BeforeAll
public static void setupProvider() {
BouncyCastleActivator.init();
}
@BeforeEach
public void freshContext() {
Ctx.INSTANCE.clear();
}
/**
* Generates a repeating plaintext message longer than BUF_SIZE.
*/
private String generateLongPlaintext() {
String phrase = "This is a long plaintext message for AES encryption test. ";
StringBuilder sb = new StringBuilder();
while (sb.length() <= 3 * SymmetricStreamBuilder.BUF_SIZE) {
sb.append(phrase);
}
return sb.toString();
}
/**
* Test method for {@link zeroecho.data.processing.AesEncryptor#getStream()}.
*/
@Test
void testGetStream() throws Exception {
System.out.println("testGetStream");
final String input = generateLongPlaintext();
final byte[] aad = { 1, 2, 3 };
for (AesMode mode : AesMode.values()) {
for (AesCipherType cipher : AesCipherType.values()) {
System.out.println("..." + mode.toString() + "/" + cipher.getId());
// Generate AES key and IV
final AesParameters params = AesSupport.generateKeyAndIV(mode, cipher, aad);
final AesEncryptor aes = new AesEncryptor(params);
KeyParameter key = params.key();
byte[] iv = params.iv();
System.out.printf("...key: %s%n", Arrays.toString(key.getKey()));
System.out.printf("...iv: %s%n", Arrays.toString(iv));
// Encrypt to ByteArrayOutputStream using BC core streams
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStream out = AesSupport.encrypt(key, baos, iv, cipher, aad);
System.out.printf("...before write() encrypted length: %d%n", baos.size());
out.write(input.getBytes(StandardCharsets.UTF_8));
System.out.printf("...before flush() encrypted length: %d%n", baos.size());
out.flush();
System.out.printf("...before close() encrypted length: %d%n", baos.size());
out.close();
System.out.printf("...input length: %d, encrypted length: %d%n", input.length(), baos.size());
// Encrypt using SymmetricStreamBuilder
aes.setInput(new PlainBytes(input.getBytes(StandardCharsets.UTF_8)));
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
try (InputStream encrypted = aes.getStream()) {
System.out.println("... --> stream: " + encrypted.getClass().getName());
encrypted.transferTo(baos2);
}
byte[] encryptedBytes = baos2.toByteArray();
System.out.printf("... --> input length: %d, encrypted length: %d%n", input.length(),
encryptedBytes.length);
// Byte-by-byte comparison
byte[] encryptedViaLowLevelStreamBytes = baos.toByteArray();
assertArrayEquals(encryptedViaLowLevelStreamBytes, encryptedBytes, "Encrypted arrays should match");
System.out.println();
}
}
System.out.println("...ok");
}
}

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 static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.KeyPair;
import java.util.Arrays;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.KeySupport;
import zeroecho.util.RandomSupport;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.asymmetric.AsymmetricContext;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
@EnabledForJreRange(min = JRE.JAVA_21)
class KEMEncryptorDecryptorTest {
@BeforeAll
public static void setupProvider() {
BouncyCastleActivator.init();
}
@Test
void testAllKemAlgorithms() throws Exception {
System.out.println("testAllKemAlgorithms");
byte[] testData = RandomSupport.generateRandom(2048);
System.out.println("...Initial input size: " + testData.length + " bytes");
for (KeyPairAlgorithm algo : KeyPairAlgorithm.values()) {
KeyPair kp;
try {
System.out.println("...Generating key for algo: " + algo);
if (algo == KeyPairAlgorithm.ELGAMAL_2048) {
System.out.println("...Algo " + algo + " generation is slow, skipping");
continue;
}
kp = algo.generateKeyPair();
} catch (Exception e) {
System.out.println("...Skipping algo " + algo + " (not supported: " + e.getMessage() + ")");
continue;
}
try {
AsymmetricContext pubCtx = KeySupport.fromKey(kp.getPublic());
AsymmetricContext privCtx = KeySupport.fromKey(kp.getPrivate());
if (!(pubCtx instanceof KEMAsymmetricContext) || !(privCtx instanceof KEMAsymmetricContext)) {
System.out.println("...Skipping algo " + algo + " (not a KEM algorithm)");
continue;
}
KEMAsymmetricContext sender = (KEMAsymmetricContext) pubCtx;
KEMAsymmetricContext receiver = (KEMAsymmetricContext) privCtx;
if (sender.generator() == null || receiver.extractor() == null) {
System.out.println("...Skipping algo " + algo + " (cannot provide KEM's generator/extractor)");
continue;
}
for (AesCipherType cipherType : AesCipherType.values()) {
for (byte[] aad : new byte[][] { null, new byte[] { 1, 2, 3 } }) {
runEncryptionDecryptionCycle(sender, receiver, cipherType, aad, testData, algo);
}
}
} catch (IllegalArgumentException e) {
System.out.println("...Skipping algo " + algo + " (cannot provide AsymmetricContext)");
}
}
System.out.println("...ok");
}
private void runEncryptionDecryptionCycle(KEMAsymmetricContext sender, KEMAsymmetricContext receiver,
AesCipherType cipherType, byte[] aad, byte[] testData, KeyPairAlgorithm algo) throws IOException {
System.out.printf("...Algo=%s, Cipher=%s, AAD=%s%n", algo, cipherType,
(aad == null ? "null" : Arrays.toString(aad)));
// Encryption
KEMEncryptor encryptor = new KEMEncryptor(sender, cipherType, aad);
encryptor.setInput(() -> new ByteArrayInputStream(testData));
byte[] encryptedBytes = encryptor.toBytes();
System.out.println("......Encrypted size: " + encryptedBytes.length + " bytes");
// Decryption
KEMDecryptor decryptor = new KEMDecryptor(receiver, cipherType, aad);
decryptor.setInput(() -> new ByteArrayInputStream(encryptedBytes));
byte[] decrypted = decryptor.toBytes();
System.out.println("......Decrypted size: " + decrypted.length + " bytes");
// Validation
assertArrayEquals(testData, decrypted, () -> "Decryption mismatch for algo=" + algo + ", cipher=" + cipherType
+ ", aad=" + Arrays.toString(aad));
}
}

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.data.processing;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import conflux.Ctx;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.SymmetricStreamBuilder;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
class PasswordBasedAesEncryptorTest {
private static final Logger LOG = Logger.getLogger(PasswordBasedAesEncryptorTest.class.getName());
@BeforeAll
public static void setupProvider() {
BouncyCastleActivator.init();
}
@BeforeEach
public void freshContext() {
Ctx.INSTANCE.clear();
}
/**
* Generates a long plaintext message longer than BUF_SIZE for stress testing.
*/
private String generateLongPlaintext() {
String phrase = "This is a long plaintext message for AES encryption test. ";
StringBuilder sb = new StringBuilder();
while (sb.length() <= 3 * SymmetricStreamBuilder.BUF_SIZE) {
sb.append(phrase);
}
return sb.toString();
}
/**
* Tests encryption and decryption with PasswordBasedAesEncryptor and
* PasswordBasedAesDecryptor.
*/
@Test
void testEncryptDecrypt() throws Exception {
System.out.println("testEncryptDecrypt");
final String input = generateLongPlaintext();
final String password = "StrongPassword123!";
final int iterations = AesSupport.KEY_ITERATIONS;
final byte[] aad = { 1, 2, 3 };
for (AesMode mode : AesMode.values()) {
for (AesCipherType cipher : AesCipherType.values()) {
System.out.printf("...Testing mode: %s, cipher: %s%n", mode, cipher);
final PasswordBasedAesEncryptor encryptor = new PasswordBasedAesEncryptor(password, iterations, aad,
mode, cipher);
encryptor.setInput(new PlainBytes(input.getBytes(StandardCharsets.UTF_8)));
byte[] encryptedBytes;
try (InputStream encryptedStream = encryptor.getStream()) {
encryptedBytes = encryptedStream.readAllBytes();
}
LOG.log(Level.INFO, "Encrypted length: {0}", encryptedBytes.length);
final PasswordBasedAesDecryptor decryptor = new PasswordBasedAesDecryptor(password, aad, mode, cipher);
decryptor.setInput(new PlainBytes(encryptedBytes));
byte[] decryptedBytes;
try (InputStream decryptedStream = decryptor.getStream()) {
decryptedBytes = decryptedStream.readAllBytes();
}
String decryptedText = new String(decryptedBytes, StandardCharsets.UTF_8);
assertEquals(input, decryptedText,
"Decrypted text mismatch for mode " + mode + " and cipher " + cipher);
Ctx.INSTANCE.clear();
}
}
System.out.println("...ok");
}
@Test
void testInvalidParameters() {
System.out.println("testInvalidParameters");
assertThrows(IllegalArgumentException.class, () -> {
new PasswordBasedAesEncryptor(null, 1000, AesMode.AES_128);
}, "Expected IllegalArgumentException for null password");
assertThrows(IllegalArgumentException.class, () -> {
new PasswordBasedAesEncryptor("password", -1, AesMode.AES_128);
}, "Expected IllegalArgumentException for negative iterations");
assertThrows(IllegalArgumentException.class, () -> {
new PasswordBasedAesEncryptor("password", 1000, null);
}, "Expected IllegalArgumentException for null AES mode");
System.out.println("...ok");
}
}

View File

@@ -0,0 +1,204 @@
/**
* 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 static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.util.Arrays;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import conflux.Ctx;
import zeroecho.data.EncryptedContent;
import zeroecho.data.PlainContent;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.RandomSupport;
class SecretMultiRecipientCryptorTest {
@BeforeAll
public static void setupProvider() {
BouncyCastleActivator.init();
}
@BeforeEach
public void freshContext() {
Ctx.INSTANCE.clear();
}
@Test
void test() throws Exception {
System.out.println("multiRecipientCryptor-header");
SecureRandom rnd = new SecureRandom();
byte[] secret = RandomSupport.generateRandom(128);
System.out.println("...Demo users keys generation");
PublicKey pub[] = new PublicKey[16];
PrivateKey priv[] = new PrivateKey[16];
final KeyPairAlgorithm algo[] = new KeyPairAlgorithm[16];
// prepare various keys (pub+priv)
for (int i = 0; i < pub.length; i++) {
algo[i] = switch (rnd.nextInt(6)) {
case 0 -> KeyPairAlgorithm.RSA_1024;
case 1 -> KeyPairAlgorithm.RSA_2048;
case 2 -> KeyPairAlgorithm.KYBER_512;
case 3 -> KeyPairAlgorithm.KYBER_768;
case 4 -> KeyPairAlgorithm.ELGAMAL_1024;
case 5 -> KeyPairAlgorithm.ELGAMAL_512;
default -> KeyPairAlgorithm.RSA_4096;
};
System.out.println("...key " + algo[i]);
final KeyPair kp = algo[i].generateKeyPair();
pub[i] = kp.getPublic();
priv[i] = kp.getPrivate();
}
System.out.println("...Decoys keys generation");
PublicKey pubDecoy[] = new PublicKey[16];
PrivateKey privDecoy[] = new PrivateKey[16];
final KeyPairAlgorithm algo_decoy[] = new KeyPairAlgorithm[16];
// prepare various keys (pub+priv)
for (int i = 0; i < pubDecoy.length; i++) {
algo_decoy[i] = switch (rnd.nextInt(6)) {
case 0 -> KeyPairAlgorithm.RSA_1024;
case 1 -> KeyPairAlgorithm.RSA_2048;
case 2 -> KeyPairAlgorithm.KYBER_512;
case 3 -> KeyPairAlgorithm.KYBER_768;
case 4 -> KeyPairAlgorithm.ELGAMAL_1024;
case 5 -> KeyPairAlgorithm.ELGAMAL_512;
default -> KeyPairAlgorithm.RSA_4096;
};
System.out.println("...key " + algo_decoy[i]);
final KeyPair kp = algo_decoy[i].generateKeyPair();
pubDecoy[i] = kp.getPublic();
privDecoy[i] = kp.getPrivate();
}
System.out.println("...keys generated");
// simulate an empty data stream with the header
SecretMultiRecipientCryptor writer = new SecretMultiRecipientCryptor(pub, pubDecoy);
writer.setInput(new EncryptedContent() {
@Override
public InputStream getStream() throws IOException {
return InputStream.nullInputStream();
}
});
Ctx.INSTANCE.put(SecretMultiRecipientCryptor.SECRET, secret);
// generate a header for us
ByteArrayOutputStream headerOut = new ByteArrayOutputStream();
writer.getStream().transferTo(headerOut);
byte header[] = headerOut.toByteArray();
Ctx.INSTANCE.clear();
// test that all keys can reach the secret in the header
System.out.println("...can all recipients access the shared secret?");
for (int i = 0; i < pub.length; i++) {
System.out.printf("... recipient #%d %s%n", i, algo[i].toString());
SecretMultiRecipientCryptor reader = new SecretMultiRecipientCryptor(priv[i]);
reader.setInput(new PlainContent() {
@Override
public InputStream getStream() throws IOException {
return new SequenceInputStream(new ByteArrayInputStream(header), InputStream.nullInputStream());
}
});
// find the secret in a header
ByteArrayOutputStream headerIn = new ByteArrayOutputStream();
reader.getStream().transferTo(headerIn);
assertEquals(0, headerIn.size()); // there is nothing else than the header
assertArrayEquals(secret, Ctx.INSTANCE.get(SecretMultiRecipientCryptor.SECRET));
}
System.out.println("... --> yes!");
Ctx.INSTANCE.clear();
// test that all decoy keys can reach the decoy secret in the header
System.out.println("...cannot all decoys access the shared secret?");
for (int i = 0; i < pubDecoy.length; i++) {
System.out.printf("... decoy #%d %s%n", i, algo_decoy[i].toString());
SecretMultiRecipientCryptor reader = new SecretMultiRecipientCryptor(privDecoy[i]);
reader.setInput(new PlainContent() {
@Override
public InputStream getStream() throws IOException {
return new SequenceInputStream(new ByteArrayInputStream(header), InputStream.nullInputStream());
}
});
// find the secret in a header
ByteArrayOutputStream headerIn = new ByteArrayOutputStream();
reader.getStream().transferTo(headerIn);
assertEquals(0, headerIn.size()); // there is nothing else than the header
assertFalse(Arrays.equals(secret, Ctx.INSTANCE.get(SecretMultiRecipientCryptor.SECRET)));
}
System.out.println("... --> yes!");
System.out.println("...ok");
}
}

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.
******************************************************************************/
package zeroecho.data.processing;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class SecretPasswordTest {
@Test
void testGetPlainText() {
int len = 12;
System.out.printf("generating password, %d characters...%n", len);
SecretPassword sp = new SecretPassword(len);
System.out.printf("...string %s (length %d)%n", sp.toText(), sp.toText().length());
assertEquals(len, sp.toText().length());
}
}

Some files were not shown because too many files have changed in this diff Show More