Initial commit
This commit is contained in:
19
app/.classpath
Normal file
19
app/.classpath
Normal 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
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/app/
|
||||
23
app/.project
Normal file
23
app/.project
Normal 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
31
app/LICENSE
Normal 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
52
app/build.gradle
Normal 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
|
||||
|
||||
}
|
||||
235
app/src/main/java/zeroecho/AsymetricKeysManagement.java
Normal file
235
app/src/main/java/zeroecho/AsymetricKeysManagement.java
Normal 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;
|
||||
}
|
||||
}
|
||||
274
app/src/main/java/zeroecho/KEMAes.java
Normal file
274
app/src/main/java/zeroecho/KEMAes.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
186
app/src/main/java/zeroecho/KeyStoreManagement.java
Normal file
186
app/src/main/java/zeroecho/KeyStoreManagement.java
Normal 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;
|
||||
}
|
||||
}
|
||||
361
app/src/main/java/zeroecho/MultiRecipientAes.java
Normal file
361
app/src/main/java/zeroecho/MultiRecipientAes.java
Normal 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 <inputFile></b>: File to encrypt.</li>
|
||||
* <li><b>-d / --decrypt <inputFile></b>: File to decrypt.</li>
|
||||
* <li><b>-k / --keystore <keystoreFile></b>: Path to keystore file
|
||||
* (required).</li>
|
||||
* <li><b>-p / --pubkeys <usernames></b>: List of recipient usernames for
|
||||
* encryption.</li>
|
||||
* <li><b>-s / --privkey <username></b>: Username with private key for
|
||||
* decryption.</li>
|
||||
* <li><b>-o / --output <outputFile></b>: Output file path (optional;
|
||||
* defaults to inputFile.enc or inputFile.dec).</li>
|
||||
* <li><b>-m / --mode <aesMode></b>: AES mode (AES-128, AES-192, AES-256).
|
||||
* Default is AES-256.</li>
|
||||
* <li><b>-c / --cipher <cipherType></b>: Cipher transformation
|
||||
* (AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding). Default is
|
||||
* AES/CBC/PKCS7Padding.</li>
|
||||
* <li><b>-x / --decoys <algorithms></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;
|
||||
}
|
||||
}
|
||||
294
app/src/main/java/zeroecho/PasswordBasedAes.java
Normal file
294
app/src/main/java/zeroecho/PasswordBasedAes.java
Normal 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 <inputFile></b>: Specifies the file to
|
||||
* encrypt.</li>
|
||||
* <li><b>-d / --decrypt <inputFile></b>: Specifies the file to
|
||||
* decrypt.</li>
|
||||
* <li><b>-p / --password <password></b>: Password for
|
||||
* encryption/decryption.</li>
|
||||
* <li><b>-o / --output <outputFile></b>: Output file path (optional,
|
||||
* defaults to input file with .enc or .dec extension).</li>
|
||||
* <li><b>-m / --mode <aesMode></b>: AES key size mode (AES-128, AES-192,
|
||||
* AES-256). Defaults to AES-256.</li>
|
||||
* <li><b>-c / --cipher <cipherType></b>: Cipher transformation
|
||||
* (AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding). Defaults to
|
||||
* AES/CBC/PKCS7Padding.</li>
|
||||
* <li><b>-n / --iterations <iterations></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);
|
||||
}
|
||||
}
|
||||
}
|
||||
208
app/src/main/java/zeroecho/ZeroEcho.java
Normal file
208
app/src/main/java/zeroecho/ZeroEcho.java
Normal 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:04 PM
|
||||
/**
|
||||
* 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:25 PM
|
||||
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;
|
||||
}
|
||||
}
|
||||
208
app/src/test/java/zeroecho/AsymetricKeysManagementTest.java
Normal file
208
app/src/test/java/zeroecho/AsymetricKeysManagementTest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
156
app/src/test/java/zeroecho/KEMAesTest.java
Normal file
156
app/src/test/java/zeroecho/KEMAesTest.java
Normal 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");
|
||||
}
|
||||
}
|
||||
130
app/src/test/java/zeroecho/KeyStoreManagementTest.java
Normal file
130
app/src/test/java/zeroecho/KeyStoreManagementTest.java
Normal 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");
|
||||
}
|
||||
}
|
||||
203
app/src/test/java/zeroecho/MultiRecipientAesTest.java
Normal file
203
app/src/test/java/zeroecho/MultiRecipientAesTest.java
Normal 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");
|
||||
}
|
||||
}
|
||||
135
app/src/test/java/zeroecho/PasswordBasedAesTest.java
Normal file
135
app/src/test/java/zeroecho/PasswordBasedAesTest.java
Normal 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));
|
||||
}
|
||||
}
|
||||
74
app/src/test/java/zeroecho/ZeroEchoTest.java
Normal file
74
app/src/test/java/zeroecho/ZeroEchoTest.java
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user