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

19
app/.classpath Normal file
View File

@@ -0,0 +1,19 @@
<?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="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>

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/app/

23
app/.project Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app</name>
<comment>Project app 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
app/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.

52
app/build.gradle Normal file
View File

@@ -0,0 +1,52 @@
plugins {
id 'buildlogic.java-application-conventions'
id 'com.palantir.git-version' version '4.0.0'
}
group 'org.egothor'
version gitVersion(prefix:'release@')
dependencies {
implementation 'org.apache.commons:commons-text'
implementation 'commons-cli:commons-cli'
implementation project(':lib')
// might be removed if I move BC ops to the lib
testImplementation 'org.bouncycastle:bcpkix-jdk18on'
}
application {
// Define the main class for the application.
mainClass = 'zeroecho.ZeroEcho'
}
jar {
manifest {
attributes(
'Main-Class': application.mainClass,
'Implementation-Title': rootProject.name,
'Implementation-Version': "${version}"
)
}
// Include compiled classes from main source set
from sourceSets.main.output
dependsOn configurations.runtimeClasspath
from {
configurations.runtimeClasspath.collect { dep ->
if (dep.isDirectory()) {
dep
} else {
zipTree(dep).matching {
exclude 'META-INF/LICENSE.*'
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
}
}
}
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

View File

@@ -0,0 +1,235 @@
/**
* 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;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.util.X509CertificationAuthority;
import zeroecho.util.X509CertificationAuthority.NewCertificate;
import zeroecho.util.X509Support;
/**
* A utility class for managing asymmetric cryptographic keys and X.509
* certificates via command-line interface.
* <p>
* This class supports operations such as issuing certificates (from a CSR file
* or by generating a new key pair), revoking existing certificates, and
* retrieving all certificates associated with a given username. It relies on
* command-line options for input parameters.
* </p>
*
* Supported operations:
* <ul>
* <li>Issue a certificate from a CSR file or generate a new key pair and
* certificate</li>
* <li>Revoke an existing certificate using its PEM file</li>
* <li>Retrieve all certificates issued to a specific username</li>
* </ul>
*
* Supported command-line options:
* <ul>
* <li><b>-d, --directory</b>: Directory to store Certificate Authority (CA)
* data (default: current directory)</li>
* <li><b>-o, --organization</b>: Organization name for CA (default:
* "ACME")</li>
* <li><b>-u, --username</b>: Username for certificate issuance, revocation, or
* retrieval</li>
* <li><b>-s, --subject</b>: Subject Distinguished Name (DN) for certificate
* issuance (e.g., "CN=John Doe")</li>
* <li><b>-i, --issue</b>: Issue a certificate. Optionally takes a CSR file as
* an argument. If omitted, a new key pair is generated.</li>
* <li><b>-r, --revoke</b>: Revoke a certificate using its PEM file</li>
* <li><b>-a, --getAll</b>: Retrieve all certificates associated with the given
* username</li>
* </ul>
*/
public final class AsymetricKeysManagement {
private static final Logger LOG = Logger.getLogger(AsymetricKeysManagement.class.getName());
/**
* Private constructor to prevent instantiation of this utility class.
* <p>
* All methods in this class are static and it is not intended to be
* instantiated.
* </p>
*/
private AsymetricKeysManagement() {
}
/**
* Main entry point for executing asymmetric key management operations based on
* provided command-line arguments.
* <p>
* This method parses the given arguments, validates required inputs, and
* performs the specified operation by interacting with the
* {@link X509CertificationAuthority} class.
* </p>
*
* @param args Command-line arguments passed to the application.
* @param options The set of {@link org.apache.commons.cli.Options} defining
* allowed command-line options.
* @return error-code
* @throws ParseException if command-line arguments are missing required
* options, are malformed, or mutually exclusive options
* conflict.
*
* @see X509CertificationAuthority
* @see org.apache.commons.cli.CommandLine
*/
public static int main(final String[] args, final Options options) throws ParseException { // NOPMD
final Option DIR_OPTION = Option.builder("d").longOpt("directory").hasArg().argName("dir")
.desc("Directory to store CA data (default: .)").build();
final Option ORG_OPTION = Option.builder("o").longOpt("organization").hasArg().argName("org")
.desc("Organization name (default: ACME)").build();
final Option USERNAME_OPTION = Option.builder("u").longOpt("username").hasArg().argName("username")
.desc("Username for issuance, revocation or filtering").build();
final Option SUBJECT_OPTION = Option.builder("s").longOpt("subject").hasArg().argName("subject")
.desc("Subject DN (e.g., CN=John Doe)").build();
final Option ISSUE_OPTION = Option.builder("i").longOpt("issue").optionalArg(true).argName("csrFile")
.desc("Issue a certificate from CSR PEM file or generate keypair if omitted").build();
final Option REVOKE_OPTION = Option.builder("r").longOpt("revoke").hasArg().argName("certFile")
.desc("Revoke a certificate from PEM file").build();
final Option GET_ALL_OPTION = Option.builder("a").longOpt("getAll")
.desc("Get all certificates for given username").build();
final OptionGroup OPERATIONS = new OptionGroup();
OPERATIONS.addOption(ISSUE_OPTION);
OPERATIONS.addOption(REVOKE_OPTION);
OPERATIONS.addOption(GET_ALL_OPTION);
OPERATIONS.setRequired(true); // At least one required
options.addOption(DIR_OPTION);
options.addOption(ORG_OPTION);
options.addOption(USERNAME_OPTION);
options.addOption(SUBJECT_OPTION);
options.addOptionGroup(OPERATIONS);
final CommandLine cmd;
try {
final CommandLineParser parser = new DefaultParser();
cmd = parser.parse(options, args);
final String directory = cmd.getOptionValue(DIR_OPTION, ".");
final String organization = cmd.getOptionValue(ORG_OPTION, "ACME");
final String username = cmd.getOptionValue(USERNAME_OPTION);
final String subject = cmd.getOptionValue(SUBJECT_OPTION);
final X509CertificationAuthority ca = new X509CertificationAuthority(directory, organization);
ca.initialize();
if (cmd.hasOption(ISSUE_OPTION.getOpt())) {
if (username == null) {
throw new ParseException("--username is required when issuing a certificate.");
}
final String csrFile = cmd.getOptionValue(ISSUE_OPTION.getOpt());
if (csrFile != null) {
final Certificate cert = ca.issueFromCSRFile(username, csrFile);
if (cert != null) {
X509Support.printCertificate(cert);
} else {
LOG.log(Level.SEVERE, "Certificate issuance failed.");
}
} else {
if (subject == null) {
throw new ParseException("--subject is required if no CSR file is provided.");
}
final NewCertificate cert = ca.issue(username, subject);
if (cert != null) {
X509Support.printCertificate(cert.cert());
X509Support.printPrivateKey(cert.key().getPrivate());
} else {
LOG.log(Level.SEVERE, "Certificate issuance failed.");
}
}
} else if (cmd.hasOption(REVOKE_OPTION.getOpt())) {
if (username == null) {
throw new ParseException("--username is required when revoking a certificate.");
}
final String certFile = cmd.getOptionValue(REVOKE_OPTION.getOpt());
final Certificate cert = X509Support.loadCertificate(certFile);
if (cert == null) {
LOG.log(Level.SEVERE, "Failed to load certificate from file: {0}", certFile);
return 1;
}
final boolean revoked = ca.revoke(username, cert);
if (revoked) {
System.out.println("Certificate revoked successfully:");
X509Support.printCertificate(cert);
} else {
LOG.log(Level.SEVERE, "Certificate revocation failed or certificate not found.");
}
} else if (cmd.hasOption(GET_ALL_OPTION.getOpt())) {
if (username == null) {
throw new ParseException("--username is required when issuing a certificate.");
}
final Certificate[] certs = ca.getAll(username);
if (certs.length == 0) {
System.out.println("No certificates found for username: " + username);
} else {
for (final Certificate cert : certs) {
X509Support.printCertificate(cert);
System.out.println("-----");
}
}
}
} catch (NumberFormatException | IOException | GeneralSecurityException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Cannot proceed: " + e.toString());
}
return 1;
}
return 0;
}
}

View File

@@ -0,0 +1,274 @@
/**
* 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;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.HexFormat;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.builder.AesBuilder;
import zeroecho.builder.DataContentChainBuilder;
import zeroecho.builder.KEMAesParametersBuilder;
import zeroecho.builder.PlainFileBuilder;
import zeroecho.data.DataContent;
import zeroecho.util.UniversalKeyStoreFile;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
/**
* Utility class providing command-line support for AES encryption and
* decryption using Key Encapsulation Mechanism (KEM) and authenticated
* encryption modes like GCM.
*
* <p>
* Supports AES modes of varying key lengths (128/192/256) and cipher types such
* as CBC, GCM, or CTR. It relies on keys provided in a ZeroEcho-compatible
* keystore and supports both public and private key operations based on the
* selected mode.
* </p>
*
* <p>
* This class is non-instantiable and should only be invoked through its static
* {@code main} method.
* </p>
*/
public final class KEMAes {
private static final Logger LOG = Logger.getLogger(KEMAes.class.getName());
private KEMAes() {
// Utility class; prevent instantiation.
}
/**
* Entry point for command-line execution.
*
* @param args command-line arguments
* @param options CLI options container (used for external extensions/tests)
* @return exit code: 0 for success, 1 for failure
* @throws ParseException if CLI parsing fails or required options are missing
*/
public static int main(final String[] args, final Options options) throws ParseException { // NOPMD
// Define and register CLI options
final Option ENCRYPT_OPTION = Option.builder("e").longOpt("encrypt").hasArg().argName("inputFile")
.desc("Encrypt the given file").build();
final Option DECRYPT_OPTION = Option.builder("d").longOpt("decrypt").hasArg().argName("inputFile")
.desc("Decrypt the given file").build();
final Option OUTPUT_OPTION = Option.builder("o").longOpt("output").hasArg().argName("outputFile")
.desc("Output file path (default: inputFile + .enc or .dec)").build();
final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("keystoreFile")
.desc("File with keys (ZeroEcho's *.keystore)").required().build();
final Option PUB_OPTION = Option.builder("p").longOpt("pubkeys").hasArg().argName("usernames")
.desc("Recipient's username list (in keystore)").build();
final Option PRIV_OPTION = Option.builder("s").longOpt("privkey").hasArg().argName("username")
.desc("Username with private key for decryption (in keystore)").build();
final Option MODE_OPTION = Option.builder("m").longOpt("mode").hasArg().argName("aesMode")
.desc("AES mode: AES-128, AES-192, AES-256 (default: " + AesMode.AES_256 + ")").build();
final Option CIPHER_TYPE_OPTION = Option.builder("c").longOpt("cipher").hasArg().argName("cipherType")
.desc("Cipher type: AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding (default: "
+ AesCipherType.GCM + ")")
.build();
final Option AAD_OPTION = Option.builder("a").longOpt("aad").hasArg().argName("hex")
.desc("Optional AAD (hex-encoded) for authenticated AES modes like GCM").build();
final OptionGroup OPERATION_GROUP = new OptionGroup();
OPERATION_GROUP.addOption(ENCRYPT_OPTION);
OPERATION_GROUP.addOption(DECRYPT_OPTION);
OPERATION_GROUP.setRequired(true);
options.addOptionGroup(OPERATION_GROUP);
options.addOption(OUTPUT_OPTION);
options.addOption(KEYSTORE_OPTION);
options.addOption(PUB_OPTION);
options.addOption(PRIV_OPTION);
options.addOption(MODE_OPTION);
options.addOption(CIPHER_TYPE_OPTION);
options.addOption(AAD_OPTION);
try {
final CommandLineParser parser = new DefaultParser();
final CommandLine cmd = parser.parse(options, args);
final boolean isEncryption = cmd.hasOption(ENCRYPT_OPTION.getOpt());
final boolean isDecryption = cmd.hasOption(DECRYPT_OPTION.getOpt());
final String inputPath = cmd.getOptionValue(ENCRYPT_OPTION.getOpt(),
cmd.getOptionValue(DECRYPT_OPTION.getOpt()));
final String outputPath = cmd.getOptionValue(OUTPUT_OPTION, inputPath + (isEncryption ? ".enc" : ".dec"));
final String keystorePath = cmd.getOptionValue(KEYSTORE_OPTION);
final AesMode mode = AesMode.fromString(cmd.getOptionValue(MODE_OPTION, AesMode.AES_256.toString()));
final AesCipherType cipher = AesCipherType
.fromString(cmd.getOptionValue(CIPHER_TYPE_OPTION, AesCipherType.GCM.toString()));
final byte[] aad = cmd.hasOption(AAD_OPTION.getOpt())
? HexFormat.of().parseHex(cmd.getOptionValue(AAD_OPTION))
: new byte[0];
final UniversalKeyStoreFile db = new UniversalKeyStoreFile(keystorePath);
if (isEncryption) {
final String pub = cmd.getOptionValue(PUB_OPTION);
if (pub == null) {
throw new ParseException("Option is required for encryption: " + PUB_OPTION.getOpt() + " ("
+ PUB_OPTION.getLongOpt() + ")");
}
final PublicKey recipient = getPublicKey(db, pub);
performEncryption(recipient, mode, cipher, aad, inputPath, outputPath);
} else if (isDecryption) {
final String priv = cmd.getOptionValue(PRIV_OPTION);
if (priv == null) {
throw new ParseException("Option is required for decryption: " + PRIV_OPTION.getOpt() + " ("
+ PRIV_OPTION.getLongOpt() + ")");
}
performDecryption(db.loadPrivateKey(priv), mode, cipher, aad, inputPath, outputPath);
}
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Error during encryption/decryption: " + e.toString());
}
return 1;
}
return 0;
}
/**
* Encrypts a file using the specified public key and AES configuration.
*
* @param recipient recipient's public key
* @param mode AES key mode (128/192/256 bits)
* @param cipher AES cipher type (GCM, CBC, etc.)
* @param aad optional Additional Authenticated Data (may be empty)
* @param inputPath path to the input file
* @param outputPath path where the encrypted file will be written
* @throws IOException if reading or writing files fails
*/
private static void performEncryption(final PublicKey recipient, final AesMode mode, final AesCipherType cipher,
final byte[] aad, final String inputPath, final String outputPath) throws IOException {
DataContent encrypted = DataContentChainBuilder.encrypt()
// input file
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL()))
// AES process
.add(AesBuilder.builder())
// configured by KEM
.add(KEMAesParametersBuilder.builder().withKey(recipient).withCipherType(cipher).withAesMode(mode)
.withAAD(aad))
// build!
.build();
try (InputStream encryptedStream = encrypted.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
encryptedStream.transferTo(fileOut);
System.err.println("Encryption complete. Output saved to: " + outputPath);
}
}
/**
* Decrypts a file using the specified private key and AES configuration.
*
* @param recipient recipient's private key
* @param mode AES key mode
* @param cipher AES cipher type
* @param aad optional Additional Authenticated Data (may be empty)
* @param inputPath path to the encrypted file
* @param outputPath path where the decrypted file will be written
* @throws IOException if reading or writing files fails
*/
private static void performDecryption(final PrivateKey recipient, final AesMode mode, final AesCipherType cipher,
final byte[] aad, final String inputPath, final String outputPath) throws IOException {
DataContent decrypted = DataContentChainBuilder.decrypt()
// input file
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL()))
// encrypted by KEM
.add(KEMAesParametersBuilder.builder().withKey(recipient).withCipherType(cipher).withAesMode(mode)
.withAAD(aad))
// AES process
.add(AesBuilder.builder())
// build!
.build();
try (InputStream decryptedStream = decrypted.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
decryptedStream.transferTo(fileOut);
System.err.println("Decryption complete. Output saved to: " + outputPath);
}
}
/**
* Retrieves a public key for the given username from the keystore.
*
* @param db the loaded keystore
* @param username username to fetch the public key for
* @return the public key
* @throws NoSuchElementException if user not found or key construction fails
*/
private static PublicKey getPublicKey(final UniversalKeyStoreFile db, final String username) {
try {
return db.loadPublicKey(username);
} catch (NoSuchElementException e) {
LOG.logp(Level.FINE, "KEMAes", "getPublicKey", "Exception", e);
throw new NoSuchElementException("unknown user <" + username + ">", e);
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException | IOException e) {
LOG.logp(Level.FINE, "KEMAes", "getPublicKey", "Exception", e);
throw new NoSuchElementException("cannot construct public key for <" + username + ">", e);
}
}
}

View File

@@ -0,0 +1,186 @@
/**
* 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;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.UniversalKeyStoreFile;
import zeroecho.util.X509Support;
/**
* Utility class for managing a simple file-based keystore via command-line
* interface (CLI).
* <p>
* This class provides a static {@code main} method that enables two primary
* operations:
* </p>
* <ul>
* <li>Importing a public key from a PEM-encoded X.509 certificate</li>
* <li>Generating and storing a key pair (public/private) for a specified
* owner</li>
* </ul>
* <p>
* The keystore is backed by a flat file managed by
* {@link UniversalKeyStoreFile}, and keys are stored using symbolic owner
* identifiers.
* </p>
*
* <p>
* The following command-line options are supported:
* </p>
* <ul>
* <li>{@code -k, --keystore <file>}: Path to the keystore file (required)</li>
* <li>{@code -o, --owner <name>}: Owner name used as the key identifier
* (required)</li>
* <li>{@code -c, --cert <pem-file>}: Path to a PEM-encoded X.509 certificate
* file</li>
* <li>{@code -g, --generate}: Flag indicating that a key pair should be
* generated</li>
* <li>{@code -a, --algorithm <type>}: Algorithm and key size (e.g.,
* {@code RSA:2048}, {@code EC:256}). Defaults to {@code RSA:2048}</li>
* </ul>
* <p>
* At least one of {@code --cert} or {@code --generate} must be specified.
* </p>
*
* <p>
* This class is not meant to be instantiated.
* </p>
*
* @author Leo Galambos
*/
public final class KeyStoreManagement {
private static final Logger LOG = Logger.getLogger(KeyStoreManagement.class.getName());
/**
* Private constructor to prevent instantiation of this utility class.
*/
private KeyStoreManagement() {
// Utility class; prevent instantiation.
}
/**
* Main entry point for executing keystore operations via CLI.
*
* @param args the command-line arguments to parse
* @param options the Apache Commons CLI {@link Options} object to populate with
* supported options
* @return exit code: {@code 0} on success, {@code 2} on I/O error or processing
* failure
* @throws ParseException if argument parsing fails
* @throws NoSuchAlgorithmException if the specified key generation
* algorithm is unavailable
* @throws NoSuchProviderException if the Bouncy Castle provider is
* not registered
* @throws InvalidAlgorithmParameterException if the specified key size of
* EC-algorithms is invalid
*/
public static int main(String[] args, final Options options) throws ParseException, NoSuchAlgorithmException,
NoSuchProviderException, InvalidAlgorithmParameterException {
final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("file")
.desc("Path to keystore file").required().build();
final Option OWNER_OPTION = Option.builder("o").longOpt("owner").hasArg().argName("name")
.desc("Owner name for key").required().build();
final Option CERT_OPTION = Option.builder("c").longOpt("cert").hasArg().argName("pem-file")
.desc("PEM certificate file to extract public key").build();
final Option GENERATE_OPTION = Option.builder("g").longOpt("generate").desc("Generate key pair for the owner")
.build();
final Option ALGORITHM_OPTION = Option.builder("a").longOpt("algorithm").hasArg().argName("type")
.desc("Key pair algorithm (e.g., RSA:4096, EC:256). Default is RSA:2048.").build();
options.addOption(KEYSTORE_OPTION);
options.addOption(OWNER_OPTION);
options.addOption(CERT_OPTION);
options.addOption(GENERATE_OPTION);
options.addOption(ALGORITHM_OPTION);
final CommandLineParser parser = new DefaultParser();
try {
final CommandLine cmd = parser.parse(options, args);
final String keystorePath = cmd.getOptionValue(KEYSTORE_OPTION);
final String owner = cmd.getOptionValue(OWNER_OPTION);
final String certPath = cmd.getOptionValue(CERT_OPTION);
final boolean generate = cmd.hasOption(GENERATE_OPTION);
if (certPath == null && !generate) {
throw new ParseException("You must specify either --cert or --generate");
}
final UniversalKeyStoreFile keystore = new UniversalKeyStoreFile(Paths.get(keystorePath));
if (certPath != null) {
final Certificate cert = X509Support.loadCertificate(certPath);
keystore.addPubKey(owner, cert.getPublicKey());
LOG.log(Level.INFO, "Public key added for owner: {0}", owner);
}
if (generate) {
final String algorithmString = cmd.getOptionValue(ALGORITHM_OPTION, "RSA:2048");
final KeyPair keyPair = KeyPairAlgorithm.fromString(algorithmString).generateKeyPair();
keystore.addPubKey(owner, keyPair.getPublic());
keystore.addPrivKey(owner, keyPair.getPrivate());
LOG.log(Level.INFO, "Key pair generated and added for owner: {0}", owner);
}
} catch (final IOException e) {
LOG.log(Level.SEVERE, "Unexpected error", e);
return 2;
}
return 0;
}
}

View File

@@ -0,0 +1,361 @@
/**
* 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;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Collection;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.builder.AesBuilder;
import zeroecho.builder.AesRandomBuilder;
import zeroecho.builder.DataContentChainBuilder;
import zeroecho.builder.MultiRecipientCryptorBuilder;
import zeroecho.builder.PlainFileBuilder;
import zeroecho.data.DataContent;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.UniversalKeyStoreFile;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
/**
* Utility class for multi-recipient AES encryption and decryption of files.
* <p>
* This class provides a command-line interface (CLI) entry point to encrypt or
* decrypt files using AES with multiple recipients public keys or a single
* private key from a keystore.
* </p>
* <p>
* Supported command-line options:
* </p>
* <ul>
* <li><b>-e / --encrypt &lt;inputFile&gt;</b>: File to encrypt.</li>
* <li><b>-d / --decrypt &lt;inputFile&gt;</b>: File to decrypt.</li>
* <li><b>-k / --keystore &lt;keystoreFile&gt;</b>: Path to keystore file
* (required).</li>
* <li><b>-p / --pubkeys &lt;usernames&gt;</b>: List of recipient usernames for
* encryption.</li>
* <li><b>-s / --privkey &lt;username&gt;</b>: Username with private key for
* decryption.</li>
* <li><b>-o / --output &lt;outputFile&gt;</b>: Output file path (optional;
* defaults to inputFile.enc or inputFile.dec).</li>
* <li><b>-m / --mode &lt;aesMode&gt;</b>: AES mode (AES-128, AES-192, AES-256).
* Default is AES-256.</li>
* <li><b>-c / --cipher &lt;cipherType&gt;</b>: Cipher transformation
* (AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding). Default is
* AES/CBC/PKCS7Padding.</li>
* <li><b>-x / --decoys &lt;algorithms&gt;</b>: List of decoy key algorithms to
* include (e.g., RSA:2048, EC:256).</li>
* </ul>
* <p>
* This is a utility class and cannot be instantiated.
* </p>
*
* @author Leo Galambos
*/
public final class MultiRecipientAes {
private static final Logger LOG = Logger.getLogger(MultiRecipientAes.class.getName());
private MultiRecipientAes() {
// Utility class; prevent instantiation.
}
/**
* Parses command-line arguments and executes either encryption or decryption
* accordingly.
* <p>
* Encryption requires:
* <ul>
* <li>Option {@code -e} with input file path to encrypt.</li>
* <li>Option {@code -p} with one or more recipient usernames whose public keys
* are used.</li>
* <li>Option {@code -k} with the keystore file containing recipient keys.</li>
* </ul>
* Decryption requires:
* <ul>
* <li>Option {@code -d} with input file path to decrypt.</li>
* <li>Option {@code -s} with the username whose private key will decrypt the
* file.</li>
* <li>Option {@code -k} with the keystore file containing the private key.</li>
* </ul>
* <p>
* Optional parameters include the output file path, AES mode, and cipher type.
* </p>
*
* @param args Command-line arguments specifying operation mode and
* parameters.
* @param options Pre-configured Apache Commons CLI {@link Options} object to
* which this method will add supported options.
* @return Exit code {@code 0} if operation was successful; {@code 1} if an
* error occurred.
* @throws ParseException If parsing the command-line arguments fails due to
* invalid or missing options.
*/
public static int main(final String[] args, final Options options) throws ParseException { // NOPMD
// Define and register CLI options
final Option ENCRYPT_OPTION = Option.builder("e").longOpt("encrypt").hasArg().argName("inputFile")
.desc("Encrypt the given file").build();
final Option DECRYPT_OPTION = Option.builder("d").longOpt("decrypt").hasArg().argName("inputFile")
.desc("Decrypt the given file").build();
final Option OUTPUT_OPTION = Option.builder("o").longOpt("output").hasArg().argName("outputFile")
.desc("Output file path (default: inputFile + .enc or .dec)").build();
final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("keystoreFile")
.desc("File with keys (ZeroEcho's *.keystore)").required().build();
final Option PUB_OPTION = Option.builder("p").longOpt("pubkeys").hasArgs().argName("list of usernames")
.desc("Recipients' usernames list (in keystore)").build();
final Option PRIV_OPTION = Option.builder("s").longOpt("privkey").hasArgs().argName("username")
.desc("Username with private key for decryption (in keystore)").build();
final Option MODE_OPTION = Option.builder("m").longOpt("mode").hasArg().argName("aesMode")
.desc("AES mode: AES-128, AES-192, AES-256 (default: AES-256)").build();
final Option CIPHER_TYPE_OPTION = Option.builder("c").longOpt("cipher").hasArg().argName("cipherType").desc(
"Cipher type: AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding (default: AES/CBC/PKCS7Padding)")
.build();
final Option DECOY_OPTION = Option.builder("x").longOpt("decoys").hasArgs().argName("keyAlgorithms")
.desc("List of decoy key algorithms (e.g., RSA:2048, EC:256)").build();
final OptionGroup OPERATION_GROUP = new OptionGroup();
OPERATION_GROUP.addOption(ENCRYPT_OPTION);
OPERATION_GROUP.addOption(DECRYPT_OPTION);
OPERATION_GROUP.setRequired(true);
options.addOptionGroup(OPERATION_GROUP);
options.addOption(OUTPUT_OPTION);
options.addOption(KEYSTORE_OPTION);
options.addOption(PUB_OPTION);
options.addOption(PRIV_OPTION);
options.addOption(MODE_OPTION);
options.addOption(CIPHER_TYPE_OPTION);
options.addOption(DECOY_OPTION);
try {
final CommandLineParser parser = new DefaultParser();
final CommandLine cmd = parser.parse(options, args);
final boolean isEncryption = cmd.hasOption(ENCRYPT_OPTION.getOpt());
final boolean isDecryption = cmd.hasOption(DECRYPT_OPTION.getOpt());
final String inputPath = cmd.getOptionValue(ENCRYPT_OPTION.getOpt(),
cmd.getOptionValue(DECRYPT_OPTION.getOpt()));
final String outputPath = cmd.getOptionValue(OUTPUT_OPTION, inputPath + (isEncryption ? ".enc" : ".dec"));
final String keystorePath = cmd.getOptionValue(KEYSTORE_OPTION);
final AesMode mode = AesMode.fromString(cmd.getOptionValue(MODE_OPTION, "AES-256"));
final AesCipherType cipher = AesCipherType
.fromString(cmd.getOptionValue(CIPHER_TYPE_OPTION, "AES/CBC/PKCS7Padding"));
final UniversalKeyStoreFile db = new UniversalKeyStoreFile(keystorePath);
if (isEncryption) {
final String[] pub = cmd.getOptionValues(PUB_OPTION);
if (pub == null) {
throw new ParseException("Option is required for encryption: " + PUB_OPTION.getOpt() + " ("
+ PUB_OPTION.getLongOpt() + ")");
}
final Collection<PublicKey> recipient = getPublicKeys(db, pub);
final Collection<PublicKey> decoy = cmd.hasOption(DECOY_OPTION)
? getDecoyKeys(cmd.getOptionValues(DECOY_OPTION))
: new HashSet<>();
performEncryption(recipient, decoy, mode, cipher, inputPath, outputPath);
} else if (isDecryption) {
final String priv = cmd.getOptionValue(PRIV_OPTION);
if (priv == null) {
throw new ParseException("Option is required for decryption: " + PRIV_OPTION.getOpt() + " ("
+ PRIV_OPTION.getLongOpt() + ")");
}
performDecryption(db.loadPrivateKey(priv), mode, cipher, inputPath, outputPath);
}
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Error during encryption/decryption: " + e.toString());
}
return 1;
}
return 0;
}
/**
* Encrypts the input file for multiple recipients using AES and writes the
* encrypted data to output file.
*
* @param recipient Collection of recipients' public keys to encrypt the AES
* key.
* @param decoy Collection of recipients' public keys (decoys) to encrypt
* the false AES key.
* @param mode AES mode (e.g., AES-128, AES-256).
* @param cipher Cipher type (e.g., AES/CBC/PKCS7Padding).
* @param inputPath Path to the plaintext input file.
* @param outputPath Path where the encrypted output file will be saved.
* @throws IOException If file reading or writing fails.
*/
private static void performEncryption(final Collection<PublicKey> recipient, final Collection<PublicKey> decoy,
final AesMode mode, final AesCipherType cipher, final String inputPath, final String outputPath)
throws IOException {
DataContent encrypted = DataContentChainBuilder.encrypt()
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL())).add(AesBuilder.builder())
.add(MultiRecipientCryptorBuilder.builder().addRecipients(recipient).addDecoys(decoy))
.add(AesRandomBuilder.builder().mode(mode).cipherType(cipher)).build();
try (InputStream encryptedStream = encrypted.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
encryptedStream.transferTo(fileOut);
System.err.println("Encryption complete. Output saved to: " + outputPath);
}
}
/**
* Decrypts the input file using the specified recipient's private key and
* writes the plaintext to output file.
*
* @param recipient Private key of the recipient used to unwrap AES key and
* decrypt the file.
* @param mode AES mode (e.g., AES-128, AES-256).
* @param cipher Cipher type (e.g., AES/CBC/PKCS7Padding).
* @param inputPath Path to the encrypted input file.
* @param outputPath Path where the decrypted output file will be saved.
* @throws IOException If file reading or writing fails.
*/
private static void performDecryption(final PrivateKey recipient, final AesMode mode, final AesCipherType cipher,
final String inputPath, final String outputPath) throws IOException {
DataContent decrypted = DataContentChainBuilder.decrypt()
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL()))
.add(AesRandomBuilder.builder().mode(mode).cipherType(cipher))
.add(MultiRecipientCryptorBuilder.builder().privateKey(recipient)).add(AesBuilder.builder()).build();
try (InputStream decryptedStream = decrypted.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
decryptedStream.transferTo(fileOut);
System.err.println("Decryption complete. Output saved to: " + outputPath);
}
}
/**
* Retrieves a collection of public keys for the specified usernames from the
* keystore.
*
* @param db The {@link UniversalKeyStoreFile} instance representing the
* keystore.
* @param usernames The list of usernames whose public keys should be fetched.
* @return Collection of {@link PublicKey} instances corresponding to the given
* usernames.
* @throws NoSuchElementException If any username is not found or their public
* key cannot be constructed.
*/
private static Collection<PublicKey> getPublicKeys(final UniversalKeyStoreFile db, final String... usernames) {
final Collection<PublicKey> set = new HashSet<>();
for (String user : usernames) {
try {
set.add(db.loadPublicKey(user));
} catch (NoSuchElementException e) {
LOG.logp(Level.FINE, "MultiRecipientAes", "getPublicKeys", "Exception", e);
throw new NoSuchElementException("unknown user <" + user + ">", e);
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException | IOException e) {
LOG.logp(Level.FINE, "MultiRecipientAes", "getPublicKeys", "Exception", e);
throw new NoSuchElementException("cannot construct public key for <" + user + ">", e);
}
}
return set;
}
/**
* Generates a collection of public keys based on the specified decoy key
* algorithms.
* <p>
* Each algorithm string should correspond to a recognized
* {@link KeyPairAlgorithm} format, such as "RSA:2048" or "EC:256". For each
* algorithm string provided, this method attempts to generate a new key pair
* and collect the public key.
* </p>
* <p>
* These keys can be used as decoys in multi-recipient encryption schemes to
* enhance security by adding plausible but non-functional recipients.
* </p>
*
* @param decoyAlgorithms an array of strings specifying the key pair algorithms
* for decoy key generation (e.g., "RSA:2048", "EC:256")
* @return a {@link Collection} of generated {@link PublicKey} instances
* corresponding to the decoy algorithms provided
* @throws NoSuchElementException if any of the specified algorithms is invalid
* or if key generation fails for any algorithm,
* wrapping the underlying cause
*/
private static Collection<PublicKey> getDecoyKeys(final String... decoyAlgorithms) {
final Collection<PublicKey> set = new HashSet<>();
for (String algStr : decoyAlgorithms) {
try {
final KeyPairAlgorithm alg = KeyPairAlgorithm.fromString(algStr);
final KeyPair pair = alg.generateKeyPair();
set.add(pair.getPublic());
} catch (IllegalArgumentException | NoSuchAlgorithmException | NoSuchProviderException
| InvalidAlgorithmParameterException e) {
LOG.log(Level.FINE, "Failed to generate decoy key for algorithm: {0}", algStr);
throw new NoSuchElementException("cannot construct key pair for <" + algStr + ">", e);
}
}
return set;
}
}

View File

@@ -0,0 +1,294 @@
/**
* 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;
import java.io.Console;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.data.DataContent;
import zeroecho.data.processing.PasswordBasedAesDecryptor;
import zeroecho.data.processing.PasswordBasedAesEncryptor;
import zeroecho.data.processing.PlainFile;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
/**
* Utility class providing AES encryption and decryption based on a password
* using PBKDF2 key derivation. This class exposes a command-line interface
* (CLI) entry point for encrypting and decrypting files with configurable
* options.
*
* <p>
* The supported command-line parameters are as follows:
* <ul>
* <li><b>-e / --encrypt &lt;inputFile&gt;</b>: Specifies the file to
* encrypt.</li>
* <li><b>-d / --decrypt &lt;inputFile&gt;</b>: Specifies the file to
* decrypt.</li>
* <li><b>-p / --password &lt;password&gt;</b>: Password for
* encryption/decryption.</li>
* <li><b>-o / --output &lt;outputFile&gt;</b>: Output file path (optional,
* defaults to input file with .enc or .dec extension).</li>
* <li><b>-m / --mode &lt;aesMode&gt;</b>: AES key size mode (AES-128, AES-192,
* AES-256). Defaults to AES-256.</li>
* <li><b>-c / --cipher &lt;cipherType&gt;</b>: Cipher transformation
* (AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding). Defaults to
* AES/CBC/PKCS7Padding.</li>
* <li><b>-n / --iterations &lt;iterations&gt;</b>: PBKDF2 iteration count.
* Defaults to 100,000.</li>
* </ul>
*
* <p>
* This class cannot be instantiated.
*/
public final class PasswordBasedAes {
private static final Logger LOG = Logger.getLogger(PasswordBasedAes.class.getName());
/**
* Private constructor to prevent instantiation.
*/
private PasswordBasedAes() {
// Utility class; prevent instantiation.
}
/**
* Main method that parses command-line arguments and performs encryption or
* decryption.
*
* @param args Command-line arguments.
* @param options Predefined CLI options to configure.
* @return Exit code: 0 if successful, 1 if an error occurred.
* @throws ParseException If argument parsing fails.
*/
public static int main(final String[] args, final Options options) throws ParseException {
// Define and register CLI options
final Option ENCRYPT_OPTION = Option.builder("e").longOpt("encrypt").hasArg().argName("inputFile")
.desc("Encrypt the given file").build();
final Option DECRYPT_OPTION = Option.builder("d").longOpt("decrypt").hasArg().argName("inputFile")
.desc("Decrypt the given file").build();
final Option PASSWORD_OPTION = Option.builder("p").longOpt("password").hasArg().argName("password")
.desc("Password used for encryption/decryption").build();
final Option OUTPUT_OPTION = Option.builder("o").longOpt("output").hasArg().argName("outputFile")
.desc("Output file path (default: inputFile + .enc or .dec)").build();
final Option MODE_OPTION = Option.builder("m").longOpt("mode").hasArg().argName("aesMode")
.desc("AES mode: AES-128, AES-192, AES-256 (default: AES-256)").build();
final Option CIPHER_TYPE_OPTION = Option.builder("c").longOpt("cipher").hasArg().argName("cipherType").desc(
"Cipher type: AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding (default: AES/CBC/PKCS7Padding)")
.build();
final Option ITER_OPTION = Option.builder("n").longOpt("iterations").hasArg().argName("iterations")
.desc("PBKDF2 iteration count (default: 100000)").build();
final Option AAD_OPTION = Option.builder("a").longOpt("aad").hasArg().argName("aadHex")
.desc("Additional Authenticated Data (AAD) as hex string (optional)").build();
final OptionGroup OPERATION_GROUP = new OptionGroup();
OPERATION_GROUP.addOption(ENCRYPT_OPTION);
OPERATION_GROUP.addOption(DECRYPT_OPTION);
OPERATION_GROUP.setRequired(true);
options.addOptionGroup(OPERATION_GROUP);
options.addOption(PASSWORD_OPTION);
options.addOption(OUTPUT_OPTION);
options.addOption(MODE_OPTION);
options.addOption(CIPHER_TYPE_OPTION);
options.addOption(ITER_OPTION);
options.addOption(AAD_OPTION);
try {
final CommandLineParser parser = new DefaultParser();
final CommandLine cmd = parser.parse(options, args);
final boolean isEncryption = cmd.hasOption(ENCRYPT_OPTION.getOpt());
final boolean isDecryption = cmd.hasOption(DECRYPT_OPTION.getOpt());
final String inputPath = cmd.getOptionValue(ENCRYPT_OPTION.getOpt(),
cmd.getOptionValue(DECRYPT_OPTION.getOpt()));
final String outputPath = cmd.getOptionValue(OUTPUT_OPTION, inputPath + (isEncryption ? ".enc" : ".dec"));
final AesMode mode = AesMode.fromString(cmd.getOptionValue(MODE_OPTION, "AES-256"));
final AesCipherType cipher = AesCipherType
.fromString(cmd.getOptionValue(CIPHER_TYPE_OPTION, "AES/CBC/PKCS7Padding"));
final int iterations = Integer.parseInt(cmd.getOptionValue(ITER_OPTION, "100000"));
String password = cmd.getOptionValue(PASSWORD_OPTION);
if (password == null) {
password = promptForPassword(isEncryption);
}
final byte[] aad = cmd.hasOption(AAD_OPTION.getOpt())
? HexFormat.of().parseHex(cmd.getOptionValue(AAD_OPTION.getOpt()))
: null;
final DataContent fileIn = new PlainFile(Path.of(inputPath).toUri().toURL());
if (isEncryption) {
performEncryption(password, iterations, aad, mode, cipher, fileIn, outputPath);
} else if (isDecryption) {
performDecryption(password, aad, mode, cipher, fileIn, outputPath);
}
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Error during encryption/decryption: {0}", e.toString());
}
return 1;
}
return 0;
}
/**
* Prompts the user to enter a password (optionally with confirmation).
*
* @param confirm Whether to ask for password confirmation (encryption mode).
* @return The confirmed password as a string.
* @throws ParseException If user input is invalid or confirmation fails.
* @throws IOException If the system console is not available.
*/
private static String promptForPassword(final boolean confirm) throws ParseException, IOException { // NOPMD
final Console console = System.console();
if (console == null) {
throw new IOException("No console available for password input. Please use the --password option.");
}
final char[] pwdArray;
if (confirm) {
pwdArray = console.readPassword("Enter password for encryption: ");
final char[] confirmArray = console.readPassword("Confirm password: ");
if (pwdArray == null || confirmArray == null || pwdArray.length == 0) {
throw new ParseException("No password entered.");
}
if (!Arrays.equals(pwdArray, confirmArray)) {
throw new ParseException("Passwords do not match.");
}
} else {
pwdArray = console.readPassword("Enter password for decryption: ");
if (pwdArray == null || pwdArray.length == 0) {
throw new ParseException("No password entered.");
}
}
final String password = new String(pwdArray);
Arrays.fill(pwdArray, ' '); // Clear sensitive data
return password;
}
/**
* Encrypts a file using password-based AES encryption with the specified
* parameters.
*
* @param password the password used for key derivation; must not be
* {@code null}
* @param iterations the number of PBKDF2 iterations to use; must be positive
* @param aad additional authenticated data (AAD) for authenticated
* encryption modes; may be {@code null}
* @param mode the AES mode specifying key length (e.g., AES-128,
* AES-256); must not be {@code null}
* @param cipher the AES cipher configuration (e.g., CBC, GCM); must not be
* {@code null}
* @param fileIn the input data content representing the plaintext file;
* must not be {@code null}
* @param outputPath the path to write the encrypted output file; must not be
* {@code null} or empty
* @throws IOException if an I/O error occurs during reading or
* writing files
* @throws GeneralSecurityException if an error occurs during encryption or key
* derivation
*/
private static void performEncryption(final String password, final int iterations, final byte[] aad,
final AesMode mode, final AesCipherType cipher, final DataContent fileIn, final String outputPath)
throws IOException, GeneralSecurityException {
final PasswordBasedAesEncryptor encryptor = new PasswordBasedAesEncryptor(password, iterations, aad, mode,
cipher);
encryptor.setInput(fileIn);
try (InputStream encryptedStream = encryptor.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
encryptedStream.transferTo(fileOut);
System.err.println("Encryption complete. Output saved to: " + outputPath);
}
}
/**
* Decrypts a file using password-based AES decryption with the specified
* parameters.
*
* @param password the password used for key derivation; must not be
* {@code null}
* @param aad additional authenticated data (AAD) for authenticated
* encryption modes; may be {@code null}
* @param mode the AES mode specifying key length (e.g., AES-128,
* AES-256); must not be {@code null}
* @param cipher the AES cipher configuration (e.g., CBC, GCM); must not be
* {@code null}
* @param fileIn the input data content representing the encrypted file;
* must not be {@code null}
* @param outputPath the path to write the decrypted output file; must not be
* {@code null} or empty
* @throws IOException if an I/O error occurs during reading or
* writing files
* @throws GeneralSecurityException if an error occurs during decryption or key
* derivation
*/
private static void performDecryption(final String password, final byte[] aad, final AesMode mode,
final AesCipherType cipher, final DataContent fileIn, final String outputPath)
throws IOException, GeneralSecurityException {
final PasswordBasedAesDecryptor decryptor = new PasswordBasedAesDecryptor(password, aad, mode, cipher);
decryptor.setInput(fileIn);
try (InputStream decryptedStream = decryptor.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
decryptedStream.transferTo(fileOut);
System.err.println("Decryption complete. Output saved to: " + outputPath);
}
}
}

View File

@@ -0,0 +1,208 @@
/*******************************************************************************
* 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;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.MissingOptionException;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.util.BouncyCastleActivator;
/**
* ZeroEcho is a command-line utility for managing asymmetric keys and
* certificates, primarily focusing on Certificate Authority (CA) operations
* such as issuing, revoking, and listing certificates.
* <p>
* It supports command-line options for asymmetric key management, including
* issuing certificates from CSRs or subject names, revoking certificates, and
* retrieving all certificates for a user.
* </p>
* <p>
* This class initializes Bouncy Castle security provider and uses Apache
* Commons CLI for command-line parsing.
* </p>
*
* @author Leo Galambos
*/
public class ZeroEcho { // NOPMD by Leo Galambos on 6/1/25, 1:04PM
/**
* Logger instance for the {@code ZeroEcho} class used to log messages and
* events.
* <p>
* This logger is configured with the name of the {@code ZeroEcho} class,
* allowing for fine-grained logging control specific to this class.
* </p>
*/
public static final Logger LOG = Logger.getLogger(ZeroEcho.class.getName());
static {
BouncyCastleActivator.init();
}
/**
* Default constructor for ZeroEcho.
* <p>
* This constructor does not perform any initialization since all operations are
* handled via static methods and blocks.
* </p>
*/
private ZeroEcho() {
// No initialization needed
}
/**
* Main entry point for the ZeroEcho application. Parses command-line arguments
* and dispatches to the appropriate subcommand for asymmetric key management or
* prints the help message.
*
* @param args command-line arguments passed to the program
*/
public static void main(final String[] args) {
final int errorCode = mainProcess(args);
if (errorCode == 0) {
System.out.println("OK");
} else {
System.out.println("ERR: " + errorCode);
}
}
/**
* Entry point for the ZeroEcho application. Parses command-line arguments and
* dispatches to the appropriate subcommand for asymmetric key management or
* prints the help message.
*
* @param args command-line arguments passed to the program
* @return error-code
*/
public static int mainProcess(final String[] args) { // NOPMD by Leo Galambos on 6/11/25, 10:25PM
final Option ASYMETRIC_OPTION = Option.builder("A").longOpt("asm").desc("asymetric keys management").build();
final Option KEM_OPTION = Option.builder("E").longOpt("kem").desc("KEM encryption/decryption").build();
final Option MULTI_AES_OPTION = Option.builder("M").longOpt("multi-aes").desc("AES for multiple recipients")
.build();
final Option KEYSTORE_OPTION = Option.builder("K").longOpt("ksm").desc("key store management").build();
final Option AES_PSW_OPTION = Option.builder("P").longOpt("aes-psw").desc("AES with password").build();
final OptionGroup OPERATION_GROUP = new OptionGroup();
OPERATION_GROUP.addOption(ASYMETRIC_OPTION);
OPERATION_GROUP.addOption(AES_PSW_OPTION);
OPERATION_GROUP.addOption(MULTI_AES_OPTION);
OPERATION_GROUP.addOption(KEYSTORE_OPTION);
OPERATION_GROUP.addOption(KEM_OPTION);
OPERATION_GROUP.setRequired(true); // At least one required
Options options = new Options();
options.addOptionGroup(OPERATION_GROUP);
final CommandLineParser parser = new DefaultParser();
try {
// parse the command line arguments (allow remaining arguments for subcommands)
parser.parse(options, args, true);
switch (OPERATION_GROUP.getSelected()) {
case "A" -> {
return AsymetricKeysManagement.main(args, options = new Options().addOption(ASYMETRIC_OPTION));
}
case "E" -> {
return KEMAes.main(args, options = new Options().addOption(KEM_OPTION));
}
case "P" -> {
return PasswordBasedAes.main(args, options = new Options().addOption(AES_PSW_OPTION));
}
case "M" -> {
return MultiRecipientAes.main(args, options = new Options().addOption(MULTI_AES_OPTION));
}
case "K" -> {
return KeyStoreManagement.main(args, options = new Options().addOption(KEYSTORE_OPTION));
}
default -> {
return 1;
}
}
} catch (MissingOptionException ex) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, ex.getMessage());
}
return help(options);
} catch (ParseException ex) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Unexpected exception", ex.getMessage());
}
return help(options);
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
LOG.logp(Level.WARNING, "ZeroEcho", "mainProcess", e.getMessage(), e);
return -1;
} finally {
LOG.log(Level.INFO, "Completed.");
}
}
/**
* Prints the usage help message for the command-line interface of the ZeroEcho
* application.
*
* <p>
* This method automatically generates and displays a help statement based on
* the provided command-line {@link Options}. It then terminates the program
* with exit status {@code 1}.
*
* @param options The {@link Options} instance defining the available
* command-line options.
* @return always {@code 1}
*/
protected static int help(final Options options) {
// automatically generate the help statement
final HelpFormatter formatter = new HelpFormatter();
formatter.printHelp(ZeroEcho.class.getName(), options);
return 1;
}
}

View File

@@ -0,0 +1,208 @@
/*******************************************************************************
* 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;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.cert.X509Certificate;
import java.util.Date;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.BouncyCastleActivator;
class AsymetricKeysManagementTest {
static {
BouncyCastleActivator.init();
}
@TempDir
Path tempDir;
private static final String USERNAME = "realUser";
private static final String SUBJECT_DN = "CN=Real Subject, O=RealOrg";
@Test
void testIssueWithRealCSR() throws Exception {
System.out.println("Running testIssueWithRealCSR...");
Path csrFile = generateCSRFile(SUBJECT_DN);
String[] args = { "-d", tempDir.toString(), "-u", USERNAME, "-i", csrFile.toString() };
Options options = new Options();
AsymetricKeysManagement.main(args, options);
// Validate output (e.g., check that a certificate was generated) - it is
// printed out to STDOUT only for now
// Path certPath = tempDir.resolve(USERNAME + ".crt");
// assertTrue(Files.exists(certPath), "Certificate should be created");
System.out.println("...ok");
}
@Test
void testIssueWithGeneratedKeypair() throws Exception {
System.out.println("Running testIssueWithGeneratedKeypair...");
String[] args = { "-d", tempDir.toString(), "-u", USERNAME, "-s", SUBJECT_DN, "-i" };
Options options = new Options();
AsymetricKeysManagement.main(args, options);
// Validate output (certificate and private key) - it is printed out to STDOUT
// only for now
// Path certPath = tempDir.resolve(USERNAME + ".crt");
// Path keyPath = tempDir.resolve(USERNAME + ".key");
// assertTrue(Files.exists(certPath), "Certificate should be created");
// assertTrue(Files.exists(keyPath), "Private key should be created");
System.out.println("...ok");
}
@Test
void testRevokeRealCertificate() throws Exception {
System.out.println("Running testRevokeRealCertificate...");
// Generate key pair and CSR within this test
KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
X500Name subject = new X500Name(SUBJECT_DN);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
PKCS10CertificationRequest csr = new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic())
.build(signer);
// Save CSR to file within tempDir
Path csrFile = tempDir.resolve("tempRevokeTest.csr");
try (JcaPEMWriter pemWriter = new JcaPEMWriter(Files.newBufferedWriter(csrFile))) {
pemWriter.writeObject(csr);
}
// Issue certificate
String[] issueArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-i", csrFile.toString() };
Options issueOptions = new Options();
AsymetricKeysManagement.main(issueArgs, issueOptions);
// Since ZeroEcho likely prints the cert but does not store it, we simulate
// loading it back
// Let's generate a self-signed cert to match the issued one (or ideally
// ZeroEcho should return it)
// Here we re-use the CSR to simulate a real certificate loading
X509Certificate issuedCert = generateSelfSignedCertificate(subject, keyPair);
// Save the issued cert to a temporary file
Path certFile = tempDir.resolve("tempIssuedCert.pem");
try (JcaPEMWriter pemWriter = new JcaPEMWriter(Files.newBufferedWriter(certFile))) {
pemWriter.writeObject(issuedCert);
}
// Now revoke the issued certificate
String[] revokeArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-r", certFile.toString() };
Options revokeOptions = new Options();
AsymetricKeysManagement.main(revokeArgs, revokeOptions);
System.out.println("...ok");
}
private static X509Certificate generateSelfSignedCertificate(X500Name subject, KeyPair keyPair) throws Exception {
long now = System.currentTimeMillis();
Date notBefore = new Date(now - 1000L * 60);
Date notAfter = new Date(now + (1000L * 60 * 60 * 24));
BigInteger serial = BigInteger.valueOf(now);
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(subject, serial, notBefore, notAfter,
subject, keyPair.getPublic());
return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME)
.getCertificate(certBuilder.build(contentSigner));
}
@Test
void testGetAllCertificates() throws Exception {
System.out.println("Running testGetAllCertificates...");
// Issue certificate first
String[] issueArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-s", SUBJECT_DN, "-i" };
AsymetricKeysManagement.main(issueArgs, new Options());
// Get all certificates for USERNAME
String[] getAllArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-a" };
AsymetricKeysManagement.main(getAllArgs, new Options());
// (Optional) You might want to capture System.out and validate contents
System.out.println("...ok");
}
@Test
void testMissingUsernameThrows() {
System.out.println("Running testMissingUsernameThrows...");
String[] args = { "-d", tempDir.toString(), "-s", SUBJECT_DN, "-i" };
Options options = new Options();
assertThrows(ParseException.class, () -> AsymetricKeysManagement.main(args, options));
System.out.println("...ok");
}
/**
* Helper: Generate a real CSR PEM file for testing.
*/
private Path generateCSRFile(String subjectDN) throws Exception {
KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
X500Name subject = new X500Name(subjectDN);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
PKCS10CertificationRequest csr = new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic())
.build(signer);
Path csrFile = tempDir.resolve("test.csr");
try (JcaPEMWriter pemWriter = new JcaPEMWriter(Files.newBufferedWriter(csrFile))) {
pemWriter.writeObject(csr);
}
return csrFile;
}
}

View File

@@ -0,0 +1,156 @@
/**
* 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;
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 static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.cli.Options;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.UniversalKeyStoreFile;
class KEMAesTest {
@TempDir
Path tempDir;
private Path keystorePath;
private UniversalKeyStoreFile keystore;
private static final int DATA_SIZE = 8 * 1024;
private static final int TOTAL_USERS = 10;
private Map<String, KeyPair> kemUsers;
@BeforeAll
static void initializeCrypto() {
BouncyCastleActivator.init();
}
@BeforeEach
void setupKeystore() throws Exception {
System.out.println();
keystorePath = tempDir.resolve("kem_test.keystore");
keystore = new UniversalKeyStoreFile(keystorePath);
kemUsers = new HashMap<>();
for (int i = 0; i < TOTAL_USERS; i++) {
String username = "user-" + i + "-" + UUID.randomUUID();
String[] args = { "-k", keystorePath.toString(), "-o", username, "-g", "-a",
KeyPairAlgorithm.KYBER_512.toString() };
int exit = KeyStoreManagement.main(args, new Options());
assertEquals(0, exit, "Key generation failed for " + username);
KeyPair kp = new KeyPair(keystore.loadPublicKey(username), keystore.loadPrivateKey(username));
kemUsers.put(username, kp);
}
}
@Test
void testKEMAesEncryptionDecryption() throws Exception {
System.out.println("testKEMAesEncryptionDecryption");
// Write random input
Path inputFile = tempDir.resolve("plain_input.dat");
byte[] inputContent = new byte[DATA_SIZE];
new Random().nextBytes(inputContent);
Files.write(inputFile, inputContent);
// Encrypt using the first user's public key
String encryptUser = kemUsers.keySet().iterator().next();
Path encryptedFile = tempDir.resolve("encrypted_output.dat");
System.out.println("Encrypting as recipient: " + encryptUser);
String[] encryptArgs = { "-e", inputFile.toString(), "-k", keystorePath.toString(), "-p", encryptUser, "-o",
encryptedFile.toString() };
int encryptExit = KEMAes.main(encryptArgs, new Options());
assertEquals(0, encryptExit, "Encryption failed");
assertTrue(Files.exists(encryptedFile), "Encrypted file not created");
// Decrypt using corresponding private key
Path decryptedFile = tempDir.resolve("decrypted_output.dat");
String[] decryptArgs = { "-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s", encryptUser, "-o",
decryptedFile.toString() };
System.out.println("Decrypting as recipient: " + encryptUser);
int decryptExit = KEMAes.main(decryptArgs, new Options());
assertEquals(0, decryptExit, "Decryption failed");
assertTrue(Files.exists(decryptedFile), "Decrypted file not created");
byte[] decrypted = Files.readAllBytes(decryptedFile);
assertArrayEquals(inputContent, decrypted, "Decrypted content does not match");
// Attempt decryption with wrong private key (should fail)
List<String> others = kemUsers.keySet().stream().filter(k -> !k.equals(encryptUser))
.collect(Collectors.toList());
assertFalse(others.isEmpty(), "No alternative user for invalid decryption test");
for (String wrongUser : others) {
Path wrongOutput = tempDir.resolve("wrong_decrypt_output.dat");
String[] wrongDecryptArgs = { "-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s",
wrongUser, "-o", wrongOutput.toString() };
System.out.println("Attempting decryption as wrong user: " + wrongUser);
int wrongDecryptExit = KEMAes.main(wrongDecryptArgs, new Options());
assertEquals(1, wrongDecryptExit, "Expected decryption failure with wrong key");
}
System.out.println("...test completed");
}
}

View File

@@ -0,0 +1,130 @@
/**
* 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;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.nio.file.Path;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.cli.Options;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.CryptoAlgorithmsNames;
import zeroecho.util.UniversalKeyStoreFile;
class KeyStoreManagementTest {
@TempDir
Path tempDir;
private static final Map<String, KeyPairAlgorithm> ownerToAlgorithm = new HashMap<>();
@BeforeAll
public static void setupCryptoProvider() {
BouncyCastleActivator.init();
}
@Test
public void testGenerateAndVerifyAllKeyPairAlgorithms() throws Exception {
System.out.println("testGenerateAndVerifyAllKeyPairAlgorithms");
Path keystorePath = tempDir.resolve("test-keystore.txt");
List<String> argsList = new ArrayList<>();
Options options = new Options();
for (KeyPairAlgorithm algorithm : KeyPairAlgorithm.SERIALIZABLE_ALGORITHMS) {
if (algorithm == KeyPairAlgorithm.ELGAMAL_2048) {
// too slow => skip over
continue;
}
if (algorithm.getAlgorithmName().equals(CryptoAlgorithmsNames.FRODO.displayName())) {
// unsupported
continue;
}
String owner = "owner_" + UUID.randomUUID();
ownerToAlgorithm.put(owner, algorithm);
System.out.println("...generate new user " + owner + " with " + algorithm.toString());
argsList.clear();
argsList.add("--keystore");
argsList.add(keystorePath.toString());
argsList.add("--owner");
argsList.add(owner);
argsList.add("--generate");
argsList.add("--algorithm");
argsList.add(algorithm.toString());
int result = KeyStoreManagement.main(argsList.toArray(new String[0]), options);
assertEquals(0, result, "KeyStoreManagement should return 0 for success");
}
// Verify contents of keystore
UniversalKeyStoreFile keystore = new UniversalKeyStoreFile(keystorePath);
for (Map.Entry<String, KeyPairAlgorithm> entry : ownerToAlgorithm.entrySet()) {
String owner = entry.getKey();
System.out.println("...loading keys for " + owner + " algorithm(" + entry.getValue() + ")");
PublicKey publicKey = keystore.loadPublicKey(owner);
PrivateKey privateKey = keystore.loadPrivateKey(owner);
assertNotNull(publicKey, "Public key should be present for " + owner);
assertNotNull(privateKey, "Private key should be present for " + owner);
// Optional: Check algorithm consistency
assertEquals(entry.getValue().getAlgorithmName(), CryptoAlgorithmsNames.fromString(publicKey.getAlgorithm()).displayName(),
"Public key algorithm mismatch for " + owner);
assertEquals(entry.getValue().getAlgorithmName(), CryptoAlgorithmsNames.fromString(privateKey.getAlgorithm()).displayName(),
"Private key algorithm mismatch for " + owner);
}
System.out.println("...ok");
}
}

View File

@@ -0,0 +1,203 @@
/**
* 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;
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 static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.cli.Options;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.UniversalKeyStoreFile;
class MultiRecipientAesTest {
@TempDir
Path tempDir;
private Path keystorePath;
private UniversalKeyStoreFile keystore;
private static final int DATA_SIZE = 16 * 1024;
private static final int TOTAL_OWNERS = 20;
private static final int RECIPIENTS_COUNT = 10;
private Map<String, KeyPair> ownerKeyPairs;
@BeforeAll
public static void setupCryptoProvider() {
BouncyCastleActivator.init();
}
@BeforeEach
void setupKeystore() throws Exception {
System.out.println("");
keystorePath = tempDir.resolve("test.keystore");
keystore = new UniversalKeyStoreFile(keystorePath);
ownerKeyPairs = new HashMap<>();
// Use KeyStoreManagement to generate 20 RSA:2048 key pairs for random owners
Options options = new Options();
for (int i = 0; i < TOTAL_OWNERS; i++) {
String owner = "owner-" + i + "-" + UUID.randomUUID();
System.out.println("");
String[] args = { "-k", keystorePath.toString(), "-o", owner, "-g", "-a", "RSA:2048" };
int exitCode = KeyStoreManagement.main(args, options);
assertEquals(0, exitCode, "Key generation failed for owner " + owner);
// Reload keys after each addition
KeyPair kp = new KeyPair(keystore.loadPublicKey(owner), keystore.loadPrivateKey(owner));
ownerKeyPairs.put(owner, kp);
}
System.out.println("");
}
@Test
void testEncryptDecryptWithMultiRecipientAes() throws Exception {
System.out.println("testEncryptDecryptWithMultiRecipientAes");
// Prepare test file with random data
Path inputFile = tempDir.resolve("plaintext.dat");
byte[] originalContent = new byte[DATA_SIZE]; // 1 KB random data
System.out.println("...preparing test data, size=" + DATA_SIZE + " bytes");
new Random().nextBytes(originalContent);
Files.write(inputFile, originalContent);
// Pick 10 random owners for encryption recipients
List<String> allOwners = new ArrayList<>(ownerKeyPairs.keySet());
Collections.shuffle(allOwners);
List<String> recipientOwners = allOwners.subList(0, RECIPIENTS_COUNT);
System.out.println("...encrypting for " + RECIPIENTS_COUNT + " recipients");
// Encrypt with public keys of the 10 recipients
Path encryptedFile = tempDir.resolve("encrypted.dat");
List<String> encryptArgs = new ArrayList<>();
encryptArgs.add("-e");
encryptArgs.add(inputFile.toString());
encryptArgs.add("-k");
encryptArgs.add(keystorePath.toString());
encryptArgs.add("-p");
encryptArgs.addAll(recipientOwners);
encryptArgs.add("-o");
encryptArgs.add(encryptedFile.toString());
int encryptExit = MultiRecipientAes.main(encryptArgs.toArray(new String[0]), new Options());
assertEquals(0, encryptExit, "Encryption failed");
assertTrue(Files.exists(encryptedFile), "Encrypted file not created");
// Pick one owner from recipients to decrypt
String decryptOwner = recipientOwners.get(0);
Path decryptedFile = tempDir.resolve("decrypted.dat");
System.out.println("...testing decryption for the user " + decryptOwner);
List<String> decryptArgs = List.of("-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s",
decryptOwner, "-o", decryptedFile.toString());
int decryptExit = MultiRecipientAes.main(decryptArgs.toArray(new String[0]), new Options());
assertEquals(0, decryptExit, "Decryption failed for recipient owner");
System.out.println("...verifying decrypted content matches original");
// Verify decrypted content matches original
byte[] decryptedContent = Files.readAllBytes(decryptedFile);
assertArrayEquals(originalContent, decryptedContent, "Decrypted content does not match original");
// Pick owner NOT in recipients and test that decryption produces incorrect
// output or fails
List<String> nonRecipients = allOwners.stream().filter(o -> !recipientOwners.contains(o))
.collect(Collectors.toList());
assertFalse(nonRecipients.isEmpty(), "No non-recipient owners to test");
String nonRecipientOwner = nonRecipients.get(0);
Path decryptedWrongFile = tempDir.resolve("decrypted_wrong.dat");
System.out.println("...testing whether decryption fails for the user " + nonRecipientOwner
+ " who was not amongst recipients");
List<String> decryptWrongArgs = List.of("-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s",
nonRecipientOwner, "-o", decryptedWrongFile.toString());
int wrongDecryptExit = MultiRecipientAes.main(decryptWrongArgs.toArray(new String[0]), new Options());
// Decryption may or may not fail gracefully, but output should not match
// original content
assertEquals(1, wrongDecryptExit, "Decryption with non-recipient owner failed unexpectedly");
// The decryptedWrongFile should NOT exist because decryption failed (no output
// file created)
assertFalse(Files.exists(decryptedWrongFile), "Decrypted file should NOT exist for non-recipient owner");
// Trying to read should throw NoSuchFileException
assertThrows(NoSuchFileException.class, () -> {
Files.readAllBytes(decryptedWrongFile);
});
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;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.cli.Options;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.BouncyCastleActivator;
/**
* Unit tests for {@link PasswordBasedAes} using its static CLI-based main
* method.
*/
class PasswordBasedAesTest {
static {
BouncyCastleActivator.init();
}
@TempDir
Path tempDir;
private static final String PASSWORD = "secure-password";
/**
* Tests that encryption followed by decryption restores the original plain
* text.
*/
@Test
void testEncryptAndDecryptShortText() throws Exception {
String content = "This is a simple secret message.";
Path inputFile = writeTempFile("short.txt", content);
Path encryptedFile = tempDir.resolve("short.txt.enc");
Path decryptedFile = tempDir.resolve("short.txt.dec");
runEncryption(inputFile, encryptedFile);
runDecryption(encryptedFile, decryptedFile);
String decrypted = Files.readString(decryptedFile, StandardCharsets.UTF_8);
assertEquals(content, decrypted.trim());
}
/**
* Tests encryption/decryption of a longer file to ensure streaming works.
*/
@Test
void testEncryptAndDecryptLongFile() throws Exception {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
builder.append("Line ").append(i).append(": This is a long test line.\n");
}
String longText = builder.toString();
Path inputFile = writeTempFile("long.txt", longText);
Path encryptedFile = tempDir.resolve("long.txt.enc");
Path decryptedFile = tempDir.resolve("long.txt.dec");
runEncryption(inputFile, encryptedFile);
runDecryption(encryptedFile, decryptedFile);
String decrypted = Files.readString(decryptedFile, StandardCharsets.UTF_8);
assertEquals(longText, decrypted);
}
/**
* Ensures that a nonexistent input file causes no crash (is caught internally).
*/
@Test
void testNonexistentInputHandledGracefully() {
String[] args = { "-e", "nonexistent.txt", "-p", PASSWORD, "-o", "out.bin" };
assertDoesNotThrow(() -> PasswordBasedAes.main(args, new Options()));
}
// --- Helper methods ---
private Path writeTempFile(String name, String content) throws IOException {
Path file = tempDir.resolve(name);
Files.writeString(file, content, StandardCharsets.UTF_8);
return file;
}
private void runEncryption(Path input, Path output) throws Exception {
String[] args = { "-e", input.toString(), "-p", PASSWORD, "-o", output.toString() };
PasswordBasedAes.main(args, new Options());
assertTrue(Files.exists(output));
}
private void runDecryption(Path input, Path output) throws Exception {
String[] args = { "-d", input.toString(), "-p", PASSWORD, "-o", output.toString() };
PasswordBasedAes.main(args, new Options());
assertTrue(Files.exists(output));
}
}

View File

@@ -0,0 +1,74 @@
/*******************************************************************************
* 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;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class ZeroEchoTest {
@Test
void testAsymetricOptionWithoutParamsReturnsOne() {
System.out.println("testAsymetricOptionWithoutParamsReturnsOne");
int result = ZeroEcho.mainProcess(new String[] { "-A" });
assertEquals(1, result, "Asymetric option without parameters should return 1 (error/help)");
System.out.println("...ok");
}
@Test
void testAesPswOptionWithoutParamsReturnsOne() {
System.out.println("testAesPswOptionWithoutParamsReturnsOne");
int result = ZeroEcho.mainProcess(new String[] { "-P" });
assertEquals(1, result, "AES-PSW option without parameters should return 1 (error/help)");
System.out.println("...ok");
}
@Test
void testNoOptionReturnsOne() {
System.out.println("testNoOptionReturnsOne");
int result = ZeroEcho.mainProcess(new String[] {});
assertEquals(1, result, "No options should return 1 (error/help)");
System.out.println("...ok");
}
@Test
void testInvalidOptionReturnsOne() {
System.out.println("testInvalidOptionReturnsOne");
int result = ZeroEcho.mainProcess(new String[] { "-X" });
assertEquals(1, result, "Invalid option should return 1 (error/help)");
System.out.println("...ok");
}
}