commit 2cc988925ae4903076ea46458269b5159a330dea Author: Leo Galambos Date: Tue Sep 16 23:14:24 2025 +0200 Initial commit (history reset) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f91f646 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary + diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..dcb6148 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,92 @@ +name: Release + +on: + push: + tags: + - 'release@*' + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Java 21 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 21 + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Setup SSH key and rsync + run: | + mkdir -p ~/.ssh + echo "${{ secrets.JAVADOC_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + echo "${{ secrets.CI_KNOWN_HOSTS }}" > ~/.ssh/known_hosts + chmod -R 600 ~/.ssh + rm /etc/apt/sources.list.d/microsoft-prod.list + apt-get update + apt install -y rsync + + - name: Build and publish to Gitea Maven and JavaDoc to the website + run: ./gradlew clean publish uploadJavadoc --no-daemon -PgiteaToken=${{ secrets.CI_PUBLISH_TOKEN }} -PjavadocUser=${{ vars.JAVADOC_USER }} -PjavadocHost=${{ vars.JAVADOC_HOST }} -PjavadocPath=${{ vars.JAVADOC_PATH }} -PjavadocKeyPath=~/.ssh/id_rsa + + - name: Generate release notes + id: notes + run: | + current_tag="${{ github.ref_name }}" + + # strip the prefix for sorting, keep prefix for matching + prefix="release@" + + # get all matching tags, strip prefix, sort them + all_versions=$(git tag --list "${prefix}*" | sed "s/^${prefix}//" | sort -V) + + # find previous version + previous_tag="" + for v in $all_versions; do + if [[ "$prefix$v" == "$current_tag" ]]; then + break + fi + previous_tag="$prefix$v" + done + + if [[ -z "$previous_tag" ]]; then + range="" + else + range="$previous_tag..$current_tag" + fi + + echo "Comparing range: $range" + + body="## What's New" + + for category in "feat: Features" "fix: Bug Fixes" "docs: Documentation" "chore: Chores"; do + prefix="${category%%:*}" + title="${category##*: }" + entries=$(git log $range --pretty=format:"%B" --no-merges | ( grep "^${prefix}" || true ) | sed "s/^${prefix}:/- /") + if [[ -n "$entries" ]]; then + body="$body\n\n### $title\n$entries" + fi + done + + echo -e "$body" > /tmp/release_notes.md + + - name: Create Gitea Release + uses: softprops/action-gh-release@v2 + with: + files: app/build/libs/*.jar + body_path: /tmp/release_notes.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5df6a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,109 @@ +##---------------------------------------------------------------------------------------- Java + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +##---------------------------------------------------------------------------------------- Eclipse + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +# PMD plugin conf +.pmd + +##---------------------------------------------------------------------------------------- Gradle +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + + +# Ignore Gradle build output directory +build diff --git a/.project b/.project new file mode 100644 index 0000000..96646f7 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + ZeroEcho + + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + net.sourceforge.pmd.eclipse.plugin.pmdBuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + net.sourceforge.pmd.eclipse.plugin.pmdNature + + diff --git a/.ruleset b/.ruleset new file mode 100644 index 0000000..ef6e7d9 --- /dev/null +++ b/.ruleset @@ -0,0 +1,356 @@ + + + Egothor preferences rule set + .*/package-info\.java + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2008568 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright (C) 2024, Leo Galambos +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. All advertising materials mentioning features or use of this software must + display the following acknowledgement: + This product includes software developed by the Egothor project. + +4. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6220aa2 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# ZeroEcho + + + +*No Signal Is Ever Truly Silent: A Modular Toolkit for Covert, Resilient, and Future-Proof Communication* + +**ZeroEcho** is a modular cryptographic toolkit designed to support secure, scriptable, and resilient data workflows — even in low-connectivity, offline, or constrained environments. Built with flexibility in mind, it enables developers, researchers, and advanced users to construct encryption pipelines using modern cryptographic algorithms and robust deployment models. + +Whether you're working on secure file storage, encrypted communication, or privacy-focused data exchange, ZeroEcho offers a toolkit to build reliable, modern cryptographic workflows — with a focus on portability, future-proofing, and offline survivability. + +**Key Features** + +- Post-Quantum Cryptography (PQC) + - Supports NIST-standardized algorithms like ML-KEM (Kyber) + - Includes signature schemes such as SPHINCS+ for long-term integrity protection + - Modular structure allows easy integration of additional post-quantum providers + - Designed to protect against future quantum-based attacks + +- Classic Cryptography + - Full support for proven algorithms such as RSA, ECDSA, and Ed25519 + - Works seamlessly alongside PQC for hybrid deployments + - Flexible signing and verification APIs for modern workflows + +- Multi-Recipient Encryption and KEM + - Encrypt a single payload for multiple recipients, each with their own key + - No shared secrets or central authority required + - Optional decoy data streams to enhance confidentiality and recipient privacy + +- Offline and Indirect Deployment Workflows + - Generate encrypted payloads entirely offline, suitable for air-gapped environments + - Transfer via physical media (USB, SD card, etc.) with no online exposure + - Optional script generation to automate upload or staging to public endpoints + (cloud drives, pastebins, file hosts, etc.) + - Enables asynchronous or indirect delivery models where sender and receiver never + need to be online at the same time + +- Steganographic Embedding (Incubator) + - Optionally embed ciphertext into common media formats: images, audio, video + - Enables discreet transport of encrypted data over everyday channels + - Currently in transition: may evolve into a standalone pluggable project + +- CLI Tools & Keystore Management + + +## Development Status + +ZeroEcho is stable and actively maintained. +Some subcomponents (such as steganography and script export) are provided with basic functionality while being moved out of the incubator stage. It is under evaluation whether they will remain integrated in the core library or become separate pluggable projects. + +The core cryptographic engine, context model, and CLI tools are production-ready, with a strong focus on **security, robustness, and maintainability**. + +## Documentation + +- [API is documented via Javadoc](https://www.egothor.org/javadoc/zeroecho/lib/) and complemented by [CLI usage examples](https://www.egothor.org/javadoc/zeroecho/app/). +- Some Javadoc sections are still being updated after recent structural changes. + If you encounter **discrepancies in examples or descriptions**, we welcome feedback and contributions to ensure clarity and correctness. + + +## Ideal Use Cases + +- Building secure backup and archive workflows +- Sending encrypted messages across public platforms +- Creating educational or research-grade cryptographic tools +- Learning applied cryptography and encryption systems design +- Exploring secure data transport in offline or limited-access environments + +No deep programming experience is required to get started. Most features are available through intuitive CLI utilities and well-documented examples. + +--- + +**🧭 Responsible Use Notice:** ZeroEcho is intended for lawful and ethical use only. +It is designed to support privacy, secure communication, academic research, and freedom of expression. The author does **not condone or support** the use of this software for any illegal or malicious activities, including but not limited to data theft, unauthorized surveillance evasion, or digital espionage. + +Users are responsible for complying with all applicable laws and regulations in their jurisdiction. This is a tool for empowerment, not exploitation. diff --git a/ZeroEcho-logo.png b/ZeroEcho-logo.png new file mode 100644 index 0000000..993a505 Binary files /dev/null and b/ZeroEcho-logo.png differ diff --git a/ZeroEcho-logo.svg b/ZeroEcho-logo.svg new file mode 100644 index 0000000..a530529 --- /dev/null +++ b/ZeroEcho-logo.svg @@ -0,0 +1,46 @@ + + + + diff --git a/app/.classpath b/app/.classpath new file mode 100644 index 0000000..f6df624 --- /dev/null +++ b/app/.classpath @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..f13ae4e --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/app/ diff --git a/app/.project b/app/.project new file mode 100644 index 0000000..62349b5 --- /dev/null +++ b/app/.project @@ -0,0 +1,29 @@ + + + app + Project app created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + net.sourceforge.pmd.eclipse.plugin.pmdBuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + net.sourceforge.pmd.eclipse.plugin.pmdNature + + diff --git a/app/LICENSE b/app/LICENSE new file mode 100644 index 0000000..208140a --- /dev/null +++ b/app/LICENSE @@ -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. diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..57661d0 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,59 @@ +plugins { + id 'buildlogic.java-application-conventions' + id 'com.palantir.git-version' +} + +group 'org.egothor' + +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}" + ) + } + + from sourceSets.main.output + + dependsOn configurations.runtimeClasspath + + // Include each JAR dependency + configurations.runtimeClasspath.findAll { it.exists() && it.name.endsWith('.jar') }.each { jarFile -> + def jarName = jarFile.name.replaceAll(/\.jar$/, '') + + from(zipTree(jarFile)) { + // Exclude signature-related files + exclude 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA' + + // Rename license/notice files to avoid conflicts + eachFile { file -> + if (file.path ==~ /META-INF\/(LICENSE|NOTICE)(\..*)?/) { + file.path = "META-INF/licenses-from-${jarName}/${file.name}" + } + } + + includeEmptyDirs = false + } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +javadoc { + options.links("https://www.egothor.org/javadoc/zeroecho/lib") +} + diff --git a/app/src/main/java/zeroecho/CovertCommand.java b/app/src/main/java/zeroecho/CovertCommand.java new file mode 100644 index 0000000..2e584a8 --- /dev/null +++ b/app/src/main/java/zeroecho/CovertCommand.java @@ -0,0 +1,194 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho; + +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.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +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.sdk.integrations.covert.jpeg.JpegExifEmbedder; +import zeroecho.sdk.integrations.covert.jpeg.Slot; + +/** + * Command-line extension of ZeroEcho for covert embedding and extraction of + * binary payloads in JPEG files using EXIF metadata slots. + * + *

+ * This extension operates in two primary modes: + *

+ * + * + *

+ * The embedding process preserves all original metadata except for the specific + * slots being reused. Slot selection can be customized, or a default set of + * predefined high-capacity EXIF fields will be used automatically. + *

+ * + *

+ * The following command-line options are supported: + *

+ * + * + *

+ * If {@code --slots} is omitted, a default list of slots is used with realistic + * capacities suitable for several kilobytes of covert data. Slots are defined + * using EXIF field tags grouped by logical metadata regions (e.g., IFD0, Exif + * IFD, GPS IFD). + *

+ * + *

+ * Text-based EXIF slots (e.g., ASCII) will automatically encode binary payloads + * using Base64. Extraction decodes them back transparently. + *

+ * + *

+ * This class is not meant to be instantiated. + *

+ * + * @author Leo Galambos + */ +public final class CovertCommand { + private CovertCommand() { + } + + /** + * Entry point for command-line usage of the covert embedding and extraction + * tool. + *

+ * Parses command-line arguments to embed or extract binary payloads in JPEG + * files using EXIF metadata slots. Embedding requires a payload file, a JPEG + * input, and an output destination. Extraction requires only the JPEG input and + * output file. Slot behavior can be customized using semicolon-separated slot + * specifications. + *

+ * + * @param args Command-line arguments + * @param options An {@link Options} instance to populate with supported CLI + * options + * @return {@code 0} on success, {@code 1} on I/O error + * @throws ParseException if the arguments are invalid or incomplete + */ + public static int main(String[] args, Options options) throws ParseException { + final Option EMBED_OPTION = Option.builder().longOpt("embed").desc("Embed a payload into a JPEG").build(); + final Option EXTRACT_OPTION = Option.builder().longOpt("extract").desc("Extract a payload from a JPEG").build(); + final Option JPEG_OPTION = Option.builder().longOpt("jpeg").hasArg().argName("input.jpg") + .desc("Input JPEG file").required().build(); + final Option PAYLOAD_OPTION = Option.builder().longOpt("payload").hasArg().argName("payload.dat") + .desc("Binary payload file to embed").build(); + final Option OUTPUT_OPTION = Option.builder().longOpt("output").hasArg().argName("outputFile") + .desc("Output JPEG or payload file").required().build(); + final Option SLOTS_OPTION = Option.builder().longOpt("slots").hasArgs().valueSeparator(';') + .argName("slot1;slot2;...") + .desc("Custom EXIF slots (e.g. Exif.UserComment:4096;Exif.Custom/tag=700,ascii,64,exif:1024)").build(); + + OptionGroup modeGroup = new OptionGroup(); + modeGroup.addOption(EMBED_OPTION); + modeGroup.addOption(EXTRACT_OPTION); + modeGroup.setRequired(true); + + options.addOptionGroup(modeGroup); + options.addOption(JPEG_OPTION); + options.addOption(PAYLOAD_OPTION); + options.addOption(OUTPUT_OPTION); + options.addOption(SLOTS_OPTION); + + CommandLineParser parser = new DefaultParser(); + CommandLine cmd = parser.parse(options, args); + + Path jpegPath = Path.of(cmd.getOptionValue("jpeg")); + + List slots; + if (cmd.hasOption("slots")) { + slots = Arrays.stream(cmd.getOptionValues("slots")).map(Slot::parse).collect(Collectors.toList()); + } else { + slots = Slot.defaults(); + } + + JpegExifEmbedder processor = new JpegExifEmbedder(); + processor.setSlots(slots); + + try { + if (cmd.hasOption("embed")) { + if (!cmd.hasOption("payload")) { + throw new ParseException("--payload is required for embedding"); + } + try (InputStream payload = Files.newInputStream(Paths.get(cmd.getOptionValue("payload"))); + OutputStream output = Files.newOutputStream(Paths.get(cmd.getOptionValue("output")))) { + processor.embed(jpegPath, payload, output); + } + } else if (cmd.hasOption("extract")) { + try (OutputStream output = Files.newOutputStream(Paths.get(cmd.getOptionValue("output")))) { + processor.extract(jpegPath, output); + } + } + } catch (IOException e) { + System.err.println("I/O error: " + e.getMessage()); + return 1; + } + + return 0; + } +} diff --git a/app/src/main/java/zeroecho/Guard.java b/app/src/main/java/zeroecho/Guard.java new file mode 100644 index 0000000..ad7751a --- /dev/null +++ b/app/src/main/java/zeroecho/Guard.java @@ -0,0 +1,506 @@ +/******************************************************************************* + * 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.SecureRandom; +import java.util.HexFormat; +import java.util.Locale; + +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.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.KemContext; +import zeroecho.core.storage.KeyringStore; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder; +import zeroecho.sdk.builders.core.PlainFileBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.guard.MultiRecipientDataSourceBuilder; +import zeroecho.sdk.guard.UnlockMaterial; + +/** + * Guard is a unified subcommand that encrypts and decrypts using a + * multi-recipient envelope with AES or ChaCha payloads. + * + *

Overview

The class replaces the previous MultiRecipientAes and + * PasswordBasedAes CLIs by exposing a single entry point. It builds an envelope + * with a recipients table (real and decoy) via + * {@code MultiRecipientDataSourceBuilder}, then delegates the payload to either + * {@code AesDataContentBuilder} or {@code ChaChaDataContentBuilder}. + * + *

+ * Default behavior: + *

+ * + * + *

Keyring resolution

Recipient aliases and the unlocking alias are + * resolved from {@code KeyringStore}. The algorithm id stored in the keyring + * drives which recipient method to call, so the user does not have to specify + * the algorithm again. + * + *

Payload selection

Use {@code --alg} to choose one of: + * {@code aes-gcm}, {@code aes-ctr}, {@code aes-cbc-pkcs7}, + * {@code aes-cbc-nopad}, {@code chacha-aead}, {@code chacha-stream}. + * + *

Examples

{@code
+ * # Encrypt with KEM + RSA recipients, 2 decoy passwords, AES-GCM
+ * ZeroEcho -G --encrypt in.bin --ks keys.txt \
+ *   --to-alias alice --to-alias bob \
+ *   --decoy-psw-rand 2 \
+ *   --alg aes-gcm --tag-bits 128 --aad-hex 01ff
+ *
+ * # Decrypt with private key (payload is ChaCha20-Poly1305)
+ * ZeroEcho -G --decrypt blob.bin --ks keys.txt \
+ *   --priv-alias alice \
+ *   --alg chacha-aead
+ * }
+ */ +public final class Guard { + + private Guard() { + } + + /** + * Entry point for the Guard subcommand. It preserves Apache Commons CLI usage + * and the subcommand invocation pattern used by {@code ZeroEcho}. + * + * @param args command-line arguments + * @param options options object provided by the dispatcher; this method + * augments it + * @return exit code (0 = success, non-zero = failure) + * @throws ParseException on CLI parsing errors + * @throws IOException on I/O errors + * @throws GeneralSecurityException on cryptographic setup or keyring errors + */ + public static int main(final String[] args, final Options options) // NOPMD + throws ParseException, IOException, GeneralSecurityException { + + // ---- operation selection + final Option OPT_ENCRYPT = Option.builder("e").longOpt("encrypt").hasArg().argName("in-file") + .desc("Encrypt the given file").build(); + final Option OPT_DECRYPT = Option.builder("d").longOpt("decrypt").hasArg().argName("in-file") + .desc("Decrypt the given file").build(); + final OptionGroup OP = new OptionGroup().addOption(OPT_ENCRYPT).addOption(OPT_DECRYPT); + OP.setRequired(true); + + // ---- common I/O + final Option OPT_OUT = Option.builder("o").longOpt("output").hasArg().argName("out-file") + .desc("Output file (default: .enc for encrypt, .dec for decrypt)").build(); + final Option OPT_KEYRING = Option.builder().longOpt("keyring").hasArg().argName("keyring.txt") + .desc("KeyringStore file for aliases (required when aliases are used)").build(); + + // ---- payload selection and parameters + final Option OPT_ALG = Option.builder().longOpt("alg").hasArg().argName("name") + .desc("Payload: aes-gcm | aes-ctr | aes-cbc-pkcs7 | aes-cbc-nopad | chacha-aead | chacha-stream " + + "(default: aes-gcm)") + .build(); + final Option OPT_AAD_HEX = Option.builder("a").longOpt("aad-hex").hasArg().argName("hex") + .desc("Additional authenticated data as hex (AEAD modes)").build(); + final Option OPT_TAG_BITS = Option.builder().longOpt("tag-bits").hasArg().argName("96..128") + .desc("AES-GCM tag length in bits (default 128)").build(); + final Option OPT_NONCE_HEX = Option.builder().longOpt("nonce-hex").hasArg().argName("hex") + .desc("ChaCha nonce (12-byte hex)").build(); + final Option OPT_INIT_CTR = Option.builder().longOpt("init-ctr").hasArg().argName("int") + .desc("ChaCha stream initial counter (default 1)").build(); + final Option OPT_CTR = Option.builder().longOpt("ctr").hasArg().argName("int") + .desc("ChaCha stream counter override (propagated via context)").build(); + final Option OPT_NO_HDR = Option.builder().longOpt("no-header") + .desc("Do not write or expect a symmetric header").build(); + + // ---- envelope parameters + final Option OPT_CEK_BYTES = Option.builder().longOpt("cek-bytes").hasArg().argName("len") + .desc("Payload key (CEK) length in bytes (default 32)").build(); + final Option OPT_MAX_RECIPS = Option.builder().longOpt("max-recipients").hasArg().argName("n") + .desc("Max recipients in the envelope header (default 64)").build(); + final Option OPT_MAX_ENTRY = Option.builder().longOpt("max-entry-len").hasArg().argName("bytes") + .desc("Max single recipient-entry length (default 1048576)").build(); + final Option OPT_NO_SHUFFLE = Option.builder().longOpt("no-shuffle") + .desc("Disable shuffling of recipients (enabled by default)").build(); + + // ---- recipients (real) + final Option OPT_TO_ALIAS = Option.builder().longOpt("to-alias").hasArg().argName("alias") + .desc("Add recipient by alias from keyring (repeatable)").build(); + final Option OPT_TO_PSW = Option.builder().longOpt("to-psw").hasArg().argName("password") + .desc("Add password recipient (repeatable)").build(); + final Option OPT_PSW_ITER = Option.builder().longOpt("to-iter").hasArg().argName("n") + .desc("PBKDF2 iterations for password recipients (default 200000)").build(); + final Option OPT_PSW_SALT = Option.builder().longOpt("to-salt-len").hasArg().argName("bytes") + .desc("PBKDF2 salt length for password recipients (default 16)").build(); + final Option OPT_PSW_KEK = Option.builder().longOpt("to-kek-bytes").hasArg().argName("bytes") + .desc("Derived KEK length for password recipients (default 32)").build(); + + // ---- decoys (all types) + final Option OPT_DECOY_ALIAS = Option.builder().longOpt("decoy-alias").hasArg().argName("alias") + .desc("Add a decoy recipient from keyring (repeatable)").build(); + final Option OPT_DECOY_PSW = Option.builder().longOpt("decoy-psw").hasArg().argName("password") + .desc("Add a decoy password recipient (repeatable)").build(); + final Option OPT_DECOY_PSW_RAND = Option.builder().longOpt("decoy-psw-rand").hasArg().argName("n") + .desc("Add N random decoy password recipients").build(); + + // ---- unlock (decrypt) + final Option OPT_PRIV_ALIAS = Option.builder().longOpt("priv-alias").hasArg().argName("alias") + .desc("Unlock with private key from keyring").build(); + final Option OPT_PASSWORD = Option.builder("p").longOpt("password").hasArg().argName("password") + .desc("Unlock with password").build(); + + options.addOptionGroup(OP); + options.addOption(OPT_OUT); + options.addOption(OPT_KEYRING); + + options.addOption(OPT_ALG); + options.addOption(OPT_AAD_HEX); + options.addOption(OPT_TAG_BITS); + options.addOption(OPT_NONCE_HEX); + options.addOption(OPT_INIT_CTR); + options.addOption(OPT_CTR); + options.addOption(OPT_NO_HDR); + + options.addOption(OPT_CEK_BYTES); + options.addOption(OPT_MAX_RECIPS); + options.addOption(OPT_MAX_ENTRY); + options.addOption(OPT_NO_SHUFFLE); + + options.addOption(OPT_TO_ALIAS); + options.addOption(OPT_TO_PSW); + options.addOption(OPT_PSW_ITER); + options.addOption(OPT_PSW_SALT); + options.addOption(OPT_PSW_KEK); + + options.addOption(OPT_DECOY_ALIAS); + options.addOption(OPT_DECOY_PSW); + options.addOption(OPT_DECOY_PSW_RAND); + + options.addOption(OPT_PRIV_ALIAS); + options.addOption(OPT_PASSWORD); + + final CommandLineParser parser = new DefaultParser(); + final CommandLine cmd = parser.parse(options, args); + + final boolean encrypt = cmd.hasOption(OPT_ENCRYPT); + final Path inPath = Paths.get(cmd.getOptionValue(encrypt ? OPT_ENCRYPT : OPT_DECRYPT)); + final Path outPath = computeOutPath(cmd, inPath, encrypt); + + final String alg = cmd.getOptionValue(OPT_ALG, "aes-gcm").toLowerCase(Locale.ROOT); + + final byte[] aad = cmd.hasOption(OPT_AAD_HEX) ? parseHex(cmd.getOptionValue(OPT_AAD_HEX)) : null; + final int tagBits = Integer.parseInt(cmd.getOptionValue(OPT_TAG_BITS, "128")); + final byte[] chachaNonce = cmd.hasOption(OPT_NONCE_HEX) ? parseHex(cmd.getOptionValue(OPT_NONCE_HEX)) : null; + final Integer initCtr = cmd.hasOption(OPT_INIT_CTR) ? Integer.valueOf(cmd.getOptionValue(OPT_INIT_CTR)) : null; + final Integer ctrOverride = cmd.hasOption(OPT_CTR) ? Integer.valueOf(cmd.getOptionValue(OPT_CTR)) : null; + final boolean header = !cmd.hasOption(OPT_NO_HDR); + + final int cekBytes = Integer.parseInt(cmd.getOptionValue(OPT_CEK_BYTES, "32")); + final int maxRecipients = Integer.parseInt(cmd.getOptionValue(OPT_MAX_RECIPS, "64")); + final int maxEntryLen = Integer.parseInt(cmd.getOptionValue(OPT_MAX_ENTRY, "1048576")); + final boolean shuffle = !cmd.hasOption(OPT_NO_SHUFFLE); // default true + + // configure symmetric builder + final AesDataContentBuilder aes; + final ChaChaDataContentBuilder chacha; + switch (alg) { + case "aes-gcm" -> { + aes = AesDataContentBuilder.builder().modeGcm(tagBits); + if (header) { + aes.withHeader(); + } + if (aad != null) { + aes.withAad(aad); + } + chacha = null; + } + case "aes-ctr" -> { + aes = AesDataContentBuilder.builder().modeCtr(); + if (header) { + aes.withHeader(); + } + chacha = null; + } + case "aes-cbc-pkcs7" -> { + aes = AesDataContentBuilder.builder().modeCbcPkcs5(); + if (header) { + aes.withHeader(); + } + chacha = null; + } + case "aes-cbc-nopad" -> { + aes = AesDataContentBuilder.builder().modeCbcNoPadding(); + if (header) { + aes.withHeader(); + } + chacha = null; + } + case "chacha-aead" -> { + chacha = ChaChaDataContentBuilder.builder(); + // selecting AEAD: if the user did not supply AAD, pass empty to pick AEAD + chacha.withAad(aad != null ? aad : new byte[0]); + if (header) { + chacha.withHeader(); + } + if (chachaNonce != null) { + chacha.withNonce(chachaNonce); + } + if (ctrOverride != null || initCtr != null) { + // providing counters together with AAD would be conflicting; builder enforces + // it + if (initCtr != null) { + chacha.initialCounter(initCtr); + } + if (ctrOverride != null) { + chacha.withCounter(ctrOverride); + } + } + aes = null; + } + case "chacha-stream" -> { + chacha = ChaChaDataContentBuilder.builder(); + if (header) { + chacha.withHeader(); + } + if (chachaNonce != null) { + chacha.withNonce(chachaNonce); + } + if (initCtr != null) { + chacha.initialCounter(initCtr); + } + if (ctrOverride != null) { + chacha.withCounter(ctrOverride); + } + aes = null; + } + default -> throw new ParseException("Unknown --alg: " + alg); + } + + // envelope builder (new API) + final MultiRecipientDataSourceBuilder env = new MultiRecipientDataSourceBuilder().payloadKeyBytes(cekBytes) + .headerLimits(maxRecipients, maxEntryLen); + if (aes != null) { + env.withAes(aes); + } else { + env.withChaCha(chacha); + } + // shuffle on by default + if (shuffle) { + env.shuffle(); + } + // recipients and decoys only apply on encrypt + if (encrypt) { + final int iter = Integer.parseInt(cmd.getOptionValue(OPT_PSW_ITER, "200000")); + final int saltLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_SALT, "16")); + final int kekLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_KEK, "32")); + + final KeyringStore ks = loadKeyringIfPresent(cmd, OPT_KEYRING); + // real recipients by alias + for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0] + : cmd.getOptionValues(OPT_TO_ALIAS)) { + addRecipientFromAlias(env, ks, alias, kekLen, saltLen, false); + } + // real password recipients + for (String psw : cmd.getOptionValues(OPT_TO_PSW) == null ? new String[0] + : cmd.getOptionValues(OPT_TO_PSW)) { + env.addPasswordRecipient(psw.toCharArray(), iter, saltLen, kekLen); + } + // decoys by alias (key types) + for (String alias : cmd.getOptionValues(OPT_DECOY_ALIAS) == null ? new String[0] + : cmd.getOptionValues(OPT_DECOY_ALIAS)) { + addRecipientFromAlias(env, ks, alias, kekLen, saltLen, true); + } + // decoy passwords (explicit) + for (String psw : cmd.getOptionValues(OPT_DECOY_PSW) == null ? new String[0] + : cmd.getOptionValues(OPT_DECOY_PSW)) { + env.addPasswordRecipientDecoy(psw.toCharArray(), iter, saltLen, kekLen); + } + // decoy passwords (random) + final int rndCount = Integer.parseInt(cmd.getOptionValue(OPT_DECOY_PSW_RAND, "0")); + for (int i = 0; i < rndCount; i++) { + env.addPasswordRecipientDecoy(randomPassword(), iter, saltLen, kekLen); + } + } else { + // unlock material for decrypt + final String privAlias = cmd.getOptionValue(OPT_PRIV_ALIAS); + final String password = cmd.getOptionValue(OPT_PASSWORD); + + if ((privAlias == null && password == null) || (privAlias != null && password != null)) { + throw new ParseException("Specify exactly one of --priv-alias or --password for decryption"); + } + if (privAlias != null) { + final KeyringStore ks = requireKeyring(cmd, OPT_KEYRING); + final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias); + env.unlockWith(new UnlockMaterial.Private(pr.key())); + } else { + env.unlockWith(new UnlockMaterial.Password(password.toCharArray())); + } + } + + // connect upstream and run + final DataContent content = env.build(encrypt); // env installs default openers on decrypt if none were added + content.setInput(PlainFileBuilder.builder().url(inPath.toUri().toURL()).build(encrypt)); + try (InputStream in = content.getStream(); OutputStream out = Files.newOutputStream(outPath)) { + in.transferTo(out); + } + + return 0; + } + + // ------------------------------------------------------------------------- + // Helpers (package-private/private) + // ------------------------------------------------------------------------- + + private static Path computeOutPath(CommandLine cmd, Path inPath, boolean encrypt) { + if (cmd.hasOption("output")) { + return Paths.get(cmd.getOptionValue("output")); + } + final String s = inPath.toString(); + final String suffix = encrypt ? ".enc" : ".dec"; + return Paths.get(s + suffix); + } + + private static byte[] parseHex(String s) throws ParseException { + try { + return HexFormat.of().parseHex(s); + } catch (IllegalArgumentException ex) { + throw new ParseException("Bad hex: " + ex.getMessage()); // NOPMD + } + } + + /** + * Adds a recipient to the given multi-recipient encryption builder by resolving + * a public key from the keyring. + * + *

+ * The method looks up the alias in the {@link KeyringStore}, extracts its + * algorithm identifier and public key, and then attempts to create an + * appropriate cryptographic context: + *

+ *
    + *
  • KEM path: If the algorithm supports + * {@link zeroecho.core.context.KemContext} under + * {@link zeroecho.core.KeyUsage#ENCAPSULATE}, a key encapsulation recipient is + * added with the provided key-encryption key (KEK) size and salt length.
  • + *
  • Fallback path: If KEM encapsulation is not supported, the method + * falls back to {@link zeroecho.core.context.EncryptionContext} under + * {@link zeroecho.core.KeyUsage#ENCRYPT} for classic public-key + * encryption.
  • + *
+ * + *

+ * In both cases, the created context is consumed by + * {@link MultiRecipientDataSourceBuilder#addRecipient(Object)} and is closed + * internally by the builder. + *

+ * + * @param env target builder to which the recipient is added + * @param ks keyring store that provides public keys by alias + * @param alias alias name of the recipient's public key in the keyring + * @param kekBytes desired length in bytes of the key-encryption key when using + * KEM + * @param saltLen salt length in bytes when using KEM + * @param decoy whether the recipient is a decoy + * @throws GeneralSecurityException if the algorithm does not support the + * requested usage or key material is invalid + * @throws IOException if context creation or builder operations + * require I/O and fail + */ + private static void addRecipientFromAlias(MultiRecipientDataSourceBuilder env, KeyringStore ks, String alias, + int kekBytes, int saltLen, boolean decoy) throws GeneralSecurityException, IOException { + KeyringStore.PublicWithId r = ks.getPublicWithId(alias); + final String algId = r.algorithm(); + final java.security.PublicKey pub = r.key(); + + // Try KEM first + try (KemContext kem = CryptoAlgorithms.create(algId, KeyUsage.ENCAPSULATE, pub)) { + if (decoy) { + env.addRecipientDecoy(kem, kekBytes, saltLen); // builder closes context + } else { + env.addRecipient(kem, kekBytes, saltLen); // builder closes context + } + return; + } catch (Exception notKem) { // NOPMD + // fall back to public-key encryption + } + + try (EncryptionContext enc = CryptoAlgorithms.create(algId, KeyUsage.ENCRYPT, pub)) { + if (decoy) { + env.addRecipientDecoy(enc); // builder closes context + } else { + env.addRecipient(enc); // builder closes context + } + } + } + + private static char[] randomPassword() { + // simple random alnum for decoy purposes only + final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + final SecureRandom rnd = new SecureRandom(); + final int len = 16 + rnd.nextInt(17); // 16..32 + final char[] out = new char[len]; + for (int i = 0; i < len; i++) { + out[i] = alphabet.charAt(rnd.nextInt(alphabet.length())); + } + return out; + } + + private static KeyringStore loadKeyringIfPresent(CommandLine cmd, Option optKs) throws IOException { + if (!cmd.hasOption(optKs)) { + return new KeyringStore(); + } + return KeyringStore.load(Paths.get(cmd.getOptionValue(optKs))); + } + + private static KeyringStore requireKeyring(CommandLine cmd, Option optKs) throws IOException, ParseException { + if (!cmd.hasOption(optKs)) { + throw new ParseException("--keyring is required when aliases are used"); + } + return KeyringStore.load(Paths.get(cmd.getOptionValue(optKs))); + } +} diff --git a/app/src/main/java/zeroecho/Kem.java b/app/src/main/java/zeroecho/Kem.java new file mode 100644 index 0000000..a817ffb --- /dev/null +++ b/app/src/main/java/zeroecho/Kem.java @@ -0,0 +1,515 @@ +/******************************************************************************* + * 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.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.EnumSet; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +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.core.AlgorithmFamily; +import zeroecho.core.CatalogSelector; +import zeroecho.core.KeyUsage; +import zeroecho.core.storage.KeyringStore; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder; +import zeroecho.sdk.builders.alg.KemDataContentBuilder; +import zeroecho.sdk.builders.core.DataContentChainBuilder; +import zeroecho.sdk.builders.core.PlainFileBuilder; +import zeroecho.sdk.content.api.DataContent; + +/** + * Command-line utility that performs hybrid file encryption and decryption + * using a KEM envelope and an AES or ChaCha payload. + * + *

Overview

This tool encapsulates a symmetric key with a selected KEM + * (post-quantum friendly) and then applies a symmetric cipher to the payload. + * The symmetric stage is configured on {@code KemDataContentBuilder} and + * supports AES (GCM/CTR/CBC) and ChaCha (AEAD or stream) including AAD, + * IV/nonce, counters, and an optional compact header. Keys are loaded from + * {@code KeyringStore} by alias. Use {@code --list-kems} to discover valid KEM + * identifiers. + * + *

Examples

{@code
+ * # List available KEM identifiers
+ * ZeroEcho -E --list-kems
+ *
+ * # Encrypt with Kyber768 + AES-GCM (128-bit tag), writing a header
+ * ZeroEcho -E --encrypt in.bin -o out.bin --keyring keys.txt --pub alice-pub \
+ *   --kem ML-KEM --hkdf 5a45524f --key-bytes 32 --max-kem-ct 65536 \
+ *   --aes --aes-cipher gcm --aes-tag-bits 128 --aad DEADBEEF --header
+ *
+ * # Decrypt with the corresponding private key
+ * ZeroEcho -E --decrypt out.bin -o plain.bin --keyring keys.txt --priv alice-priv \
+ *   --kem ML-KEM --aes --aes-cipher gcm --aes-tag-bits 128 --header
+ *
+ * # Encrypt with ChaCha20-Poly1305 (AEAD) and header
+ * ZeroEcho -E --encrypt in.bin -o out.cc20p.bin --keyring keys.txt --pub alice-pub \
+ *   --kem ML-KEM --chacha --chacha-nonce 00112233445566778899AABB \
+ *   --aad 01020304 --header
+ *
+ * # Encrypt with ChaCha20 stream mode and explicit counters
+ * ZeroEcho -E --encrypt in.bin -o out.cc20.bin --keyring keys.txt --pub alice-pub \
+ *   --kem ML-KEM --chacha --chacha-nonce 00112233445566778899AABB \
+ *   --chacha-initial 1 --chacha-counter 00000007
+ * }
+ */ +public final class Kem { // NOPMD + private static final Logger LOG = Logger.getLogger(Kem.class.getName()); + + // --------------------------------------------------------------------- + // CLI option constants + // --------------------------------------------------------------------- + + /** Encrypt mode: -e|--encrypt <input> */ + public static final Option OPT_ENCRYPT = Option.builder("e").longOpt("encrypt").hasArg().argName("input") + .desc("Encrypt the input file").build(); + + /** Decrypt mode: -d|--decrypt <input> */ + public static final Option OPT_DECRYPT = Option.builder("d").longOpt("decrypt").hasArg().argName("input") + .desc("Decrypt the input file").build(); + + /** Output path: -o|--output <file> */ + public static final Option OPT_OUTPUT = Option.builder("o").longOpt("output").hasArg().argName("file") + .desc("Output file path (default: <input>.enc for encrypt, <input>.dec for decrypt)").build(); + + /** Keyring path: -K|--keyring <file> */ + public static final Option OPT_KEYRING = Option.builder("K").longOpt("keyring").hasArg().argName("file") + .desc("Path to KeyringStore file").build(); + + /** Recipient public alias (encrypt): --pub <alias> */ + public static final Option OPT_PUB = Option.builder().longOpt("pub").hasArg().argName("alias") + .desc("Recipient public key alias (encryption)").build(); + + /** Recipient private alias (decrypt): --priv <alias> */ + public static final Option OPT_PRIV = Option.builder().longOpt("priv").hasArg().argName("alias") + .desc("Recipient private key alias (decryption)").build(); + + /** KEM id: --kem <id> */ + public static final Option OPT_KEM = Option.builder().longOpt("kem").hasArg().argName("id") + .desc("KEM algorithm id (see --list-kems)").build(); + + /** Discovery: --list-kems */ + public static final Option OPT_LIST_KEMS = Option.builder().longOpt("list-kems") + .desc("List KEM algorithms that support ENCAPSULATE and DECAPSULATE and exit").build(); + + /** Payload switch: --aes */ + public static final Option OPT_AES = Option.builder().longOpt("aes") + .desc("Use AES payload (select mode via --aes-cipher)").build(); + + /** Payload switch: --chacha */ + public static final Option OPT_CHACHA = Option.builder().longOpt("chacha") + .desc("Use ChaCha payload (AEAD if --aad is provided, otherwise stream)").build(); + + /** AAD (hex): --aad <hex> */ + public static final Option OPT_AAD = Option.builder().longOpt("aad").hasArg().argName("hex") + .desc("Additional Authenticated Data (hex)").build(); + + /** Header toggle: --header */ + public static final Option OPT_HEADER = Option.builder().longOpt("header") + .desc("Write/read a compact symmetric header (IV/AAD/params) when supported").build(); + + /** HKDF: --hkdf [infoHex] */ + public static final Option OPT_HKDF = Option.builder().longOpt("hkdf").optionalArg(true).hasArg().argName("infoHex") + .desc("Use HKDF-SHA256 for KEM secret; optional info as hex (default internal info)").build(); + + /** Direct secret: --direct */ + public static final Option OPT_DIRECT = Option.builder().longOpt("direct") + .desc("Use the raw KEM shared secret directly (disable HKDF)").build(); + + /** Derived key bytes: --key-bytes <int> */ + public static final Option OPT_KEY_BYTES = Option.builder().longOpt("key-bytes").hasArg().argName("int") + .type(Number.class).desc("Derived symmetric key length in bytes (default 32)").build(); + + /** Max KEM ciphertext len: --max-kem-ct <int> */ + public static final Option OPT_MAX_KEM_CT = Option.builder().longOpt("max-kem-ct").hasArg().argName("int") + .type(Number.class).desc("Maximum accepted KEM ciphertext length in bytes (default 65536)").build(); + + /** AES mode: --aes-cipher gcm|ctr|cbc */ + public static final Option OPT_AES_CIPHER = Option.builder().longOpt("aes-cipher").hasArg().argName("gcm|ctr|cbc") + .desc("AES cipher variant for payload (default gcm)").build(); + + /** AES IV: --aes-iv <hex> */ + public static final Option OPT_AES_IV = Option.builder().longOpt("aes-iv").hasArg().argName("hex") + .desc("AES IV/nonce (hex)").build(); + + /** AES tag bits: --aes-tag-bits <int> */ + public static final Option OPT_AES_TAG_BITS = Option.builder().longOpt("aes-tag-bits").hasArg().argName("int") + .type(Number.class).desc("AES-GCM authentication tag length in bits (default 128)").build(); + + /** ChaCha nonce: --chacha-nonce <hex> */ + public static final Option OPT_CHACHA_NONCE = Option.builder().longOpt("chacha-nonce").hasArg().argName("hex") + .desc("ChaCha nonce (hex, usually 12 bytes)").build(); + + /** ChaCha counter value: --chacha-counter <int> */ + public static final Option OPT_CHACHA_COUNTER = Option.builder().longOpt("chacha-counter").hasArg().argName("int") + .type(Number.class).desc("ChaCha counter value for stream mode (integer)").build(); + + /** ChaCha initial counter: --chacha-initial <int> */ + public static final Option OPT_CHACHA_INITIAL = Option.builder().longOpt("chacha-initial").hasArg().argName("int") + .type(Number.class).desc("ChaCha initial counter (integer)").build(); + + private Kem() { + // no instances + } + + public static int main(String[] args, Options opts) throws ParseException, IOException, GeneralSecurityException { // NOPMD + defineOptions(opts); + CommandLineParser parser = new DefaultParser(); + CommandLine cmd = parser.parse(opts, args); + + // Fast path: listing does not require any other arguments (keyring, mode, etc.) + if (cmd.hasOption(OPT_LIST_KEMS.getLongOpt())) { + listKems(); + return 0; + } + + // Validate mode + boolean encrypt = cmd.hasOption(OPT_ENCRYPT.getLongOpt()) || cmd.hasOption(OPT_ENCRYPT.getOpt()); + boolean decrypt = cmd.hasOption(OPT_DECRYPT.getLongOpt()) || cmd.hasOption(OPT_DECRYPT.getOpt()); + if (encrypt == decrypt) { + throw new ParseException("Choose exactly one of --encrypt or --decrypt"); + } + + // Validate payload selection + boolean wantAes = cmd.hasOption(OPT_AES.getLongOpt()); + boolean wantChaCha = cmd.hasOption(OPT_CHACHA.getLongOpt()); + if (wantAes == wantChaCha) { + throw new ParseException("Choose exactly one payload cipher: --aes or --chacha"); + } + + // Required arguments for actual work + require(cmd, OPT_KEM, "Missing required option: --kem"); + require(cmd, OPT_KEYRING, "Missing required option: --keyring"); + + final String input = encrypt ? cmd.getOptionValue(OPT_ENCRYPT) : cmd.getOptionValue(OPT_DECRYPT); + if (input == null || input.isBlank()) { + throw new ParseException("Missing input file for the selected mode"); + } + + final Path output = Path.of(cmd.getOptionValue(OPT_OUTPUT.getLongOpt(), input + (encrypt ? ".enc" : ".dec"))); + + final String kemId = cmd.getOptionValue(OPT_KEM.getLongOpt()); + final Path keyringPath = Path.of(cmd.getOptionValue(OPT_KEYRING.getLongOpt())); + final KeyringStore keyring = KeyringStore.load(keyringPath); + + // Configure KEM envelope + KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId); + if (cmd.hasOption(OPT_DIRECT.getLongOpt())) { + kem = kem.directSecret(); + } else { + byte[] info = parseOptionalHex(cmd, OPT_HKDF, "ZeroEcho-KEM".getBytes()); + kem = kem.hkdfSha256(info); + } + // typed numeric options + Integer keyBytes = parsedIntOpt(cmd, OPT_KEY_BYTES); + if (keyBytes != null) { + kem = kem.derivedKeyBytes(keyBytes); + } + Integer maxKemCt = parsedIntOpt(cmd, OPT_MAX_KEM_CT); + if (maxKemCt != null) { + kem = kem.maxKemCiphertextLen(maxKemCt); + } + + // Common symmetric knobs + final byte[] aad = parseHexOpt(cmd, OPT_AAD); + final boolean wantHeader = cmd.hasOption(OPT_HEADER.getLongOpt()); + + // AES payload + if (wantAes) { + String mode = cmd.getOptionValue(OPT_AES_CIPHER.getLongOpt(), "gcm").toLowerCase(Locale.ROOT); + AesDataContentBuilder aes = AesDataContentBuilder.builder(); + switch (mode) { + case "gcm" -> { + Integer tagBitsOpt = parsedIntOpt(cmd, OPT_AES_TAG_BITS); + int tagBits = tagBitsOpt == null ? 128 : tagBitsOpt; + + aes = aes.modeGcm(tagBits); + } + case "ctr" -> aes = aes.modeCtr(); + case "cbc" -> aes = aes.modeCbcPkcs5(); + default -> throw new ParseException("Unsupported --aes-cipher: " + mode); + } + byte[] iv = parseHexOpt(cmd, OPT_AES_IV); + if (iv != null) { + aes = aes.withIv(iv); + } + if (aad != null && aad.length > 0) { + aes = aes.withAad(aad); + } + if (wantHeader) { + aes = aes.withHeader(); + } + kem = kem.withAes(aes); + } + + // ChaCha payload + if (wantChaCha) { + ChaChaDataContentBuilder cc = ChaChaDataContentBuilder.builder(); + byte[] nonce = parseHexOpt(cmd, OPT_CHACHA_NONCE); + if (nonce != null) { + cc = cc.withNonce(nonce); + } + // counter is an integer, not bytes; use typed parsed option + Integer counter = parsedIntOpt(cmd, OPT_CHACHA_COUNTER); + if (counter != null) { + cc = cc.withCounter(counter); + } + Integer initial = parsedIntOpt(cmd, OPT_CHACHA_INITIAL); + if (initial != null) { + cc = cc.initialCounter(initial); + } + if (aad != null && aad.length > 0) { + cc = cc.withAad(aad); // selects AEAD + } + if (wantHeader) { + cc = cc.withHeader(); + } + kem = kem.withChaCha(cc); + } + + // Pipeline: source -> kem payload stage + DataContent chain; + if (encrypt) { + String alias = require(cmd, OPT_PUB, "Missing --pub for encryption"); + PublicKey recipient = keyring.getPublic(alias); + chain = DataContentChainBuilder.encrypt() + .add(PlainFileBuilder.builder().url(Path.of(input).toUri().toURL())) + .add(kem.recipientPublic(recipient)).build(); + } else { + String alias = require(cmd, OPT_PRIV, "Missing --priv for decryption"); + PrivateKey recipient = keyring.getPrivate(alias); + chain = DataContentChainBuilder.decrypt() + .add(PlainFileBuilder.builder().url(Path.of(input).toUri().toURL())) + .add(kem.recipientPrivate(recipient)).build(); + } + + try (InputStream in = chain.getStream(); OutputStream out = Files.newOutputStream(output)) { + in.transferTo(out); + } catch (IOException ex) { + if (LOG.isLoggable(Level.SEVERE)) { + LOG.log(Level.SEVERE, "I/O error", ex); + } + return 1; + } + return 0; + } + + /** + * Builds the CLI option set. + * + * @return populated {@link Options} + */ + private static Options defineOptions(Options opts) { + // Mode group (not required up-front; validated after --list-kems short-circuit) + OptionGroup mode = new OptionGroup(); + mode.addOption(OPT_ENCRYPT); + mode.addOption(OPT_DECRYPT); + opts.addOptionGroup(mode); + + // Payload selection group + OptionGroup payload = new OptionGroup(); + payload.addOption(OPT_AES); + payload.addOption(OPT_CHACHA); + opts.addOptionGroup(payload); + + // General and catalog options + opts.addOption(OPT_OUTPUT); + opts.addOption(OPT_KEYRING); + opts.addOption(OPT_PUB); + opts.addOption(OPT_PRIV); + opts.addOption(OPT_KEM); + opts.addOption(OPT_LIST_KEMS); + + // Symmetric common knobs + opts.addOption(OPT_AAD); + opts.addOption(OPT_HEADER); + + // AES knobs + opts.addOption(OPT_AES_CIPHER); + opts.addOption(OPT_AES_IV); + opts.addOption(OPT_AES_TAG_BITS); + + // ChaCha knobs + opts.addOption(OPT_CHACHA_NONCE); + opts.addOption(OPT_CHACHA_COUNTER); + opts.addOption(OPT_CHACHA_INITIAL); + + // KEM knobs + opts.addOption(OPT_HKDF); + opts.addOption(OPT_DIRECT); + opts.addOption(OPT_KEY_BYTES); + opts.addOption(OPT_MAX_KEM_CT); + + return opts; + } + + /** + * Prints KEM algorithm identifiers that belong to family {@code KEM} and + * support both {@code ENCAPSULATE} and {@code DECAPSULATE}. + */ + private static void listKems() { + List ids = CatalogSelector.selectByFamilyAndRoles(AlgorithmFamily.KEM, + EnumSet.of(KeyUsage.ENCAPSULATE, KeyUsage.DECAPSULATE)); + if (ids.isEmpty()) { + System.out.println("(no KEM algorithms found)"); + return; + } + for (String id : ids) { + System.out.println(id); + } + } + + /** + * Returns the non-empty value of the given option or throws a + * {@link ParseException}. + * + * @param cmd parsed command line + * @param opt option definition + * @param message error message when missing + * @return option value string + * @throws ParseException if missing or empty + */ + private static String require(CommandLine cmd, Option opt, String message) throws ParseException { + String v = cmd.getOptionValue(opt.getLongOpt()); + if (v == null || v.isBlank()) { + throw new ParseException(message); + } + return v; + } + + /** + * Parses an optional integer using Commons CLI typed parsing. + * + *

+ * Requires the {@link Option} to declare {@code .type(Number.class)}. If the + * option is present and convertible, returns its {@code intValue()}, otherwise + * {@code null}. + *

+ * + * @param cmd parsed command line + * @param opt the option to parse + * @return {@code Integer} value or {@code null} if not present + * @throws ParseException if present but not a valid integer + */ + private static Integer parsedIntOpt(CommandLine cmd, Option opt) throws ParseException { + // getParsedOptionValue returns null if absent; otherwise a Number if + // .type(Number.class) is set + Object v = cmd.getParsedOptionValue(opt.getLongOpt()); + if (v == null) { + return null; + } + if (v instanceof Number) { + return ((Number) v).intValue(); + } + // Fallback: attempt manual parse when type not respected by the parser + String s = cmd.getOptionValue(opt.getLongOpt()); + if (s == null || s.isBlank()) { + return null; + } + try { + return Integer.valueOf(s); + } catch (NumberFormatException e) { + throw new ParseException("Invalid integer for --" + opt.getLongOpt() + ": " + s); // NOPMD + } + } + + /** + * Parses an optional hex string for the given option. + * + * @param cmd parsed command line + * @param opt option definition + * @return byte array or {@code null} if absent + * @throws ParseException if the hex value is invalid + */ + private static byte[] parseHexOpt(CommandLine cmd, Option opt) throws ParseException { + if (!cmd.hasOption(opt.getLongOpt())) { + return null; // NOPMD + } + String v = cmd.getOptionValue(opt.getLongOpt()); + if (v == null || v.isBlank()) { + return null; // NOPMD + } + try { + return HexFormat.of().parseHex(v); + } catch (IllegalArgumentException e) { + throw new ParseException("Invalid hex for --" + opt.getLongOpt() + ": " + e.getMessage()); // NOPMD + } + } + + /** + * Returns the hex-decoded bytes from an optional value; if the option is + * absent, returns the provided default. + * + * @param cmd parsed command line + * @param opt option definition + * @param def default bytes to use when option is not present + * @return decoded bytes or default when absent + * @throws ParseException if the option is present but the value is not valid + * hex + */ + private static byte[] parseOptionalHex(CommandLine cmd, Option opt, byte[] def) throws ParseException { + if (!cmd.hasOption(opt.getLongOpt())) { + return def; + } + String v = cmd.getOptionValue(opt.getLongOpt()); + if (v == null || v.isBlank()) { + return def; + } + try { + return HexFormat.of().parseHex(v); + } catch (IllegalArgumentException e) { + throw new ParseException("Invalid hex for --" + opt.getLongOpt() + ": " + e.getMessage()); // NOPMD + } + } +} diff --git a/app/src/main/java/zeroecho/KeyStoreManagement.java b/app/src/main/java/zeroecho/KeyStoreManagement.java new file mode 100644 index 0000000..c28eb80 --- /dev/null +++ b/app/src/main/java/zeroecho/KeyStoreManagement.java @@ -0,0 +1,673 @@ +/******************************************************************************* + * 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.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import javax.crypto.SecretKey; + +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.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.storage.KeyringStore; + +/** + * Command-line utility for managing key material in a text-based keyring store. + * + *

Overview

The {@code KeyStoreManagement} subcommand provides + * lifecycle operations on key material stored in + * {@link zeroecho.core.storage.KeyringStore}. It supports listing algorithms + * and aliases, generating key pairs or symmetric keys, exporting/importing + * versioned text snippets, and overwriting existing entries. + * + * The keystore is a plain-text file with aliases mapped to keys. Import/export + * uses line-oriented snippets with format-version headers, suitable for + * exchanging public keys or migrating key material between systems. + * + *

Usage

Invoked as:
{@code
+ * ZeroEcho -K [options]
+ * }
+ * + *

Modes

Exactly one action must be chosen: + *
    + *
  • {@code --list-algorithms} - list catalog algorithms and whether they + * support symmetric/asymmetric builders.
  • + *
  • {@code --list-aliases} - list aliases present in the keystore.
  • + *
  • {@code --generate} - generate a new key pair or secret and store under + * the given alias.
  • + *
  • {@code --export} - export one or more aliases as a versioned + * snippet.
  • + *
  • {@code --import} - import a versioned snippet into the keystore.
  • + *
+ * + *

General options

+ *
    + *
  • {@code -k | --keystore } - path to keystore file (required).
  • + *
  • {@code --overwrite} - overwrite existing aliases on conflict.
  • + *
+ * + *

Generate options

+ *
    + *
  • {@code --alg } - algorithm id (e.g., RSA, Ed25519, AES, Frodo).
  • + *
  • {@code --alias } - base alias; for asymmetric, both public and + * private entries will be created.
  • + *
  • {@code --kind sym|asym} - force symmetric or asymmetric if the algorithm + * supports both (optional).
  • + *
  • {@code --pub-suffix } - suffix for public alias (default: + * .pub).
  • + *
  • {@code --prv-suffix } - suffix for private alias (default: + * .prv).
  • + *
+ * + *

Export options

+ *
    + *
  • {@code --aliases a,b,c} - comma-separated list of aliases to export + * (default: all).
  • + *
  • {@code --out + *
+ * + *

Import options

+ *
    + *
  • {@code --in + *
  • {@code --overwrite} - allow replacing existing aliases when + * importing.
  • + *
+ * + *

Examples

{@code
+ * # List available algorithms
+ * ZeroEcho -K --list-algorithms
+ *
+ * # Generate a new RSA key pair and store as alice.pub / alice.prv
+ * ZeroEcho -K --generate --alg RSA --alias alice --keystore keys.txt
+ *
+ * # Generate a new AES secret key and store as "backup-key"
+ * ZeroEcho -K --generate --alg AES --alias backup-key --kind sym --keystore keys.txt
+ *
+ * # List aliases in the keystore
+ * ZeroEcho -K --list-aliases --keystore keys.txt
+ *
+ * # Export selected aliases to a file
+ * ZeroEcho -K --export --aliases alice.pub,alice.prv --out alice-keys.txt --keystore keys.txt
+ *
+ * # Import aliases from stdin, overwriting if necessary
+ * ZeroEcho -K --import --in - --overwrite --keystore keys.txt < alice-keys.txt
+ * }
+ * + *

Exit codes

+ *
    + *
  • 0 - operation succeeded
  • + *
  • non-zero - error occurred (parse error, I/O failure, or invalid + * arguments)
  • + *
+ * + * @since 1.0 + */ +public final class KeyStoreManagement { // NOPMD + + private final static String STD_IN_OUT = "-"; + + // --------------------------------------------------------------------- + // Option constants (centralized for maintainability) + // --------------------------------------------------------------------- + + private static final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("file") + .desc("Path to keyring store").build(); + + private static final Option LIST_ALGORITHMS_OPTION = Option.builder().longOpt("list-algorithms") + .desc("List catalog algorithms with symmetric/asymmetric support").build(); + + private static final Option LIST_ALIASES_OPTION = Option.builder().longOpt("list-aliases") + .desc("List aliases present in the keyring").build(); + + private static final Option GENERATE_OPTION = Option.builder().longOpt("generate") + .desc("Generate a keypair or a secret").build(); + + private static final Option ALG_OPTION = Option.builder().longOpt("alg").hasArg().argName("id") + .desc("Algorithm id (e.g., RSA, Ed25519, AES, Frodo)").build(); + + private static final Option ALIAS_OPTION = Option.builder().longOpt("alias").hasArg().argName("name") + .desc("Alias base; for asymmetric, two entries will be written").build(); + + private static final Option KIND_OPTION = Option.builder().longOpt("kind").hasArg().argName("sym|asym") + .desc("Force symmetric or asymmetric when algorithm supports both").build(); + + private static final Option PUB_SUFFIX_OPTION = Option.builder().longOpt("pub-suffix").hasArg().argName("sfx") + .desc("Suffix for public alias (default .pub)").build(); + + private static final Option PRV_SUFFIX_OPTION = Option.builder().longOpt("prv-suffix").hasArg().argName("sfx") + .desc("Suffix for private alias (default .prv)").build(); + + private static final Option OVERWRITE_OPTION = Option.builder().longOpt("overwrite") + .desc("Overwrite existing aliases on conflict").build(); + + private static final Option EXPORT_OPTION = Option.builder().longOpt("export") + .desc("Export selected aliases as a versioned text snippet").build(); + + private static final Option IMPORT_OPTION = Option.builder().longOpt("import") + .desc("Import a versioned text snippet into the keyring").build(); + + private static final Option ALIASES_OPTION = Option.builder().longOpt("aliases").hasArg().argName("a,b,c") + .desc("Comma-separated aliases to export; empty means all").build(); + + private static final Option OUTFILE_OPTION = Option.builder().longOpt("out").hasArg().argName("file|-") + .desc("Output file for export (default '-' for stdout)").build(); + + private static final Option INFILE_OPTION = Option.builder().longOpt("in").hasArg().argName("file|-") + .desc("Input file for import (default '-' for stdin)").build(); + + /** Prevents instantiation. */ + private KeyStoreManagement() { + } + + // --------------------------------------------------------------------- + // Entry point - lets exceptions bubble to the caller (ZeroEcho) + // --------------------------------------------------------------------- + + /** + * Parses arguments, executes the requested action, and returns an exit code. + * Parser and IO exceptions are intentionally propagated for the central CLI to + * handle. + * + * @param args arguments passed by the application dispatcher + * @param dispatcherOptions an existing {@code Options} instance used by the + * dispatcher; this method only adds its own options + * @return process exit code (0 for success; non-zero for semantic errors) + * @throws ParseException if the parser fails + * @throws IOException if I/O fails + */ + public static int main(final String[] args, final Options dispatcherOptions) throws ParseException, IOException { + defineOptions(dispatcherOptions); + CommandLineParser parser = new DefaultParser(); + CommandLine cmd = parser.parse(dispatcherOptions, args); + + if (cmd.hasOption(LIST_ALGORITHMS_OPTION.getLongOpt())) { + listAlgorithms(); + return 0; + } + + if (!cmd.hasOption(KEYSTORE_OPTION.getLongOpt())) { + throw new ParseException("Missing required option: k/keystore"); + } + + Path keyringPath = Path.of(cmd.getOptionValue(KEYSTORE_OPTION.getLongOpt())); + KeyringStore store = Files.exists(keyringPath) ? KeyringStore.load(keyringPath) : new KeyringStore(); + + if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) { + listAliases(store); + return 0; + } + if (cmd.hasOption(GENERATE_OPTION.getLongOpt())) { + doGenerate(store, cmd); + store.save(keyringPath); + return 0; + } + if (cmd.hasOption(EXPORT_OPTION.getLongOpt())) { + doExportSnippet(store, cmd); + return 0; + } + if (cmd.hasOption(IMPORT_OPTION.getLongOpt())) { + doImportSnippet(store, cmd); + store.save(keyringPath); + return 0; + } + + throw new IllegalArgumentException("No operation selected"); + } + + /** + * Adds this subcommand's options to the provided {@code Options} instance. + * + * @param options an existing {@code Options} instance from the central + * dispatcher + */ + public static void defineOptions(final Options options) { + options.addOption(KEYSTORE_OPTION); + + OptionGroup actions = new OptionGroup(); + actions.setRequired(true); + actions.addOption(LIST_ALGORITHMS_OPTION); + actions.addOption(LIST_ALIASES_OPTION); + actions.addOption(GENERATE_OPTION); + actions.addOption(EXPORT_OPTION); + actions.addOption(IMPORT_OPTION); + options.addOptionGroup(actions); + + options.addOption(ALG_OPTION); + options.addOption(ALIAS_OPTION); + options.addOption(KIND_OPTION); + options.addOption(PUB_SUFFIX_OPTION); + options.addOption(PRV_SUFFIX_OPTION); + options.addOption(OVERWRITE_OPTION); + + options.addOption(ALIASES_OPTION); + options.addOption(OUTFILE_OPTION); + options.addOption(INFILE_OPTION); + } + + // --------------------------------------------------------------------- + // Actions + // --------------------------------------------------------------------- + + /** + * Lists available algorithms with builder availability to stdout. + */ + public static void listAlgorithms() { + PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD + Set ids = CryptoAlgorithms.available(); + for (String id : ids) { + CryptoAlgorithm a = CryptoAlgorithms.require(id); + boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty(); + boolean hasSym = !a.symmetricBuildersInfo().isEmpty(); + out.printf(Locale.ROOT, "%-12s asym:%s sym:%s%n", id, hasAsym, hasSym); + } + } + + /** + * Lists aliases present in the store to stdout. + * + * @param store loaded keyring store + */ + public static void listAliases(final KeyringStore store) { + PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD + List aliases = store.aliases(); + if (aliases.isEmpty()) { + out.println("(empty)"); + return; + } + for (String alias : aliases) { + out.println(alias); + } + } + + /** + * Generates a keypair or secret and stores entries under the chosen aliases. + * + *

+ * This reuses the same import-spec construction heuristic as the project's + * dynamic tests: find plausible import-spec classes and build specs from + * SPKI/PKCS8/RAW bytes. + *

+ * + * @param store keyring store to mutate + * @param cmd parsed command line + */ + public static void doGenerate(final KeyringStore store, final CommandLine cmd) { // NOPMD + String algId = required(cmd, ALG_OPTION, "--alg is required for --generate"); + String aliasBase = required(cmd, ALIAS_OPTION, "--alias is required for --generate"); + String kind = cmd.getOptionValue(KIND_OPTION.getLongOpt()); + String pubSfx = cmd.getOptionValue(PUB_SUFFIX_OPTION.getLongOpt(), ".pub"); + String prvSfx = cmd.getOptionValue(PRV_SUFFIX_OPTION.getLongOpt(), ".prv"); + boolean overwrite = cmd.hasOption(OVERWRITE_OPTION.getLongOpt()); + + CryptoAlgorithm alg = CryptoAlgorithms.require(algId); + boolean canAsym = !alg.asymmetricBuildersInfo().isEmpty(); + boolean canSym = !alg.symmetricBuildersInfo().isEmpty(); + + boolean doAsym = "asym".equalsIgnoreCase(kind) || (kind == null && canAsym && !canSym); + boolean doSym = "sym".equalsIgnoreCase(kind) || (kind == null && canSym && !canAsym); + + if (!doAsym && !doSym && canAsym && canSym) { + throw new IllegalArgumentException("Algorithm supports both; specify --kind sym|asym"); + } + + if (doAsym) { + KeyPair kp = null; + CryptoAlgorithm.AsymBuilderInfo used = null; + for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) { + if (bi.defaultKeySpec == null) { + continue; + } + @SuppressWarnings("unchecked") + Class st = (Class) bi.specType; + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(st); + try { + kp = b.generateKeyPair((AlgorithmKeySpec) bi.defaultKeySpec); + if (kp != null) { + used = bi; + break; + } + } catch (Throwable ignore) { // NOPMD + } + } + if (kp == null || used == null) { + throw new IllegalStateException("No asymmetric builder with default spec worked for " + algId); + } + + Class pubImp = null; + Class prvImp = null; + for (CryptoAlgorithm.AsymBuilderInfo x : alg.asymmetricBuildersInfo()) { + if (looksLikeImportSpecForPublic(x.specType)) { + pubImp = x.specType; + } + if (looksLikeImportSpecForPrivate(x.specType)) { + prvImp = x.specType; + } + } + if (pubImp == null && prvImp == null) { + throw new IllegalStateException("No import spec class found for " + algId + " (asymmetric)"); + } + + byte[] spki = kp.getPublic() != null ? kp.getPublic().getEncoded() : null; + byte[] pkcs8 = kp.getPrivate() != null ? kp.getPrivate().getEncoded() : null; + + AlgorithmKeySpec pubSpec = pubImp != null ? makeImportSpec(pubImp, spki, algId, used.defaultKeySpec) : null; + AlgorithmKeySpec prvSpec = prvImp != null ? makeImportSpec(prvImp, pkcs8, algId, used.defaultKeySpec) + : null; + + if (pubImp != null && pubSpec == null) { + throw new IllegalStateException("Cannot construct public import spec for " + algId); + } + if (prvImp != null && prvSpec == null) { + throw new IllegalStateException("Cannot construct private import spec for " + algId); + } + + String pubAlias = aliasBase + pubSfx; + String prvAlias = aliasBase + prvSfx; + ensureWritable(store, pubAlias, overwrite); + ensureWritable(store, prvAlias, overwrite); + + store.putPublic(pubAlias, algId, pubSpec); + store.putPrivate(prvAlias, algId, prvSpec); + + PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD + out.printf("Generated %s -> %s, %s%n", algId, pubAlias, prvAlias); + } + + if (doSym) { + SecretKey sk = null; + CryptoAlgorithm.SymBuilderInfo used = null; + for (CryptoAlgorithm.SymBuilderInfo bi : alg.symmetricBuildersInfo()) { + if (bi.defaultKeySpec() == null) { + continue; + } + @SuppressWarnings("unchecked") + Class st = (Class) bi.specType(); + SymmetricKeyBuilder b = alg.symmetricKeyBuilder(st); + try { + sk = b.generateSecret((AlgorithmKeySpec) bi.defaultKeySpec()); + if (sk != null) { + used = bi; + break; + } + } catch (Throwable ignore) { // NOPMD + } + } + if (sk == null || used == null) { + throw new IllegalStateException("No symmetric builder with default spec worked for " + algId); + } + + Class impSym = findSymmetricImportSpecClass(alg); + if (impSym == null) { + throw new IllegalStateException("No symmetric import spec class for " + algId); + } + + byte[] raw = sk.getEncoded(); + AlgorithmKeySpec secSpec = makeImportSpec(impSym, raw, algId, used.defaultKeySpec()); + if (secSpec == null) { + throw new IllegalStateException("Cannot construct symmetric import spec for " + algId); + } + + ensureWritable(store, aliasBase, overwrite); + store.putSecret(aliasBase, algId, secSpec); + + PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD + out.printf("Generated %s -> %s%n", algId, aliasBase); + } + } + + /** + * Exports a versioned, line-oriented snippet to stdout or a file. + * + * @param store source keyring store + * @param cmd parsed command line + * @throws IOException if writing fails + */ + public static void doExportSnippet(final KeyringStore store, final CommandLine cmd) throws IOException { + List aliases = parseCsv(cmd.getOptionValue(ALIASES_OPTION.getLongOpt(), "")); + if (aliases.isEmpty()) { + aliases = store.aliases(); + } + + String text = store.exportText(aliases); + String outFile = cmd.getOptionValue(OUTFILE_OPTION.getLongOpt(), STD_IN_OUT); + + if (STD_IN_OUT.equals(outFile)) { + try (PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8)) { + out.print(text); + if (!text.endsWith("\n")) { + out.println(); + } + } + } else { + Files.writeString(Path.of(outFile), text, StandardCharsets.UTF_8); + System.out.printf("Wrote snippet: %s%n", outFile); + } + } + + /** + * Imports a versioned, line-oriented snippet from stdin or a file. + * + * @param store destination keyring store + * @param cmd parsed command line + * @throws IOException if reading fails or the snippet is invalid + */ + public static void doImportSnippet(final KeyringStore store, final CommandLine cmd) throws IOException { + boolean overwrite = cmd.hasOption(OVERWRITE_OPTION.getLongOpt()); + String inFile = cmd.getOptionValue(INFILE_OPTION.getLongOpt(), STD_IN_OUT); + + String text; + if (STD_IN_OUT.equals(inFile)) { + StringBuilder sb = new StringBuilder(1024); + try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) { + String line = br.readLine(); + while (line != null) { + sb.append(line).append('\n'); + line = br.readLine(); + } + } + text = sb.toString(); + } else { + text = Files.readString(Path.of(inFile), StandardCharsets.UTF_8); + } + + store.importText(text, overwrite); + PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD + out.println("Imported snippet."); + } + + // --------------------------------------------------------------------- + // Helpers - EXACTLY the dynamic-test strategy + // --------------------------------------------------------------------- + + private static boolean looksLikeImportSpecForPublic(Class specType) { + String n = specType.getSimpleName(); + return n.contains("Public") || n.endsWith("PublicKeySpec"); + } + + private static boolean looksLikeImportSpecForPrivate(Class specType) { + String n = specType.getSimpleName(); + return n.contains("Private") || n.endsWith("PrivateKeySpec"); + } + + private static Class findSymmetricImportSpecClass(CryptoAlgorithm alg) { + for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) { + String n = x.specType().getSimpleName(); + if (n.contains("Import") || n.endsWith("KeyImportSpec") || n.endsWith("SecretSpec")) { + return x.specType(); + } + } + for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) { + try { + x.specType().getConstructor(byte[].class); + return x.specType(); + } catch (NoSuchMethodException ignored) { + } + } + return null; + } + + /** + * Constructs an AlgorithmKeySpec from encoded bytes using conventional + * factories/ctors. Mirrors the makeImportSpec approach used in the dynamic + * test. + * + * @param specType target spec class + * @param material encoded bytes (SPKI, PKCS#8, or RAW) + * @param algId algorithm id (used for variant name heuristics) + * @param defaultSpec the default spec used by the builder (for optional variant + * hints) + * @return constructed AlgorithmKeySpec or null if none matched + */ + private static AlgorithmKeySpec makeImportSpec(Class specType, byte[] material, String algId, + Object defaultSpec) { + if (material == null) { + return null; + } + try { + Method m = specType.getMethod("fromRaw", byte[].class); + Object spec = m.invoke(null, material); + return (AlgorithmKeySpec) spec; + } catch (NoSuchMethodException ignored) { + } catch (Throwable t) { // NOPMD + } + try { + Method m = specType.getMethod("fromRaw", String.class, byte[].class); + String name = deriveVariantNameForImport(algId, defaultSpec); + Object spec = m.invoke(null, name, material); + return (AlgorithmKeySpec) spec; + } catch (NoSuchMethodException ignored) { + } catch (Throwable t) { // NOPMD + } + try { + Method m = specType.getMethod("of", byte[].class); + Object spec = m.invoke(null, material); + return (AlgorithmKeySpec) spec; + } catch (NoSuchMethodException ignored) { + } catch (Throwable t) { // NOPMD + } + try { + Constructor c = specType.getConstructor(byte[].class); + Object spec = c.newInstance(material); + return (AlgorithmKeySpec) spec; + } catch (NoSuchMethodException ignored) { + } catch (Throwable t) { // NOPMD + } + try { + Constructor c = specType.getConstructor(String.class); + String b64 = Base64.getEncoder().encodeToString(material); + Object spec = c.newInstance(b64); + return (AlgorithmKeySpec) spec; + } catch (NoSuchMethodException ignored) { + } catch (Throwable t) { // NOPMD + } + return null; + } + + private static String deriveVariantNameForImport(String algId, Object defaultSpec) { + if (defaultSpec != null) { + try { + Method m = defaultSpec.getClass().getMethod("macName"); + Object v = m.invoke(defaultSpec); + if (v instanceof String) { + return (String) v; + } + } catch (Throwable ignored) { // NOPMD + } + } + if ("HMAC".equalsIgnoreCase(algId)) { // NOPMD + return "HmacSHA256"; + } + return algId; + } + + private static String required(CommandLine cmd, Option opt, String message) { + if (!cmd.hasOption(opt.getLongOpt())) { + throw new IllegalArgumentException(message); + } + return cmd.getOptionValue(opt.getLongOpt()); + } + + private static List parseCsv(String csv) { + List list = new ArrayList<>(); + if (csv == null || csv.isBlank()) { + return list; + } + String[] parts = csv.split("\\s*,\\s*"); + for (String part : parts) { + if (!part.isBlank()) { + list.add(part); + } + } + return list; + } + + /** + * Ensures that an alias can be written under the current collision policy. + * + * @param store keyring store + * @param alias alias to check + * @param overwrite whether collisions are allowed + * @throws IllegalArgumentException if alias exists and overwrite is false + */ + private static void ensureWritable(KeyringStore store, String alias, boolean overwrite) { + if (store.contains(alias) && !overwrite) { + throw new IllegalArgumentException("Alias already exists: " + alias + " (use --overwrite)"); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/zeroecho/Tag.java b/app/src/main/java/zeroecho/Tag.java new file mode 100644 index 0000000..0eec860 --- /dev/null +++ b/app/src/main/java/zeroecho/Tag.java @@ -0,0 +1,330 @@ +/******************************************************************************* + * 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.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Locale; +import java.util.Objects; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import zeroecho.core.alg.digest.DigestSpec; +import zeroecho.core.err.VerificationException; +import zeroecho.core.spec.ContextSpec; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.storage.KeyringStore; +import zeroecho.core.tag.TagEngineBuilder; +import zeroecho.sdk.builders.TagTrailerDataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; +import zeroecho.sdk.content.builtin.PlainFile; + +/** + * CLI entry point for producing and verifying trailer tags based on digital + * signatures or digests. + * + *

Overview

This class wires command line options to content-processing + * pipelines that either append a tag (produce mode) or validate and strip a tag + * (verify mode). The tag can be a digital signature or a message digest, + * depending on the selected type and algorithm. + * + *

Usage

{@code
+ * ZeroEcho -T --type signature --mode produce --alg Ed25519 \
+ *         --ks keys.txt --priv my-signing-key --in file.bin --out file.tagged
+ *
+ * ZeroEcho -T --type signature --mode verify --alg Ed25519 \
+ *         --ks keys.txt --pub my-verify-key --in file.tagged --out file.bin
+ *
+ * ZeroEcho -T --type digest --mode produce --alg SHA-256 \
+ *         --in file.bin --out file.tagged
+ *
+ * ZeroEcho -T --type digest --mode verify --alg SHA-256 \
+ *         --in file.tagged --out file.bin
+ *
+ * # Use "-" to read from STDIN or write to STDOUT:
+ * ZeroEcho -T --type digest --mode produce --alg SHA-256 --in - --out -
+ * }
+ * + *

Notes

The signature mode requires a keyring file. In produce mode a + * private key is required, while in verify mode a public key is required. + * Digest mode does not require keys. A non-zero exit status indicates a + * verification mismatch or input errors. + */ +public final class Tag { // NOPMD + /** + * Conventional marker for standard input or output stream. + */ + private final static String STD_IN_OUT = "-"; + + // ---- All options as constants + private static final Option TYPE_OPT = Option.builder().longOpt("type").hasArg().argName("signature|digest") + .desc("tag primitive type").build(); + + private static final Option MODE_OPT = Option.builder().longOpt("mode").hasArg().argName("produce|verify") + .desc("operation mode").build(); + + private static final Option ALG_OPT = Option.builder().longOpt("alg").hasArg().argName("id") + .desc("algorithm id (signature: Ed25519/Ed448/ECDSA/RSA; digest: SHA-256/.../SHAKE256:N)").build(); + + private static final Option KS_OPT = Option.builder().longOpt("ks").hasArg().argName("file") + .desc("keyring file (required for signature)").build(); + + private static final Option PRIV_OPT = Option.builder().longOpt("priv").hasArg().argName("alias") + .desc("private key alias (signature + produce)").build(); + + private static final Option PUB_OPT = Option.builder().longOpt("pub").hasArg().argName("alias") + .desc("public key alias (signature + verify)").build(); + + private static final Option IN_OPT = Option.builder().longOpt("in").hasArg().argName("file|-") + .desc("input file or - for STDIN").build(); + + private static final Option OUT_OPT = Option.builder().longOpt("out").hasArg().argName("file|-") + .desc("output file or - for STDOUT").build(); + + // ---- Allowed values and defaults (no enums) + private static final String TYPE_SIGNATURE = "signature"; + private static final String TYPE_DIGEST = "digest"; + private static final String MODE_PRODUCE = "produce"; + private static final String MODE_VERIFY = "verify"; + + private Tag() { + } + + /** + * Parses CLI arguments and executes produce or verify operation for signature + * or digest tags. + * + *

Behavior

The method validates required options, constructs the + * appropriate processing pipeline, and streams data from the input to the + * output while producing or verifying the trailing tag. On verification + * mismatch, it returns a non-zero exit code and attempts to remove a corrupted + * output file when applicable. + * + * @param args the command line arguments that follow {@code -T} + * @param root the options container to which this method appends its own + * options before parsing + * @return process exit code: {@code 0} on success, {@code 1} on verification + * mismatch or I/O error during verification, {@code 2} on invalid + * arguments or unsupported options + * @throws ParseException if CLI parsing fails + * @throws IOException if an I/O error occurs while reading or + * writing streams + * @throws GeneralSecurityException if a cryptographic error occurs during + * signature or digest processing + */ + public static int main(String[] args, Options root) throws ParseException, IOException, GeneralSecurityException { + Options opts = root; + opts.addOption(TYPE_OPT); + opts.addOption(MODE_OPT); + opts.addOption(ALG_OPT); + opts.addOption(KS_OPT); + opts.addOption(PRIV_OPT); + opts.addOption(PUB_OPT); + opts.addOption(IN_OPT); + opts.addOption(OUT_OPT); + + CommandLine cli = new DefaultParser().parse(opts, args); + + if (!has(cli, TYPE_OPT) || !has(cli, MODE_OPT) || !has(cli, ALG_OPT) || !has(cli, IN_OPT) + || !has(cli, OUT_OPT)) { + new HelpFormatter().printHelp("zeroecho -T [options]", opts); + return 2; + } + + String type = opt(cli, TYPE_OPT).trim().toLowerCase(Locale.ROOT); + String mode = opt(cli, MODE_OPT).trim().toLowerCase(Locale.ROOT); + if (!isOneOf(type, TYPE_SIGNATURE, TYPE_DIGEST)) { + System.err.println("Unsupported --type: " + type); + return 2; + } + if (!isOneOf(mode, MODE_PRODUCE, MODE_VERIFY)) { + System.err.println("Unsupported --mode: " + mode); + return 2; + } + + String alg = opt(cli, ALG_OPT); + String inPath = opt(cli, IN_OPT); + String outPath = opt(cli, OUT_OPT); + + PlainContent source = source(inPath); + boolean produce = MODE_PRODUCE.equals(mode); + DataContent tail; + + if (TYPE_SIGNATURE.equals(type)) { + String ksPath = require(cli, KS_OPT, "--ks is required for --type signature"); + KeyringStore keyring = KeyringStore.load(Path.of(ksPath)); + ContextSpec spec = VoidSpec.INSTANCE; + + if (produce) { + String privAlias = require(cli, PRIV_OPT, "signature produce requires --priv "); + PrivateKey priv = keyring.getPrivate(privAlias); + tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(alg, priv, spec)).build(true); + } else { + String pubAlias = require(cli, PUB_OPT, "signature verify requires --pub "); + PublicKey pub = keyring.getPublic(pubAlias); + tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(alg, pub, spec)).build(false); + } + } else { // digest + DigestSpec spec = parseDigest(alg); + tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(spec)).build(produce); + } + + tail.setInput(source); + + try (InputStream in = tail.getStream(); OutputStream out = sink(outPath)) { + in.transferTo(out); + } catch (IOException verifyFail) { + System.out.println("ERROR: " + verifyFail.getMessage()); + if (verifyFail.getCause() instanceof VerificationException) { + try { + // remove the file if it is corrupted + Files.deleteIfExists(Path.of(outPath)); + } catch (IOException e1) { // NOPMD + // ignore any errors + } + } + return 1; // non-zero exit for verify mismatch + } + + return 0; + } + + // ---------- helpers ---------- + + private static boolean has(CommandLine cli, Option opt) { + return cli.hasOption(opt.getLongOpt()); + } + + private static String opt(CommandLine cli, Option opt) { + return cli.getOptionValue(opt.getLongOpt()); + } + + private static String require(CommandLine cli, Option opt, String msg) { + String v = cli.getOptionValue(opt.getLongOpt()); + if (v == null) { + throw new IllegalArgumentException(msg); + } + return v; + } + + private static boolean isOneOf(String s, String a, String b) { + return a.equalsIgnoreCase(s) || b.equalsIgnoreCase(s); + } + + private static PlainContent source(String path) throws IOException { + if (STD_IN_OUT.equals(path)) { + return new StdinContent(System.in); + } + return new PlainFile(Path.of(path).toUri().toURL()); + } + + private static OutputStream sink(String path) throws IOException { + if (STD_IN_OUT.equals(path)) { + return new NonClosingBufferedOutputStream(System.out, 64 * 1024); + } + return new BufferedOutputStream(Files.newOutputStream(Path.of(path)), 64 * 1024); + } + + private static DigestSpec parseDigest(String s) { + String u = s.trim().toUpperCase(Locale.ROOT); + if ("SHA-256".equals(u) || "SHA256".equals(u)) { + return DigestSpec.sha256(); + } + if ("SHA-512".equals(u) || "SHA512".equals(u)) { + return DigestSpec.sha512(); + } + if ("SHA3-256".equals(u) || "SHA3_256".equals(u)) { + return DigestSpec.sha3_256(); + } + if ("SHA3-512".equals(u) || "SHA3_512".equals(u)) { + return DigestSpec.sha3_512(); + } + if (u.startsWith("SHAKE128:")) { + int len = Integer.parseInt(u.substring("SHAKE128:".length())); + return DigestSpec.shake128(len); + } + if (u.startsWith("SHAKE256:")) { + int len = Integer.parseInt(u.substring("SHAKE256:".length())); + return DigestSpec.shake256(len); + } + throw new IllegalArgumentException("Unsupported digest: " + s); + } + + /** STDIN wrapper that implements PlainContent. */ + private static final class StdinContent implements PlainContent { + private final InputStream in; + + private StdinContent(InputStream in) { + this.in = Objects.requireNonNull(in, "stdin"); + } + + @Override + public void setInput(DataContent upstream) { + if (upstream != null) { + throw new IllegalArgumentException("stdin is a source; no input allowed"); + } + } + + @Override + public InputStream getStream() { + return in; + } + } + + /** Buffered stream that never closes the underlying stream (for STDOUT). */ + private static final class NonClosingBufferedOutputStream extends BufferedOutputStream { + private NonClosingBufferedOutputStream(OutputStream out, int size) { + super(out, size); + } + + @Override + public void close() throws IOException { + flush(); + } // do not close underlying System.out + } +} diff --git a/app/src/main/java/zeroecho/ZeroEcho.java b/app/src/main/java/zeroecho/ZeroEcho.java new file mode 100644 index 0000000..ef1a2bb --- /dev/null +++ b/app/src/main/java/zeroecho/ZeroEcho.java @@ -0,0 +1,198 @@ +/******************************************************************************* + * 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.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.sdk.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. + *

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

+ *

+ * This class initializes Bouncy Castle security provider and uses Apache + * Commons CLI for command-line parsing. + *

+ * + * @author Leo Galambos + */ +public final class ZeroEcho { + /** + * Logger instance for the {@code ZeroEcho} class used to log messages and + * events. + *

+ * This logger is configured with the name of the {@code ZeroEcho} class, + * allowing for fine-grained logging control specific to this class. + *

+ */ + public static final Logger LOG = Logger.getLogger(ZeroEcho.class.getName()); + + static { + BouncyCastleActivator.init(); + } + + /** + * Default constructor for ZeroEcho. + *

+ * This constructor does not perform any initialization since all operations are + * handled via static methods and blocks. + *

+ */ + 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) { + final Option KEM_OPTION = Option.builder("E").longOpt("kem").desc("KEM encryption/decryption").build(); + final Option GUARD_OPTION = Option.builder("G").longOpt("guard") + .desc("multi-recipient encryption/decryption (keys+passwords), AES/ChaCha").build(); + final Option KEYSTORE_OPTION = Option.builder("K").longOpt("ksm").desc("key store management").build(); + final Option COVERT_OPTION = Option.builder("C").longOpt("covert").desc("covert channel processing").build(); + final Option TAG_OPTION = Option.builder("T").longOpt("tag") + .desc("tag subcommand (signature/digest; produce/verify)").build(); + + final OptionGroup OPERATION_GROUP = new OptionGroup(); + OPERATION_GROUP.addOption(GUARD_OPTION); + OPERATION_GROUP.addOption(KEYSTORE_OPTION); + OPERATION_GROUP.addOption(KEM_OPTION); + OPERATION_GROUP.addOption(COVERT_OPTION); + OPERATION_GROUP.addOption(TAG_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); + + return switch (OPERATION_GROUP.getSelected()) { + case "E" -> Kem.main(args, options = new Options().addOption(KEM_OPTION)); + case "G" -> Guard.main(args, options = new Options().addOption(GUARD_OPTION)); + case "K" -> KeyStoreManagement.main(args, options = new Options().addOption(KEYSTORE_OPTION)); + case "C" -> CovertCommand.main(args, options = new Options().addOption(COVERT_OPTION)); + case "T" -> Tag.main(args, options = new Options().addOption(TAG_OPTION)); + default -> 1; + + }; + + } catch (MissingOptionException ex) { + if (LOG.isLoggable(Level.SEVERE)) { + LOG.log(Level.SEVERE, ex.getMessage()); + } + return help(options); + + } catch (ParseException | GeneralSecurityException ex) { + if (LOG.isLoggable(Level.WARNING)) { + LOG.log(Level.WARNING, "Unexpected exception", ex.getMessage()); + } + return help(options); + + } catch (IOException 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. + * + *

+ * 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} + */ + private static int help(final Options options) { + // automatically generate the help statement + final HelpFormatter formatter = new HelpFormatter(); + formatter.setWidth(80); + formatter.printHelp(ZeroEcho.class.getName(), options); + + return 1; + } +} diff --git a/app/src/main/java/zeroecho/package-info.java b/app/src/main/java/zeroecho/package-info.java new file mode 100644 index 0000000..d31181c --- /dev/null +++ b/app/src/main/java/zeroecho/package-info.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Command-line tooling built around streaming cryptographic pipelines for + * encryption, tagging, key management, and covert transport. + * + *

+ * This package provides a small set of composable subcommands exposed through a + * single dispatcher and implemented on top of builder-style SDK components. The + * design favors streaming I/O so large inputs are never fully buffered in + * memory, and it emphasizes strong defaults with explicit opt-ins for advanced + * parameters. + *

+ * + *

Design goals

+ *
    + *
  • Single front end, multiple tools: the {@link ZeroEcho} dispatcher + * selects a subcommand and passes through the remaining CLI arguments to it, + * allowing each tool to define its own options without global conflicts.
  • + *
  • Streaming composition: subcommands assemble {@code DataContent} + * chains so input is transformed on the fly (encrypt, tag, verify, embed) and + * written to the destination stream. This keeps memory usage low and makes + * stdin/stdout a first-class I/O mode.
  • + *
  • Safe, explicit defaults: symmetric modes default to authenticated + * variants when available, headers are on by default when they carry required + * parameters, and verification errors cause clear exit codes and cleanup of + * incomplete outputs.
  • + *
+ * + *

Core ideas and algorithms

+ *

+ * The tools build on two families of primitives: + *

+ *
    + *
  • Asymmetric/KEM envelopes: hybrid encryption encapsulates a + * content-encryption key with a chosen KEM, then applies a symmetric cipher + * (AES-GCM/CTR/CBC or ChaCha AEAD/stream). HKDF-SHA256 is supported for + * deriving the payload key from the KEM secret, with limits on accepted + * KEM-ciphertext sizes.
  • + *
  • Trailer tags: a trailer at the end of the stream carries either a + * digital signature or a digest. Produce mode computes and appends the tag; + * verify mode validates and strips it, returning a non-zero status on + * mismatch.
  • + *
+ *

+ * Key material is loaded from a simple, text-based keyring that supports + * listing, generation, and import/export of versioned snippets for both + * symmetric and asymmetric algorithms. + *

+ * + *

Subcommands overview

+ *
    + *
  • {@link Guard} - multi-recipient envelope that mixes public-key recipients + * and passwords, optionally shuffling recipients; payload is AES or ChaCha with + * optional header and AAD.
  • + *
  • {@link Kem} - KEM-based hybrid encryption and decryption with AES or + * ChaCha payloads, HKDF support, counters/nonces, and compact symmetric + * headers.
  • + *
  • {@link KeyStoreManagement} - maintenance of a text keyring: list + * algorithms and aliases, generate keys, and import/export versioned + * snippets.
  • + *
  • {@link Tag} - produce or verify trailer tags using digital signatures or + * message digests.
  • + *
  • {@link CovertCommand} - embed or extract a binary payload in JPEG files + * via configurable EXIF slots.
  • + *
+ * + *

I/O conventions and exit codes

+ *
    + *
  • All tools accept "-" for stdin/stdout when appropriate, enabling shell + * pipelines.
  • + *
  • Exit code {@code 0} indicates success; non-zero codes signal parse + * errors, verification failures, or I/O problems. Verification failures attempt + * to remove partially written outputs.
  • + *
+ * + *

Typical usage

{@code
+ * # Multi-recipient envelope (AES-GCM) with shuffled recipients
+ * ZeroEcho -G --encrypt in.bin --output out.enc \
+ *   --keyring keyring.txt --to-alias alice --to-psw s3cret \
+ *   --alg aes-gcm --tag-bits 128 --aad-hex DEADBEEF
+ *
+ * # KEM + ChaCha AEAD with header
+ * ZeroEcho -E --encrypt in.bin -o out.enc \
+ *   --keyring keyring.txt --pub alice-pub \
+ *   --kem Kyber-768 --chacha --chacha-nonce 00112233445566778899AABB \
+ *   --aad 01020304 --header
+ *
+ * # Generate a key into the keyring
+ * ZeroEcho -K --keystore keyring.txt --generate --alg Ed25519 --alias signing
+ *
+ * # Trailer signature (produce/verify)
+ * ZeroEcho -T --type signature --mode produce --alg Ed25519 \
+ *   --ks keyring.txt --priv signing.prv --in file.bin --out file.tagged
+ *
+ * # Covert EXIF embedding
+ * ZeroEcho -C --embed --jpeg in.jpg --payload secret.bin --output out.jpg
+ * }
+ * + * @since 1.0 + */ +package zeroecho; diff --git a/app/src/main/javadoc/overview.html b/app/src/main/javadoc/overview.html new file mode 100644 index 0000000..ef7806d --- /dev/null +++ b/app/src/main/javadoc/overview.html @@ -0,0 +1,78 @@ + + + + + ZeroEcho App Overview + + +

ZeroEcho Command-Line App

+ +

+ The ZeroEcho CLI is a streaming, security-first front end built on the lib module. + It exposes practical workflows for key management, hybrid/KEM envelopes, multi-recipient + protection, and covert payload embedding in JPEG EXIF metadata. The app favors explicit + configuration, safe defaults, and pipelines that avoid materializing large payloads. +

+ +

Commands

+
    +
  • guard - multi-recipient envelopes (public keys and/or passwords) with AES or ChaCha payloads.
  • +
  • kem - hybrid encryption: derive a content key via a KEM (e.g., Kyber), then encrypt the payload (AES/ChaCha).
  • +
  • keystore - manage a human-editable text keyring: list, generate, import, export.
  • +
  • covert - embed or extract a binary payload in JPEG EXIF fields using configurable slots.
  • +
+ +

Global usage

+

+ Each command supports --help for exact flags and examples. Inputs and outputs are streamed; + large files do not need to be fully loaded in memory. +

+ +

I/O conventions

+
    +
  • Streams are processed lazily; errors in verification surface at end of stream.
  • +
  • Authenticated modes (AES-GCM, ChaCha20-Poly1305) are the default where applicable.
  • +
  • For hybrid flows, shared secrets from agreement/KEM are fed through a KDF before use.
  • +
+ +

Keyring format

+

+ The keyring is a compact UTF-8 text file of entries with algorithm id, spec class, and encoded material. + It is intended to be versionable by humans but must be treated as sensitive data. +

+ +

Security notes

+
    +
  • Prefer authenticated encryption and strong KEM parameter sets.
  • +
  • Protect keyrings with OS permissions; avoid committing them to VCS.
  • +
  • Export encrypted content when targeting untrusted destinations; do not embed secrets in cleartext scripts.
  • +
+ +

Exit codes and logging

+
    +
  • Commands return 0 on success; non-zero indicates failure.
  • +
  • Errors go to STDERR; enable verbose logging for diagnostics as needed.
  • +
+ +

Examples (illustrative)

+
+# Generate a signing key into a text keyring
+zeroecho keystore --keyring keyring.txt --generate --alg Ed25519 --alias signing
+
+# Hybrid envelope with a KEM-derived content key and AES-GCM payload
+zeroecho kem --encrypt --keyring keyring.txt --recipient alice --kem Kyber-768 --symmetric aes-gcm --tag-bits 128
+
+# Multi-recipient envelope (password + public key)
+zeroecho guard --encrypt --keyring keyring.txt --to-password s3cret --to-alias bob
+
+# Covert EXIF embedding
+zeroecho covert --embed --jpeg in.jpg --payload secret.bin --slots exif.usercomment --output out.jpg
+
+ +

System requirements

+
    +
  • Java 21 or newer.
  • +
  • At least one JCA provider supplying the selected algorithms (e.g., JDK defaults, Bouncy Castle, a PQC provider).
  • +
+ + \ No newline at end of file diff --git a/app/src/test/java/zeroecho/CovertCommandTest.java b/app/src/test/java/zeroecho/CovertCommandTest.java new file mode 100644 index 0000000..65ad4e1 --- /dev/null +++ b/app/src/test/java/zeroecho/CovertCommandTest.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class CovertCommandTest { + + @TempDir + Path tempDir; + + private Path copyTestJpeg(String name) throws IOException { + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name)) { + if (inputStream == null) { + throw new IllegalArgumentException("Missing resource: " + name); + } + + Path target = tempDir.resolve(name); + Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING); + return target; + } + } + + @Test + void testEmbedAndExtractDefaultSlots() throws Exception { + System.out.println("testEmbedAndExtractDefaultSlots"); + + // Prepare input JPEG and payload + Path jpeg = copyTestJpeg("test.jpg"); + Path payload = tempDir.resolve("secret.txt"); + Path stegoOutput = tempDir.resolve("stego.jpg"); + Path extractedOutput = tempDir.resolve("extracted.dat"); + + String message = "SecretMessage123!"; + Files.write(payload, message.getBytes(StandardCharsets.UTF_8)); + + // --- Embed --- + Options embedOptions = new Options(); + int embedCode = CovertCommand.main(new String[] { "--embed", "--jpeg", jpeg.toString(), "--payload", + payload.toString(), "--output", stegoOutput.toString() }, embedOptions); + assertEquals(0, embedCode); + assertTrue(Files.exists(stegoOutput)); + assertTrue(Files.size(stegoOutput) > Files.size(jpeg)); + + // --- Extract --- + Options extractOptions = new Options(); + int extractCode = CovertCommand.main( + new String[] { "--extract", "--jpeg", stegoOutput.toString(), "--output", extractedOutput.toString() }, + extractOptions); + assertEquals(0, extractCode); + assertTrue(Files.exists(extractedOutput)); + + // Verify content + String extracted = Files.readString(extractedOutput); + assertEquals(message, extracted); + System.out.println("...ok"); + } + + @Test + void testEmbedFailsWithoutPayload() { + System.out.println("testEmbedFailsWithoutPayload"); + + Exception thrown = assertThrows(ParseException.class, () -> { + Options options = new Options(); + CovertCommand.main(new String[] { "--embed", "--jpeg", "input.jpg", "--output", "out.jpg" }, options); + }); + + assertTrue(thrown.getMessage().contains("--payload is required")); + System.out.println("...ok"); + } + + @Test + void testCustomSlotsEmbedding() throws Exception { + System.out.println("testCustomSlotsEmbedding"); + + Path jpeg = copyTestJpeg("test.jpg"); + Path payload = tempDir.resolve("msg.bin"); + Path stego = tempDir.resolve("custom_stego.jpg"); + Path extracted = tempDir.resolve("custom_extracted.bin"); + + String secret = "This uses custom slots!"; + Files.write(payload, secret.getBytes(StandardCharsets.UTF_8)); + + // Correctly defined custom slot string + String slotString = "Exif.UserComment:2048;Exif.CustomDesc/tag=700,ascii,64,exif:1024"; + + Options embedOptions = new Options(); + int code = CovertCommand.main(new String[] { "--embed", "--jpeg", jpeg.toString(), "--payload", + payload.toString(), "--output", stego.toString(), "--slots", slotString }, embedOptions); + assertEquals(0, code); + assertTrue(Files.exists(stego)); + + Options extractOptions = new Options(); + code = CovertCommand.main(new String[] { "--extract", "--jpeg", stego.toString(), "--output", + extracted.toString(), "--slots", slotString }, extractOptions); + assertEquals(0, code); + + String extractedMsg = Files.readString(extracted); + assertEquals(secret, extractedMsg); + System.out.println("...ok"); + } +} diff --git a/app/src/test/java/zeroecho/GuardTest.java b/app/src/test/java/zeroecho/GuardTest.java new file mode 100644 index 0000000..30980c2 --- /dev/null +++ b/app/src/test/java/zeroecho/GuardTest.java @@ -0,0 +1,354 @@ +/******************************************************************************* + * 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.assertThrows; + +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Random; + +import org.apache.commons.cli.Options; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import zeroecho.sdk.util.BouncyCastleActivator; + +/** + * CLI-level round-trip tests for the Guard subcommand. + * + *

Scope

These tests drive + * {@link Guard#main(String[], org.apache.commons.cli.Options)} with only the + * options implemented by Guard. They exercise: + *
    + *
  • Password-only encryption and decryption,
  • + *
  • RSA recipients with private-key based decryption,
  • + *
  • Mixed recipients (RSA + ElGamal + password) with decoys and default + * shuffling,
  • + *
  • AES-GCM (with tag bits and AAD) and ChaCha20-Poly1305 (with AAD and + * explicit nonce).
  • + *
+ * + *

+ * Keys are generated using the existing KeyStoreManagement CLI to populate a + * real KeyringStore file, as done in KemTest. The CLI persists aliases as + * {@code .pub} and {@code .prv}. + *

+ */ +public class GuardTest { + + /** All temporary files live here and are auto-cleaned by JUnit. */ + @TempDir + Path tmp; + + private PrintStream savedOut; + + @BeforeAll + static void bootBouncyCastle() { + System.out.println("bootBouncyCastle()"); + // The project initializes BC explicitly where needed. + BouncyCastleActivator.init(); + System.out.println("bootBouncyCastle...ok"); + } + + @AfterEach + void restoreStdout() { + if (savedOut != null) { + System.setOut(savedOut); + savedOut = null; + } + } + + /** + * Password-only round trip using AES-GCM with header and AAD. + * + *

+ * This does not require a keyring. + *

+ */ + @Test + void password_aesGcm_roundTrip_ok() throws Exception { + final String method = "password_aesGcm_roundTrip_ok()"; + final String password = "Tr0ub4dor&3"; + final int size = 4096; + final String aadHex = "A1B2C3D4"; + final int tagBits = 128; + + System.out.println(method); + System.out.println("...params: size=" + size + " tagBits=" + tagBits + " aadHex=" + aadHex); + + Path in = writeRandom(tmp.resolve("pt.bin"), size, 0xA11CE01); + Path enc = tmp.resolve("pt.bin.enc"); + Path dec = tmp.resolve("pt.bin.dec"); + + // Encrypt + String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", password, "--alg", + "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex }; + System.out.println("...encrypt: " + Arrays.toString(encArgs)); + int e = Guard.main(encArgs, new Options()); + assertEquals(0, e, "... encrypt expected exit code 0"); + + // Decrypt (using password) + String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--password", password, "--alg", + "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex }; + System.out.println("...decrypt: " + Arrays.toString(decArgs)); + int d = Guard.main(decArgs, new Options()); + assertEquals(0, d, "... decrypt expected exit code 0"); + + assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec), "AES-GCM password round-trip mismatch"); + System.out.println("...ok"); + } + + /** + * RSA recipient round trips with both AES-GCM and ChaCha20-Poly1305 payloads. + * + *

+ * Keys are generated through the real KeyStoreManagement CLI and read via Guard + * with --to-alias and --priv-alias. + *

+ */ + @Test + void rsa_alias_aesGcm_and_chacha_roundTrip_ok() throws Exception { + final String method = "rsa_alias_aesGcm_and_chacha_roundTrip_ok()"; + final String base = "alice"; + final String rsaId = "RSA"; + final int sizeAes = 8192; + final int sizeCha = 3072; + final int tagBits = 128; + final String aadAes = "010203"; + final String aadCha = "D00DFEED"; + final String chNonce = "00112233445566778899AABB"; + + System.out.println(method); + System.out.println("...params: sizeAes=" + sizeAes + " sizeCha=" + sizeCha + " tagBits=" + tagBits + " aadAes=" + + aadAes + " aadCha=" + aadCha + " chNonce=" + chNonce); + + // Prepare keyring with RSA pair + Path ring = tmp.resolve("ring-rsa.txt"); + KeyAliases rsa = generateIntoKeyStore(ring, rsaId, base); + + // AES-GCM round-trip + { + Path in = writeRandom(tmp.resolve("rsa-pt-aes.bin"), sizeAes, 0x5157A11); + Path enc = tmp.resolve("rsa-pt-aes.bin.enc"); + Path dec = tmp.resolve("rsa-pt-aes.bin.dec"); + + String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(), + "--to-alias", rsa.pub, "--alg", "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", + aadAes }; + System.out.println("...AES encrypt: " + Arrays.toString(encArgs)); + int e = Guard.main(encArgs, new Options()); + assertEquals(0, e, "... AES encrypt rc"); + + String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--keyring", ring.toString(), + "--priv-alias", rsa.prv, "--alg", "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", + aadAes }; + System.out.println("...AES decrypt: " + Arrays.toString(decArgs)); + int d = Guard.main(decArgs, new Options()); + assertEquals(0, d, "... AES decrypt rc"); + + assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec), "RSA AES-GCM round-trip mismatch"); + System.out.println("...AES round-trip ok"); + } + + // ChaCha20-Poly1305 round-trip + { + Path in = writeRandom(tmp.resolve("rsa-pt-ch.bin"), sizeCha, 0xC0FFEE1); + Path enc = tmp.resolve("rsa-pt-ch.bin.enc"); + Path dec = tmp.resolve("rsa-pt-ch.bin.dec"); + + String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(), + "--to-alias", rsa.pub, "--alg", "chacha-aead", "--aad-hex", aadCha, "--nonce-hex", chNonce }; + System.out.println("...ChaCha encrypt: " + Arrays.toString(encArgs)); + int e = Guard.main(encArgs, new Options()); + assertEquals(0, e, "... ChaCha encrypt rc"); + + String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--keyring", ring.toString(), + "--priv-alias", rsa.prv, "--alg", "chacha-aead", "--aad-hex", aadCha, "--nonce-hex", chNonce }; + System.out.println("...ChaCha decrypt: " + Arrays.toString(decArgs)); + int d = Guard.main(decArgs, new Options()); + assertEquals(0, d, "... ChaCha decrypt rc"); + + assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec), + "RSA ChaCha20-Poly1305 round-trip mismatch"); + System.out.println("...ChaCha round-trip ok"); + } + + System.out.println("...ok"); + } + + /** + * Mixed recipients with decoys: RSA + password recipients plus an ElGamal decoy + * alias. + * + *

+ * Verifies that default shuffling does not prevent decryption and that both + * password and private-key paths can unlock. + *

+ */ + @Test + void mixed_recipients_with_decoys_roundTrip_ok() throws Exception { + final String method = "mixed_recipients_with_decoys_roundTrip_ok()"; + final int size = 4096; + + System.out.println(method); + + // Prepare keyring with RSA and ElGamal + Path ring = tmp.resolve("ring-mixed.txt"); + KeyAliases rsa = generateIntoKeyStore(ring, "RSA", "bob"); + KeyAliases elg = generateIntoKeyStore(ring, "ElGamal", "carol"); // used as decoy alias + + Path in = writeRandom(tmp.resolve("pt-mixed.bin"), size, 0xBADC0DE); + Path enc = tmp.resolve("pt-mixed.bin.enc"); + Path dec1 = tmp.resolve("pt-mixed.bin.dec1"); + Path dec2 = tmp.resolve("pt-mixed.bin.dec2"); + + final String password = "correct horse battery staple"; + final String aad = "FEEDFACE"; + final int tagBits = 128; + + // Encrypt with: RSA real recipient, password real recipient, ElGamal decoy + // alias, + // plus 2 random password decoys. Recipients are shuffled by default. + String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(), + "--to-alias", rsa.pub, "--to-psw", password, "--decoy-alias", elg.pub, "--decoy-psw-rand", "2", "--alg", + "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad }; + System.out.println("...encrypt: " + Arrays.toString(encArgs)); + int e = Guard.main(encArgs, new Options()); + assertEquals(0, e, "... encrypt rc"); + + // Decrypt via private RSA key + String[] decPriv = { "--decrypt", enc.toString(), "--output", dec1.toString(), "--keyring", ring.toString(), + "--priv-alias", rsa.prv, "--alg", "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", + aad }; + System.out.println("...decrypt(private): " + Arrays.toString(decPriv)); + int d1 = Guard.main(decPriv, new Options()); + assertEquals(0, d1, "... decrypt(private) rc"); + assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec1), + "mixed recipients decrypt(private) mismatch"); + + // Decrypt via password instead of key + String[] decPwd = { "--decrypt", enc.toString(), "--output", dec2.toString(), "--password", password, "--alg", + "aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad }; + System.out.println("...decrypt(password): " + Arrays.toString(decPwd)); + int d2 = Guard.main(decPwd, new Options()); + assertEquals(0, d2, "... decrypt(password) rc"); + assertArrayEquals(Files.readAllBytes(in), Files.readAllBytes(dec2), + "mixed recipients decrypt(password) mismatch"); + + System.out.println("...ok"); + } + + /** + * Negative: decryption should fail to parse when both --priv-alias and + * --password are given. + */ + @Test + void decrypt_with_both_unlock_material_rejected() throws Exception { + final String method = "decrypt_with_both_unlock_material_rejected()"; + System.out.println(method); + + // Minimal valid blob: encrypt with password, then try to decrypt specifying + // both unlock options + Path in = writeRandom(tmp.resolve("pt-neg.bin"), 256, 0xABCD); + Path enc = tmp.resolve("pt-neg.bin.enc"); + String pwd = "x"; + + String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", pwd, "--alg", + "aes-gcm", "--tag-bits", "128" }; + int e = Guard.main(encArgs, new Options()); + assertEquals(0, e, "... encrypt rc"); + + // Supply both options on purpose + Exception ex = assertThrows(Exception.class, () -> { + String[] bad = { "--decrypt", enc.toString(), "--output", tmp.resolve("out-neg.bin").toString(), + "--password", pwd, "--priv-alias", "whatever", "--alg", "aes-gcm" }; + Guard.main(bad, new Options()); + }); + System.out.println("...got expected exception: " + ex); + System.out.println("...ok"); + } + + // --------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------- + + private static Path writeRandom(Path p, int size, long seed) throws Exception { + byte[] b = new byte[size]; + new Random(seed).nextBytes(b); + Files.write(p, b); + return p; + } + + /** + * Generates an asymmetric keypair using the KeyStoreManagement CLI. + * + *

+ * The CLI stores aliases as {@code .pub} and {@code .prv}. + *

+ * + * @param ring keyring file path + * @param algId algorithm id, e.g. "RSA", "ElGamal", "ML-KEM" + * @param baseAlias base alias without suffix + * @return public/private aliases to be used with Guard + */ + private static KeyAliases generateIntoKeyStore(Path ring, String algId, String baseAlias) throws Exception { + String[] genArgs = { "--keystore", ring.toString(), "--generate", "--alg", algId, "--alias", baseAlias, + "--kind", "asym" }; + System.out.println("...KeyStoreManagement generate: " + Arrays.toString(genArgs)); + int rc = KeyStoreManagement.main(genArgs, new Options()); + if (rc != 0) { + throw new GeneralSecurityException("KeyStoreManagement failed with rc=" + rc + " for " + algId); + } + return new KeyAliases(baseAlias + ".pub", baseAlias + ".prv"); + } + + private static final class KeyAliases { + final String pub; + final String prv; + + KeyAliases(String pub, String prv) { + this.pub = pub; + this.prv = prv; + } + } +} diff --git a/app/src/test/java/zeroecho/KemTest.java b/app/src/test/java/zeroecho/KemTest.java new file mode 100644 index 0000000..a1a7e24 --- /dev/null +++ b/app/src/test/java/zeroecho/KemTest.java @@ -0,0 +1,319 @@ +/******************************************************************************* + * 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 java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import org.apache.commons.cli.Options; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import zeroecho.core.storage.KeyringStore; +import zeroecho.sdk.util.BouncyCastleActivator; + +/** + * CLI-level tests for KEMAes that: + *
    + *
  • Verify {@code --list-kems} runs without a keyring.
  • + *
  • Iterate over all available KEM ids discovered via + * {@code --list-kems} and for each id perform hybrid round-trips: + *
      + *
    • AES-GCM with header and AAD,
    • + *
    • ChaCha20-Poly1305 with header and AAD.
    • + *
    + *
  • + *
+ * + *

+ * The tests: + *

    + *
  • use the real KeyStore format by calling {@link KeyStoreManagement} to + * generate a keypair,
  • + *
  • pass the resulting aliases into + * {@code KEMAes.main(String[], Options)},
  • + *
  • print the method name and parameters first, progress lines prefixed by + * {@code "..."},
  • + *
  • end with {@code "...ok"} on success.
  • + *
+ */ +public class KemTest { + + /** All temporary files live here and are auto-cleaned by JUnit. */ + @TempDir + Path tmp; + + private PrintStream savedOut; + + @BeforeAll + static void bootBouncyCastle() { + System.out.println("bootBouncyCastle()"); + // The project initializes BC explicitly for KEM implementations. + BouncyCastleActivator.init(); + System.out.println("bootBouncyCastle...ok"); + } + + @AfterEach + void restoreStdout() { + if (savedOut != null) { + System.setOut(savedOut); + savedOut = null; + } + } + + /** + * Confirms that {@code --list-kems} short-circuits and exits 0 without + * requiring a keyring. + */ + @Test + void listKems_runs() throws Exception { + System.out.println("listKems_runs()"); + Options opts = new Options(); + String[] args = { "--list-kems" }; + + System.out.println("...invoking: " + Arrays.toString(args)); + int rc = Kem.main(args, opts); + assertEquals(0, rc, "... expected exit code 0"); + System.out.println("...ok"); + } + + /** + * Runs hybrid round-trips for every KEM listed by {@code --list-kems}: + *
    + *
  1. Generate a real KeyStore with a fresh keypair for the KEM,
  2. + *
  3. Encrypt+decrypt using AES-GCM (header+AAD),
  4. + *
  5. Encrypt+decrypt using ChaCha20-Poly1305 (header+AAD).
  6. + *
+ * + *

+ * If any KEM fails at any step, the test records it and continues. At the end, + * it fails with a concise summary of all failures. + *

+ */ + @Test + void allKems_encryptDecrypt_aesAndChacha() throws Exception { + final String method = "allKems_encryptDecrypt_aesAndChacha()"; + final int aesSize = 8192; + final int chachaSize = 4096; + final int gcmTagBits = 128; + final String aadAes = "A1B2C3"; + final String aadChaCha = "DEADBEEF"; + final String nonceChaCha = "00112233445566778899AABB"; + + System.out.println(method); + System.out.println("...params: aesSize=" + aesSize + " chachaSize=" + chachaSize + " gcmTagBits=" + gcmTagBits + + " aesAAD=" + aadAes + " chachaAAD=" + aadChaCha + " chachaNonce=" + nonceChaCha); + + // Discover KEM ids via the CLI (ensures we use exactly the ids users will see). + List kemIds = listKemsViaCli(); + System.out.println("...discovered " + kemIds.size() + " KEM ids"); + if (kemIds.isEmpty()) { + throw new GeneralSecurityException("No KEM algorithms reported by --list-kems"); + } + + List failures = new ArrayList<>(); + + for (String kemId : kemIds) { + System.out.println("...KEM " + kemId + " begin"); + + try { + // Real keystore for this KEM + Path ring = tmp.resolve("ring-" + kemId.replace('/', '_') + ".txt"); + KeyAliases aliases = generateKemIntoKeyStore(ring, kemId, "alias-" + shortId(kemId)); + + // Sanity: re-open to ensure the file is valid + KeyringStore ks = KeyringStore.load(ring); + if (!(ks.contains(aliases.pub) && ks.contains(aliases.prv))) { + throw new IllegalStateException("Keyring does not contain expected aliases for " + kemId); + } + + // AES-GCM round-trip + { + byte[] content = randomBytes(aesSize); + Path plain = tmp.resolve("plain-aes-" + shortId(kemId) + ".bin"); + Path enc = tmp.resolve("enc-aes-" + shortId(kemId) + ".bin"); + Path dec = tmp.resolve("dec-aes-" + shortId(kemId) + ".bin"); + Files.write(plain, content); + System.out.println("...[" + kemId + "] AES encrypt"); + int e = Kem.main(new String[] { "--encrypt", plain.toString(), "--output", enc.toString(), + "--keyring", ring.toString(), "--pub", aliases.pub, "--kem", kemId, "--aes", "--aes-cipher", + "gcm", "--aes-tag-bits", Integer.toString(gcmTagBits), "--header", "--aad", aadAes }, + new Options()); + if (e != 0) { + throw new IllegalStateException("AES encrypt rc=" + e); + } + System.out.println("...[" + kemId + "] AES decrypt"); + int d = Kem.main(new String[] { "--decrypt", enc.toString(), "--output", dec.toString(), + "--keyring", ring.toString(), "--priv", aliases.prv, "--kem", kemId, "--aes", + "--aes-cipher", "gcm", "--aes-tag-bits", Integer.toString(gcmTagBits), "--header", "--aad", + aadAes }, new Options()); + if (d != 0) { + throw new IllegalStateException("AES decrypt rc=" + d); + } + byte[] back = Files.readAllBytes(dec); + assertArrayEquals(content, back, "[" + kemId + "] AES-GCM round-trip mismatch"); + System.out.println("...[" + kemId + "] AES round-trip ok"); + } + + // ChaCha20-Poly1305 round-trip (AEAD implied by AAD) + { + byte[] content = randomBytes(chachaSize); + Path plain = tmp.resolve("plain-cc20-" + shortId(kemId) + ".bin"); + Path enc = tmp.resolve("enc-cc20-" + shortId(kemId) + ".bin"); + Path dec = tmp.resolve("dec-cc20-" + shortId(kemId) + ".bin"); + Files.write(plain, content); + System.out.println("...[" + kemId + "] ChaCha encrypt"); + int e = Kem.main(new String[] { "--encrypt", plain.toString(), "--output", enc.toString(), + "--keyring", ring.toString(), "--pub", aliases.pub, "--kem", kemId, "--chacha", + "--chacha-nonce", nonceChaCha, "--aad", aadChaCha, "--header" }, new Options()); + if (e != 0) { + throw new IllegalStateException("ChaCha encrypt rc=" + e); + } + System.out.println("...[" + kemId + "] ChaCha decrypt"); + int d = Kem.main(new String[] { "--decrypt", enc.toString(), "--output", dec.toString(), + "--keyring", ring.toString(), "--priv", aliases.prv, "--kem", kemId, "--chacha", + "--chacha-nonce", nonceChaCha, "--aad", aadChaCha, "--header" }, new Options()); + if (d != 0) { + throw new IllegalStateException("ChaCha decrypt rc=" + d); + } + byte[] back = Files.readAllBytes(dec); + assertArrayEquals(content, back, "[" + kemId + "] ChaCha20-Poly1305 round-trip mismatch"); + System.out.println("...[" + kemId + "] ChaCha round-trip ok"); + } + + System.out.println("...KEM " + kemId + " ok"); + } catch (Throwable t) { + System.out.println("...KEM " + kemId + " FAILED: " + t); + failures.add(kemId + " -> " + t.getClass().getSimpleName() + ": " + t.getMessage()); + } + } + + if (!failures.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append("Some KEM(s) failed:\n"); + for (String f : failures) { + sb.append(" - ").append(f).append('\n'); + } + throw new AssertionError(sb.toString()); + } + + System.out.println("...ok"); + } + + // --------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------- + + /** + * Calls the CLI entrypoint with {@code --list-kems} and parses stdout lines to + * a list of ids. + */ + private List listKemsViaCli() throws Exception { + savedOut = System.out; + ByteArrayOutputStream sink = new ByteArrayOutputStream(); + System.setOut(new PrintStream(sink, true, StandardCharsets.UTF_8)); + try { + int rc = Kem.main(new String[] { "--list-kems" }, new Options()); + if (rc != 0) { + throw new IllegalStateException("--list-kems rc=" + rc); + } + } finally { + System.setOut(savedOut); + savedOut = null; + } + String out = sink.toString(StandardCharsets.UTF_8); + List ids = new ArrayList<>(); + for (String line : out.split("\\R")) { + String id = line.trim(); + if (!id.isEmpty() && !id.startsWith("(")) { + ids.add(id); + } + } + return ids; + } + + /** + * Generates a KEM keypair using the real KeyStoreManagement CLI and returns the + * public/private aliases. The CLI stores aliases as {@code .pub} and + * {@code .prv}. + */ + private static KeyAliases generateKemIntoKeyStore(Path ring, String kemId, String baseAlias) throws Exception { + String[] genArgs = { "--keystore", ring.toString(), "--generate", "--alg", kemId, "--alias", baseAlias, + "--kind", "asym" }; + System.out.println("...KeyStoreManagement generate: " + Arrays.toString(genArgs)); + int rc = KeyStoreManagement.main(genArgs, new Options()); + if (rc != 0) { + throw new GeneralSecurityException("KeyStoreManagement failed with rc=" + rc + " for " + kemId); + } + return new KeyAliases(baseAlias + ".pub", baseAlias + ".prv"); + } + + private static String shortId(String kemId) { + String s = kemId.replaceAll("[^A-Za-z0-9]+", ""); + if (s.length() > 16) { + s = s.substring(0, 16); + } + return s; + } + + private static byte[] randomBytes(int n) { + byte[] b = new byte[n]; + new Random(0x5EEDC0DEL).nextBytes(b); + return b; + } + + private static final class KeyAliases { + final String pub; + final String prv; + + KeyAliases(String pub, String prv) { + this.pub = pub; + this.prv = prv; + } + } +} diff --git a/app/src/test/java/zeroecho/KeyStoreManagementTest.java b/app/src/test/java/zeroecho/KeyStoreManagementTest.java new file mode 100644 index 0000000..320b875 --- /dev/null +++ b/app/src/test/java/zeroecho/KeyStoreManagementTest.java @@ -0,0 +1,241 @@ +/******************************************************************************* + * 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.assertTrue; + +import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.UUID; + +import javax.crypto.SecretKey; + +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.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.storage.KeyringStore; +import zeroecho.sdk.util.BouncyCastleActivator; + +/** + * KeyStoreManagementTest drives the KeyStoreManagement CLI across all available + * algorithms, printing progress and verifying that generated entries can be + * materialized via KeyringStore. + * + *

What it does

+ *
    + *
  • For each algorithm that exposes a default-capable asymmetric builder, + * generates a keypair.
  • + *
  • For each algorithm that exposes a default-capable symmetric builder, + * generates a secret.
  • + *
  • Reloads the keyring and materializes keys using KeyringStore.
  • + *
  • Prints progress and brief details to stdout during the run.
  • + *
+ */ +public class KeyStoreManagementTest { + + @TempDir + Path tmp; + + @BeforeAll + static void setupProviders() { + BouncyCastleActivator.init(); + } + + /** + * Generates keypairs and secrets where supported and verifies materialization. + * + * @throws Exception on unexpected failure + */ + @Test + public void generateAndVerifyAllAlgorithms() throws Exception { + Path ring = tmp.resolve("ring.txt"); + + Set algIds = CryptoAlgorithms.available(); + System.out.println("Algorithms: " + algIds); + assertTrue(!algIds.isEmpty(), "Catalog should not be empty"); + + int attempted = 0; + for (String id : algIds) { + Options dispatcher = new Options(); + CryptoAlgorithm alg = CryptoAlgorithms.require(id); + boolean doAsym = hasAsymmetricDefault(alg); + boolean doSym = hasSymmetricDefault(alg); + + if (doAsym) { + String alias = "asym-" + sanitize(id) + "-" + UUID.randomUUID().toString().substring(0, 8); + System.out.println("Generating asymmetric: " + id + " -> " + alias + ".pub/.prv"); + String[] argv = new String[] { "--keystore", ring.toString(), "--generate", "--alg", id, "--alias", + alias, "--kind", "asym" }; + try { + int rc = KeyStoreManagement.main(argv, dispatcher); + System.out.println(" rc=" + rc); + assertTrue(rc == 0, "asymmetric generation failed for " + id); + attempted++; + } catch (Throwable t) { + System.out.println( + " SKIP asym " + id + " due to " + t.getClass().getSimpleName() + ": " + t.getMessage()); + } + } + + if (doSym) { + String alias = "sym-" + sanitize(id) + "-" + UUID.randomUUID().toString().substring(0, 8); + System.out.println("Generating symmetric: " + id + " -> " + alias); + String[] argv = new String[] { "--keystore", ring.toString(), "--generate", "--alg", id, "--alias", + alias, "--kind", "sym" }; + try { + int rc = KeyStoreManagement.main(argv, dispatcher); + System.out.println(" rc=" + rc); + assertTrue(rc == 0, "symmetric generation failed for " + id); + attempted++; + } catch (Throwable t) { + System.out.println( + " SKIP sym " + id + " due to " + t.getClass().getSimpleName() + ": " + t.getMessage()); + } + } + } + + assertTrue(attempted > 0, "No generation attempts were successful"); + + // Verify by reloading and materializing. + KeyringStore store = KeyringStore.load(ring); + List aliases = store.aliases(); + System.out.println("Reloaded aliases (" + aliases.size() + "): " + aliases); + + int ok = 0; + for (int i = 0; i < aliases.size(); i++) { + String a = aliases.get(i); + boolean good = false; + try { + PublicKey pub = store.getPublic(a); + if (pub != null) { + System.out.println(" OK public: " + a + " alg=" + pub.getAlgorithm() + " encLen=" + + encLen(pub.getEncoded())); + good = true; + } + } catch (Throwable ignored) { + } + if (!good) { + try { + PrivateKey prv = store.getPrivate(a); + if (prv != null) { + System.out.println(" OK private: " + a + " alg=" + prv.getAlgorithm() + " encLen=" + + encLen(prv.getEncoded())); + good = true; + } + } catch (Throwable ignored) { + } + } + if (!good) { + try { + SecretKey sk = store.getSecret(a); + if (sk != null) { + byte[] raw = sk.getEncoded(); + System.out.println(" OK secret: " + a + " alg=" + sk.getAlgorithm() + " len=" + + (raw == null ? 0 : raw.length)); + good = true; + } + } catch (Throwable ignored) { + } + } + if (good) { + ok++; + } + } + assertTrue(ok > 0, "No entries could be materialized back"); + } + + // ---- helpers ---- + + private static boolean hasAsymmetricDefault(CryptoAlgorithm alg) { + try { + List infos = alg.asymmetricBuildersInfo(); + for (int i = 0; i < infos.size(); i++) { + CryptoAlgorithm.AsymBuilderInfo bi = infos.get(i); + if (bi.defaultKeySpec == null) { + continue; + } + @SuppressWarnings("unchecked") + Class st = (Class) bi.specType; + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(st); + if (b != null) { + return true; + } + } + } catch (Throwable t) { + return false; + } + return false; + } + + private static boolean hasSymmetricDefault(CryptoAlgorithm alg) { + try { + List infos = alg.symmetricBuildersInfo(); + for (int i = 0; i < infos.size(); i++) { + CryptoAlgorithm.SymBuilderInfo bi = infos.get(i); + if (bi.defaultKeySpec() == null) { + continue; + } + @SuppressWarnings("unchecked") + Class st = (Class) bi.specType(); + SymmetricKeyBuilder b = alg.symmetricKeyBuilder(st); + if (b != null) { + return true; + } + } + } catch (Throwable t) { + return false; + } + return false; + } + + private static String sanitize(String id) { + return id.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]+", "-"); + } + + private static String encLen(byte[] der) { + return der == null ? "0" : Integer.toString(der.length); + } +} diff --git a/app/src/test/java/zeroecho/TagTest.java b/app/src/test/java/zeroecho/TagTest.java new file mode 100644 index 0000000..1b03bb1 --- /dev/null +++ b/app/src/test/java/zeroecho/TagTest.java @@ -0,0 +1,314 @@ +/******************************************************************************* + * 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.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Random; + +import org.apache.commons.cli.Options; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import zeroecho.core.storage.KeyringStore; +import zeroecho.sdk.util.BouncyCastleActivator; + +/** + * CLI-level tests for the Tag subcommand (signature + digest). + * + *

Scope

+ *
    + *
  • Signature produce+verify with Ed25519 via a real keyring file,
  • + *
  • Signature verify failure appends marker text and returns rc=1,
  • + *
  • Digest produce+verify (SHA-256) round trip,
  • + *
  • Digest verify failure appends marker text and returns rc=1,
  • + *
  • Digest round trip over STDIN/STDOUT.
  • + *
+ */ +public class TagTest { + + @TempDir + Path tmp; + + private PrintStream savedOut; + private PrintStream savedErr; + private java.io.InputStream savedIn; + + @BeforeAll + static void bootBouncyCastle() { + BouncyCastleActivator.init(); + } + + @AfterEach + void restoreStd() { + if (savedOut != null) { + System.setOut(savedOut); + savedOut = null; + } + if (savedErr != null) { + System.setErr(savedErr); + savedErr = null; + } + if (savedIn != null) { + System.setIn(savedIn); + savedIn = null; + } + } + + // ---------- boilerplate logging ---------- + private static void logBegin(Object... params) { + String thisClass = TagTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = TagTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + System.out.println(); + } + + /** Ed25519 signature round-trip using files. */ + @Test + void signature_ed25519_roundTrip_files_ok() throws Exception { + logBegin("signature_ed25519_roundTrip_files_ok"); + + Path ring = tmp.resolve("ring-ed25519.txt"); + KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed"); + // sanity + KeyringStore ks = KeyringStore.load(ring); + assertTrue(ks.contains(ed.pub) && ks.contains(ed.prv), "missing expected aliases"); + + byte[] pt = randomBytes(4096); + Path plain = tmp.resolve("plain.bin"); + Path signed = tmp.resolve("signed.bin"); + Path recovered = tmp.resolve("recovered.bin"); + Files.write(plain, pt); + + // produce + String[] produce = { "--type", "signature", "--mode", "produce", "--alg", "Ed25519", "--ks", ring.toString(), + "--priv", ed.prv, "--in", plain.toString(), "--out", signed.toString() }; + assertEquals(0, Tag.main(produce, new Options()), "produce rc"); + + // verify (match) + String[] verify = { "--type", "signature", "--mode", "verify", "--alg", "Ed25519", "--ks", ring.toString(), + "--pub", ed.pub, "--in", signed.toString(), "--out", recovered.toString() }; + assertEquals(0, Tag.main(verify, new Options()), "verify rc"); + + assertArrayEquals(pt, Files.readAllBytes(recovered), "round-trip mismatch"); + + logEnd(); + } + + @Test + void signature_verify_mismatch_throws_and_appends_text() throws Exception { + logBegin("signature_verify_mismatch_throws_and_appends_text"); + + Path ring = tmp.resolve("ring-ed25519-neg.txt"); + KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed-neg"); + + byte[] pt = randomBytes(1024); + Path plain = tmp.resolve("plain-neg.bin"); + Path signed = tmp.resolve("signed-neg.bin"); + Path out = tmp.resolve("out-neg.bin"); + Files.write(plain, pt); + + // produce + assertEquals(0, + Tag.main(new String[] { "--type", "signature", "--mode", "produce", "--alg", "Ed25519", "--ks", + ring.toString(), "--priv", ed.prv, "--in", plain.toString(), "--out", signed.toString() }, + new Options())); + + // corrupt last byte -> break signature + flipLastByte(signed); + + // verify (mismatch): expect throw + marker appended + assertEquals(1, + Tag.main( + new String[] { "--type", "signature", "--mode", "verify", "--alg", "Ed25519", "--ks", + ring.toString(), "--pub", ed.pub, "--in", signed.toString(), "--out", out.toString() }, + new Options())); + + assertTrue(Files.notExists(out, LinkOption.NOFOLLOW_LINKS)); + + logEnd(); + } + + /** SHA-256 digest round-trip using files. */ + @Test + void digest_sha256_roundTrip_files_ok() throws Exception { + logBegin("digest_sha256_roundTrip_files_ok"); + + byte[] pt = randomBytes(5000); + Path plain = tmp.resolve("plain-d.bin"); + Path tagged = tmp.resolve("tagged-d.bin"); + Path recovered = tmp.resolve("recovered-d.bin"); + Files.write(plain, pt); + + // produce + assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in", + plain.toString(), "--out", tagged.toString() }, new Options())); + + // verify (match) + assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in", + tagged.toString(), "--out", recovered.toString() }, new Options())); + + assertArrayEquals(pt, Files.readAllBytes(recovered), "digest round-trip mismatch"); + + logEnd(); + } + + @Test + void digest_verify_mismatch_throws_and_appends_text() throws Exception { + logBegin("digest_verify_mismatch_throws_and_appends_text"); + + byte[] pt = randomBytes(2048); + Path plain = tmp.resolve("plain-d-neg.bin"); + Path tagged = tmp.resolve("tagged-d-neg.bin"); + Path out = tmp.resolve("out-d-neg.bin"); + Files.write(plain, pt); + + // produce + assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in", + plain.toString(), "--out", tagged.toString() }, new Options())); + + // corrupt last byte -> break digest + flipLastByte(tagged); + + // verify (mismatch): expect throw + default marker ("digest invalid") + assertEquals(1, Tag.main(new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in", + tagged.toString(), "--out", out.toString() }, new Options())); + + assertTrue(Files.notExists(out, LinkOption.NOFOLLOW_LINKS)); + + logEnd(); + } + + /** Digest round trip over STDIN/STDOUT. */ + @Test + void digest_roundTrip_stdin_stdout_ok() throws Exception { + logBegin("digest_roundTrip_stdin_stdout_ok"); + + byte[] pt = randomBytes(3000); + + // produce (stdin -> stdout) + savedIn = System.in; + savedOut = System.out; + ByteArrayInputStream src = new ByteArrayInputStream(pt); + ByteArrayOutputStream producedSink = new ByteArrayOutputStream(); + System.setIn(src); + System.setOut(new PrintStream(producedSink, true, StandardCharsets.UTF_8)); + + assertEquals(0, Tag.main( + new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in", "-", "--out", "-" }, + new Options())); + + // save produced bytes + Path tagged = tmp.resolve("stdio-tagged.bin"); + Files.write(tagged, producedSink.toByteArray()); + + // verify (file -> stdout) + ByteArrayOutputStream verifiedSink = new ByteArrayOutputStream(); + System.setIn(savedIn); // not used in this step + System.setOut(new PrintStream(verifiedSink, true, StandardCharsets.UTF_8)); + + assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in", + tagged.toString(), "--out", "-" }, new Options())); + + assertArrayEquals(pt, verifiedSink.toByteArray(), "stdio round-trip mismatch"); + + logEnd(); + } + + // --------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------- + + /** Generates an asymmetric keypair using the real KeyStoreManagement CLI. */ + private static KeyAliases generateIntoKeyStore(Path ring, String algId, String baseAlias) throws Exception { + String[] genArgs = { "--keystore", ring.toString(), "--generate", "--alg", algId, "--alias", baseAlias, + "--kind", "asym" }; + int rc = KeyStoreManagement.main(genArgs, new Options()); + if (rc != 0) { + throw new GeneralSecurityException("KeyStoreManagement failed with rc=" + rc + " for " + algId); + } + return new KeyAliases(baseAlias + ".pub", baseAlias + ".prv"); + } + + private static void flipLastByte(Path file) throws Exception { + byte[] all = Files.readAllBytes(file); + if (all.length == 0) { + throw new IllegalStateException("cannot flip byte in empty file"); + } + all[all.length - 1] ^= 0xFF; + Files.write(file, all); + } + + private static byte[] randomBytes(int n) { + byte[] b = new byte[n]; + new Random(0xBADC0FFEL).nextBytes(b); + return b; + } + + private static final class KeyAliases { + final String pub; + final String prv; + + KeyAliases(String pub, String prv) { + this.pub = pub; + this.prv = prv; + } + } +} \ No newline at end of file diff --git a/app/src/test/java/zeroecho/ZeroEchoTest.java b/app/src/test/java/zeroecho/ZeroEchoTest.java new file mode 100644 index 0000000..45ba976 --- /dev/null +++ b/app/src/test/java/zeroecho/ZeroEchoTest.java @@ -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"); + } +} diff --git a/app/src/test/resources/test.jpg b/app/src/test/resources/test.jpg new file mode 100644 index 0000000..fa80f7b Binary files /dev/null and b/app/src/test/resources/test.jpg differ diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..7508092 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,9 @@ +plugins { + // Support convention plugins written in Groovy. Convention plugins are build scripts in 'src/main' that automatically become available as plugins in the main build. + id 'groovy-gradle-plugin' +} + +repositories { + // Use the plugin portal to apply community plugins in convention plugins. + gradlePluginPortal() +} diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 0000000..98a8010 --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1,8 @@ +dependencyResolutionManagement { + // Reuse version catalog from the main build. + versionCatalogs { + create('libs', { from(files("../gradle/libs.versions.toml")) }) + } +} + +rootProject.name = 'buildSrc' diff --git a/buildSrc/src/main/groovy/buildlogic.java-application-conventions.gradle b/buildSrc/src/main/groovy/buildlogic.java-application-conventions.gradle new file mode 100644 index 0000000..de9862a --- /dev/null +++ b/buildSrc/src/main/groovy/buildlogic.java-application-conventions.gradle @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + // Apply the common convention plugin for shared build configuration between library and application projects. + id 'buildlogic.java-common-conventions' + + // Apply the application plugin to add support for building a CLI application in Java. + id 'application' +} diff --git a/buildSrc/src/main/groovy/buildlogic.java-common-conventions.gradle b/buildSrc/src/main/groovy/buildlogic.java-common-conventions.gradle new file mode 100644 index 0000000..d293c19 --- /dev/null +++ b/buildSrc/src/main/groovy/buildlogic.java-common-conventions.gradle @@ -0,0 +1,165 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + // Apply the java Plugin to add support for Java. + id 'java' + id 'maven-publish' + id 'pmd' + id 'com.palantir.git-version' +} + +import java.time.LocalDate + +def currentYear = LocalDate.now().getYear() + +project.version = gitVersion(prefix:'release@') + +repositories { + + maven { + name = "GiteaMaven" + url = uri("https://gitea.egothor.org/api/packages/Egothor/maven") + } + + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + constraints { + // Define dependency versions as constraints + implementation 'org.apache.commons:commons-text:1.11.0' + implementation 'commons-cli:commons-cli:1.9.0' + implementation 'org.bouncycastle:bcpkix-jdk18on:1.81' + implementation 'org.egothor:conflux:[1.0,2.0)' + implementation 'org.apache.commons:commons-imaging:1.0.0-alpha6' + } + + // Use JUnit Jupiter for testing. + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +pmd { + consoleOutput = true + toolVersion = '7.16.0' + sourceSets = [sourceSets.main] + ruleSetFiles = files(rootProject.file(".ruleset")) +} + +tasks.withType(Pmd) { + maxHeapSize = "16g" +} + +// Apply a specific Java toolchain to ease working on different environments. +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +javadoc { + failOnError = false + + options.addStringOption('Xdoclint:all,-missing', '-quiet') + options.addBooleanOption('html5', true) + options.tags('apiNote:a:API Note:') + options.tags('implSpec:a:Implementation Requirements:') + options.tags('implNote:a:Implementation Note:') + options.tags('param') + options.tags('return') + options.tags('throws') + options.tags('since') + options.tags('version') + options.tags('serialData') + options.tags('factory') + options.tags('see') + + options.use = true + options.author = true + options.version = true + options.windowTitle = 'ZeroEcho' + options.docTitle = 'ZeroEcho API' + + options.bottom = '
Copyright © ' + currentYear + + ' Egothor - Version ' + version + + ' - License' + + '
' + + source = sourceSets.main.allJava +} + +task uploadJavadoc(type: Exec) { + dependsOn javadoc + + doFirst { + def javadocDir = tasks.javadoc.destinationDir + def relativeJavadocDir = project.projectDir.toPath().relativize(javadocDir.toPath()).toString() + def moduleName = project.name // Dynamically get the module name + + println "Uploading Javadoc for module: ${moduleName}" + println "Uploading from relative path: $relativeJavadocDir" + + // Upload to a folder named after the module + commandLine "rsync", "-avz", "--delete", + "-e", "ssh -i ${javadocKeyPath} -o IdentitiesOnly=yes", + relativeJavadocDir + '/', "${javadocUser}@${javadocHost}:${javadocPath}/${project.name}" + } +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} + +if (project.hasProperty('giteaToken') && project.giteaToken) { + publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifactId = "${rootProject.name}-${project.name}" + } + } + repositories { + maven { + name = "GiteaMaven" + url = uri("https://gitea.egothor.org/api/packages/Egothor/maven") + + credentials(HttpHeaderCredentials) { + name = "Authorization" + value = "token ${giteaToken}" + } + + authentication { + header(HttpHeaderAuthentication) + } + } + } + } +} else { + println "No giteaToken defined - skipping publishing configuration" +} + +gradle.taskGraph.whenReady { taskGraph -> + def banner = """ +\u001B[34m + +8888888888 .d8888b. .d88888b. 88888888888 888 888 .d88888b. 8888888b. +888 d88P Y88b d88P" "Y88b 888 888 888 d88P" "Y88b 888 Y88b +888 888 888 888 888 888 888 888 888 888 888 888 +8888888 888 888 888 888 8888888888 888 888 888 d88P +888 888 88888 888 888 888 888 888 888 888 8888888P" +888 888 888 888 888 888 888 888 888 888 888 T88b +888 Y88b d88P Y88b. .d88P 888 888 888 Y88b. .d88P 888 T88b +8888888888 "Y8888P88 "Y88888P" 888 888 888 "Y88888P" 888 T88b + +\u001B[36m + Project : ${project.name} + Version : ${project.version} +\u001B[0m +""" + println banner +} diff --git a/buildSrc/src/main/groovy/buildlogic.java-library-conventions.gradle b/buildSrc/src/main/groovy/buildlogic.java-library-conventions.gradle new file mode 100644 index 0000000..bc615e2 --- /dev/null +++ b/buildSrc/src/main/groovy/buildlogic.java-library-conventions.gradle @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + // Apply the common convention plugin for shared build configuration between library and application projects. + id 'buildlogic.java-common-conventions' + + // Apply the java-library plugin for API and implementation separation. + id 'java-library' +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..0f591a1 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g +javadocPath=/var/www/html/javadoc/zeroecho/ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..4ac3234 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,2 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..2c35211 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..09523c0 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/.classpath b/lib/.classpath new file mode 100644 index 0000000..70187d2 --- /dev/null +++ b/lib/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/.project b/lib/.project new file mode 100644 index 0000000..38ea66b --- /dev/null +++ b/lib/.project @@ -0,0 +1,29 @@ + + + lib + Project lib created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + net.sourceforge.pmd.eclipse.plugin.pmdBuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + net.sourceforge.pmd.eclipse.plugin.pmdNature + + diff --git a/lib/LICENSE b/lib/LICENSE new file mode 100644 index 0000000..208140a --- /dev/null +++ b/lib/LICENSE @@ -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. diff --git a/lib/build.gradle b/lib/build.gradle new file mode 100644 index 0000000..043bdef --- /dev/null +++ b/lib/build.gradle @@ -0,0 +1,56 @@ +plugins { + id 'buildlogic.java-library-conventions' + id 'com.palantir.git-version' +} + +group 'org.egothor' + +dependencies { + implementation 'org.bouncycastle:bcpkix-jdk18on' + implementation 'org.egothor:conflux' + implementation 'org.apache.commons:commons-imaging' +} + + +def generatedDir = layout.buildDirectory.dir("generated/docs").get().asFile +def staticOverview = file("src/main/javadoc/overview.html") +def overviewCss = file("src/main/javadoc/css/overview.css") + +tasks.register('generateCryptoTable', JavaExec) { + group = 'documentation' + description = 'Generates the Crypto Catalog table fragment' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'zeroecho.core.util.GenerateCryptoCatalogTable' + args file("$generatedDir/crypto-catalog-table.html").absolutePath + dependsOn classes +} + +tasks.register('composeOverview') { + group = 'documentation' + description = 'Produces a final overview.html by injecting the generated table into the static template' + inputs.file(staticOverview) + inputs.file("$generatedDir/crypto-catalog-table.html") + outputs.file("$generatedDir/overview.composed.html") + dependsOn tasks.named('generateCryptoTable') + doLast { + def template = staticOverview.getText('UTF-8') + def table = file("$generatedDir/crypto-catalog-table.html").getText('UTF-8') + def marker = "" + if (!template.contains(marker)) { + throw new GradleException("Marker not found in ${staticOverview}: ${marker}") + } + def composed = template.replace(marker, table) + file("$generatedDir/overview.composed.html").setText(composed, 'UTF-8') + } +} + +javadoc { + dependsOn tasks.named('composeOverview') + options.overview = file("$generatedDir/overview.composed.html") + options.encoding = 'UTF-8' + // options.stylesheetFile = overviewCss + options.addStringOption("-add-stylesheet", overviewCss.absolutePath) + + options.links("https://www.egothor.org/javadoc/conflux") + // options.overview = file("src/main/javadoc/overview.html") +} diff --git a/lib/src/main/java/zeroecho/core/AlgorithmFamily.java b/lib/src/main/java/zeroecho/core/AlgorithmFamily.java new file mode 100644 index 0000000..e02a1ee --- /dev/null +++ b/lib/src/main/java/zeroecho/core/AlgorithmFamily.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * 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.core; + +/** + * High-level classification of cryptographic algorithms. + *

+ * Each {@code AlgorithmFamily} groups primitives with similar lifecycle + * constraints, key properties, and safe usage patterns. + * + *

Families

+ *
    + *
  • {@link #ASYMMETRIC}: Public-key algorithms such as signature schemes + * (Ed25519, RSA) or public-key encryption.
  • + *
  • {@link #SYMMETRIC}: Shared-key algorithms such as block/stream ciphers + * and message authentication codes.
  • + *
  • {@link #KEM}: Key encapsulation mechanisms, including post-quantum + * schemes.
  • + *
  • {@link #DIGEST}: Unkeyed hash functions and extendable-output functions + * (e.g., SHA-2, SHA-3, BLAKE3).
  • + *
  • {@link #AGREEMENT}: Key-agreement schemes (e.g., X25519, ECDH), distinct + * from KEMs but with similar goals.
  • + *
+ * + *

+ * Usage: Libraries and protocols can branch on this classification to + * enforce correct API surfaces (e.g., demanding nonces for symmetric AEAD, or + * key pairs for asymmetric operations). + *

+ * + * @since 1.0 + */ +public enum AlgorithmFamily { + /** Public-key primitives (signatures, RSA, etc.). */ + ASYMMETRIC, + /** Shared-key primitives (ciphers, MACs). */ + SYMMETRIC, + /** Key encapsulation mechanisms (encapsulate/decapsulate). */ + KEM, + /** Unkeyed hash functions or XOFs. */ + DIGEST, + /** Key-agreement schemes such as ECDH/X25519. */ + AGREEMENT +} diff --git a/lib/src/main/java/zeroecho/core/Capability.java b/lib/src/main/java/zeroecho/core/Capability.java new file mode 100644 index 0000000..4f7dc78 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/Capability.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core; + +import java.security.Key; +import java.util.Objects; +import java.util.function.Supplier; + +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.spec.ContextSpec; +import zeroecho.core.spi.ContextConstructorKS; + +/** + * Immutable descriptor of an algorithm capability. + * + *

+ * A {@code Capability} describes one role supported by a + * {@link CryptoAlgorithm}, including: + *

+ *
    + *
  • the algorithm identifier,
  • + *
  • its high-level {@link AlgorithmFamily},
  • + *
  • the {@link KeyUsage} role (e.g., ENCRYPT, VERIFY),
  • + *
  • the expected {@link CryptoContext} type,
  • + *
  • the accepted {@link Key} type,
  • + *
  • the accepted {@link ContextSpec} type, and
  • + *
  • a supplier for a default spec.
  • + *
+ * + *

Purpose

Capabilities allow discovery, inspection, and documentation + * of what an algorithm can do. Higher layers (e.g., protocol builders, + * registries, tooling) can enumerate capabilities via + * {@link CryptoAlgorithm#listCapabilities()} and adapt automatically. + * + *

+ * Each capability corresponds to a call to + * {@link AbstractCryptoAlgorithm#capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}. + *

+ * + *

Thread-safety

{@code Capability} instances are immutable and safe to + * share across threads. + * + * @since 1.0 + */ +public record Capability(String algorithmId, AlgorithmFamily family, KeyUsage role, + Class contextType, Class keyType, Class specType, + Supplier defaultSpec) { + + /** + * Creates a new capability descriptor. + * + * @param algorithmId identifier of the algorithm this capability belongs to + * @param family high-level algorithm family classification + * @param role supported {@link KeyUsage} role + * @param contextType expected {@link CryptoContext} type for this role + * @param keyType accepted {@link Key} type for this role + * @param specType accepted {@link ContextSpec} type for this role + * @param defaultSpec supplier of a default spec (used when {@code null} is + * passed) + * @throws NullPointerException if any argument is {@code null} + */ + public Capability(String algorithmId, AlgorithmFamily family, KeyUsage role, + Class contextType, Class keyType, + Class specType, Supplier defaultSpec) { + this.algorithmId = Objects.requireNonNull(algorithmId, "algorithmId must not be null"); + this.family = Objects.requireNonNull(family, "family must not be null"); + this.role = Objects.requireNonNull(role, "role must not be null"); + this.contextType = Objects.requireNonNull(contextType, "contextType must not be null"); + this.keyType = Objects.requireNonNull(keyType, "keyType must not be null"); + this.specType = Objects.requireNonNull(specType, "specType must not be null"); + this.defaultSpec = Objects.requireNonNull(defaultSpec, "defaultSpec must not be null"); + } +} diff --git a/lib/src/main/java/zeroecho/core/CatalogSelector.java b/lib/src/main/java/zeroecho/core/CatalogSelector.java new file mode 100644 index 0000000..3cb0275 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/CatalogSelector.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * 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.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Helper routines for catalog selection by family and roles. + * + *

Purpose

This final static nested class provides reusable filtering + * helpers over {@code CryptoAlgorithms} that can be shared by other CLI + * utilities. The selection logic iterates the discovered algorithm identifiers + * and checks metadata exposed by {@code CryptoAlgorithm}. + */ +public final class CatalogSelector { + + private CatalogSelector() { + // no instances + } + + /** + * Returns algorithm ids that belong to the given family and contain all + * required roles. + * + * @param family required {@link AlgorithmFamily} + * @param requireAllRoles set of {@link KeyUsage} roles that must be supported + * @return list of matching algorithm ids in discovery order + * @throws NullPointerException if {@code family} or {@code requireAllRoles} is + * null + */ + public static List selectByFamilyAndRoles(AlgorithmFamily family, Collection requireAllRoles) { + Objects.requireNonNull(family, "family"); + Objects.requireNonNull(requireAllRoles, "requireAllRoles"); + List out = new ArrayList<>(); + Set ids = CryptoAlgorithms.available(); + for (String id : ids) { + CryptoAlgorithm alg = CryptoAlgorithms.require(id); + boolean familyMatch = alg.listCapabilities().stream().anyMatch(c -> c.family() == family); + if (!familyMatch) { + continue; + } + if (!alg.roles().containsAll(requireAllRoles)) { + continue; + } + out.add(id); + } + return out; + } + + /** + * Returns algorithm ids that belong to the given family, regardless of roles. + * + * @param family required {@link AlgorithmFamily} + * @return list of matching algorithm ids in discovery order + */ + public static List selectByFamily(AlgorithmFamily family) { + return selectByFamilyAndRoles(family, EnumSet.noneOf(KeyUsage.class)); + } + + /** + * Returns algorithm ids that contain all the given roles, regardless of family. + * + * @param requireAllRoles set of roles to be present + * @return list of matching algorithm ids in discovery order + */ + public static List selectByRoles(Collection requireAllRoles) { + Objects.requireNonNull(requireAllRoles, "requireAllRoles"); + List out = new ArrayList<>(); + Set ids = CryptoAlgorithms.available(); + for (String id : ids) { + CryptoAlgorithm alg = CryptoAlgorithms.require(id); + if (alg.roles().containsAll(requireAllRoles)) { + out.add(id); + } + } + return out; + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/ConfluxKeys.java b/lib/src/main/java/zeroecho/core/ConfluxKeys.java new file mode 100644 index 0000000..d11bdf0 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/ConfluxKeys.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core; + +import conflux.Key; + +/** + * Shared typed keys for ephemeral cryptographic parameters. + * + *

+ * {@code ConfluxKeys} provides strongly typed, namespaced keys for common + * transient values such as IVs, nonces, AAD, and authentication tags. These + * keys are typically used with a key–value parameter store (e.g., a + * {@code Map,Object>} or a dedicated context object) to exchange + * per-operation metadata between algorithms and higher layers. + *

+ * + *

Design goals

+ *
    + *
  • Type safety: each key carries its value type (e.g., + * {@code Key} vs {@code Key}).
  • + *
  • Namespacing: all keys include the algorithm identifier in their + * name, preventing collisions when multiple algorithms share a context.
  • + *
  • Consistency: avoids ad-hoc string constants; discoverable via + * {@link CryptoAlgorithm#listCapabilities()} and related APIs.
  • + *
+ * + *

+ * Instances are created via static factories; this class cannot be + * instantiated. + *

+ * + * @since 1.0 + */ +public final class ConfluxKeys { + final private static String PREFIX = "crypto."; + + private ConfluxKeys() { + } + + /** + * Returns a typed key for the initialization vector (IV) of a given algorithm. + * + *

+ * IVs are required by block cipher modes such as CBC or GCM. Each call produces + * a key namespaced as {@code "crypto..iv"}. + *

+ * + * @param algoId canonical algorithm identifier + * @return key for IV values, of type {@code byte[]} + */ + public static Key iv(String algoId) { + return Key.of(PREFIX + algoId + ".iv", byte[].class); + } + + /** + * Returns a typed key for additional authenticated data (AAD). + * + *

+ * Used in AEAD schemes such as AES-GCM to bind unencrypted headers into the + * authentication tag. Namespaced as {@code "crypto..aad"}. + *

+ * + * @param algoId canonical algorithm identifier + * @return key for AAD values, of type {@code byte[]} + */ + public static Key aad(String algoId) { + return Key.of(PREFIX + algoId + ".aad", byte[].class); + } + + /** + * Returns a typed key for a nonce value of a given algorithm. + * + *

+ * Nonces are required by stream ciphers and AEAD modes to ensure uniqueness per + * key. Namespaced as {@code "crypto..nonce"}. + *

+ * + * @param algoId canonical algorithm identifier + * @return key for nonce values, of type {@code byte[]} + */ + public static Key nonce(String algoId) { + return Key.of(PREFIX + algoId + ".nonce", byte[].class); + } + + /** + * Returns a typed key for the authentication tag of a given algorithm. + * + *

+ * AEAD modes output a tag that must be preserved for decryption/verification. + * Namespaced as {@code "crypto..tag"}. + *

+ * + * @param algoId canonical algorithm identifier + * @return key for authentication tag values, of type {@code byte[]} + */ + public static Key tag(String algoId) { + return Key.of(PREFIX + algoId + ".tag", byte[].class); + } + + /** + * Returns a typed key for the number of authentication tag bits. + * + *

+ * Some AEAD constructions allow truncated tags (e.g., 96-bit or 64-bit). This + * key represents the chosen bit-length. Namespaced as + * {@code "crypto..tagBits"}. + *

+ * + * @param algoId canonical algorithm identifier + * @return key for tag length values, of type {@code Integer} + */ + public static Key tagBits(String algoId) { + return Key.of(PREFIX + algoId + ".tagBits", Integer.class); + } +} diff --git a/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java b/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java new file mode 100644 index 0000000..53b5d99 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/CryptoAlgorithm.java @@ -0,0 +1,846 @@ +/******************************************************************************* + * 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.core; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Supplier; + +import javax.crypto.SecretKey; + +import zeroecho.core.context.CryptoContext; +import zeroecho.core.err.UnsupportedRoleException; +import zeroecho.core.err.UnsupportedSpecException; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spec.ContextSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.ContextConstructorKS; +import zeroecho.core.spi.SymmetricKeyBuilder; + +/** + * Abstract base class for all cryptographic algorithm definitions in ZeroEcho. + *

+ * A {@code CryptoAlgorithm} declares: + *

    + *
  • Metadata: an identifier, display name, provider, and priority.
  • + *
  • Capabilities: declared features such as AEAD, streaming, or deterministic + * signatures.
  • + *
  • Roles: supported {@link KeyUsage} operations (e.g., ENCRYPT, SIGN) bound + * to concrete {@link CryptoContext} constructors.
  • + *
  • Key builders: factories for symmetric and asymmetric key material via + * {@link SymmetricKeyBuilder} and {@link AsymmetricKeyBuilder}.
  • + *
+ * + *

Metadata

Each algorithm instance is uniquely identified by + * {@link #id()}, a canonical string such as {@code "AES/GCM/NOPADDING"} or + * {@code "Ed25519"}. A human-readable {@link #displayName()} is provided for + * logs and diagnostics. + * + *

Capabilities

Algorithms may declare extra {@link Capability} flags + * that can be inspected by higher layers. This allows adaptive protocols to + * choose the right primitive (e.g., preferring AEAD over raw block ciphers). + * + *

Roles and contexts

Each algorithm may support multiple + * {@link KeyUsage} roles. For each role, the algorithm binds a key type, + * context type, and optional {@link ContextSpec}. When + * {@link #create(KeyUsage, Key, ContextSpec)} is called: + *
    + *
  1. The binding for the role is located.
  2. + *
  3. The supplied key and spec are validated against the expected types.
  4. + *
  5. A new {@link CryptoContext} is constructed using the registered + * factory.
  6. + *
+ * + *

Key builders

+ *
    + *
  • Asymmetric builders: registered via {@link #registerAsymmetricKeyBuilder} + * and accessed through {@link #asymmetricKeyBuilder(Class)} or convenience + * methods like {@link #generateKeyPair(AlgorithmKeySpec)}.
  • + *
  • Symmetric builders: registered via {@link #registerSymmetricKeyBuilder} + * and accessed through {@link #symmetricKeyBuilder(Class)} or convenience + * methods like {@link #generateSecret(AlgorithmKeySpec)}.
  • + *
+ * + *

Provider model

Each algorithm belongs to a {@code providerName}, + * allowing multiple providers (e.g., JCA, BouncyCastle, ZeroEcho-native) to + * coexist. {@link #priority()} can be used to prefer one provider over another + * when resolving duplicates. + * + *

Thread safety

{@code CryptoAlgorithm} instances are immutable once + * constructed and are safe to share across threads. The created + * {@link CryptoContext} instances, however, are not necessarily thread-safe. + * + *

+ * Security note: Algorithms must enforce strong validation of keys and + * specs during registration and {@link #create(KeyUsage, Key, ContextSpec)} to + * prevent downgrade or misuse attacks. + *

+ * + * @since 1.0 + */ +public abstract class CryptoAlgorithm { // NOPMD + + private final String _id; + private final String _displayName; + private final int _priority; + private final String _providerName; + + private final List capabilities = new ArrayList<>(); + private final Map>> ctxBindings = new EnumMap<>(KeyUsage.class); + private final Map, AsymEntry> asymBuilders = new HashMap<>(); + private final Map, SymEntry> symBuilders = new HashMap<>(); + + /** + * Create a new algorithm with default priority and provider. + * + * @param id unique canonical identifier + * @param displayName human-readable name + */ + protected CryptoAlgorithm(String id, String displayName) { + this(id, displayName, 0, "default"); + } + + /** + * Create a new algorithm with default priority and a named provider. + * + * @param id unique canonical identifier + * @param displayName human-readable name + * @param providerName provider or implementation source + */ + protected CryptoAlgorithm(String id, String displayName, String providerName) { + this(id, displayName, 0, providerName); + } + + /** + * Create a new algorithm with explicit metadata. + * + * @param id unique canonical identifier + * @param displayName human-readable name + * @param priority preference when multiple providers offer the same + * algorithm + * @param providerName provider or implementation source + */ + protected CryptoAlgorithm(String id, String displayName, int priority, String providerName) { + this._id = Objects.requireNonNull(id, "id must not be null"); + this._displayName = Objects.requireNonNull(displayName, "displayName must not be null"); + this._priority = priority; + this._providerName = Objects.requireNonNull(providerName, "providerName must not be null"); + } + + /** + * Returns the canonical identifier of this algorithm. + *

+ * The identifier is a stable, implementation-independent string such as + * {@code "AES/GCM/NOPADDING"} or {@code "Ed25519"}. It is suitable for + * persistence in configuration files, protocol negotiation, or audit logs. + *

+ * Unlike {@link #displayName()}, the identifier is not localized and should be + * treated as a primary key across providers. + * + * @return canonical, provider-independent algorithm identifier + */ + public final String id() { + return _id; + } + + /** + * Returns a human-readable display name for this algorithm. + *

+ * This name is intended for logs, error messages, and user interfaces. Unlike + * {@link #id()}, the display name may vary by provider and is not guaranteed to + * be stable across versions. + * + * @return human-friendly algorithm name + */ + public final String displayName() { + return _displayName; + } + + /** + * Returns the priority of this algorithm within its provider. + *

+ * When multiple providers expose the same {@link #id()}, the priority is used + * as a tiebreaker. Higher values indicate stronger preference. + *

+ * Priority values are advisory; applications may still override selection based + * on policy. + * + * @return numeric provider preference (higher means more preferred) + */ + public int priority() { + return _priority; + } + + /** + * Returns the provider that supplies this algorithm implementation. + *

+ * Typical values include {@code "default"}, {@code "JCA"}, + * {@code "BouncyCastle"}, or a project-specific label. + *

+ * Provider names allow coexistence of multiple implementations of the same + * algorithm identifier. + * + * @return provider or implementation source name + */ + public String providerName() { + return _providerName; + } + + /** + * Adds a capability flag to this algorithm. + * + *

+ * Intended for use by concrete subclasses during construction to advertise + * features (e.g., AEAD support, deterministic signatures). Adding capabilities + * after publication is discouraged as callers may have already inspected them. + *

+ * + * @param capability non-null capability to add + * @throws NullPointerException if {@code capability} is {@code null} + */ + protected final void addCapability(Capability capability) { + capabilities.add(Objects.requireNonNull(capability, "capability must not be null")); + } + + /** + * Returns an immutable view of the algorithm’s declared capabilities. + * + * @return unmodifiable list of capability flags + */ + public final List listCapabilities() { + return Collections.unmodifiableList(capabilities); + } + + /** + * Internal binding record connecting a role to its required types and + * constructor. + *

+ * For a given {@link KeyUsage} role, the binding specifies the expected + * {@link CryptoContext} type, the accepted {@link Key} type, the optional + * {@link ContextSpec} type, a constructor factory, and a default spec supplier. + * + * @param context type + * @param key type + * @param spec type + */ + private static final class RoleBinding { + private final Class ctxType; + private final Class keyType; + private final Class specType; + private final ContextConstructorKS ctor; + private final Supplier defaultSpec; + + private RoleBinding(Class ctxType, Class keyType, Class specType, ContextConstructorKS ctor, + Supplier defaultSpec) { + this.ctxType = ctxType; + this.keyType = keyType; + this.specType = specType; + this.ctor = ctor; + this.defaultSpec = defaultSpec; + } + + private boolean accepts(Key key, ContextSpec spec) { + return keyType.isInstance(key) && (spec == null || specType.isInstance(spec)); + } + } + + /** + * Binds a role to a concrete context factory and its expected key/spec types. + * + *

+ * Concrete algorithms call this during construction to declare support for + * specific roles (e.g., {@code ENCRYPT}, {@code VERIFY}). When + * {@link #create(KeyUsage, Key, ContextSpec)} is later invoked, the provided + * {@code key} and optional {@code spec} are matched against these bindings. + *

+ * + * @param role supported {@link KeyUsage} role + * @param ctxType context type to be returned by the factory + * @param keyType key type accepted by the factory + * @param specType spec type accepted by the factory (may be a marker type) + * @param factory constructor that creates a context for (key, spec) + * @param defaultSpec default spec supplier used when {@code spec} is + * {@code null} + * @param context type + * @param key type + * @param spec type + * @throws NullPointerException if any class or factory argument is {@code null} + */ + protected final void bind(KeyUsage role, + Class ctxType, Class keyType, Class specType, ContextConstructorKS factory, + Supplier defaultSpec) { + ctxBindings.computeIfAbsent(role, r -> new ArrayList<>()) + .add(new RoleBinding<>(ctxType, keyType, specType, factory, defaultSpec)); + } + + /** + * Returns whether this algorithm supports the given role. + * + * @param role a {@link KeyUsage} role + * @return {@code true} if a binding exists, otherwise {@code false} + */ + public final boolean supports(KeyUsage role) { + return ctxBindings.containsKey(role); + } + + /** + * Returns the set of roles supported by this algorithm. + * + * @return unmodifiable set of supported {@link KeyUsage} values + */ + public final Set roles() { + return Collections.unmodifiableSet(ctxBindings.keySet()); + } + + /** + * Creates a new {@link CryptoContext} for the given role, key, and optional + * spec. + * + *

+ * Resolution proceeds as follows: + *

+ *
    + *
  1. Locate bindings for {@code role}; if none exist, throw + * {@link UnsupportedRoleException}.
  2. + *
  3. For each binding, check that {@code key} is an instance of the required + * key type and {@code spec} is either {@code null} or an instance of the + * required spec type.
  4. + *
  5. If matched, resolve the effective spec: use the provided {@code spec} or + * obtain one from the binding’s {@code defaultSpec} supplier.
  6. + *
  7. Invoke the factory to create a context and verify the returned type + * matches the declared {@code ctxType}.
  8. + *
+ * + * @param role the intended {@link KeyUsage} for the created context + * @param key key instance compatible with the binding + * @param spec optional context spec; if {@code null}, the binding’s default is + * used + * @param context type + * @param key type + * @param spec type + * @return a newly constructed context suitable for the requested role + * @throws UnsupportedRoleException if the algorithm does not support + * {@code role} + * @throws UnsupportedSpecException if no binding accepts the provided key/spec + * @throws IllegalStateException if the factory returns an unexpected context + * type + * @throws IOException if the factory encounters I/O while + * constructing the context + */ + @SuppressWarnings("unchecked") + public final C create(KeyUsage role, K key, S spec) + throws IOException { + + List> list = ctxBindings.get(role); + if (list == null || list.isEmpty()) { + throw new UnsupportedRoleException(_id + " does not support role " + role); + } + for (RoleBinding rb0 : list) { + RoleBinding rb = (RoleBinding) rb0; + if (rb.accepts(key, spec)) { + S resolved = (spec != null) ? spec : rb.defaultSpec.get(); + C ctx = rb.ctor.create(key, resolved); + // Enforce the declared context type contract: + if (!rb.ctxType.isInstance(ctx)) { + throw new IllegalStateException(_id + " factory returned " + ctx.getClass().getName() + + " but capability declares " + rb.ctxType.getName()); + } + return ctx; + } + } + throw new UnsupportedSpecException(_id + " cannot create for " + role + " with key=" + key.getClass().getName() + + (spec == null ? " (default spec)" : " and spec=" + spec.getClass().getName())); + } + + /** + * Immutable descriptor for an asymmetric builder registered with this + * algorithm. + *

+ * Used for discovery and documentation (e.g., tool UIs). + *

+ */ + public static final class AsymBuilderInfo { + public final Class specType; + public final Object defaultKeySpec; + + private AsymBuilderInfo(Class specType, Object defaultKeySpec) { + this.specType = specType; + this.defaultKeySpec = defaultKeySpec; + } + } + + /** + * Internal entry binding a registered asymmetric key builder to its default key + * specification supplier. + * + *

+ * Each {@code AsymEntry} is keyed by a specific {@link AlgorithmKeySpec} + * subtype. It holds the {@link AsymmetricKeyBuilder} instance capable of + * generating or importing keys for that spec, and an optional supplier that + * provides a safe default spec (if the algorithm wants to support "generate + * with defaults"). + *

+ * + *

Usage

+ *
    + *
  • Created during calls to + * {@link #registerAsymmetricKeyBuilder(Class, AsymmetricKeyBuilder, Supplier)}.
  • + *
  • Looked up later by {@link #asymmetricKeyBuilder(Class)} and used by + * key-generation/import convenience methods such as + * {@link #generateKeyPair(AlgorithmKeySpec)}.
  • + *
+ * + *

Thread-safety

Immutable once constructed; safe to share between + * threads. + * + * @param the type of {@link AlgorithmKeySpec} handled by this entry + */ + private record AsymEntry(AsymmetricKeyBuilder builder, + Supplier defaultKeySpec) { + + /** + * Creates a new binding between a key builder and its optional default spec. + * + * @throws NullPointerException if {@code builder} is {@code null} + */ + AsymEntry { + Objects.requireNonNull(builder, "builder must not be null"); + } + } + + /** + * Registers an asymmetric key builder for a specific spec type. + * + *

+ * Concrete algorithms call this during construction. The {@code specType} acts + * as a key for later lookup and must be unique within this algorithm. + *

+ * + * @param specType the spec class accepted by {@code builder} + * @param builder builder that can generate/import keys for + * {@code specType} + * @param defaultKeySpecOrNull optional supplier for a default spec (may be + * {@code null}) + * @param spec type + * @throws NullPointerException if {@code specType} or {@code builder} is + * {@code null} + */ + protected final void registerAsymmetricKeyBuilder(Class specType, + AsymmetricKeyBuilder builder, Supplier defaultKeySpecOrNull) { + Objects.requireNonNull(specType, "specType must not be null"); + asymBuilders.put(specType, new AsymEntry<>(builder, defaultKeySpecOrNull)); + } + + /** + * Returns the asymmetric key builder associated with the given spec type. + * + * @param specType spec class used as a lookup key + * @param spec type + * @return the registered {@link AsymmetricKeyBuilder} + * @throws IllegalArgumentException if no builder is registered for + * {@code specType} + */ + @SuppressWarnings("unchecked") + public final AsymmetricKeyBuilder asymmetricKeyBuilder(Class specType) { + AsymEntry e = asymBuilders.get(specType); + if (e == null) { + throw new IllegalArgumentException(_id + " has no asymmetric key builder for " + specType.getName()); + } + return (AsymmetricKeyBuilder) e.builder; + } + + /** + * Returns metadata about all registered asymmetric builders. + * + *

+ * The default spec value is best-effort; suppliers may throw, in which case + * {@code defaultKeySpec} is reported as {@code null}. + *

+ * + * @return immutable list of {@link AsymBuilderInfo} descriptors + */ + public final List asymmetricBuildersInfo() { + List out = new ArrayList<>(); + for (Map.Entry, AsymEntry> e : asymBuilders.entrySet()) { + Object def = null; + if (e.getValue().defaultKeySpec != null) { + try { + def = e.getValue().defaultKeySpec.get(); + } catch (Throwable t) { // NOPMD + def = null; + } + } + out.add(new AsymBuilderInfo(e.getKey(), def)); + } + return Collections.unmodifiableList(out); + } + + /** + * Immutable descriptor for a symmetric key builder registered with this + * algorithm. + * + *

+ * Each {@code SymBuilderInfo} describes the specification type that a + * {@link SymmetricKeyBuilder} can handle, along with an optional default + * specification object. These descriptors are used for discovery and + * documentation purposes, for example when rendering catalog information in + * tooling or UIs. + *

+ * + *

Usage

+ *
    + *
  • Produced by {@link #symmetricBuildersInfo()}.
  • + *
  • Displayed to clients for inspection and documentation, but not used + * directly in cryptographic operations.
  • + *
+ * + *

Thread-safety

Being a {@code record}, this type is immutable and + * safe to share between threads. + * + * @param specType the specification type supported by the builder + * @param defaultKeySpec an optional default key specification instance, or + * {@code null} if no default is provided + */ + public record SymBuilderInfo(Class specType, Object defaultKeySpec) { + } + + /** + * Internal entry binding a registered symmetric key builder to its optional + * default key specification supplier. + * + *

+ * Each {@code SymEntry} is keyed by a specific {@link AlgorithmKeySpec} + * subtype. It holds the {@link SymmetricKeyBuilder} instance capable of + * generating or importing keys for that spec, and a supplier that may produce a + * default spec when none is provided explicitly. + *

+ * + *

Usage

+ *
    + *
  • Created during calls to + * {@link #registerSymmetricKeyBuilder(Class, SymmetricKeyBuilder, Supplier)}.
  • + *
  • Looked up internally when methods such as + * {@link #generateSecret(AlgorithmKeySpec)} or + * {@link #importSecret(AlgorithmKeySpec)} are invoked.
  • + *
+ * + *

Thread-safety

Immutable and thread-safe by design as a + * {@code record}. + * + * @param builder the builder instance that can create or import keys; + * must not be {@code null} + * @param defaultKeySpec supplier for a default specification, or {@code null} + * if no sensible default exists + * @param the type of {@link AlgorithmKeySpec} handled by this + * entry + */ + private record SymEntry(SymmetricKeyBuilder builder, + Supplier defaultKeySpec) { + + /** + * Compact constructor that enforces non-null builder. + * + * @throws NullPointerException if {@code builder} is {@code null} + */ + SymEntry { + Objects.requireNonNull(builder, "builder must not be null"); + } + } + + /** + * Registers a symmetric key builder for a specific spec type. + * + * @param specType the spec class accepted by {@code builder} + * @param builder builder that can generate/import keys for + * {@code specType} + * @param defaultKeySpecOrNull optional supplier for a default spec (may be + * {@code null}) + * @param spec type + * @throws NullPointerException if {@code specType} or {@code builder} is + * {@code null} + */ + protected final void registerSymmetricKeyBuilder(Class specType, + SymmetricKeyBuilder builder, Supplier defaultKeySpecOrNull) { + Objects.requireNonNull(specType, "specType must not be null"); + symBuilders.put(specType, new SymEntry<>(builder, defaultKeySpecOrNull)); + } + + /** + * Returns the symmetric key builder associated with the given spec type. + * + * @param specType spec class used as a lookup key + * @param spec type + * @return the registered {@link SymmetricKeyBuilder} + * @throws IllegalArgumentException if no builder is registered for + * {@code specType} + */ + @SuppressWarnings("unchecked") + public final SymmetricKeyBuilder symmetricKeyBuilder(Class specType) { + SymEntry e = symBuilders.get(specType); + if (e == null) { + throw new IllegalArgumentException(_id + " has no symmetric key builder for " + specType.getName()); + } + return (SymmetricKeyBuilder) e.builder; + } + + /** + * Returns metadata about all registered symmetric builders. + * + *

+ * The default spec value is best-effort; suppliers may throw, in which case + * {@code defaultKeySpec} is reported as {@code null}. + *

+ * + * @return immutable list of {@link SymBuilderInfo} descriptors + */ + public final List symmetricBuildersInfo() { + List out = new ArrayList<>(); + for (Map.Entry, SymEntry> e : symBuilders.entrySet()) { + Object def = null; + if (e.getValue().defaultKeySpec != null) { + try { + def = e.getValue().defaultKeySpec.get(); + } catch (Throwable t) { // NOPMD + def = null; + } + } + out.add(new SymBuilderInfo(e.getKey(), def)); + } + return Collections.unmodifiableList(out); + } + + /** + * Generates a fresh symmetric {@link SecretKey} using the registered builder + * for {@code spec}. + * + * @param spec algorithm-specific key specification (must match a registered + * symmetric builder) + * @param spec type + * @return newly generated secret key + * @throws NullPointerException if {@code spec} is {@code null} + * @throws IllegalArgumentException if no symmetric builder is registered for + * {@code spec.getClass()} + * @throws GeneralSecurityException if key generation fails or parameters are + * unsupported + */ + @SuppressWarnings("unchecked") + public final SecretKey generateSecret(S spec) throws GeneralSecurityException { + Objects.requireNonNull(spec, "spec must not be null"); + SymmetricKeyBuilder b = symmetricKeyBuilder((Class) spec.getClass()); + return b.generateSecret(spec); + } + + /** + * Imports an existing symmetric {@link SecretKey} using the registered builder + * for {@code spec}. + * + * @param spec algorithm-specific key specification including raw + * material/format + * @param spec type + * @return wrapped secret key validated against the spec + * @throws NullPointerException if {@code spec} is {@code null} + * @throws IllegalArgumentException if no symmetric builder is registered for + * {@code spec.getClass()} + * @throws GeneralSecurityException if the material is invalid or does not match + * the algorithm + */ + @SuppressWarnings("unchecked") + public final SecretKey importSecret(S spec) throws GeneralSecurityException { + Objects.requireNonNull(spec, "spec must not be null"); + SymmetricKeyBuilder b = symmetricKeyBuilder((Class) spec.getClass()); + return b.importSecret(spec); + } + + /** + * Attempts to generate a {@link KeyPair} using the given asymmetric builder's + * default key spec. This method is fully generic and avoids raw types by + * capturing the concrete spec type parameter. + * + * @param specType the spec class label used for diagnostics + * @param entry the typed asymmetric builder entry + * @param concrete {@link AlgorithmKeySpec} type + * @return a freshly generated key pair + * @throws GeneralSecurityException if the supplier or builder fails + */ + private KeyPair tryGenerateWithDefault(Class specType, + AsymEntry entry) throws GeneralSecurityException { + + if (entry.defaultKeySpec == null) { + throw new GeneralSecurityException("no default spec supplier"); + } + + final S spec; + try { + spec = entry.defaultKeySpec.get(); + } catch (Throwable t) { // NOPMD + throw new GeneralSecurityException("defaultSpec supplier failed for " + specType.getSimpleName() + ": " + + t.getClass().getSimpleName() + ": " + t.getMessage(), t); + } + if (spec == null) { + throw new GeneralSecurityException("defaultSpec supplier returned null for " + specType.getSimpleName()); + } + + // No raw types here: S is captured from entry. + return entry.builder.generateKeyPair(spec); + } + + /** + * Generates a fresh {@link KeyPair} using the first asymmetric builder that + * successfully provides a default key specification. + * + *

+ * This convenience method iterates over all registered asymmetric key builders + * that declare a non-null default {@link AlgorithmKeySpec} supplier. For each, + * it attempts to obtain the default spec and generate a key pair. If a builder + * fails (e.g., the builder only supports import or rejects the parameters), the + * method records the failure and continues with the next candidate. + *

+ * + *

Example

{@code
+     * CryptoAlgorithm algo = CryptoAlgorithms.require("Ed25519");
+     * KeyPair kp = algo.generateKeyPair();
+     * }
+ * + * @return a newly generated key pair using a default spec from one of the + * registered asymmetric builders + * @throws IllegalStateException if no builder declares a default spec + * supplier + * @throws GeneralSecurityException if all candidate builders fail to generate a + * key pair; the exception message details + * individual causes + */ + public final KeyPair generateKeyPair() throws GeneralSecurityException { + StringBuilder reasons = new StringBuilder(128); + boolean attempted = false; + + for (Map.Entry, AsymEntry> e : asymBuilders.entrySet()) { + AsymEntry entry = e.getValue(); + if (entry.defaultKeySpec == null) { + continue; + } + attempted = true; + try { + // Wildcard capture lets the compiler infer without casts. + return tryGenerateWithDefault(e.getKey(), entry); + } catch (GeneralSecurityException ex) { + reasons.append(" - ").append(e.getKey().getSimpleName()).append(": ") + .append(ex.getClass().getSimpleName()).append(": ").append(String.valueOf(ex.getMessage())) + .append('\n'); + // keep trying other builders + } + } + + if (!attempted) { + throw new IllegalStateException(_id + " has no default asymmetric key spec"); + } + throw new GeneralSecurityException( + _id + " failed to generate a default key pair. Reasons:\n" + reasons.toString().trim()); + } + + /** + * Generates a fresh {@link KeyPair} using the registered asymmetric builder for + * {@code spec}. + * + * @param spec algorithm-specific key specification (must match a registered + * asymmetric builder) + * @param spec type + * @return newly generated key pair + * @throws NullPointerException if {@code spec} is {@code null} + * @throws IllegalArgumentException if no asymmetric builder is registered for + * {@code spec.getClass()} + * @throws GeneralSecurityException if key generation fails or parameters are + * unsupported + */ + @SuppressWarnings("unchecked") + public final KeyPair generateKeyPair(S spec) throws GeneralSecurityException { + Objects.requireNonNull(spec, "spec must not be null"); + AsymmetricKeyBuilder b = asymmetricKeyBuilder((Class) spec.getClass()); + return b.generateKeyPair(spec); + } + + /** + * Imports a {@link PublicKey} using the registered asymmetric builder for + * {@code spec}. + * + * @param spec algorithm-specific key specification including encoded public + * material/format + * @param spec type + * @return wrapped public key validated against the spec + * @throws NullPointerException if {@code spec} is {@code null} + * @throws IllegalArgumentException if no asymmetric builder is registered for + * {@code spec.getClass()} + * @throws GeneralSecurityException if the material is invalid or does not match + * the algorithm + */ + @SuppressWarnings("unchecked") + public final PublicKey importPublic(S spec) throws GeneralSecurityException { + Objects.requireNonNull(spec, "spec must not be null"); + AsymmetricKeyBuilder b = asymmetricKeyBuilder((Class) spec.getClass()); + return b.importPublic(spec); + } + + /** + * Imports a {@link PrivateKey} using the registered asymmetric builder for + * {@code spec}. + * + * @param spec algorithm-specific key specification including encoded private + * material/format + * @param spec type + * @return wrapped private key validated against the spec + * @throws NullPointerException if {@code spec} is {@code null} + * @throws IllegalArgumentException if no asymmetric builder is registered for + * {@code spec.getClass()} + * @throws GeneralSecurityException if the material is invalid or does not match + * the algorithm + */ + @SuppressWarnings("unchecked") + public final PrivateKey importPrivate(S spec) throws GeneralSecurityException { + Objects.requireNonNull(spec, "spec must not be null"); + AsymmetricKeyBuilder b = asymmetricKeyBuilder((Class) spec.getClass()); + return b.importPrivate(spec); + } +} diff --git a/lib/src/main/java/zeroecho/core/CryptoAlgorithms.java b/lib/src/main/java/zeroecho/core/CryptoAlgorithms.java new file mode 100644 index 0000000..5812302 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/CryptoAlgorithms.java @@ -0,0 +1,609 @@ +/******************************************************************************* + * 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.core; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +import javax.crypto.SecretKey; + +import zeroecho.core.audit.AuditListener; +import zeroecho.core.audit.AuditedContexts; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.context.DigestContext; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MacContext; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.err.UnsupportedRoleException; +import zeroecho.core.err.UnsupportedSpecException; +import zeroecho.core.policy.CryptoPolicy; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spec.ContextSpec; + +/** + * Static façade and registry for {@link CryptoAlgorithm} providers. + * + *

+ * {@code CryptoAlgorithms} discovers algorithms via {@link ServiceLoader} and + * exposes: + *

+ *
    + *
  • a registry from canonical algorithm id to implementation,
  • + *
  • policy hooks that validate requested operations before contexts are + * created,
  • + *
  • global audit wiring (listener + wrapping mode), and
  • + *
  • convenience methods for context creation and key generation/import.
  • + *
+ * + *

Discovery & identity

Implementations register themselves using + * the Java SPI for {@link CryptoAlgorithm}. If multiple providers advertise the + * same {@linkplain CryptoAlgorithm#id() id}, the registry throws at startup to + * avoid ambiguous resolution. + * + *

Policy

The active {@link CryptoPolicy} is consulted before any + * context is created. Policies can deny weak parameters, enforce key-usage + * separation, or restrict algorithms. If {@link #setPolicy(CryptoPolicy)} is + * never called or is set to {@code null}, a permissive policy is used. + * + *

Auditing

All key lifecycle events and context creation can be + * reported to a global {@link AuditListener}. The {@link AuditMode} determines + * whether contexts are wrapped with auditing proxies or relied upon to emit + * events directly. + * + *

Thread-safety

The registry map and global hooks are safe to read + * concurrently. Hooks are backed by {@code volatile} fields and can be swapped + * at runtime; there is no global lock. + * + * @since 1.0 + */ +public final class CryptoAlgorithms { + + private static final Map BY_ID; + private static volatile CryptoPolicy POLICY = CryptoPolicy.permissive(); // NOPMD + private static volatile AuditListener AUDIT = AuditListener.noop(); // NOPMD + private static volatile AuditMode AUDIT_MODE = AuditMode.OFF; // NOPMD + + private CryptoAlgorithms() { + } + + static { + Map m = new HashMap<>(); + for (CryptoAlgorithm a : ServiceLoader.load(CryptoAlgorithm.class)) { + CryptoAlgorithm prev = m.put(a.id(), a); + if (prev != null) { + throw new IllegalStateException("Duplicate algorithm id: " + a.id()); + } + } + BY_ID = Collections.unmodifiableMap(m); + } + + /** + * Returns the set of available algorithm identifiers discovered via + * {@link ServiceLoader}. + * + *

+ * The returned set is backed by an unmodifiable registry snapshot. Use these + * identifiers with {@link #require(String)} or the convenience methods below. + *

+ * + * @return unmodifiable set of canonical algorithm ids + */ + public static Set available() { + return BY_ID.keySet(); + } + + /** + * Looks up an algorithm implementation by its canonical identifier. + * + *

+ * If the id is unknown, an {@link IllegalArgumentException} is thrown. This + * method is preferred over direct access to ensure consistent error handling + * and to centralize future selection logic. + *

+ * + * @param id canonical algorithm identifier (e.g., {@code "AES/GCM"} or + * {@code "Ed25519"}) + * @return the corresponding {@link CryptoAlgorithm} implementation + * @throws IllegalArgumentException if no algorithm is registered under + * {@code id} + */ + public static CryptoAlgorithm require(String id) { + CryptoAlgorithm a = BY_ID.get(id); + if (a == null) { + throw new IllegalArgumentException("Unknown algorithm id: " + id); + } + return a; + } + + /** + * Sets the global cryptographic policy applied before any context creation. + * + *

+ * Pass {@code null} to revert to a permissive policy. Policies should be fast + * and side-effect free; they are invoked on every + * {@link #create(String, KeyUsage, Key, ContextSpec)} call. + *

+ * + * @param p policy to install, or {@code null} to use + * {@link CryptoPolicy#permissive()} + */ + public static void setPolicy(CryptoPolicy p) { + POLICY = (p == null ? CryptoPolicy.permissive() : p); + } + + /** + * Sets the global {@link AuditListener}. + * + *

+ * Pass {@code null} to disable custom auditing (a no-op listener will be + * installed). The listener may be invoked by context proxies (in + * {@link AuditMode#WRAP}) and by the convenience key factory methods below. + *

+ * + * @param l listener instance or {@code null} for a no-op listener + */ + public static void setAuditListener(AuditListener l) { + AUDIT = (l == null ? AuditListener.noop() : l); + } + + /** + * Returns the current global {@link AuditListener}. + * + * @return the active audit listener (never {@code null}) + */ + public static AuditListener audit() { + return AUDIT; + } + + /** + * Declares how auditing is applied to cryptographic contexts. + * + *

+ * The {@code AuditMode} controls whether contexts created by + * {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)} + * are wrapped in auditing proxies or whether auditing is delegated entirely to + * the caller. + *

+ * + *

Modes

+ *
    + *
  • {@link #OFF} - No automatic wrapping of contexts (default). Only explicit + * events triggered at creation are emitted; no per-operation auditing is + * injected.
  • + * + *
  • {@link #WRAP} - Supported contexts are wrapped in dynamic proxies that + * emit additional stream-level and per-operation auditing events. Creation + * events originate from the proxy rather than the factory method.
  • + * + *
  • {@link #MANUAL} - No automatic wrapping and no automatic event emission. + * The caller is fully responsible for invoking audit methods (e.g., + * {@link CryptoAlgorithms#audit()}) at the appropriate times.
  • + *
+ * + * @since 1.0 + */ + public enum AuditMode { + /** + * No automatic wrapping of contexts (default). + * + *

+ * Only explicit events emitted here (e.g., + * {@link AuditListener#onContextCreated}) are sent to the listener; + * stream-level or per-operation auditing is not injected. + *

+ */ + OFF, + /** + * Wraps supported contexts in dynamic proxies that emit stream-level auditing. + * + *

+ * In this mode, creation events are emitted by the proxy rather than here, and + * subsequent operations (e.g., updates, finalization) may also be audited + * depending on the proxy implementation. + *

+ */ + WRAP, + /** + * No wrapping and no automatic events. + * + *

+ * The caller is responsible for emitting all relevant audit events via the + * {@link #audit()} listener. + *

+ */ + MANUAL + } + + /** + * Sets the auditing mode for subsequently created contexts. + * + *

+ * Passing {@code null} resets the mode to {@link AuditMode#OFF}. + *

+ * + * @param mode desired auditing strategy or {@code null} for {@code OFF} + */ + public static void setAuditMode(AuditMode mode) { + AUDIT_MODE = (mode == null ? AuditMode.OFF : mode); + } + + /** + * Returns the current auditing mode. + * + * @return active {@link AuditMode}; never {@code null} + */ + public static AuditMode getAuditMode() { + return AUDIT_MODE; + } + + /** + * Creates a {@link CryptoContext} for the given algorithm id and role, applying + * policy validation and optional auditing/wrapping. + * + *

+ * Flow: + *

+ *
    + *
  1. Policy validation via + * {@link CryptoPolicy#validate(String, KeyUsage, Key, ContextSpec)}.
  2. + *
  3. Algorithm resolution via {@link #require(String)} and context + * construction via + * {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)}.
  4. + *
  5. Auditing behavior based on {@link #getAuditMode()}: + *
      + *
    • {@link AuditMode#OFF}/{@link AuditMode#MANUAL}: emit a creation event + * immediately via + * {@link AuditListener#onContextCreated(String, String, KeyUsage, Key, ContextSpec)}.
    • + *
    • {@link AuditMode#WRAP}: return a proxy (where supported) that emits + * creation and stream-level events; unknown context types are returned + * unwrapped.
    • + *
    + *
  6. + *
+ * + * @param id canonical algorithm identifier + * @param role desired {@link KeyUsage} (e.g., ENCRYPT, VERIFY) + * @param key key instance for the role + * @param spec optional context specification; may be {@code null} to use + * algorithm defaults + * @param context type + * @param key type + * @param spec type + * @return a context ready for use; may be a proxy if {@link AuditMode#WRAP} is + * active + * @throws IOException if the underlying algorithm fails to create + * a context + * @throws IllegalArgumentException if {@code id} is unknown + * @throws UnsupportedRoleException if the algorithm does not support + * {@code role} + * @throws UnsupportedSpecException if the provided key/spec are incompatible + * with the role + */ + public static C create(String id, KeyUsage role, + K key, S spec) throws IOException { + + POLICY.validate(id, role, key, spec); + + CryptoAlgorithm algo = require(id); + C ctx = algo.create(role, key, spec); + + // In WRAP mode, the proxy will emit creation metadata/events. + if (AUDIT_MODE != AuditMode.WRAP) { + AUDIT.onContextCreated(algo.id(), algo.providerName(), role, key, spec); + } + + if (AUDIT_MODE == AuditMode.WRAP) { + final AuditListener listener = AUDIT; // pass through the global listener + if (ctx instanceof SignatureContext) { + @SuppressWarnings("unchecked") + C out = (C) AuditedContexts.wrap(ctx, listener, role); + return out; + } else if (ctx instanceof EncryptionContext) { + @SuppressWarnings("unchecked") + C out = (C) AuditedContexts.wrap(ctx, listener, role); + return out; + } else if (ctx instanceof KemContext) { + @SuppressWarnings("unchecked") + C out = (C) AuditedContexts.wrap(ctx, listener, role); + return out; + } else if (ctx instanceof DigestContext) { + @SuppressWarnings("unchecked") + C out = (C) AuditedContexts.wrap(ctx, listener, role); + return out; + } else if (ctx instanceof MacContext) { + @SuppressWarnings("unchecked") + C out = (C) AuditedContexts.wrap(ctx, listener, role); + return out; + } + // Unknown context type: return as-is (no wrapping). + } + + return ctx; + } + + /** + * Creates a {@link CryptoContext} using the algorithm’s default spec for the + * role. + * + *

+ * Equivalent to {@code create(id, role, key, null)}. + *

+ * + * @param id canonical algorithm identifier + * @param role desired {@link KeyUsage} + * @param key key instance for the role + * @param context type + * @param key type + * @return a context ready for use + * @throws IOException if the underlying algorithm fails to create + * a context + * @throws IllegalArgumentException if {@code id} is unknown + * @throws UnsupportedRoleException if the algorithm does not support + * {@code role} + */ + public static C create(String id, KeyUsage role, K key) + throws IOException { + return create(id, role, key, null); + } + + /** + * Generates a fresh asymmetric {@link KeyPair} for the given algorithm id and + * spec. + * + *

+ * Emits + * {@link AuditListener#onKeyGenerated(String, String, AlgorithmKeySpec, KeyPair)} + * on success. + *

+ * + * @param id canonical algorithm identifier + * @param spec algorithm-specific key specification + * @param spec type + * @return newly generated key pair + * @throws GeneralSecurityException if key generation fails + * @throws IllegalArgumentException if {@code id} is unknown or the spec is + * unsupported + */ + public static KeyPair keyPair(String id, S spec) throws GeneralSecurityException { + CryptoAlgorithm algo = require(id); + @SuppressWarnings("unchecked") + KeyPair kp = algo.asymmetricKeyBuilder((Class) spec.getClass()).generateKeyPair(spec); + AUDIT.onKeyGenerated(algo.id(), algo.providerName(), spec, kp); + return kp; + } + + /** + * Imports a {@link PublicKey} using the algorithm’s registered asymmetric + * builder. + * + *

+ * Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)} + * on success. + *

+ * + * @param id canonical algorithm identifier + * @param spec algorithm-specific key specification containing encoded public + * material + * @param spec type + * @return imported public key + * @throws GeneralSecurityException if import fails or material is invalid + * @throws IllegalArgumentException if {@code id} is unknown or the spec is + * unsupported + */ + public static PublicKey publicKey(String id, S spec) throws GeneralSecurityException { + CryptoAlgorithm algo = require(id); + @SuppressWarnings("unchecked") + PublicKey k = algo.asymmetricKeyBuilder((Class) spec.getClass()).importPublic(spec); + AUDIT.onKeyBuilt(algo.id(), algo.providerName(), spec, k); + return k; + } + + /** + * Imports a {@link PrivateKey} using the algorithm’s registered asymmetric + * builder. + * + *

+ * Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)} + * on success. + *

+ * + * @param id canonical algorithm identifier + * @param spec algorithm-specific key specification containing encoded private + * material + * @param spec type + * @return imported private key + * @throws GeneralSecurityException if import fails or material is invalid + * @throws IllegalArgumentException if {@code id} is unknown or the spec is + * unsupported + */ + public static PrivateKey privateKey(String id, S spec) + throws GeneralSecurityException { + CryptoAlgorithm algo = require(id); + @SuppressWarnings("unchecked") + PrivateKey k = algo.asymmetricKeyBuilder((Class) spec.getClass()).importPrivate(spec); + AUDIT.onKeyBuilt(algo.id(), algo.providerName(), spec, k); + return k; + } + + /** + * Imports a symmetric {@link SecretKey} using the algorithm’s registered + * builder. + * + *

+ * Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)} + * on success. + *

+ * + * @param id canonical algorithm identifier + * @param spec algorithm-specific key specification containing raw/encoded + * material + * @param spec type + * @return imported secret key + * @throws GeneralSecurityException if import fails or material is invalid + * @throws IllegalArgumentException if {@code id} is unknown or the spec is + * unsupported + */ + public static SecretKey secretKey(String id, S spec) throws GeneralSecurityException { + CryptoAlgorithm algo = require(id); + @SuppressWarnings("unchecked") + SecretKey k = algo.symmetricKeyBuilder((Class) spec.getClass()).importSecret(spec); + AUDIT.onKeyBuilt(algo.id(), algo.providerName(), spec, k); + return k; + } + + /** + * Attempts to destroy a key via the JDK {@code Destroyable} interface. + * + *

+ * If destruction succeeds, + * {@link AuditListener#onKeyDestroyed(String, String, Key)} is emitted. Any + * exceptions from {@code destroy()} are swallowed; the method returns + * {@code false} when destruction did not occur. + *

+ * + * @param algoId algorithm identifier used for audit metadata + * @param provider provider name used for audit metadata + * @param key key to destroy + * @return {@code true} if the key reported destroyed, {@code false} otherwise + */ + public static boolean destroyKey(String algoId, String provider, Key key) { + boolean destroyed = false; + try { + if (key instanceof javax.security.auth.Destroyable) { + javax.security.auth.Destroyable d = (javax.security.auth.Destroyable) key; + if (!d.isDestroyed()) { + d.destroy(); + destroyed = true; + } + } + } catch (Exception ignored) { // NOPMD + // swallow and report via audit only if destroyed + } + if (destroyed) { + AUDIT.onKeyDestroyed(algoId, provider, key); + } + return destroyed; + } + + /** + * Convenience wrapper for + * {@link CryptoAlgorithm#generateSecret(AlgorithmKeySpec)}. + * + * @param id canonical algorithm identifier + * @param spec algorithm-specific key specification + * @param spec type + * @return newly generated secret key + * @throws GeneralSecurityException if key generation fails + * @throws IllegalArgumentException if {@code id} is unknown + */ + public static SecretKey generateSecret(String id, S spec) + throws GeneralSecurityException { + return require(id).generateSecret(spec); + } + + /** + * Convenience wrapper for + * {@link CryptoAlgorithm#generateKeyPair(AlgorithmKeySpec)}. + * + * @param id canonical algorithm identifier + * @param spec algorithm-specific key specification + * @param spec type + * @return newly generated key pair + * @throws GeneralSecurityException if key generation fails + * @throws IllegalArgumentException if {@code id} is unknown + */ + public static KeyPair generateKeyPair(String id, S spec) + throws GeneralSecurityException { + return require(id).generateKeyPair(spec); + } + + /** + * Convenience wrapper for + * {@link CryptoAlgorithm#importPublic(AlgorithmKeySpec)}. + * + * @param id canonical algorithm identifier + * @param spec algorithm-specific key specification + * @param spec type + * @return imported public key + * @throws GeneralSecurityException if import fails + * @throws IllegalArgumentException if {@code id} is unknown + */ + public static PublicKey importPublic(String id, S spec) + throws GeneralSecurityException { + return require(id).importPublic(spec); + } + + /** + * Convenience wrapper for + * {@link CryptoAlgorithm#importPrivate(AlgorithmKeySpec)}. + * + * @param id canonical algorithm identifier + * @param spec algorithm-specific key specification + * @param spec type + * @return imported private key + * @throws GeneralSecurityException if import fails + * @throws IllegalArgumentException if {@code id} is unknown + */ + public static PrivateKey importPrivate(String id, S spec) + throws GeneralSecurityException { + return require(id).importPrivate(spec); + } + + /** + * Convenience wrapper for + * {@link CryptoAlgorithm#importSecret(AlgorithmKeySpec)}. + * + * @param id canonical algorithm identifier + * @param spec algorithm-specific key specification + * @param spec type + * @return imported secret key + * @throws GeneralSecurityException if import fails + * @throws IllegalArgumentException if {@code id} is unknown + */ + public static SecretKey importSecret(String id, S spec) + throws GeneralSecurityException { + return require(id).importSecret(spec); + } +} diff --git a/lib/src/main/java/zeroecho/core/CryptoCatalog.java b/lib/src/main/java/zeroecho/core/CryptoCatalog.java new file mode 100644 index 0000000..4786f3f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/CryptoCatalog.java @@ -0,0 +1,322 @@ +/******************************************************************************* + * 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.core; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.annotation.DisplayName; + +/** + * Immutable snapshot of discovered {@link CryptoAlgorithm} implementations, + * with utilities to validate and serialize the catalog. + * + *

+ * {@code CryptoCatalog} is a lightweight registry built at a point in time via + * {@link #load()}. It collects algorithms published through the Java SPI for + * {@link CryptoAlgorithm}, ensures identifier uniqueness, and exposes: + *

+ * + *
    + *
  • {@link #validate()} — sanity checks that each algorithm publishes at + * least one capability or key builder,
  • + *
  • {@link #toJson()} — a compact JSON description of algorithms, + * capabilities, and registered key builders, and
  • + *
  • {@link #toXml()} — an XML rendition of the same data.
  • + *
+ * + *

Identity and uniqueness

Algorithm ids are treated as canonical keys. + * If two providers expose the same {@linkplain CryptoAlgorithm#id() id}, the + * catalog build fails with {@link IllegalStateException}. + * + *

Immutability & thread-safety

After construction, the internal + * map is unmodifiable and safe to share across threads. This class performs no + * lazy discovery or on-demand mutation. + * + *

Serialization formats

+ *
    + *
  • JSON: produced by {@link #toJson()} as a single object with an + * {@code algorithms} array; trivial string escaping is applied.
  • + *
  • XML: produced by {@link #toXml()} with a {@code } + * root; {@code &<>} escaping is applied to element text and + * attributes.
  • + *
+ * + *

+ * Note: Default spec / key-spec values shown in outputs are derived from + * {@code Supplier}s registered by algorithms. Suppliers may compute labels or + * return lightweight descriptors; their intent is documentation, not + * round‑tripping. + *

+ * + * @since 1.0 + */ +public final class CryptoCatalog { + private final Map algos; + + private CryptoCatalog(Map algos) { + this.algos = algos; + } + + /** + * Discovers {@link CryptoAlgorithm} implementations via {@link ServiceLoader} + * and returns an immutable catalog snapshot. + * + *

+ * During loading, algorithm ids are checked for uniqueness. A duplicate id + * results in an {@link IllegalStateException} to prevent ambiguous resolution. + *

+ * + * @return an immutable {@code CryptoCatalog} with all discovered algorithms + * @throws IllegalStateException if two providers declare the same algorithm id + */ + public static CryptoCatalog load() { + Map m = new HashMap<>(); + ServiceLoader.load(CryptoAlgorithm.class).forEach(a -> { + CryptoAlgorithm prev = m.put(a.id(), a); + if (prev != null) { + throw new IllegalStateException("Duplicate algorithm id: " + a.id()); + } + }); + return new CryptoCatalog(Collections.unmodifiableMap(m)); + } + + /** + * Verifies that each algorithm in this catalog exposes at least one capability + * or key builder. + * + *

+ * This is a sanity check for provider completeness. If an algorithm publishes + * neither {@link CryptoAlgorithm#listCapabilities() capabilities} nor + * asymmetric/symmetric key builders, the validation fails. + *

+ * + * @throws IllegalStateException if any algorithm has no capabilities and no key + * builders + */ + public void validate() { + StringBuilder sb = null; + for (CryptoAlgorithm a : algos.values()) { + boolean hasCaps = !a.listCapabilities().isEmpty(); + boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty(); + boolean hasSym = !a.symmetricBuildersInfo().isEmpty(); + if (!hasCaps && !hasAsym && !hasSym) { + if (sb == null) { + sb = new StringBuilder(50 /* minimal record size */ * 6 /* suggested avg of error records */); // NOPMD + } + sb.append("Algorithm ").append(a.id()).append(" has no capabilities nor key builders.\n"); + } + } + if (sb != null) { + throw new IllegalStateException(sb.toString()); + } + } + + private static String labelOf(Object o) { + if (o == null) { + return "null"; + } + if (o instanceof Describable) { + return ((Describable) o).description(); + } + Class c = o instanceof Class ? (Class) o : o.getClass(); + DisplayName dn = c.getAnnotation(DisplayName.class); + if (dn != null) { + return dn.value(); + } + return c.getSimpleName(); + } + + /** + * Serializes the catalog to a compact JSON document. + * + *

+ * The schema is: + *

+ *
{@code
+     * {
+     *   "algorithms": [
+     *     {
+     *       "id": "AES/GCM",
+     *       "displayName": "AES-GCM",
+     *       "capabilities": [
+     *         {
+     *           "family": "SYMMETRIC",
+     *           "role": "ENCRYPT",
+     *           "contextType": "AeadEncryptContext",
+     *           "keyType": "SecretKey",
+     *           "specType": "AeadSpec",
+     *           "defaultSpec": "Random nonce, 128-bit tag"
+     *         }
+     *       ],
+     *       "asymmetricKeyBuilders": [
+     *         { "specType": "Ed25519Spec", "defaultKeySpec": "Ed25519 default" }
+     *       ],
+     *       "symmetricKeyBuilders": [
+     *         { "specType": "AesKeySpec", "defaultKeySpec": "AES-256" }
+     *       ]
+     *     }
+     *   ]
+     * }
+     * }
+ * + *

+ * String values are escaped for quotes and backslashes. The method does not + * attempt to pretty-print; callers can format the output if needed. + *

+ * + * @return a JSON string describing algorithms, capabilities, and key builders + */ + public String toJson() { + StringBuilder sb = new StringBuilder(4096); + sb.append("{\"algorithms\":["); + boolean firstAlgo = true; + for (CryptoAlgorithm a : algos.values()) { + if (!firstAlgo) { + sb.append(','); + } + firstAlgo = false; + + sb.append('{').append(jsonField("id", a.id())).append(',').append(jsonField("displayName", a.displayName())) + .append(",\"capabilities\":["); + + boolean fc = true; + for (Capability cap : a.listCapabilities()) { + if (!fc) { + sb.append(','); + } + fc = false; + sb.append('{').append(jsonField("family", cap.family().name())).append(',') + .append(jsonField("role", cap.role().name())).append(',') + .append(jsonField("contextType", cap.contextType().getSimpleName())).append(',') + .append(jsonField("keyType", cap.keyType().getSimpleName())).append(',') + .append(jsonField("specType", cap.specType().getSimpleName())).append(",\"defaultSpec\":") + .append(cap.defaultSpec() == null ? "null" : jsonString(labelOf(cap.defaultSpec().get()))) + .append('}'); + } + sb.append("],\"asymmetricKeyBuilders\":["); + boolean fa = true; + for (CryptoAlgorithm.AsymBuilderInfo kb : a.asymmetricBuildersInfo()) { + if (!fa) { + sb.append(','); + } + fa = false; + sb.append('{').append(jsonField("specType", kb.specType.getSimpleName())).append(",\"defaultKeySpec\":") + .append(kb.defaultKeySpec == null ? "null" : jsonString(labelOf(kb.defaultKeySpec))) + .append('}'); + } + sb.append("],\"symmetricKeyBuilders\":["); + boolean fs = true; + for (CryptoAlgorithm.SymBuilderInfo kb : a.symmetricBuildersInfo()) { + if (!fs) { + sb.append(','); + } + fs = false; + sb.append('{').append(jsonField("specType", kb.specType().getSimpleName())) + .append(",\"defaultKeySpec\":") + .append(kb.defaultKeySpec() == null ? "null" : jsonString(labelOf(kb.defaultKeySpec()))) + .append('}'); + } + sb.append("]}"); + } + sb.append("]}"); + return sb.toString(); + } + + /** + * Serializes the catalog to an XML document. + * + *

+ * The root element is {@code }. Each algorithm becomes an + * {@code } element with {@code id} and {@code name} attributes, plus + * nested sections for {@code }, {@code }, + * and {@code }. + *

+ * + *

+ * Element text and attribute values are escaped for {@code &, <, >}. + *

+ * + * @return an XML string describing algorithms, capabilities, and key builders + */ + public String toXml() { + StringBuilder sb = new StringBuilder(4096); + sb.append(""); + for (CryptoAlgorithm a : algos.values()) { + sb.append(""); + for (Capability cap : a.listCapabilities()) { + sb.append("") + .append(esc(cap.contextType().getSimpleName())).append("") + .append(esc(cap.keyType().getSimpleName())).append("") + .append(esc(cap.specType().getSimpleName())).append("") + .append(esc(labelOf(cap.defaultSpec().get()))).append(""); + } + sb.append(""); + for (CryptoAlgorithm.AsymBuilderInfo kb : a.asymmetricBuildersInfo()) { + sb.append("") + .append(kb.defaultKeySpec == null ? "" : esc(labelOf(kb.defaultKeySpec))) + .append(""); + } + sb.append(""); + for (CryptoAlgorithm.SymBuilderInfo kb : a.symmetricBuildersInfo()) { + sb.append("") + .append(kb.defaultKeySpec() == null ? "" : esc(labelOf(kb.defaultKeySpec()))) + .append(""); + } + sb.append(""); + } + sb.append(""); + return sb.toString(); + } + + private static String jsonField(String name, String value) { + return jsonString(name) + ":" + jsonString(value); + } + + private static String jsonString(String v) { + return v == null ? "null" : "\"" + v.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + } + + private static String esc(String v) { + return v == null ? "" : v.replace("&", "&").replace("<", "<").replace(">", ">"); + } +} diff --git a/lib/src/main/java/zeroecho/core/KeyUsage.java b/lib/src/main/java/zeroecho/core/KeyUsage.java new file mode 100644 index 0000000..fb4d02c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/KeyUsage.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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.core; + +/** + * Declares the intended purpose(s) of a cryptographic key. + *

+ * Keys should be bound to a specific {@code KeyUsage} at generation or import + * time. Enforcing usage prevents accidental cross-purposes — e.g., reusing a + * signing key for encryption, or using a MAC key for key agreement — which can + * lead to serious vulnerabilities. + * + *

Typical usages

+ *
    + *
  • {@link #SIGN}/{@link #VERIFY}: digital signature schemes (Ed25519, ECDSA, + * RSA-PSS).
  • + *
  • {@link #ENCRYPT}/{@link #DECRYPT}: confidentiality with symmetric or + * asymmetric encryption.
  • + *
  • {@link #ENCAPSULATE}/{@link #DECAPSULATE}: key encapsulation mechanisms + * (KEM).
  • + *
  • {@link #MAC}: keyed message authentication (e.g., HMAC, KMAC).
  • + *
  • {@link #DIGEST}: unkeyed hashing (e.g., SHA-256, SHA3-512).
  • + *
  • {@link #AGREEMENT}: key agreement (e.g., X25519, ECDH) to derive shared + * secrets.
  • + *
+ * + *

+ * Security note: Many real-world breaches trace back to cryptographic + * keys being used outside their intended purpose. Libraries should validate + * {@code KeyUsage} before performing operations. + *

+ * + * @since 1.0 + */ +public enum KeyUsage { + /** Create digital signatures. */ + SIGN, + /** Verify digital signatures. */ + VERIFY, + /** Encrypt data for confidentiality. */ + ENCRYPT, + /** Decrypt data that was encrypted under the matching key/parameters. */ + DECRYPT, + /** Encapsulate a shared secret in a KEM flow. */ + ENCAPSULATE, + /** Decapsulate a shared secret in a KEM flow. */ + DECAPSULATE, + /** Compute a keyed message authentication code (e.g., HMAC, KMAC). */ + MAC, + /** Compute an unkeyed hash (e.g., SHA-256), pipeline-friendly. */ + DIGEST, + /** Perform key agreement (e.g., X25519, ECDH). */ + AGREEMENT +} diff --git a/lib/src/main/java/zeroecho/core/NullKey.java b/lib/src/main/java/zeroecho/core/NullKey.java new file mode 100644 index 0000000..249b80a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/NullKey.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core; + +import java.security.Key; +import java.util.Objects; + +/** + * Sentinel {@link Key} representing the intentional absence of cryptographic + * key material. + * + *

+ * {@code NullKey} satisfies APIs that require a {@code Key} reference even when + * no secret exists (e.g., unkeyed digests or placeholder capabilities). It + * contains no sensitive data and is safe to log and share. + *

+ * + *

Intended uses

+ *
    + *
  • Unkeyed operations such as {@code DIGEST} roles where keys are + * semantically undefined but a {@code Key} parameter is part of a common + * interface.
  • + *
  • Testing and scaffolding where a non-null {@code Key} is required to + * exercise type and flow without provisioning secrets.
  • + *
  • Metadata publication for capabilities that don’t depend on key + * material.
  • + *
+ * + *

Security properties

+ *
    + *
  • Contains no secret; {@link #getEncoded()} returns an empty byte + * array.
  • + *
  • Algorithm and format are the literal string {@code "NONE"}.
  • + *
  • All instances are equal by type; prefer the {@link #INSTANCE} + * singleton.
  • + *
+ * + *

Thread-safety

Immutable; the singleton instance is safe to reuse + * across threads. + * + * @since 1.0 + */ +public final class NullKey implements Key { + private static final long serialVersionUID = -3423524955655163523L; + /** + * Singleton instance for all uses where a {@link Key} is required but no key + * exists. + * + *

+ * Use this constant rather than constructing new instances. Equality for + * {@code NullKey} is defined by type, so all instances compare equal; the + * singleton avoids unnecessary allocations and clarifies intent. + *

+ */ + public static final NullKey INSTANCE = new NullKey(); + + private NullKey() { + } + + /** + * Returns the algorithm identifier for this sentinel key. + * + *

+ * The value is the literal string {@code "NONE"} to signal that no real + * cryptographic algorithm is associated with this key. + *

+ * + * @return the string {@code "NONE"} + */ + @Override + public String getAlgorithm() { + return "NONE"; + } + + /** + * Returns the primary encoding format for this sentinel key. + * + *

+ * The value is the literal string {@code "NONE"}; {@link #getEncoded()} returns + * an empty byte array. + *

+ * + * @return the string {@code "NONE"} + */ + @Override + public String getFormat() { + return "NONE"; + } + + /** + * Returns the key in its primary encoding. + * + *

+ * For {@code NullKey}, this is an empty byte array because no material exists. + * Callers MAY rely on this to avoid special-casing unkeyed flows. + *

+ * + * @return a new zero-length byte array + */ + @Override + public byte[] getEncoded() { + return new byte[0]; + } + + /** + * Compares this object to another for equality. + * + *

+ * All {@code NullKey} instances are considered equal regardless of identity; + * equality is defined by type rather than state. Prefer comparing + * against {@link #INSTANCE} when intent matters, but this method ensures + * generic collections and caches behave as expected. + *

+ * + * @param obj the reference object with which to compare + * @return {@code true} if {@code obj} is a {@code NullKey}; {@code false} + * otherwise + */ + @Override + public boolean equals(Object obj) { + return obj instanceof NullKey; + } + + /** + * Returns a stable hash code consistent with {@link #equals(Object)}. + * + *

+ * Derived from a fixed class-based token so that all {@code NullKey} instances + * hash identically, matching the type-based equality semantics. + *

+ * + * @return a stable hash code value + */ + @Override + public int hashCode() { + return Objects.hash("NullKey"); + } + + /** + * Returns a diagnostic string for logs and debugging. + * + *

+ * The returned value is the literal {@code "NullKey"}. It contains no secrets + * and is safe for inclusion in logs and error messages. + *

+ * + * @return the string {@code "NullKey"} + */ + @Override + public String toString() { + return "NullKey"; + } +} diff --git a/lib/src/main/java/zeroecho/core/SymmetricHeaderCodec.java b/lib/src/main/java/zeroecho/core/SymmetricHeaderCodec.java new file mode 100644 index 0000000..22a7b65 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/SymmetricHeaderCodec.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import conflux.CtxInterface; + +/** + * Optional SPI to write/read a small header that carries runtime params (e.g., + * IV, tag length, AAD hash). If used, encryption will prepend the header and + * decryption will parse it before initializing the cipher. + */ +public interface SymmetricHeaderCodec { + /** Write header to {@code out}, using/recording params from {@code ctx}. */ + void writeHeader(OutputStream out, CryptoAlgorithm algorithm, CtxInterface ctx) throws IOException; + + /** + * Read header from {@code in}, populate params in {@code ctx}, and return an + * InputStream positioned immediately after the header. + */ + InputStream readHeader(InputStream in, CryptoAlgorithm algorithm, CtxInterface ctx) throws IOException; +} diff --git a/lib/src/main/java/zeroecho/core/alg/AbstractCryptoAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/AbstractCryptoAlgorithm.java new file mode 100644 index 0000000..3d99abd --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/AbstractCryptoAlgorithm.java @@ -0,0 +1,178 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg; + +import java.security.Key; +import java.util.function.Supplier; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.Capability; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.spec.ContextSpec; +import zeroecho.core.spi.ContextConstructorKS; + +/** + * Convenience base class for concrete {@link CryptoAlgorithm} implementations. + * + *

+ * {@code AbstractCryptoAlgorithm} streamlines two common tasks during algorithm + * construction: + *

+ * + *
    + *
  1. Binding roles to runtime factories via + * {@link #capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}, + * which registers a {@link KeyUsage role} together with its expected + * {@link CryptoContext} type, accepted {@link Key} type, optional + * {@link ContextSpec} type, the constructor factory, and a default spec + * supplier.
  2. + *
  3. Publishing descriptive capabilities by automatically creating and + * adding a {@link Capability} record that higher layers can inspect for feature + * discovery, documentation, or automated selection.
  4. + *
+ * + *

Typical usage

{@code
+ * public final class AesGcmAlgorithm extends AbstractCryptoAlgorithm {
+ *   public AesGcmAlgorithm() {
+ *     super("AES/GCM", "AES-GCM", "JCA");
+ *
+ *     capability(
+ *         AlgorithmFamily.SYMMETRIC,
+ *         KeyUsage.ENCRYPT,
+ *         AeadEncryptContext.class,
+ *         javax.crypto.SecretKey.class,
+ *         AeadSpec.class,
+ *         (key, spec) -> new JcaAesGcmEncryptContext(key, spec),
+ *         AeadSpec::withRandomNonce);
+ *
+ *     capability(
+ *         AlgorithmFamily.SYMMETRIC,
+ *         KeyUsage.DECRYPT,
+ *         AeadDecryptContext.class,
+ *         javax.crypto.SecretKey.class,
+ *         AeadSpec.class,
+ *         (key, spec) -> new JcaAesGcmDecryptContext(key, spec),
+ *         AeadSpec::withRandomNonce);
+ *   }
+ * }
+ * }
+ * + *

Thread-safety

Instances are immutable once constructed and safe to + * share across threads. The contexts created by registered factories are not + * necessarily thread-safe. + * + * @since 1.0 + */ +public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm { + /** + * Constructs an algorithm with default priority ({@code 0}) and provider + * {@code "default"}. + * + * @param id canonical, provider-independent identifier (e.g., + * {@code "AES/GCM"}) + * @param displayName human-friendly name for logs and diagnostics + * @throws NullPointerException if {@code id} or {@code displayName} is + * {@code null} + */ + protected AbstractCryptoAlgorithm(String id, String displayName) { + super(id, displayName); + } + + /** + * Constructs an algorithm with default priority ({@code 0}) and an explicit + * provider name. + * + * @param id canonical, provider-independent identifier + * @param displayName human-friendly name for logs and diagnostics + * @param providerName provider label (e.g., {@code "JCA"}, {@code "BC"}, + * {@code "default"}) + * @throws NullPointerException if any argument is {@code null} + */ + protected AbstractCryptoAlgorithm(String id, String displayName, String providerName) { + super(id, displayName, providerName); + } + + /** + * Declares a capability for this algorithm and binds a runtime factory for the + * given role. + * + *

+ * This single call performs two actions atomically: + *

+ *
    + *
  • Runtime binding: delegates to + * {@link CryptoAlgorithm#bind(KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)} + * so that {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)} can + * construct the appropriate {@link CryptoContext} when invoked.
  • + *
  • Metadata publication: creates a {@link Capability} describing this + * role (algorithm id, {@link AlgorithmFamily family}, role, context/key/spec + * types, and default spec supplier) and adds it to the algorithm’s advertised + * capabilities, discoverable via + * {@link CryptoAlgorithm#listCapabilities()}.
  • + *
+ * + *

Validation

Type checks happen at creation time (via {@code bind}) + * and again when {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)} is + * called. If a factory returns a context not assignable to {@code ctxType}, an + * {@link IllegalStateException} will be thrown. + * + * @param family high-level algorithm family classification + * @param role supported {@link KeyUsage} role (e.g., {@code ENCRYPT}, + * {@code VERIFY}) + * @param ctxType concrete {@link CryptoContext} type constructed by + * {@code factory} + * @param keyType accepted {@link Key} type for this role + * @param specType accepted {@link ContextSpec} type (may be a marker type) + * @param factory constructor that builds a context for (key, spec) + * @param defaultSpec default spec supplier used when callers pass {@code null} + * spec + * @param context type + * @param key type + * @param spec type + * @throws NullPointerException if any class/factory/supplier argument is + * {@code null} + */ + protected void capability(AlgorithmFamily family, + KeyUsage role, Class ctxType, Class keyType, Class specType, ContextConstructorKS factory, + Supplier defaultSpec) { + + // bind runtime factory + bind(role, ctxType, keyType, specType, factory, defaultSpec); + // publish metadata + addCapability(new Capability(id(), family, role, ctxType, keyType, specType, defaultSpec)); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/aes/AesAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/aes/AesAlgorithm.java new file mode 100644 index 0000000..bd11318 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/aes/AesAlgorithm.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * 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.core.alg.aes; + +import java.security.GeneralSecurityException; +import java.security.SecureRandom; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.SymmetricKeyBuilder; + +/** + * AES algorithm registration and capability wiring. + * + *

+ * Registers streaming ENCRYPT/DECRYPT contexts for the {@code "AES"} family, + * provides secure defaults (GCM/128), and exposes key builders for generation + * and import. + *

+ * + *
    + *
  • Capabilities: + *
      + *
    • ENCRYPT/DECRYPT → {@link AesCipherContext} with {@link AesSpec}
    • + *
    • VoidSpec fallback → {@code AesSpec.gcm128(null)}
    • + *
    + *
  • + *
  • Key builders: + *
      + *
    • {@link AesKeyGenSpec} → JCA {@link javax.crypto.KeyGenerator}
    • + *
    • {@link AesKeyImportSpec} → {@link javax.crypto.spec.SecretKeySpec}
    • + *
    + *
  • + *
+ * + *

Thread‑safety

Registration is static and thread‑safe. Produced + * contexts are stateful and not thread‑safe. + * + * @since 1.0 + */ +public final class AesAlgorithm extends AbstractCryptoAlgorithm { + /** + * Constructs the AES algorithm descriptor and performs capability registration. + */ + public AesAlgorithm() { + super("AES", "AES (CBC/GCM/CTR)"); + + // Context capabilities + capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, SecretKey.class, AesSpec.class, + (SecretKey k, AesSpec s) -> new AesCipherContext(this, k, true, s, new SecureRandom()), + () -> AesSpec.gcm128(null)); + + capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, SecretKey.class, AesSpec.class, + (SecretKey k, AesSpec s) -> new AesCipherContext(this, k, false, s, new SecureRandom()), + () -> AesSpec.gcm128(null)); + + capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, SecretKey.class, + VoidSpec.class, (SecretKey k, VoidSpec s) -> new AesCipherContext(this, k, true, AesSpec.gcm128(null), + new SecureRandom()), + () -> VoidSpec.INSTANCE); + + capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, SecretKey.class, + VoidSpec.class, (SecretKey k, VoidSpec s) -> new AesCipherContext(this, k, false, AesSpec.gcm128(null), + new SecureRandom()), + () -> VoidSpec.INSTANCE); + + // Secret generation builder (AesKeyGenSpec) + registerSymmetricKeyBuilder(AesKeyGenSpec.class, new SymmetricKeyBuilder<>() { + @Override + public SecretKey generateSecret(AesKeyGenSpec spec) throws GeneralSecurityException { + KeyGenerator kg = KeyGenerator.getInstance("AES"); + kg.init(spec.keySizeBits(), new SecureRandom()); + return kg.generateKey(); + } + + @Override + public SecretKey importSecret(AesKeyGenSpec spec) { + throw new UnsupportedOperationException("Use AesKeyImportSpec for importing AES keys"); + } + }, AesKeyGenSpec::aes256); + + // Secret import builder (AesKeyImportSpec) + registerSymmetricKeyBuilder(AesKeyImportSpec.class, new SymmetricKeyBuilder<>() { + @Override + public SecretKey generateSecret(AesKeyImportSpec spec) { + throw new UnsupportedOperationException("Use AesKeyGenSpec to generate AES keys"); + } + + @Override + public SecretKey importSecret(AesKeyImportSpec spec) { + return new SecretKeySpec(spec.key(), "AES"); + } + }, null); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/aes/AesCipherContext.java b/lib/src/main/java/zeroecho/core/alg/aes/AesCipherContext.java new file mode 100644 index 0000000..565219a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/aes/AesCipherContext.java @@ -0,0 +1,296 @@ +/******************************************************************************* + * 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.core.alg.aes; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; + +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.err.ProviderFailureException; +import zeroecho.core.io.CipherTransformInputStreamBuilder; +import zeroecho.core.spi.ContextAware; +import zeroecho.core.util.Strings; + +/** + * Streaming AES cipher context for GCM / CBC / CTR. + * + *

+ * IV and optional AAD are exchanged via a {@link conflux.CtxInterface} set with + * {@link #setContext(conflux.CtxInterface)}. On ENCRYPT, a fresh IV is + * generated if absent and stored back; on DECRYPT, IV must be present (from + * context or header). + *

+ * + *

+ * When a {@link zeroecho.core.SymmetricHeaderCodec} is embedded in the + * {@link AesSpec} and a context is present, the context reads/writes a minimal + * header carrying IV, tag bits, and an optional AAD hash. + *

+ * + * @since 1.0 + */ +public final class AesCipherContext implements EncryptionContext, ContextAware { + private static final Logger LOG = Logger.getLogger(AesCipherContext.class.getName()); + + private static final int AES_BLOCK = 16; + private static final int GCM_DEFAULT_IV_BYTES = 12; + + private final CryptoAlgorithm algorithm; + private final SecretKey key; + private final boolean encrypt; + private final AesSpec spec; + private final SecureRandom rnd; + + private volatile CtxInterface ctx; // NOPMD + + /** + * Creates a new AES streaming context. + * + * @param algorithm the owning algorithm descriptor (ID {@code "AES"}); not null + * @param key the AES {@link SecretKey}; not null + * @param encrypt whether this context encrypts ({@code true}) or decrypts + * ({@code false}) + * @param spec static AES settings (mode/padding and GCM tag bits); not + * null + * @param rnd secure random source; if null, a default is created + * @throws NullPointerException if any required parameter is null + * @throws IllegalArgumentException if {@code spec} is inconsistent (e.g., GCM + * without NOPADDING) + */ + public AesCipherContext(CryptoAlgorithm algorithm, SecretKey key, boolean encrypt, AesSpec spec, SecureRandom rnd) { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm must not be null"); + this.key = Objects.requireNonNull(key, "secret key must not be null"); + this.encrypt = encrypt; + this.spec = Objects.requireNonNull(spec, "spec must not be null"); + this.rnd = (rnd != null ? rnd : new SecureRandom()); + } + + /** + * Returns the algorithm descriptor (ID {@code "AES"}). + * + * @return the algorithm + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + * @return the secret key + */ + @Override + public java.security.Key key() { + return key; + } + + /** + * Sets the Conflux session context used to exchange IV/AAD/tagBits and to + * enable header I/O. + * + * @param context the context; may be {@code null} to disable context features + */ + @Override + public void setContext(CtxInterface context) { + this.ctx = context; + } + + /** + * Returns the currently set Conflux context (possibly {@code null}). + * + * @return the context or {@code null} + */ + @Override + public CtxInterface context() { + return ctx; + } + + /** + * Attaches this context to an upstream stream and returns a transforming + * stream. + *
    + *
  • Decrypt + header → reads header first, hydrates context, then + * transforms
  • + *
  • Transform → initializes JCA cipher with IV/AAD (GCM), staging final + * output if necessary
  • + *
  • Encrypt + header → prepends header after IV is determined
  • + *
+ * + * @param upstream the source stream; not null + * @return a stream that yields ciphertext (encrypt) or plaintext (decrypt) + * @throws IOException on I/O or provider errors; includes IV length mismatches + * and missing IV for decrypt + */ + @Override + public InputStream attach(InputStream upstream) throws IOException { + Objects.requireNonNull(upstream, "upstream must not be null"); + try { + // If both spec.header() and ctx are present, let this context read/write the + // header. + final SymmetricHeaderCodec header = spec.header(); + final boolean activeHeader = (ctx != null) && header != null; + + InputStream in = upstream; + if (!encrypt && activeHeader) { + // DECRYPT: parse header first; hydrate ctx (IV / tag bits / AAD check). + LOG.log(Level.FINE, "decryption: reading header for {0}", algorithm); + in = header.readHeader(in, algorithm, ctx); + } + + Cipher cipher = Cipher.getInstance(jcaTransform(spec)); + initCipher(cipher); // consumes IV/AAD from ctx if present; generates IV on ENCRYPT and may store it + // back; sets tagBits for GCM + + InputStream out = // new Stream(in, cipher, spec); + CipherTransformInputStreamBuilder.builder().withCipher(cipher).withUpstream(in) + .withUpdateStreaming().withInputBlockSize(AES_BLOCK).withOutputBlockSize(AES_BLOCK) + .withFinalizationOutputChunks(2).build(); + if (encrypt && activeHeader) { + // ENCRYPT: after IV exists in ctx, prepend header + LOG.log(Level.FINE, "encryption: reading header for {0}", algorithm); + java.io.ByteArrayOutputStream hdr = new java.io.ByteArrayOutputStream(64); + header.writeHeader(hdr, algorithm, ctx); + out = new java.io.SequenceInputStream(new java.io.ByteArrayInputStream(hdr.toByteArray()), out); + } + return out; + } catch (GeneralSecurityException e) { + throw new ProviderFailureException("AES attach/init failed", e); + } + } + + @Override + public void close() { + /* no-op; stream finalizes itself */ + } + + // ---- internals ---- + + private static String jcaTransform(AesSpec s) { + return switch (s.mode()) { + case GCM -> "AES/GCM/NOPADDING"; + case CTR -> "AES/CTR/NOPADDING"; + case CBC -> "AES/CBC/" + s.padding().name(); + }; + } + + private void initCipher(Cipher cipher) throws GeneralSecurityException, IOException { // NOPMD + final String id = algorithm.id(); + byte[] iv = getCtxBytes(ConfluxKeys.iv(id)); + + final int ivLen = (spec.mode() == AesSpec.Mode.GCM) ? GCM_DEFAULT_IV_BYTES : AES_BLOCK; + + if (encrypt) { + if (iv == null) { + iv = new byte[ivLen]; + rnd.nextBytes(iv); + putCtxBytes(ConfluxKeys.iv(id), iv); + } else if (iv.length != ivLen) { + throw new IOException("IV length mismatch: expected " + ivLen + " bytes, got " + iv.length); + } + } else { + if (iv == null) { + throw new IOException("IV not found in context for AES " + spec.mode() + " decryption"); + } + if (iv.length != ivLen) { + throw new IOException("IV length mismatch: expected " + ivLen + " bytes, got " + iv.length); + } + } + + switch (spec.mode()) { + case GCM: { + int tagBits = 0; + if (ctx != null) { + Integer val = ctx.get(ConfluxKeys.tagBits(id)); + if (val != null) { + tagBits = val; + } + } + // tagBits was not in ctx => spec settings are applied + if (tagBits < 1) { // NOPMD + tagBits = spec.tagLenBits() > 0 ? spec.tagLenBits() : 128; + } + GCMParameterSpec gps = new GCMParameterSpec(tagBits, iv); + cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, gps); + byte[] aad = getCtxBytes(ConfluxKeys.aad(id)); + if (aad == null) { + // AAD should be defined empty + aad = new byte[0]; + putCtxBytes(ConfluxKeys.aad(id), aad); + } + cipher.updateAAD(aad); + + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "GCM setup: tagBits={0} iv={1} aad={2}", + new Object[] { tagBits, Strings.toShortHexString(iv), Strings.toShortHexString(aad) }); + } + + break; + } + case CTR: + case CBC: { + cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); + break; + } + } + } + + private byte[] getCtxBytes(conflux.Key key) { + if (ctx == null || key == null) { + return null; // NOPMD + } + return ctx.get(key); + } + + private void putCtxBytes(conflux.Key key, byte[] value) { + if (ctx != null && key != null) { + ctx.put(key, value); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/aes/AesHeaderCodec.java b/lib/src/main/java/zeroecho/core/alg/aes/AesHeaderCodec.java new file mode 100644 index 0000000..9d21bac --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/aes/AesHeaderCodec.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.aes; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.io.Util; +import zeroecho.core.util.Strings; + +/** + * Minimal AES header codec that persists runtime parameters only: + *
IV | tagBits(pack7) | aadFlag(1) | [aadHash(32)]
+ * + *
    + *
  • IV: 12 bytes for GCM; 16 bytes for CBC/CTR.
  • + *
  • tagBits: 0 for non‑GCM; otherwise 96..128.
  • + *
  • aadFlag: 1 when AAD is present; the header then includes a SHA‑256 + * of the AAD.
  • + *
+ * + *

+ * No algorithm magic is written. On decrypt, the codec hydrates the Conflux + * context with IV and tag bits and enforces AAD consistency if an AAD hash is + * present. + *

+ * + * @since 1.0 + */ +public final class AesHeaderCodec implements SymmetricHeaderCodec { + private static final Logger LOG = Logger.getLogger(AesHeaderCodec.class.getName()); + + /** + * Writes the header to {@code out} using values from the provided context. + * + * @param out destination stream + * @param algorithm owning algorithm (ID {@code "AES"}) + * @param ctx source of IV, optional tag bits, and optional AAD + * @throws IOException if IV is missing or an I/O error occurs + */ + @Override + public void writeHeader(OutputStream out, CryptoAlgorithm algorithm, CtxInterface ctx) throws IOException { + final String id = algorithm.id(); // e.g., "AES" + + LOG.log(Level.FINE, "writeHeader={0}", id); + + byte[] iv = ctx.get(ConfluxKeys.iv(id)); + if (iv == null) { + throw new IOException("AesHeaderCodec: IV missing in Ctx"); + } + + // Optional AAD (hash only) + byte[] aad = ctx.get(ConfluxKeys.aad(id)); + byte[] aadHash = (aad == null || aad.length == 0) ? null : sha256(aad); + + // Optional tag bits hint for GCM (store if caller put it there) + Integer tb = ctx.get(ConfluxKeys.tagBits(id)); + int tagBits = tb == null ? 0 : tb; + + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "{4} header aad={0} aadHash={1} tagBits={2} iv={3}", + new Object[] { Strings.toShortHexString(aad), Strings.toShortHexString(aadHash), tagBits, + Strings.toShortHexString(iv), id }); + } + + Util.write(out, iv); // IV + Util.writePack7I(out, tagBits); // 0 for non-GCM + if (aadHash == null) { + out.write(0); + } else { + out.write(1); + out.write(aadHash); // 32 bytes + } + out.flush(); + } + + /** + * Reads a header from {@code in}, updates the context with IV (and tag bits), + * and verifies AAD if present. + * + * @param in source stream positioned at header + * @param algorithm owning algorithm (ID {@code "AES"}) + * @param ctx destination for IV/tag bits and AAD verification + * @return the same {@code InputStream}, positioned after the header + * @throws IOException on malformed header, missing AAD when required, or AAD + * hash mismatch + */ + @Override + public InputStream readHeader(InputStream in, CryptoAlgorithm algorithm, CtxInterface ctx) throws IOException { + final String id = algorithm.id(); + + byte[] iv = Util.read(in, 32); + int tagBits = Util.readPack7I(in); + int aadFlag = in.read(); + + byte[] aadHash = null; + if (aadFlag == 1) { // NOPMD + aadHash = in.readNBytes(32); + } + + // Hydrate Ctx + ctx.put(ConfluxKeys.iv(id), iv); + if (tagBits != 0) { + ctx.put(ConfluxKeys.tagBits(id), tagBits); + } + byte[] aad = ctx.get(ConfluxKeys.aad(id)); + if (aadHash != null) { + if (aad == null || aad.length == 0) { + throw new IOException("AES header expects AAD, but none provided in Ctx"); + } + if (!Arrays.equals(aadHash, sha256(aad))) { + throw new IOException("AES header: AAD hash mismatch"); + } + } + + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "{4} header aad={0} aadHash={1} tagBits={2} iv={3}", + new Object[] { Strings.toShortHexString(aad), Strings.toShortHexString(aadHash), tagBits, + Strings.toShortHexString(iv), id }); + } + + return in; // positioned after header + } + + private static byte[] sha256(byte[] a) throws IOException { + try { + return MessageDigest.getInstance("SHA-256").digest(a); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 unavailable", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/aes/AesKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/aes/AesKeyGenSpec.java new file mode 100644 index 0000000..3290ec1 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/aes/AesKeyGenSpec.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * 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.core.alg.aes; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Key generation parameters for AES. + * + *

+ * Instances of this record specify the size of an AES key in bits. Valid values + * are 128, 192, and 256. A {@code AesKeyGenSpec} is consumed by the registered + * AES key builder to create a new {@link javax.crypto.SecretKey}. + *

+ * + *

+ * Objects of this type are immutable and thread-safe. + *

+ * + * @param keySizeBits the AES key size in bits (128, 192, or 256) + * + * @since 1.0 + */ +public record AesKeyGenSpec(int keySizeBits) implements AlgorithmKeySpec, Describable { + /** + * Creates a new spec and validates the key size. + * + * @throws IllegalArgumentException if {@code keySizeBits} is not one of 128, + * 192, or 256 + */ + public AesKeyGenSpec { + if (keySizeBits != 128 && keySizeBits != 192 && keySizeBits != 256) { + throw new IllegalArgumentException("AES keySizeBits must be 128/192/256, got " + keySizeBits); + } + } + + /** + * Constructs a spec for generating a 128-bit AES key. + * + * @return a specification for AES-128 + */ + public static AesKeyGenSpec aes128() { + return new AesKeyGenSpec(128); + } + + /** + * Constructs a spec for generating a 192-bit AES key. + * + * @return a specification for AES-192 + */ + public static AesKeyGenSpec aes192() { + return new AesKeyGenSpec(192); + } + + /** + * Constructs a spec for generating a 256-bit AES key. + * + * @return a specification for AES-256 + */ + public static AesKeyGenSpec aes256() { + return new AesKeyGenSpec(256); + } + + /** + * Provides a short textual description of the key size. For example, + * {@code "256bits"}. + * + * @return a human-readable description + */ + @Override + public String description() { + return keySizeBits() + "bits"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/aes/AesKeyImportSpec.java b/lib/src/main/java/zeroecho/core/alg/aes/AesKeyImportSpec.java new file mode 100644 index 0000000..a9f19f4 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/aes/AesKeyImportSpec.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * 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.core.alg.aes; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.HexFormat; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Specification for importing an existing AES key. + * + *

+ * This class wraps raw key material (16, 24, or 32 bytes) for use with the AES + * algorithm. Factory methods support construction from raw bytes, hex strings, + * or Base64-encoded strings. The key material is defensively copied to maintain + * immutability. + *

+ * + *

+ * Instances are consumed by the AES key builder to produce a + * {@link javax.crypto.SecretKey}. + *

+ * + *

+ * Objects of this type are immutable and thread-safe. + *

+ * + * @since 1.0 + */ +public final class AesKeyImportSpec implements AlgorithmKeySpec { + private final byte[] key; + + private AesKeyImportSpec(byte[] key) { + Objects.requireNonNull(key, "key must not be null"); + int len = key.length; + if (len != 16 && len != 24 && len != 32) { + throw new IllegalArgumentException("AES key must be 16/24/32 bytes, got " + len); + } + this.key = Arrays.copyOf(key, len); + } + + /** + * Creates a specification from raw key bytes. + * + * @param key a 16, 24, or 32 byte array + * @return an import specification + * @throws IllegalArgumentException if the length is invalid + */ + public static AesKeyImportSpec fromRaw(byte[] key) { + return new AesKeyImportSpec(key); + } + + /** + * Creates a specification from a hex-encoded string. + * + * @param hex a string of 32, 48, or 64 hex characters + * @return an import specification + * @throws NullPointerException if {@code hex} is null + * @throws IllegalArgumentException if decoded length is invalid + */ + public static AesKeyImportSpec fromHex(String hex) { + Objects.requireNonNull(hex, "hex must not be null"); + return fromRaw(HexFormat.of().parseHex(hex)); + } + + /** + * Creates a specification from a Base64-encoded string. + * + * @param b64 Base64 text without padding + * @return an import specification + * @throws NullPointerException if {@code b64} is null + * @throws IllegalArgumentException if decoded length is invalid + */ + public static AesKeyImportSpec fromBase64(String b64) { + Objects.requireNonNull(b64, "base64 must not be null"); + return fromRaw(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a defensive copy of the key bytes. + * + * @return the raw key material + */ + public byte[] key() { + return Arrays.copyOf(key, key.length); + } + + /** + * Serializes this specification into a key/value sequence, encoding the key in + * Base64. + * + * @param spec the specification to marshal + * @return a sequence containing the key data + */ + public static PairSeq marshal(AesKeyImportSpec spec) { + String k = Base64.getEncoder().withoutPadding().encodeToString(spec.key); + return PairSeq.of("type", "AES-KEY", "k.b64", k); + } + + /** + * Reconstructs a specification from a key/value sequence. Accepts keys under + * {@code k.b64}, {@code k.hex}, or {@code k.raw}. + * + * @param p the sequence to parse + * @return a reconstructed import specification + * @throws IllegalArgumentException if no key is present + */ + public static AesKeyImportSpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + switch (k) { + case "k.b64" -> out = Base64.getDecoder().decode(v); + case "k.hex" -> out = HexFormat.of().parseHex(v); + case "k.raw" -> out = v.getBytes(StandardCharsets.ISO_8859_1); + default -> { + /* ignore */ } + } + } + if (out == null) { + throw new IllegalArgumentException("AES key missing (k.b64 / k.hex / k.raw)"); + } + return new AesKeyImportSpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/aes/AesSpec.java b/lib/src/main/java/zeroecho/core/alg/aes/AesSpec.java new file mode 100644 index 0000000..d8979b4 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/aes/AesSpec.java @@ -0,0 +1,362 @@ +/******************************************************************************* + * 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.core.alg.aes; + +import java.util.Objects; + +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.ContextSpec; + +/** + * Static configuration for AES encryption and decryption. + * + *

+ * An {@code AesSpec} captures compile‑time choices for the AES transform: the + * block mode, padding scheme (CBC only), and—when using GCM—the authentication + * tag length in bits. It may optionally embed a + * {@link zeroecho.core.SymmetricHeaderCodec} that governs how runtime + * parameters (IV/nonce, tag bits, AAD hash) are written to and read from the + * data stream. No IV/nonce or AAD bytes are stored in this type; those are + * runtime values exchanged via a Conflux session context. + *

+ * + *

Design

+ *
    + *
  • Immutability: instances are immutable and thread‑safe.
  • + *
  • Validation: GCM requires {@code NOPADDING} and a tag length + * between 96 and 128 bits (inclusive) in 8‑bit increments; non‑GCM modes must + * not specify a tag length.
  • + *
  • Header codec: when present, the codec persists IV and GCM + * parameters in‑band, and can bind AAD via a hash. The cipher context hydrates + * the runtime context from this header during decryption, and writes it during + * encryption.
  • + *
  • Runtime parameters: IV sizes are dictated by mode (12 bytes for + * GCM; 16 for CBC/CTR) and are generated or required at runtime by the cipher + * context, not by this spec.
  • + *
+ * + * @since 1.0 + */ +public final class AesSpec implements ContextSpec, Describable { + /** + * AES block modes supported by this implementation. + * + *

+ * Mode selection determines IV length and security properties. GCM is an AEAD + * mode; CBC and CTR provide confidentiality only and must be combined with a + * MAC (Encrypt‑then‑MAC) when integrity/authenticity are required. + *

+ */ + public enum Mode { + /** + * Cipher Block Chaining (CBC). + * + *

+ * Requires a 16‑byte IV. Supports {@link Padding#PKCS5PADDING} and + * {@link Padding#NOPADDING}. Not an AEAD mode; pair with a MAC for integrity. + *

+ */ + CBC, + /** + * Galois/Counter Mode (GCM), an authenticated encryption mode (AEAD). + * + *

+ * Requires a 12‑byte IV and {@link Padding#NOPADDING}. The tag length must be + * 96–128 bits (multiple of 8). Supports Additional Authenticated Data (AAD). + * Recommended default. + *

+ */ + GCM, + /** + * Counter mode (CTR). + * + *

+ * Uses a 16‑byte nonce/IV and {@link Padding#NOPADDING}. Not an AEAD mode; pair + * with a MAC for integrity. + *

+ */ + CTR + } + + /** + * Padding schemes for CBC mode. + * + *

+ * GCM and CTR always use {@link #NOPADDING}. + *

+ */ + public enum Padding { + /** + * No padding. + * + *

+ * Use only when the plaintext length is an exact multiple of the AES block size + * (16 bytes). Applicable to CBC; mandatory for GCM/CTR. + *

+ */ + NOPADDING, + /** + * PKCS#5 padding (recommended for CBC) available in the Standard JDK21+. + */ + PKCS5PADDING + } // CBC only + + private final Mode mode; + private final Padding padding; // CBC: PKCS5 or NOPADDING; GCM/CTR: NOPADDING + private final int tagLenBits; // GCM only (96..128 step 8). 0 for non-GCM. + private final SymmetricHeaderCodec header; + + private AesSpec(Mode mode, Padding padding, int tagLenBits, SymmetricHeaderCodec header) { + this.mode = Objects.requireNonNull(mode, "mode must not be null"); + this.padding = Objects.requireNonNull(padding, "padding must not be null"); + if (mode == Mode.GCM && padding != Padding.NOPADDING) { + throw new IllegalArgumentException("GCM must use NOPADDING"); + } + if (mode == Mode.GCM) { + if (tagLenBits % 8 != 0 || tagLenBits < 96 || tagLenBits > 128) { + throw new IllegalArgumentException("GCM tagLenBits must be 96..128 in steps of 8"); + } + } else if (tagLenBits != 0) { + throw new IllegalArgumentException("tagLenBits applies to GCM only"); + } + this.tagLenBits = tagLenBits; + this.header = header; // may be null + } + + /** + * Returns a new builder with safe defaults ({@link Mode#GCM}, + * {@link Padding#NOPADDING}, 128‑bit tag, no header). + * + * @return a fresh builder for {@link AesSpec} + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for {@link AesSpec}. + * + *

+ * Not thread‑safe. The resulting {@code AesSpec} is immutable. + *

+ */ + public static final class Builder { + private Mode mode = Mode.GCM; + private Padding padding = Padding.NOPADDING; + private int tagLenBits = 128; // only used for GCM + private SymmetricHeaderCodec header; + + /** + * Selects the AES mode. + * + * @param m the block mode ({@link Mode#CBC}, {@link Mode#GCM}, or + * {@link Mode#CTR}) + * @return this builder + * @throws NullPointerException if {@code m} is {@code null} + */ + public Builder mode(Mode m) { + this.mode = Objects.requireNonNull(m); + return this; + } + + /** + * Selects the padding scheme for CBC. + * + *

+ * Ignored for GCM and CTR, which always use {@link Padding#NOPADDING}. + *

+ * + * @param p the padding scheme + * @return this builder + * @throws NullPointerException if {@code p} is {@code null} + */ + public Builder padding(Padding p) { + this.padding = Objects.requireNonNull(p); + return this; + } + + /** + * Sets the GCM authentication tag length in bits. + * + *

+ * Valid only when the mode is {@link Mode#GCM}. The value must be 96, 104, 112, + * 120, or 128. + *

+ * + * @param t the tag length in bits + * @return this builder + */ + public Builder tagLenBits(int t) { + this.tagLenBits = t; + return this; + } + + /** + * Installs a header codec used by the cipher context to persist and recover + * runtime parameters (e.g., IV, tag length, AAD hash). + * + *

+ * When {@code null}, no header is written or parsed. + *

+ * + * @param codec the codec to embed, or {@code null} to disable + * @return this builder + */ + public Builder header(SymmetricHeaderCodec codec) { + this.header = codec; + return this; + } + + /** + * Builds a validated {@link AesSpec}. + * + *

+ * Validation rules: + *

+ *
    + *
  • If mode is {@link Mode#GCM}, padding must be {@link Padding#NOPADDING} + * and {@code tagLenBits} must be 96–128 in steps of 8.
  • + *
  • If mode is not GCM, {@code tagLenBits} must be 0.
  • + *
+ * + * @return an immutable specification + * @throws IllegalArgumentException if parameters are inconsistent + */ + public AesSpec build() { + int t = (mode == Mode.GCM) ? tagLenBits : 0; + return new AesSpec(mode, padding, t, header); + } + } + + /** + * Creates a GCM specification with a 128‑bit authentication tag and the + * provided header codec. + * + * @param header the header codec to embed, or {@code null} for none + * @return a specification for {@code AES/GCM/NOPADDING} with a 128‑bit tag + */ + public static AesSpec gcm128(SymmetricHeaderCodec header) { + return builder().mode(Mode.GCM).tagLenBits(128).header(header).build(); + } + + /** + * Creates a CBC specification using PKCS#7 padding and the provided header. + * + * @param header the header codec to embed, or {@code null} for none + * @return a specification for {@code AES/CBC/PKCS7Padding} + */ + public static AesSpec cbcPkcs7(SymmetricHeaderCodec header) { + return builder().mode(Mode.CBC).padding(Padding.PKCS5PADDING).header(header).build(); + } + + /** + * Creates a CTR specification with no padding and the provided header. + * + * @param header the header codec to embed, or {@code null} for none + * @return a specification for {@code AES/CTR/NOPADDING} + */ + public static AesSpec ctr(SymmetricHeaderCodec header) { + return builder().mode(Mode.CTR).padding(Padding.NOPADDING).header(header).build(); + } + + /** + * Returns the selected AES mode. + * + *

+ * This value determines IV length expectations and whether AAD and tag length + * apply (GCM only). + *

+ * + * @return the block mode used by this specification + */ + public Mode mode() { + return mode; + } + + /** + * Returns the selected padding scheme. + * + *

+ * Relevant only for CBC; GCM and CTR always operate with no padding. + *

+ * + * @return the padding scheme (never {@code null}) + */ + public Padding padding() { + return padding; + } + + /** + * Returns the authentication tag length in bits. + * + *

+ * For GCM, this is one of 96, 104, 112, 120, or 128. For non‑GCM modes, this + * method returns {@code 0}. + *

+ * + * @return the GCM tag length in bits, or {@code 0} if not applicable + */ + public int tagLenBits() { + return tagLenBits; + } + + /** + * Returns the embedded header codec, if any. + * + *

+ * When non‑null, the cipher context writes the header during encryption (after + * the IV is chosen) and reads it during decryption to hydrate the runtime + * context. + *

+ * + * @return the header codec, or {@code null} if no header is used + */ + public SymmetricHeaderCodec header() { + return header; + } + + /** + * Produces a compact human‑readable description of this specification, such as + * {@code "AES-GCM(tag=128)"} or {@code "AES-CBC/PKCS7Padding"}. + * + * @return a descriptive string for diagnostics and logs + */ + @Override + public String description() { + return "AES-" + mode + (mode == Mode.CBC ? "/" + padding : "") + + (mode == Mode.GCM ? "(tag=" + tagLenBits + ")" : ""); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/aes/package-info.java b/lib/src/main/java/zeroecho/core/alg/aes/package-info.java new file mode 100644 index 0000000..d5525cf --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/aes/package-info.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * AES algorithm implementation and runtime wiring. + * + *

+ * This package provides the AES capability set for the core layer, including + * the algorithm descriptor, a streaming cipher context for GCM / CBC / CTR, + * static configuration, a minimal header codec for runtime parameters, and key + * import/generation specifications. The design favors safe defaults (GCM with a + * 128-bit tag), explicit role-to-context binding, and clear separation between + * static configuration and per-operation parameters. + *

+ * + *

Components

+ *
    + *
  • Algorithm descriptor: {@link AesAlgorithm} registers ENCRYPT and + * DECRYPT roles, installs default specifications, and exposes symmetric key + * builders for generation and import. Capabilities are published for discovery + * by higher layers.
  • + *
  • Streaming context: {@link AesCipherContext} implements the runtime + * transform over {@code InputStream}, handling IV creation and validation, + * optional AAD, tag length (GCM), and mode-specific initialization.
  • + *
  • Static configuration: {@link AesSpec} captures compile-time + * choices (mode, padding, tag length for GCM) and may embed a header codec used + * by the context to persist runtime parameters in-band.
  • + *
  • Header codec: {@link AesHeaderCodec} writes/reads a compact header + * containing IV, a tag-length hint for GCM, and an optional AAD hash used to + * verify that decrypt-time AAD matches encrypt-time AAD.
  • + *
  • Key specifications: {@link AesKeyGenSpec} defines key-size + * parameters for generation, and {@link AesKeyImportSpec} wraps existing AES + * keys supplied as raw bytes, hex, or Base64.
  • + *
+ * + *

Runtime parameters and context exchange

+ *

+ * The streaming context exchanges ephemeral parameters (IV/nonce, GCM tag bits, + * and optional AAD) via a Conflux session context. When a header codec is + * present and a session context is set, encryption prepends a minimal header + * and decryption reads it first to hydrate the session context before + * initializing the cipher. + *

+ * + *

Safety and validation

+ *
    + *
  • GCM requires no padding and a tag length between 96 and 128 bits in 8-bit + * steps; non-GCM modes must not specify a tag length.
  • + *
  • IV length is enforced by mode (12 bytes for GCM; 16 bytes for CBC/CTR), + * and decryption fails if IV is missing or has an unexpected size.
  • + *
  • When an AAD hash is present in the header, decryption enforces + * consistency with the caller-supplied AAD.
  • + *
+ * + *

Thread-safety

+ *
    + *
  • Algorithm descriptors are immutable and safe to share.
  • + *
  • Streaming contexts are stateful and not thread-safe.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.aes; diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java new file mode 100644 index 0000000..5885a24 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java @@ -0,0 +1,219 @@ +/******************************************************************************* + * 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.core.alg.bike; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.bouncycastle.pqc.jcajce.spec.BIKEParameterSpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

Integration of BIKE (Bit Flipping Key Encapsulation) algorithm

+ * + * Declares BIKE as a {@link zeroecho.core.alg.AbstractCryptoAlgorithm} and + * registers its supported roles, contexts, and key builders within the ZeroEcho + * framework. + * + *

Capabilities

+ *
    + *
  • KEM - encapsulation and decapsulation of shared secrets using + * public and private keys.
  • + *
  • Agreement - initiator and responder flows for + * {@link MessageAgreementContext} built on top of BIKE KEM.
  • + *
  • Asymmetric key builders - generation and import of BIKE keys from + * {@link BikeKeyGenSpec}, {@link BikePublicKeySpec}, and + * {@link BikePrivateKeySpec}.
  • + *
+ * + *

Provider

Uses the BouncyCastle Post-Quantum provider + * ({@code BCPQC}). The provider must be registered in the JCA {@link Security} + * before use. + * + *

Example

{@code
+ * BikeAlgorithm bike = new BikeAlgorithm();
+ *
+ * // Generate a key pair
+ * KeyPair kp = bike.asymmetricKeyBuilder(BikeKeyGenSpec.class)
+ *                  .generateKeyPair(BikeKeyGenSpec.bike256());
+ *
+ * // Encapsulation using recipient's public key
+ * KemContext kemEnc = bike.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
+ *
+ * // Decapsulation using private key
+ * KemContext kemDec = bike.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
+ * }
+ * + * @since 1.0 + */ +public final class BikeAlgorithm extends AbstractCryptoAlgorithm { + /** + * Constructs and registers the BIKE algorithm with all its roles and key + * builders. + * + *

+ * Registers: + *

+ *
    + *
  • KEM (encapsulate/decapsulate)
  • + *
  • Agreement (initiator/responder)
  • + *
  • Asymmetric key builders for BIKE key specifications
  • + *
+ */ + public BikeAlgorithm() { + super("BIKE", "BIKE", BouncyCastlePQCProvider.PROVIDER_NAME); + + capability(AlgorithmFamily.KEM, KeyUsage.ENCAPSULATE, KemContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> new BikeKemContext(this, k), () -> VoidSpec.INSTANCE); + capability(AlgorithmFamily.KEM, KeyUsage.DECAPSULATE, KemContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> new BikeKemContext(this, k), () -> VoidSpec.INSTANCE); + + // AGREEMENT (initiator): Alice has Bob's public key → encapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← return your + // existing KemContext + PublicKey.class, // ← initiator uses recipient's public key + VoidSpec.class, // ← must implement ContextSpec + (PublicKey recipient, VoidSpec spec) -> { + // create a context bound to recipient public key for encapsulation + return KemMessageAgreementAdapter.builder().upon(new BikeKemContext(this, recipient)).asInitiator() + .build(); + }, () -> VoidSpec.INSTANCE // default + ); + + // AGREEMENT (responder): Bob has his private key → decapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← same KemContext + // type + PrivateKey.class, // ← responder uses their private key + VoidSpec.class, (PrivateKey myPriv, VoidSpec spec) -> { + return KemMessageAgreementAdapter.builder().upon(new BikeKemContext(this, myPriv)).asResponder() + .build(); + }, () -> VoidSpec.INSTANCE); + + registerAsymmetricKeyBuilder(BikeKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(BikeKeyGenSpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("BIKE", providerName()); + BIKEParameterSpec params = switch (spec.variant()) { + case BIKE_128 -> BIKEParameterSpec.bike128; + case BIKE_192 -> BIKEParameterSpec.bike192; + case BIKE_256 -> BIKEParameterSpec.bike256; + }; + kpg.initialize(params, new SecureRandom()); + return kpg.generateKeyPair(); + } + + @Override + public PublicKey importPublic(BikeKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(BikeKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + }, BikeKeyGenSpec::bike256); + + registerAsymmetricKeyBuilder(BikePublicKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(BikePublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(BikePublicKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("BIKE", providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); + } + + @Override + public PrivateKey importPrivate(BikePublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + }, null); + + registerAsymmetricKeyBuilder(BikePrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(BikePrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(BikePrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(BikePrivateKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("BIKE", providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + } + }, null); + } + + /** + * Ensures that the BouncyCastle Post-Quantum provider is available. + * + * @throws NoSuchProviderException if the provider is not registered in + * {@link Security} + */ + private static void ensureProvider() throws NoSuchProviderException { + Provider p = Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME); + if (p == null) { + throw new NoSuchProviderException("BCPQC provider not registered"); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikeKemContext.java b/lib/src/main/java/zeroecho/core/alg/bike/BikeKemContext.java new file mode 100644 index 0000000..075d391 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/bike/BikeKemContext.java @@ -0,0 +1,193 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.bike; + +import java.io.IOException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Objects; + +import javax.security.auth.DestroyFailedException; + +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.pqc.crypto.bike.BIKEKEMExtractor; +import org.bouncycastle.pqc.crypto.bike.BIKEKEMGenerator; +import org.bouncycastle.pqc.crypto.bike.BIKEPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.bike.BIKEPublicKeyParameters; +import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory; +import org.bouncycastle.pqc.crypto.util.PublicKeyFactory; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.KemContext; + +/** + *

BIKE Key Encapsulation Mechanism context

+ * + * Implements {@link zeroecho.core.context.KemContext} for the BIKE (Bit + * Flipping Key Encapsulation) post-quantum algorithm. Encapsulation and + * decapsulation flows are separated by constructor: + *
    + *
  • Construct with a {@link java.security.PublicKey} to create an + * encapsulation context.
  • + *
  • Construct with a {@link java.security.PrivateKey} to create a + * decapsulation context.
  • + *
+ * + *

Usage

{@code
+ * BikeKemContext enc = new BikeKemContext(algo, recipientPubKey);
+ * KemResult r = enc.encapsulate();
+ *
+ * BikeKemContext dec = new BikeKemContext(algo, myPrivateKey);
+ * byte[] secret = dec.decapsulate(r.ciphertext());
+ * }
+ * + * @since 1.0 + */ +public final class BikeKemContext implements KemContext { + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean encapsulate; + + /** + * Creates an encapsulation context bound to the recipient's public key. + * + * @param algorithm parent algorithm instance + * @param k recipient's public key + * @throws NullPointerException if any argument is null + */ + public BikeKemContext(CryptoAlgorithm algorithm, PublicKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = true; + } + + /** + * Creates a decapsulation context bound to the holder's private key. + * + * @param algorithm parent algorithm instance + * @param k private key for decapsulation + * @throws NullPointerException if any argument is null + */ + public BikeKemContext(CryptoAlgorithm algorithm, PrivateKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = false; + } + + /** + * Returns the algorithm that created this context. + * + * @return parent algorithm + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the underlying key bound to this context. + * + * @return public or private key depending on mode + */ + @Override + public Key key() { + return key; + } + + /** + * Releases resources associated with this context. + *

+ * No-op for BIKE; provided for API symmetry. + *

+ */ + @Override + public void close() { + // empty + } + + /** + * Generates a new shared secret and ciphertext using the recipient's public + * key. + * + * @return encapsulation result containing ciphertext and shared secret + * @throws IOException if encapsulation fails + * @throws IllegalStateException if this context is not initialized for + * encapsulation + */ + @Override + public KemResult encapsulate() throws IOException { + if (!encapsulate) { + throw new IllegalStateException("Not initialized for ENCAPSULATE"); + } + try { + final BIKEPublicKeyParameters keyParam = (BIKEPublicKeyParameters) PublicKeyFactory + .createKey(key.getEncoded()); + BIKEKEMGenerator gen = new BIKEKEMGenerator(new SecureRandom()); + SecretWithEncapsulation res = gen.generateEncapsulated(keyParam); + byte[] secret = res.getSecret(); + byte[] ct = res.getEncapsulation(); + res.destroy(); + return new KemResult(ct, secret); + } catch (DestroyFailedException e) { + throw new IOException("BIKE encapsulate failed", e); + } + } + + /** + * Recovers the shared secret from a ciphertext using the holder's private key. + * + * @param ciphertext encapsulated key material + * @return recovered shared secret bytes + * @throws IOException if decapsulation fails + * @throws IllegalStateException if this context is not initialized for + * decapsulation + */ + @Override + public byte[] decapsulate(byte[] ciphertext) throws IOException { + if (encapsulate) { + throw new IllegalStateException("Not initialized for DECAPSULATE"); + } + try { + final BIKEPrivateKeyParameters keyParam = (BIKEPrivateKeyParameters) PrivateKeyFactory + .createKey(key.getEncoded()); + BIKEKEMExtractor ex = new BIKEKEMExtractor(keyParam); + return ex.extractSecret(ciphertext); + } catch (Exception e) { // NOPMD + throw new IOException("BIKE decapsulate failed", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikeKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/bike/BikeKeyGenSpec.java new file mode 100644 index 0000000..a294655 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/bike/BikeKeyGenSpec.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * 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.core.alg.bike; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Specification for BIKE key generation

+ * + * Defines algorithm parameters for generating BIKE key pairs. Encapsulates a + * chosen {@link Variant} (BIKE-128, BIKE-192, BIKE-256). + * + *

Usage

{@code
+ * // Generate a BIKE-192 key pair
+ * KeyPair kp = bikeAlgorithm.asymmetricKeyBuilder(BikeKeyGenSpec.class)
+ *                           .generateKeyPair(BikeKeyGenSpec.bike192());
+ * }
+ * + * @since 1.0 + */ +public final class BikeKeyGenSpec implements AlgorithmKeySpec, Describable { + /** + * Available BIKE parameter sets. + */ + public enum Variant { + /** BIKE with 128-bit security strength. */ + BIKE_128, + /** BIKE with 192-bit security strength. */ + BIKE_192, + /** BIKE with 256-bit security strength. */ + BIKE_256 + } + + private final Variant variant; + + private BikeKeyGenSpec(Variant v) { + this.variant = v; + } + + /** + * Creates a new specification for the given BIKE variant. + * + * @param v variant to use + * @return a new key generation spec + */ + public static BikeKeyGenSpec of(Variant v) { + return new BikeKeyGenSpec(v); + } + + /** + * Returns a specification for BIKE-128. + * + * @return BIKE-128 key generation spec + */ + public static BikeKeyGenSpec bike128() { + return new BikeKeyGenSpec(Variant.BIKE_128); + } + + /** + * Returns a specification for BIKE-192. + * + * @return BIKE-192 key generation spec + */ + public static BikeKeyGenSpec bike192() { + return new BikeKeyGenSpec(Variant.BIKE_192); + } + + /** + * Returns a specification for BIKE-256. + * + * @return BIKE-256 key generation spec + */ + public static BikeKeyGenSpec bike256() { + return new BikeKeyGenSpec(Variant.BIKE_256); + } + + /** + * Returns the BIKE variant. + * + * @return configured variant + */ + public Variant variant() { + return variant; + } + + /** + * Returns a human-readable description of this spec. + * + *

+ * Implements {@link Describable} by returning the variant name. + *

+ * + * @return description string + */ + @Override + public String description() { + return variant.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java new file mode 100644 index 0000000..e1e7f00 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.bike; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Specification for a BIKE private key

+ * + * Wraps a BIKE private key encoded in standard PKCS#8 format. Provides + * marshalling and unmarshalling support for interchange. + * + *

Usage

{@code
+ * // Import a BIKE private key
+ * BikePrivateKeySpec spec = new BikePrivateKeySpec(pkcs8Bytes);
+ * PrivateKey key = bikeAlgorithm.importPrivate(spec);
+ *
+ * // Marshal for storage or transport
+ * PairSeq seq = BikePrivateKeySpec.marshal(spec);
+ *
+ * // Reconstruct from encoded representation
+ * BikePrivateKeySpec restored = BikePrivateKeySpec.unmarshal(seq);
+ * }
+ * + * @since 1.0 + */ +public final class BikePrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Constructs a new spec from a PKCS#8 encoded private key. + * + * @param pkcs8Der PKCS#8 bytes (DER encoded) + * @throws NullPointerException if {@code pkcs8Der} is null + */ + public BikePrivateKeySpec(byte[] pkcs8Der) { + this.pkcs8 = Objects.requireNonNull(pkcs8Der).clone(); + } + + /** + * Returns a defensive copy of the PKCS#8 encoded key. + * + * @return cloned PKCS#8 bytes + */ + public byte[] pkcs8() { + return pkcs8.clone(); + } + + /** + * Serializes the spec to a {@link PairSeq} with base64 encoding. + * + *

+ * Fields: + *

+ *
    + *
  • {@code type} = "BikePrivateKeySpec"
  • + *
  • {@code pkcs8.b64} = base64 of encoded key (no padding)
  • + *
+ * + * @param spec private key specification + * @return serialized key representation + */ + public static PairSeq marshal(BikePrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "BikePrivateKeySpec", PKCS8_B64, b64); + } + + /** + * Deserializes a spec from a {@link PairSeq}. + * + * @param p serialized representation containing {@code pkcs8.b64} + * @return reconstructed private key spec + * @throws IllegalArgumentException if required field is missing + */ + public static BikePrivateKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (PKCS8_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("BikePrivateKeySpec: missing pkcs8.b64"); + } + return new BikePrivateKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string including encoded length. + * + * @return human-readable description + */ + @Override + public String toString() { + return "BikePrivateKeySpec[len=" + pkcs8.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java new file mode 100644 index 0000000..d1d0ee5 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.bike; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Specification for a BIKE public key

+ * + * Wraps a BIKE public key encoded in standard X.509 format. Provides + * marshalling and unmarshalling support for serialization. + * + *

Usage

{@code
+ * // Import a BIKE public key
+ * BikePublicKeySpec spec = new BikePublicKeySpec(x509Bytes);
+ * PublicKey key = bikeAlgorithm.importPublic(spec);
+ *
+ * // Marshal for transport or storage
+ * PairSeq seq = BikePublicKeySpec.marshal(spec);
+ *
+ * // Reconstruct from encoded representation
+ * BikePublicKeySpec restored = BikePublicKeySpec.unmarshal(seq);
+ * }
+ * + * @since 1.0 + */ +public final class BikePublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Constructs a new spec from an X.509 encoded public key. + * + * @param x509Der X.509 bytes (DER encoded) + * @throws NullPointerException if {@code x509Der} is null + */ + public BikePublicKeySpec(byte[] x509Der) { + this.x509 = Objects.requireNonNull(x509Der).clone(); + } + + /** + * Returns a defensive copy of the X.509 encoded key. + * + * @return cloned X.509 bytes + */ + public byte[] x509() { + return x509.clone(); + } + + /** + * Serializes the spec to a {@link PairSeq} with base64 encoding. + * + *

+ * Fields: + *

+ *
    + *
  • {@code type} = "BikePublicKeySpec"
  • + *
  • {@code x509.b64} = base64 of encoded key (no padding)
  • + *
+ * + * @param spec public key specification + * @return serialized representation + */ + public static PairSeq marshal(BikePublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "BikePublicKeySpec", X509_B64, b64); + } + + /** + * Deserializes a spec from a {@link PairSeq}. + * + * @param p serialized representation containing {@code x509.b64} + * @return reconstructed public key spec + * @throws IllegalArgumentException if required field is missing + */ + public static BikePublicKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (X509_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("BikePublicKeySpec: missing x509.b64"); + } + return new BikePublicKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string including encoded length. + * + * @return human-readable description + */ + @Override + public String toString() { + return "BikePublicKeySpec[len=" + x509.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/bike/package-info.java b/lib/src/main/java/zeroecho/core/alg/bike/package-info.java new file mode 100644 index 0000000..fd38979 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/bike/package-info.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * BIKE post-quantum key encapsulation and related utilities. + * + *

+ * This package integrates the BIKE (Bit Flipping Key Encapsulation) algorithm + * into the core layer. It defines the algorithm descriptor, the runtime context + * for encapsulation and decapsulation, and key specifications for generation + * and import. The implementation relies on a Post-Quantum JCA provider and + * focuses on safe, explicit wiring of roles, contexts, and key builders. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Expose a concrete algorithm descriptor that registers BIKE roles and key + * builders.
  • + *
  • Provide a runtime context that performs encapsulation and + * decapsulation.
  • + *
  • Define key specifications for generation and for importing encoded + * keys.
  • + *
+ * + *

Components

+ *
    + *
  • Algorithm descriptor: {@link BikeAlgorithm} declares KEM roles for + * encapsulation and decapsulation, wires a message-agreement adapter built on + * KEM, and registers asymmetric key builders for BIKE key specs.
  • + *
  • Runtime context: {@link BikeKemContext} implements the key + * encapsulation mechanism and separates encapsulation from decapsulation by + * constructor selection.
  • + *
  • Key generation spec: {@link BikeKeyGenSpec} selects a BIKE variant + * and is used by the key-pair builder.
  • + *
  • Key import specs: {@link BikePublicKeySpec} and + * {@link BikePrivateKeySpec} wrap X.509 and PKCS#8 encodings and support + * marshalling for transport and storage.
  • + *
+ * + *

Provider requirements

+ *

+ * BIKE operations require a Post-Quantum JCA provider. The algorithm descriptor + * expects the BouncyCastle PQC provider to be present and may validate provider + * availability during key operations. Applications must ensure the provider is + * installed before use. + *

+ * + *

Thread-safety

+ *
    + *
  • Algorithm descriptors are immutable and safe to share across + * threads.
  • + *
  • Runtime contexts are stateful and not thread-safe.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.bike; diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java new file mode 100644 index 0000000..f075dd7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaAlgorithm.java @@ -0,0 +1,123 @@ +/******************************************************************************* + * 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.core.alg.chacha; + +import java.security.GeneralSecurityException; +import java.security.SecureRandom; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.spi.SymmetricKeyBuilder; + +/** + *

Abstract base for ChaCha family algorithms

+ * + * Provides common registration logic for ChaCha20-based algorithms within the + * ZeroEcho framework. Extends {@link zeroecho.core.alg.AbstractCryptoAlgorithm} + * and installs symmetric key builders for both key generation and key import. + * + *

Registered key builders

+ *
    + *
  • {@link zeroecho.core.alg.chacha.ChaChaKeyGenSpec} - generates new random + * ChaCha keys of configurable size (default 256-bit).
  • + *
  • {@link zeroecho.core.alg.chacha.ChaChaKeyImportSpec} - imports externally + * supplied ChaCha keys as {@link javax.crypto.SecretKey} instances.
  • + *
+ * + *

Notes

+ *
    + *
  • Generation uses {@link javax.crypto.KeyGenerator} with algorithm + * {@code "ChaCha20"}.
  • + *
  • Import wraps the raw key material with + * {@link javax.crypto.spec.SecretKeySpec}.
  • + *
  • Attempts to generate a key via {@code ChaChaKeyImportSpec} or import via + * {@code ChaChaKeyGenSpec} will throw + * {@link UnsupportedOperationException}.
  • + *
+ * + *

Example

{@code
+ * AbstractChaChaAlgorithm algo = ...;
+ *
+ * // Generate a fresh 256-bit key
+ * SecretKey key = algo.generateSecret(ChaChaKeyGenSpec.chacha256());
+ *
+ * // Import an existing key
+ * SecretKey imported = algo.importSecret(new ChaChaKeyImportSpec(rawBytes));
+ * }
+ * + * @since 1.0 + */ +abstract class AbstractChaChaAlgorithm extends AbstractCryptoAlgorithm { + /** + * Constructs a ChaCha-based algorithm definition and registers symmetric key + * builders for generation and import. + * + * @param id canonical algorithm identifier (e.g., "ChaCha20/Poly1305") + * @param title human-readable name for diagnostics + */ + protected AbstractChaChaAlgorithm(String id, String title) { + super(id, title); + + // register once for both algorithms (same 256-bit key) + registerSymmetricKeyBuilder(ChaChaKeyGenSpec.class, new SymmetricKeyBuilder<>() { + @Override + public SecretKey generateSecret(ChaChaKeyGenSpec spec) throws GeneralSecurityException { + KeyGenerator kg = KeyGenerator.getInstance("ChaCha20"); + kg.init(spec.keySizeBits(), new SecureRandom()); + return kg.generateKey(); + } + + @Override + public SecretKey importSecret(ChaChaKeyGenSpec spec) { + throw new UnsupportedOperationException("Use ChaChaKeyImportSpec for importing ChaCha keys"); + } + }, ChaChaKeyGenSpec::chacha256); + + registerSymmetricKeyBuilder(ChaChaKeyImportSpec.class, new SymmetricKeyBuilder<>() { + @Override + public SecretKey generateSecret(ChaChaKeyImportSpec spec) { + throw new UnsupportedOperationException("Use ChaChaKeyGenSpec to generate ChaCha keys"); + } + + @Override + public SecretKey importSecret(ChaChaKeyImportSpec spec) { + return new SecretKeySpec(spec.key(), "ChaCha20"); + } + }, null); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaCipherContext.java b/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaCipherContext.java new file mode 100644 index 0000000..bc3eeab --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/AbstractChaChaCipherContext.java @@ -0,0 +1,263 @@ +/******************************************************************************* + * 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.core.alg.chacha; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.err.ProviderFailureException; +import zeroecho.core.io.CipherTransformInputStreamBuilder; +import zeroecho.core.spi.ContextAware; + +/** + *

Abstract streaming cipher context for ChaCha algorithms

+ * + * Base implementation of {@link zeroecho.core.context.EncryptionContext} for + * ChaCha20 and ChaCha20-Poly1305. Provides a streaming + * {@link java.io.InputStream}-based interface for encryption and decryption, + * with support for nonce management and optional {@link SymmetricHeaderCodec} + * headers. + * + *

Features

+ *
    + *
  • Automatic 12-byte nonce generation (encryption) or validation + * (decryption).
  • + *
  • Optional header encoding/decoding via {@link SymmetricHeaderCodec} bound + * in the spec.
  • + *
  • Streaming transformation using {@link javax.crypto.Cipher} with staged + * {@code doFinal()}.
  • + *
  • Integration with {@link ConfluxKeys} for propagating IV/nonce + * values.
  • + *
+ * + *

Subclass responsibilities

+ *
    + *
  • Implement {@link #jceName()} to return a JCE transformation string (e.g., + * {@code "ChaCha20"} or {@code "ChaCha20-Poly1305"}).
  • + *
  • Implement {@link #initCipher(Cipher, byte[])} to configure parameters and + * AAD if required.
  • + *
+ * + *

Usage

{@code
+ * EncryptionContext enc = new ChaCha20Poly1305Context(algo, key, true, spec, null);
+ * InputStream ciphertext = enc.attach(plaintextStream);
+ *
+ * EncryptionContext dec = new ChaCha20Poly1305Context(algo, key, false, spec, null);
+ * InputStream plaintext = dec.attach(ciphertext);
+ * }
+ * + * @param specification type carrying ChaCha parameters + * @since 1.0 + */ +abstract class AbstractChaChaCipherContext implements EncryptionContext, ContextAware { + /** Required ChaCha nonce length (12 bytes). */ + private static final int NONCE_LEN = 12; // both variants + /** Algorithm definition that created this context. */ + protected final CryptoAlgorithm algorithm; + /** Symmetric key used by this context. */ + protected final SecretKey key; + /** + * Operation mode flag - {@code true} for encryption, {@code false} for + * decryption. + */ + protected final boolean encrypt; + /** Algorithm-specific specification (e.g., AEAD parameters). */ + protected final S spec; + /** Secure random source for nonce generation. */ + protected final SecureRandom rnd; + /** Optional per-operation context for exchanging headers, IVs, etc. */ + protected CtxInterface ctx; // optional + + /** + * Creates a new ChaCha cipher context. + * + * @param algorithm parent algorithm + * @param key ChaCha secret key + * @param encrypt {@code true} for encryption, {@code false} for decryption + * @param spec ChaCha-specific context specification + * @param rnd source of randomness for nonces (uses default if null) + */ + protected AbstractChaChaCipherContext(CryptoAlgorithm algorithm, SecretKey key, boolean encrypt, S spec, + SecureRandom rnd) { + this.algorithm = algorithm; + this.key = key; + this.encrypt = encrypt; + this.spec = spec; + this.rnd = (rnd != null ? rnd : new SecureRandom()); + } + + /** {@inheritDoc} */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** {@inheritDoc} */ + @Override + public java.security.Key key() { + return key; + } + + /** {@inheritDoc} */ + @Override + public void setContext(CtxInterface context) { + this.ctx = context; + } + + /** {@inheritDoc} */ + @Override + public CtxInterface context() { + return ctx; + } + + /** + * Returns the JCE transformation string, e.g. "ChaCha20" or + * "ChaCha20-Poly1305". + * + * @return transformation string for + * {@link javax.crypto.Cipher#getInstance(String)} + */ + protected abstract String jceName(); + + /** + * Initializes the cipher with algorithm-specific parameters and optional AAD. + * + * @param cipher configured cipher instance + * @param nonce 12-byte nonce generated or supplied from context + * @throws GeneralSecurityException if parameter initialization fails + * @throws IOException if AAD setup or parameter resolution fails + */ + protected abstract void initCipher(Cipher cipher, byte[] nonce) throws GeneralSecurityException, IOException; + + /** + * Attaches this context to an upstream input stream and returns a + * transformation stream. + * + *

+ * Encryption prepends optional headers; decryption consumes headers first. + *

+ * + * @param upstream plaintext or ciphertext input stream + * @return transformed stream (ciphertext for encryption, plaintext for + * decryption) + * @throws IOException if cipher initialization fails + */ + @Override + public InputStream attach(InputStream upstream) throws IOException { + try { + final SymmetricHeaderCodec header = spec.header(); + final boolean hasCtxHeader = ctx != null && header != null; + + InputStream in = upstream; + if (!encrypt && hasCtxHeader) { + in = header.readHeader(in, algorithm, ctx); + } + + final Cipher cipher = Cipher.getInstance(jceName()); + final byte[] nonce = ensureNonce(); // generate or require from ctx + initCipher(cipher, nonce); + + InputStream out = // new Stream(in, cipher, jceName()); // same stream pattern as AES + CipherTransformInputStreamBuilder.builder().withUpstream(in).withCipher(cipher) + .withUpdateStreaming().withInputBlockSize(64 /* chacha block */ ).withOutputBlockSize(64) + .withBufferedBlocks(100).withFinalizationOutputChunks(2).build(); + if (encrypt && hasCtxHeader) { + final ByteArrayOutputStream hdr = new ByteArrayOutputStream(48); + header.writeHeader(hdr, algorithm, ctx); + out = new SequenceInputStream(new ByteArrayInputStream(hdr.toByteArray()), out); + } + return out; + } catch (GeneralSecurityException e) { + throw new ProviderFailureException(jceName() + " attach/init failed", e); + } + } + + /** + * Releases context resources. + *

+ * No-op for ChaCha contexts. + *

+ */ + @Override + public void close() { // NOPMD + /* no-op */ + } + + /** + * Ensures a nonce is available in the context. + * + *
    + *
  • For encryption, generates a new nonce if absent and stores it in + * context.
  • + *
  • For decryption, validates presence and correct length.
  • + *
+ * + * @return 12-byte nonce + * @throws IOException if nonce is missing or invalid + */ + private byte[] ensureNonce() throws IOException { + final String id = algorithm.id(); + byte[] nonce = (ctx == null) ? null : ctx.get(ConfluxKeys.iv(id)); + if (encrypt) { + if (nonce == null) { + nonce = new byte[NONCE_LEN]; + rnd.nextBytes(nonce); + if (ctx != null) { + ctx.put(ConfluxKeys.iv(id), nonce); + } + } else if (nonce.length != NONCE_LEN) { + throw new IOException("Nonce length mismatch: expected 12 bytes, got " + nonce.length); + } + } else { + if (nonce == null || nonce.length != NONCE_LEN) { + throw new IOException("Nonce missing/invalid for " + jceName() + " decryption"); + } + } + return nonce; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java new file mode 100644 index 0000000..1d9b12c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Algorithm.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * 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.core.alg.chacha; + +import zeroecho.core.SymmetricHeaderCodec; + +/** + *

ChaCha20-Poly1305 (AEAD) algorithm

+ * + * Registers the {@code ChaCha20-Poly1305} AEAD cipher within the ZeroEcho + * framework. Extends {@link AbstractChaChaAlgorithm} and declares symmetric + * capabilities for encryption and decryption using either + * {@link ChaCha20Poly1305Spec} or {@link zeroecho.core.spec.VoidSpec} + * (convenience default mirroring AES-GCM). + * + *

Capabilities

+ *
    + *
  • Encrypt: + *
      + *
    • Family: {@link zeroecho.core.AlgorithmFamily#SYMMETRIC}
    • + *
    • Usage: {@link zeroecho.core.KeyUsage#ENCRYPT}
    • + *
    • Context: {@link zeroecho.core.context.EncryptionContext}
    • + *
    • Key: {@link javax.crypto.SecretKey}
    • + *
    • Spec: {@link ChaCha20Poly1305Spec} (or + * {@link zeroecho.core.spec.VoidSpec} default)
    • + *
    + *
  • + *
  • Decrypt: + *
      + *
    • Family: {@link zeroecho.core.AlgorithmFamily#SYMMETRIC}
    • + *
    • Usage: {@link zeroecho.core.KeyUsage#DECRYPT}
    • + *
    • Context: {@link zeroecho.core.context.EncryptionContext}
    • + *
    • Key: {@link javax.crypto.SecretKey}
    • + *
    • Spec: {@link ChaCha20Poly1305Spec} (or + * {@link zeroecho.core.spec.VoidSpec} default)
    • + *
    + *
  • + *
+ * + *

Defaults

When used with {@link zeroecho.core.spec.VoidSpec}, a + * minimal {@link ChaCha20Poly1305Spec} is synthesized with no + * {@link SymmetricHeaderCodec} header. Nonces are 12 bytes and managed by the + * corresponding cipher context. + * + *

Example

{@code
+ * var algo = new ChaCha20Poly1305Algorithm();
+ * SecretKey key = algo.generateSecret(ChaChaKeyGenSpec.chacha256());
+ *
+ * // Encrypt with explicit spec
+ * var spec = ChaCha20Poly1305Spec.builder().header(null).build();
+ * EncryptionContext enc = algo.newContext(
+ *     zeroecho.core.AlgorithmFamily.SYMMETRIC,
+ *     zeroecho.core.KeyUsage.ENCRYPT, key, spec);
+ *
+ * // Decrypt using VoidSpec default
+ * EncryptionContext dec = algo.newContext(
+ *     zeroecho.core.AlgorithmFamily.SYMMETRIC,
+ *     zeroecho.core.KeyUsage.DECRYPT, key, zeroecho.core.spec.VoidSpec.INSTANCE);
+ * }
+ * + * @since 1.0 + */ +public final class ChaCha20Poly1305Algorithm extends AbstractChaChaAlgorithm { + /** + * Creates and registers the ChaCha20-Poly1305 AEAD algorithm with + * encryption/decryption capabilities for {@link ChaCha20Poly1305Spec} and + * {@link zeroecho.core.spec.VoidSpec} defaults. + */ + public ChaCha20Poly1305Algorithm() { + super("CHACHA20-POLY1305", "ChaCha20-Poly1305 (AEAD)"); + capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.ENCRYPT, + zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, ChaCha20Poly1305Spec.class, + (k, s) -> new ChaCha20Poly1305CipherContext(this, k, true, s, new java.security.SecureRandom()), + () -> ChaCha20Poly1305Spec.builder().header(null).build()); + + capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.DECRYPT, + zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, ChaCha20Poly1305Spec.class, + (k, s) -> new ChaCha20Poly1305CipherContext(this, k, false, s, new java.security.SecureRandom()), + () -> ChaCha20Poly1305Spec.builder().header(null).build()); + + // VoidSpec defaults like AES-GCM + capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.ENCRYPT, + zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, + zeroecho.core.spec.VoidSpec.class, + (k, v) -> new ChaCha20Poly1305CipherContext(this, k, true, + ChaCha20Poly1305Spec.builder().header(null).build(), new java.security.SecureRandom()), + () -> zeroecho.core.spec.VoidSpec.INSTANCE); + + capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.DECRYPT, + zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, + zeroecho.core.spec.VoidSpec.class, + (k, v) -> new ChaCha20Poly1305CipherContext(this, k, false, + ChaCha20Poly1305Spec.builder().header(null).build(), new java.security.SecureRandom()), + () -> zeroecho.core.spec.VoidSpec.INSTANCE); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305CipherContext.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305CipherContext.java new file mode 100644 index 0000000..747315e --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305CipherContext.java @@ -0,0 +1,133 @@ +/******************************************************************************* + * 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.core.alg.chacha; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; + +/** + *

ChaCha20-Poly1305 cipher context (AEAD)

+ * + * Concrete {@link zeroecho.core.context.EncryptionContext} for the + * {@code ChaCha20-Poly1305} AEAD construction. Configures a JCE + * {@link javax.crypto.Cipher} with a 12-byte nonce (managed by the parent + * {@link AbstractChaChaCipherContext}) and applies optional AAD obtained from + * {@link ConfluxKeys#aad(String)} via the bound context. + * + *

Behavior

+ *
    + *
  • Uses transformation {@code "ChaCha20-Poly1305"}.
  • + *
  • Initializes with {@link javax.crypto.spec.IvParameterSpec} for the + * 12-byte nonce.
  • + *
  • Supplies Additional Authenticated Data (AAD) from the active context if + * present.
  • + *
+ * + *

Usage

{@code
+ * CryptoAlgorithm alg = ...;
+ * SecretKey key = ...; // ChaCha20 key (256-bit)
+ * ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build();
+ *
+ * // Encrypt
+ * EncryptionContext enc = new ChaCha20Poly1305CipherContext(alg, key, true, spec, new SecureRandom());
+ *
+ * // Decrypt
+ * EncryptionContext dec = new ChaCha20Poly1305CipherContext(alg, key, false, spec, new SecureRandom());
+ * }
+ * + * @since 1.0 + */ +final class ChaCha20Poly1305CipherContext extends AbstractChaChaCipherContext { + /** + * Creates a ChaCha20-Poly1305 context. + * + * @param alg algorithm definition + * @param key ChaCha20 secret key + * @param enc {@code true} for encryption, {@code false} for decryption + * @param spec algorithm-specific parameters + * @param rnd randomness source for nonce generation + */ + protected ChaCha20Poly1305CipherContext(CryptoAlgorithm alg, SecretKey key, boolean enc, ChaCha20Poly1305Spec spec, + java.security.SecureRandom rnd) { + super(alg, key, enc, spec, rnd); + } + + /** + * Returns {@code "ChaCha20-Poly1305"} as the JCE transformation. + * + * @return transformation string + */ + @Override + protected String jceName() { + return "ChaCha20-Poly1305"; + } + + /** + * Initializes the cipher for the current mode with the supplied nonce and + * optional AAD. + * + *

+ * Uses {@link javax.crypto.spec.IvParameterSpec} for the 12-byte nonce and, if + * present, applies AAD retrieved from the bound context under + * {@link ConfluxKeys#aad(String)}. + *

+ * + * @param cipher configured cipher instance + * @param nonce 12-byte nonce value + * @throws java.security.GeneralSecurityException if cipher initialization fails + * @throws java.io.IOException if AAD retrieval/processing + * fails + */ + @Override + protected void initCipher(Cipher cipher, byte[] nonce) throws GeneralSecurityException, IOException { + cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, new IvParameterSpec(nonce)); + + // AAD handling mirrors your AES-GCM path via ConfluxKeys.aad(id). + // + final String id = algorithm.id(); + byte[] aad = (context() == null) ? null : context().get(ConfluxKeys.aad(id)); + if (aad == null) { + aad = new byte[0]; + } + cipher.updateAAD(aad); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305HeaderCodec.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305HeaderCodec.java new file mode 100644 index 0000000..f98878c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305HeaderCodec.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.chacha; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.io.Util; + +/** + *

ChaCha20-Poly1305 streaming header codec

+ * + * Implements {@link SymmetricHeaderCodec} for the ChaCha20-Poly1305 AEAD mode. + * Encodes a compact header that precedes the ciphertext stream and conveys: + *
    + *
  • a 12-byte nonce (IV) required by ChaCha20-Poly1305, and
  • + *
  • an optional SHA-256 hash of AAD to assert integrity of externally + * supplied AAD.
  • + *
+ * + *

+ * The nonce and AAD are exchanged via the bound {@link CtxInterface} using + * {@link ConfluxKeys#iv(String)} and {@link ConfluxKeys#aad(String)} keys, + * respectively. On encryption, the codec reads these values from the context + * and writes the header. On decryption, it restores the nonce into the context + * and, when present, verifies the supplied AAD by comparing its hash to the + * header. + *

+ * + *

Header layout

+ *  [0..11]  : 12-byte nonce (IV)
+ *  [12]     : 1-byte AAD flag (0 = none, 1 = present)
+ *  [13..44] : 32-byte SHA-256(AAD) if flag == 1
+ * 
+ * + *

Failure modes

+ *
    + *
  • Missing/invalid nonce in context when writing the header.
  • + *
  • AAD expected by header but not provided in context during read.
  • + *
  • AAD hash mismatch when verifying during read.
  • + *
+ * + * @since 1.0 + */ +public final class ChaCha20Poly1305HeaderCodec implements SymmetricHeaderCodec { + /** + * Logger for debug-level diagnostics of header encode/decode operations. + */ + private static final Logger LOG = Logger.getLogger(ChaCha20Poly1305HeaderCodec.class.getName()); + /** + * Required nonce length for ChaCha20-Poly1305 headers (12 bytes). + */ + private static final int NONCE_LEN = 12; + + /** + * Writes the ChaCha20-Poly1305 header to the provided stream. + * + *

+ * Reads the 12-byte nonce and optional AAD from {@code ctx}. If AAD is + * non-empty, its SHA-256 is written after a presence flag. Flushes the output + * upon completion. + *

+ * + * @param out destination stream to receive the header + * @param algorithm the algorithm instance (used for context key scoping) + * @param ctx operation context carrying nonce and optional AAD + * @throws java.io.IOException if the nonce is missing/invalid or I/O fails + */ + @Override + public void writeHeader(OutputStream out, CryptoAlgorithm algorithm, CtxInterface ctx) throws IOException { + final String id = algorithm.id(); // "CHACHA20-POLY1305" + LOG.log(Level.FINE, "writeHeader={0}", id); + + byte[] nonce = ctx.get(ConfluxKeys.iv(id)); + if (nonce == null || nonce.length != NONCE_LEN) { + throw new IOException("ChaCha20-Poly1305 header: nonce missing/invalid in Ctx"); + } + byte[] aad = ctx.get(ConfluxKeys.aad(id)); + byte[] aadHash = (aad == null || aad.length == 0) ? null : sha256(aad); + + Util.write(out, nonce); // 12 bytes + out.write(aadHash == null ? 0 : 1); + if (aadHash != null) { + out.write(aadHash); + } + out.flush(); + } + + /** + * Reads and validates the ChaCha20-Poly1305 header from the provided stream. + * + *

+ * Restores the 12-byte nonce into {@code ctx}. If the header signals AAD + * presence, computes SHA-256 over the AAD obtained from {@code ctx} and + * verifies it against the header hash. + *

+ * + * @param in source stream containing the header and subsequent payload + * @param algorithm the algorithm instance (used for context key scoping) + * @param ctx operation context to populate (nonce) and validate (AAD) + * @return the same {@code in} stream positioned after the header + * @throws java.io.IOException if the header is malformed, AAD is missing when + * required, the AAD hash mismatches, or I/O fails + */ + @Override + public InputStream readHeader(InputStream in, CryptoAlgorithm algorithm, CtxInterface ctx) throws IOException { + final String id = algorithm.id(); + LOG.log(Level.FINE, "readHeader={0}", id); + + byte[] nonce = Util.read(in, NONCE_LEN); + int aadFlag = in.read(); + byte[] aadHash = null; + if (aadFlag == 1) { // NOPMD + aadHash = in.readNBytes(32); + } + + // hydrate Ctx + ctx.put(ConfluxKeys.iv(id), nonce); + if (aadHash != null) { + byte[] aad = ctx.get(ConfluxKeys.aad(id)); + if (aad == null || aad.length == 0) { + throw new IOException("ChaCha20-Poly1305 header expects AAD, but none provided in Ctx"); + } + if (!Arrays.equals(aadHash, sha256(aad))) { + throw new IOException("ChaCha20-Poly1305 header: AAD hash mismatch"); + } + } + return in; + } + + private static byte[] sha256(byte[] a) throws IOException { + try { + return MessageDigest.getInstance("SHA-256").digest(a); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-256 unavailable", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Spec.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Spec.java new file mode 100644 index 0000000..4fc8192 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaCha20Poly1305Spec.java @@ -0,0 +1,146 @@ +/******************************************************************************* + * 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.core.alg.chacha; + +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.annotation.Describable; + +/** + *

ChaCha20-Poly1305 algorithm specification

+ * + * Immutable parameters for configuring a ChaCha20-Poly1305 operation. + * Optionally carries a {@link SymmetricHeaderCodec} to prepend/parse per-stream + * headers (e.g., nonce and AAD hash) during encryption/decryption. + * + *

Notes

+ *
    + *
  • If {@link #header()} is {@code null}, no header is written or read; + * callers must exchange the nonce/AAD out-of-band via the context.
  • + *
  • The effective authentication tag size is 128 bits.
  • + *
+ * + *

Example

{@code
+ * ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder()
+ *     .header(new ChaCha20Poly1305HeaderCodec())
+ *     .build();
+ * }
+ * + * @since 1.0 + */ +public final class ChaCha20Poly1305Spec implements ChaChaBaseSpec, Describable { + /** + * Optional streaming header codec used to serialize/deserialize the per-stream + * parameters (e.g., nonce and AAD hash). When {@code null}, no header is used. + */ + private final SymmetricHeaderCodec header; // optional + + /** + * Creates a specification instance. + * + * @param header optional {@link SymmetricHeaderCodec}; may be {@code null} + */ + private ChaCha20Poly1305Spec(SymmetricHeaderCodec header) { + this.header = header; // may be null + } + + /** + * Returns a new builder for {@link ChaCha20Poly1305Spec}. + * + * @return a fresh {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Fluent builder for {@link ChaCha20Poly1305Spec}. + */ + public static final class Builder { + /** + * Header codec to embed/parse per-stream parameters. May be {@code null}. + */ + private SymmetricHeaderCodec header; + + /** + * Sets an optional streaming header codec. + * + * @param codec header codec to use, or {@code null} to disable headers + * @return this builder + */ + public Builder header(SymmetricHeaderCodec codec) { + this.header = codec; + return this; + } + + /** + * Builds an immutable {@link ChaCha20Poly1305Spec}. + * + * @return the constructed spec + */ + public ChaCha20Poly1305Spec build() { + return new ChaCha20Poly1305Spec(header); + } + } + + /** + * Convenience factory that returns a spec with the provided header codec. + * + * @param header optional header codec; may be {@code null} + * @return a new {@code ChaCha20Poly1305Spec} configured with {@code header} + */ + public static ChaCha20Poly1305Spec withHeader(SymmetricHeaderCodec header) { + return builder().header(header).build(); + } + + /** + * Returns the optional header codec. + * + * @return header codec or {@code null} if none + */ + @Override + public SymmetricHeaderCodec header() { + return header; + } + + /** + * Human-readable description of this spec. + * + * @return {@code "ChaCha20-Poly1305(tag=128)"} + */ + @Override + public String description() { + return "ChaCha20-Poly1305(tag=128)"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaAlgorithm.java new file mode 100644 index 0000000..217049a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaAlgorithm.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.chacha; + +/** + *

ChaCha20 (stream) algorithm

+ * + * Registers the {@code ChaCha20} stream cipher within the ZeroEcho framework. + * Extends {@link AbstractChaChaAlgorithm} and installs symmetric capabilities + * for encryption and decryption using {@link ChaChaSpec}, with convenience + * defaults for {@link zeroecho.core.spec.VoidSpec}. + * + *

Capabilities

+ *
    + *
  • Encrypt + *
      + *
    • Family: {@link zeroecho.core.AlgorithmFamily#SYMMETRIC}
    • + *
    • Usage: {@link zeroecho.core.KeyUsage#ENCRYPT}
    • + *
    • Context: {@link zeroecho.core.context.EncryptionContext}
    • + *
    • Key: {@link javax.crypto.SecretKey}
    • + *
    • Spec: {@link ChaChaSpec} (or {@link zeroecho.core.spec.VoidSpec} + * default)
    • + *
    + *
  • + *
  • Decrypt + *
      + *
    • Family: {@link zeroecho.core.AlgorithmFamily#SYMMETRIC}
    • + *
    • Usage: {@link zeroecho.core.KeyUsage#DECRYPT}
    • + *
    • Context: {@link zeroecho.core.context.EncryptionContext}
    • + *
    • Key: {@link javax.crypto.SecretKey}
    • + *
    • Spec: {@link ChaChaSpec} (or {@link zeroecho.core.spec.VoidSpec} + * default)
    • + *
    + *
  • + *
+ * + *

Defaults

When used with {@link zeroecho.core.spec.VoidSpec}, a + * minimal {@link ChaChaSpec} is synthesized with {@code initialCounter(1)} and + * no header. + * + * @since 1.0 + */ +public final class ChaChaAlgorithm extends AbstractChaChaAlgorithm { + /** + * Creates and registers the ChaCha20 stream cipher, declaring encryption and + * decryption capabilities for {@link ChaChaSpec} and + * {@link zeroecho.core.spec.VoidSpec}. + */ + public ChaChaAlgorithm() { + super("CHACHA20", "ChaCha20 (stream)"); + // ENCRYPT + capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.ENCRYPT, + zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, ChaChaSpec.class, + (k, s) -> new ChaChaCipherContext(this, k, true, s, new java.security.SecureRandom()), + () -> ChaChaSpec.builder().initialCounter(1).header(null).build()); + // DECRYPT + capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.DECRYPT, + zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, ChaChaSpec.class, + (k, s) -> new ChaChaCipherContext(this, k, false, s, new java.security.SecureRandom()), + () -> ChaChaSpec.builder().initialCounter(1).header(null).build()); + + // VoidSpec defaults (mirrors AES) + capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.ENCRYPT, + zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, + zeroecho.core.spec.VoidSpec.class, + (k, v) -> new ChaChaCipherContext(this, k, true, + ChaChaSpec.builder().initialCounter(1).header(null).build(), new java.security.SecureRandom()), + () -> zeroecho.core.spec.VoidSpec.INSTANCE); + + capability(zeroecho.core.AlgorithmFamily.SYMMETRIC, zeroecho.core.KeyUsage.DECRYPT, + zeroecho.core.context.EncryptionContext.class, javax.crypto.SecretKey.class, + zeroecho.core.spec.VoidSpec.class, + (k, v) -> new ChaChaCipherContext(this, k, false, + ChaChaSpec.builder().initialCounter(1).header(null).build(), new java.security.SecureRandom()), + () -> zeroecho.core.spec.VoidSpec.INSTANCE); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaBaseSpec.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaBaseSpec.java new file mode 100644 index 0000000..e355851 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaBaseSpec.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * 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.core.alg.chacha; + +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.spec.ContextSpec; + +/** + *

ChaCha base specification marker

+ * + * Common sealed interface for all ChaCha-family specifications. Implemented by + * {@link ChaChaSpec} (raw stream cipher) and {@link ChaCha20Poly1305Spec} + * (AEAD). + * + *

+ * Extends {@link zeroecho.core.spec.ContextSpec} to allow binding + * algorithm-specific parameters into a context. + *

+ * + *

Header support

+ *
    + *
  • {@link #header()} may return a {@link SymmetricHeaderCodec} that encodes + * parameters (e.g., nonce, AAD hash) into the ciphertext stream.
  • + *
  • If {@code null}, no header is used and parameters must be managed via + * {@code CtxInterface} or other out-of-band means.
  • + *
+ * + *

Example

{@code
+ * ChaChaBaseSpec spec = ChaCha20Poly1305Spec.withHeader(
+ *     new ChaCha20Poly1305HeaderCodec()
+ * );
+ * SymmetricHeaderCodec codec = spec.header(); // non-null
+ * }
+ * + * @since 1.0 + */ +public sealed interface ChaChaBaseSpec extends ContextSpec permits ChaChaSpec, ChaCha20Poly1305Spec { + /** + * Returns the optional header codec used to serialize/deserialize stream + * headers for this ChaCha mode. + * + * @return {@link SymmetricHeaderCodec} instance, or {@code null} if no header + */ + SymmetricHeaderCodec header(); // may be null +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaCipherContext.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaCipherContext.java new file mode 100644 index 0000000..7510e6b --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaCipherContext.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * 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.core.alg.chacha; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.ChaCha20ParameterSpec; + +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; + +/** + *

ChaCha20 cipher context (stream)

+ * + * Concrete {@link zeroecho.core.context.EncryptionContext} for the + * {@code ChaCha20} stream cipher. Relies on the parent + * {@link AbstractChaChaCipherContext} for streaming, nonce management, and + * optional header handling, while configuring ChaCha20-specific parameters: + *
    + *
  • Transformation: {@code "ChaCha20"}.
  • + *
  • 12-byte nonce via {@link javax.crypto.spec.ChaCha20ParameterSpec}.
  • + *
  • Initial counter sourced from {@link ChaChaSpec#initialCounter()}, + * optionally overridden via {@link ConfluxKeys#tagBits(String)} in the bound + * context.
  • + *
+ * + *

Counter handling

On attach, the counter is taken from the spec; if + * the active context contains an integer under {@code ConfluxKeys.tagBits(id)}, + * that value overrides the spec and is used to initialize the cipher counter. + * If absent, the spec's value is stored into the context for downstream + * consumers. + * + * @since 1.0 + */ +public final class ChaChaCipherContext extends AbstractChaChaCipherContext { + /** + * Creates a ChaCha20 context. + * + * @param alg algorithm definition + * @param key ChaCha20 secret key + * @param enc {@code true} for encryption, {@code false} for decryption + * @param spec ChaCha20 stream specification (includes initial counter) + * @param rnd randomness source for nonce generation + */ + public ChaChaCipherContext(CryptoAlgorithm alg, SecretKey key, boolean enc, ChaChaSpec spec, + java.security.SecureRandom rnd) { + super(alg, key, enc, spec, rnd); + } + + /** + * Returns {@code "ChaCha20"} as the JCE transformation. + * + * @return transformation string + */ + @Override + protected String jceName() { + return "ChaCha20"; + } + + /** + * Initializes the cipher in the configured mode with the supplied nonce and + * counter. + * + *

+ * Uses {@link javax.crypto.spec.ChaCha20ParameterSpec} with a 12-byte nonce and + * an initial counter taken from {@link ChaChaSpec#initialCounter()} or, if + * present, from {@link ConfluxKeys#tagBits(String)} in the bound context. + *

+ * + * @param cipher initialized cipher instance + * @param nonce 12-byte nonce value + * @throws java.security.GeneralSecurityException if cipher initialization fails + * @throws java.io.IOException if context access or parameter + * resolution fails + */ + @Override + protected void initCipher(Cipher cipher, byte[] nonce) throws GeneralSecurityException, IOException { + final String id = algorithm.id(); + int counter = spec.initialCounter(); + CtxInterface c = context(); + if (c != null) { + Integer ctxCtr = c.get(ConfluxKeys.tagBits(id)); + if (ctxCtr != null) { + counter = ctxCtr; + } else { + c.put(ConfluxKeys.tagBits(id), counter); + } + } + cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, + new ChaCha20ParameterSpec(nonce, counter)); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaHeaderCodec.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaHeaderCodec.java new file mode 100644 index 0000000..f87ced2 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaHeaderCodec.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.chacha; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.io.Util; + +/** + *

ChaCha20 streaming header codec

+ * + * Implements {@link SymmetricHeaderCodec} for the {@code ChaCha20} stream + * cipher. Encodes a compact header containing: + *
    + *
  • a 12-byte nonce (IV), and
  • + *
  • the stream counter as a 7-bit packed integer.
  • + *
+ * + *

+ * The nonce and counter are exchanged through the bound {@link CtxInterface} + * using {@link ConfluxKeys#iv(String)} and {@link ConfluxKeys#tagBits(String)} + * keys, namespaced by the algorithm id (e.g., {@code "CHACHA20"}). + *

+ * + *

Header layout

+ *  [0..11]   : 12-byte nonce (IV)
+ *  [12..N]   : counter encoded via 7-bit packed integer
+ * 
+ * + *

Behavior

+ *
    + *
  • writeHeader: requires {@code iv(id)} in context (12 bytes). Writes + * the nonce, then the counter from {@code tagBits(id)} if present, otherwise + * uses {@code 1}.
  • + *
  • readHeader: reads nonce and counter and stores them into the + * context under the same keys.
  • + *
+ * + * @since 1.0 + */ +public final class ChaChaHeaderCodec implements SymmetricHeaderCodec { + /** + * Required nonce length for ChaCha20 headers (12 bytes). + */ + private static final int NONCE_LEN = 12; + + /** + * Writes the ChaCha20 header to the provided output. + * + *

+ * Reads a 12-byte nonce from {@link ConfluxKeys#iv(String)} and a stream + * counter from {@link ConfluxKeys#tagBits(String)} (defaulting to {@code 1} if + * absent). Emits the nonce followed by the counter encoded as a 7-bit packed + * integer, then flushes. + *

+ * + * @param out destination stream + * @param algorithm algorithm instance used for context key scoping + * @param ctx operation context carrying nonce and optional counter + * @throws java.io.IOException if the nonce is missing/invalid or I/O fails + */ + @Override + public void writeHeader(OutputStream out, CryptoAlgorithm algorithm, CtxInterface ctx) throws IOException { + final String id = algorithm.id(); // "CHACHA20" + byte[] nonce = ctx.get(ConfluxKeys.iv(id)); + if (nonce == null || nonce.length != NONCE_LEN) { + throw new IOException("ChaChaHeaderCodec: nonce missing/invalid in Ctx"); + } + Integer ctr = ctx.get(ConfluxKeys.tagBits(id)); + int counter = ctr == null ? 1 : ctr; + Util.write(out, nonce); + Util.writePack7I(out, counter); + out.flush(); + } + + /** + * Reads a ChaCha20 header from the input and hydrates the context. + * + *

+ * Consumes a 12-byte nonce and a 7-bit packed counter, then stores them into + * {@code ctx} under {@link ConfluxKeys#iv(String)} and + * {@link ConfluxKeys#tagBits(String)}, respectively. Returns the same input + * stream positioned after the header. + *

+ * + * @param in source stream + * @param algorithm algorithm instance used for context key scoping + * @param ctx context to populate with nonce and counter + * @return the input stream positioned after the header + * @throws java.io.IOException if the header is malformed or I/O fails + */ + @Override + public InputStream readHeader(InputStream in, CryptoAlgorithm algorithm, CtxInterface ctx) throws IOException { + final String id = algorithm.id(); + byte[] nonce = Util.read(in, NONCE_LEN); + int counter = Util.readPack7I(in); + ctx.put(ConfluxKeys.iv(id), nonce); + ctx.put(ConfluxKeys.tagBits(id), counter); + return in; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyGenSpec.java new file mode 100644 index 0000000..c23fca7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyGenSpec.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * 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.core.alg.chacha; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

ChaCha20 key generation specification

+ * + * Immutable record describing the key size for ChaCha20 key generation. Only + * 256-bit keys are permitted by the ChaCha20 design. + * + *

Validation

+ *
    + *
  • The constructor enforces {@code keySizeBits == 256}.
  • + *
  • Any other size results in an {@link IllegalArgumentException}.
  • + *
+ * + *

Factory method

+ *
    + *
  • {@link #chacha256()} provides a convenient way to obtain a standard + * 256-bit spec.
  • + *
+ * + *

Usage example

{@code
+ * ChaChaKeyGenSpec spec = ChaChaKeyGenSpec.chacha256();
+ * SecretKey key = cryptoAlgorithm.generateSecret(spec);
+ * }
+ * + * @param keySizeBits key size in bits (must be 256) + * @since 1.0 + */ +public record ChaChaKeyGenSpec(int keySizeBits) implements AlgorithmKeySpec, Describable { + /** + * Constructs a new ChaCha20 key generation spec. + * + * @param keySizeBits must be 256; otherwise an exception is thrown + * @throws IllegalArgumentException if {@code keySizeBits != 256} + */ + public ChaChaKeyGenSpec { + if (keySizeBits != 256) { // NOPMD + throw new IllegalArgumentException("ChaCha20 keySizeBits must be 256"); + } + } + + /** + * Returns the standard 256-bit key generation spec. + * + * @return a new {@code ChaChaKeyGenSpec} with size 256 + */ + public static ChaChaKeyGenSpec chacha256() { + return new ChaChaKeyGenSpec(256); + } + + /** + * Returns a short human-readable description of this spec. + * + * @return the string {@code "256bits"} + */ + @Override + public String description() { + return "256bits"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java new file mode 100644 index 0000000..928dfa7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaKeyImportSpec.java @@ -0,0 +1,184 @@ +/******************************************************************************* + * 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.core.alg.chacha; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.HexFormat; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

ChaCha20 key import specification

+ * + * Wraps a raw ChaCha20 key for import into the ZeroEcho framework. Keys must be + * exactly 32 bytes (256 bits). + * + *

Construction

+ *
    + *
  • {@link #fromRaw(byte[])} - construct from a raw byte array.
  • + *
  • {@link #fromHex(String)} - construct from a hexadecimal string.
  • + *
  • {@link #fromBase64(String)} - construct from a Base64 string.
  • + *
+ * + *

Marshalling

Keys can be serialized/deserialized using + * {@link PairSeq}: + *
    + *
  • {@link #marshal(ChaChaKeyImportSpec)} encodes the key as Base64.
  • + *
  • {@link #unmarshal(PairSeq)} accepts fields {@code k.b64}, {@code k.hex}, + * or {@code k.raw} (ISO-8859-1) to restore a spec.
  • + *
+ * + *

Usage

{@code
+ * // Import from raw key bytes
+ * ChaChaKeyImportSpec spec = ChaChaKeyImportSpec.fromRaw(keyBytes);
+ * SecretKey key = cryptoAlgorithm.importSecret(spec);
+ *
+ * // Serialize to PairSeq
+ * PairSeq seq = ChaChaKeyImportSpec.marshal(spec);
+ *
+ * // Deserialize back
+ * ChaChaKeyImportSpec restored = ChaChaKeyImportSpec.unmarshal(seq);
+ * }
+ * + * @since 1.0 + */ +public final class ChaChaKeyImportSpec implements AlgorithmKeySpec { + private final byte[] key; + + /** + * Creates a new import spec with the given raw key. + * + * @param key raw 32-byte key + * @throws NullPointerException if {@code key} is null + * @throws IllegalArgumentException if {@code key.length != 32} + */ + private ChaChaKeyImportSpec(byte[] key) { + Objects.requireNonNull(key, "key must not be null"); + if (key.length != 32) { // NOPMD + throw new IllegalArgumentException("ChaCha20 key must be 32 bytes, got " + key.length); + } + this.key = Arrays.copyOf(key, 32); + } + + /** + * Creates a spec from a raw byte array. + * + * @param key 32-byte raw key + * @return spec wrapping the key + */ + public static ChaChaKeyImportSpec fromRaw(byte[] key) { + return new ChaChaKeyImportSpec(key); + } + + /** + * Creates a spec from a hexadecimal string. + * + * @param hex hex-encoded key + * @return spec wrapping the decoded key + */ + public static ChaChaKeyImportSpec fromHex(String hex) { + return fromRaw(HexFormat.of().parseHex(hex)); + } + + /** + * Creates a spec from a Base64 string. + * + * @param b64 base64-encoded key + * @return spec wrapping the decoded key + */ + public static ChaChaKeyImportSpec fromBase64(String b64) { + return fromRaw(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a defensive copy of the raw key. + * + * @return 32-byte key array + */ + public byte[] key() { + return Arrays.copyOf(key, key.length); + } + + /** + * Serializes this spec into a {@link PairSeq}, storing the key as Base64. + * + * @param spec spec to serialize + * @return serialized key representation + */ + public static PairSeq marshal(ChaChaKeyImportSpec spec) { + String k = Base64.getEncoder().withoutPadding().encodeToString(spec.key); + return PairSeq.of("type", "CHACHA-KEY", "k.b64", k); + } + + /** + * Restores a spec from a serialized {@link PairSeq}. + * + *

+ * Recognized fields: + *

+ *
    + *
  • {@code k.b64} - Base64 encoding
  • + *
  • {@code k.hex} - hexadecimal string
  • + *
  • {@code k.raw} - raw ISO-8859-1 string
  • + *
+ * + * @param p serialized key representation + * @return reconstructed spec + * @throws IllegalArgumentException if none of the recognized fields are present + */ + public static ChaChaKeyImportSpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor c = p.cursor(); + while (c.next()) { + String k = c.key(); + String v = c.value(); + switch (k) { + case "k.b64" -> out = Base64.getDecoder().decode(v); + case "k.hex" -> out = HexFormat.of().parseHex(v); + case "k.raw" -> out = v.getBytes(StandardCharsets.ISO_8859_1); + default -> { + } + } + } + if (out == null) { + throw new IllegalArgumentException("ChaCha20 key missing (k.b64 / k.hex / k.raw)"); + } + return new ChaChaKeyImportSpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaSpec.java b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaSpec.java new file mode 100644 index 0000000..9ad0285 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/ChaChaSpec.java @@ -0,0 +1,175 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.chacha; + +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.annotation.Describable; + +/** + *

ChaCha20 stream cipher specification

+ * + * Immutable parameter set for configuring a {@code ChaCha20} context. Provides + * an initial counter and an optional {@link SymmetricHeaderCodec}. + * + *

Fields

+ *
    + *
  • {@link #initialCounter()} - the initial block counter used when no + * counter is present in the context or header (must be non-negative). Default + * is {@code 1}, matching common practice.
  • + *
  • {@link #header()} - optional codec for encoding/decoding a stream header + * that carries runtime parameters such as nonce and counter.
  • + *
+ * + *

Construction

Use the fluent {@link Builder}:
{@code
+ * ChaChaSpec spec = ChaChaSpec.builder()
+ *     .initialCounter(42)
+ *     .header(new ChaChaHeaderCodec())
+ *     .build();
+ * }
+ * + *

Convenience factory

+ *
    + *
  • {@link #chacha20(SymmetricHeaderCodec)} returns a spec with initial + * counter {@code 1} and the provided header codec.
  • + *
+ * + * @since 1.0 + */ +public final class ChaChaSpec implements ChaChaBaseSpec, Describable { + private final int initialCounter; // used when counter not present in ctx/header + private final SymmetricHeaderCodec header; // optional header codec + + /** + * Creates a new ChaCha20 specification. + * + * @param initialCounter initial block counter (must be >= 0) + * @param header optional header codec (may be {@code null}) + * @throws IllegalArgumentException if {@code initialCounter < 0} + */ + private ChaChaSpec(int initialCounter, SymmetricHeaderCodec header) { + if (initialCounter < 0) { + throw new IllegalArgumentException("initialCounter must be >= 0"); + } + this.initialCounter = initialCounter; + this.header = header; // may be null + } + + /** + * Returns a new builder for constructing a {@link ChaChaSpec}. + * + * @return fresh builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Fluent builder for {@link ChaChaSpec}. + */ + public static final class Builder { + private int initialCounter = 1; // sane default per common practice + private SymmetricHeaderCodec header; // = null; + + /** + * Sets the initial counter value. + * + * @param c non-negative counter value + * @return this builder + */ + public Builder initialCounter(int c) { + this.initialCounter = c; + return this; + } + + /** + * Sets the optional header codec. + * + * @param codec header codec or {@code null} + * @return this builder + */ + public Builder header(SymmetricHeaderCodec codec) { + this.header = codec; + return this; + } + + /** + * Builds an immutable {@link ChaChaSpec}. + * + * @return constructed spec + */ + public ChaChaSpec build() { + return new ChaChaSpec(initialCounter, header); + } + } + + /** + * Convenience factory for a ChaCha20 spec with counter = 1. + * + * @param header optional header codec + * @return new spec instance + */ + public static ChaChaSpec chacha20(SymmetricHeaderCodec header) { + return builder().initialCounter(1).header(header).build(); + } + + /** + * Returns the configured initial counter. + * + * @return non-negative counter value + */ + public int initialCounter() { + return initialCounter; + } + + /** + * Returns the optional header codec. + * + * @return codec instance or {@code null} + */ + @Override + public SymmetricHeaderCodec header() { + return header; + } + + /** + * Human-readable description of this spec. + * + * @return string of the form {@code "ChaCha20(counter=N)"} + */ + @Override + public String description() { + return "ChaCha20(counter=" + initialCounter + ")"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java b/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java new file mode 100644 index 0000000..b482ce8 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/chacha/package-info.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Classic McEliece (CMCE) KEM integration and utilities. + * + *

+ * This package adapts the Bouncy Castle PQC CMCE primitives to the core SPI. It + * provides the algorithm descriptor, a runtime KEM context, and key + * specifications for generation and import. The design keeps provider-specific + * details encapsulated behind factories while exposing clear roles and metadata + * to the higher layers. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Expose a concrete algorithm descriptor that registers CMCE KEM roles and + * a KEM-backed message-agreement adapter.
  • + *
  • Provide a runtime context that performs encapsulation and + * decapsulation.
  • + *
  • Define key specifications for key-pair generation and for importing X.509 + * and PKCS#8 encodings.
  • + *
+ * + *

Components

+ *
    + *
  • Algorithm descriptor: {@link zeroecho.core.alg.cmce.CmceAlgorithm} + * declares {@code ENCAPSULATE}/{@code DECAPSULATE} KEM roles and wires an + * {@code AGREEMENT} role through a KEM-based adapter. It also registers + * asymmetric key builders for generation and import. The provider requirement + * is the Bouncy Castle PQC provider under the standard name + * {@code "BCPQC"}.
  • + *
  • Runtime context: {@link zeroecho.core.alg.cmce.CmceKemContext} + * holds state for encapsulation or decapsulation depending on which constructor + * is used.
  • + *
  • Key generation spec: {@link zeroecho.core.alg.cmce.CmceKeyGenSpec} + * selects a CMCE parameter set (variant) used by the key-pair builder.
  • + *
  • Key import specs: {@link zeroecho.core.alg.cmce.CmcePublicKeySpec} + * wraps X.509 public keys and {@link zeroecho.core.alg.cmce.CmcePrivateKeySpec} + * wraps PKCS#8 private keys; both are immutable and defensively copy their byte + * arrays.
  • + *
+ * + *

Provider requirements

+ *

+ * The algorithm expects the Bouncy Castle PQC provider to be installed before + * use; the descriptor verifies this when generating or importing keys. + *

+ * + *

Thread-safety

+ *
    + *
  • Algorithm descriptors are immutable and safe to share across + * threads.
  • + *
  • Runtime contexts are stateful and not thread-safe.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.chacha; diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java new file mode 100644 index 0000000..ca77314 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java @@ -0,0 +1,250 @@ +/******************************************************************************* + * 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.core.alg.cmce; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.bouncycastle.pqc.jcajce.spec.CMCEParameterSpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

Classic McEliece (CMCE) algorithm adapter

+ * + *

+ * Integrates the Bouncy Castle PQC CMCE primitives into the ZeroEcho SPI. This + * algorithm publishes: + *

+ * + *
    + *
  • KEM capabilities: + *
      + *
    • {@code ENCAPSULATE} using a recipient {@link PublicKey}.
    • + *
    • {@code DECAPSULATE} using a {@link PrivateKey}.
    • + *
    + *
  • + *
  • Agreement capability implemented via a KEM-backed adapter: + *
      + *
    • Initiator: constructs an agreement context that encapsulates to the peer + * public key.
    • + *
    • Responder: constructs an agreement context that decapsulates with the + * local private key.
    • + *
    + *
  • + *
  • Asymmetric key builders: + *
      + *
    • Key generation from {@link CmceKeyGenSpec} variants.
    • + *
    • Public key import from X.509 bytes.
    • + *
    • Private key import from PKCS#8 bytes.
    • + *
    + *
  • + *
+ * + *

+ * Provider requirement: the Bouncy Castle PQC provider must be + * registered under the standard name {@code "BCPQC"} before use. + *

+ * + *

+ * Usage example: + *

+ *
{@code
+ * // Register BCPQC once at startup.
+ * Security.addProvider(new BouncyCastlePQCProvider());
+ *
+ * // Obtain contexts using the CMCE algorithm id.
+ * CmceAlgorithm alg = new CmceAlgorithm();
+ *
+ * // Generate a key pair with a chosen CMCE variant.
+ * KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class)
+ *                 .generateKeyPair(CmceKeyGenSpec.mceliece8192128f());
+ *
+ * // Create a KEM encapsulation context with the recipient public key.
+ * KemContext enc = alg.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
+ *
+ * // Create an agreement initiator context backed by CMCE KEM.
+ * MessageAgreementContext initiator =
+ *     alg.create(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
+ * }
+ * + * @since 1.0 + */ +public final class CmceAlgorithm extends AbstractCryptoAlgorithm { + /** + * Constructs and registers CMCE capabilities and key builders. + * + *

+ * This constructor registers: + *

+ *
    + *
  • KEM roles for {@code ENCAPSULATE} and {@code DECAPSULATE}.
  • + *
  • Agreement role wired through a KEM-backed initiator/responder + * adapter.
  • + *
  • Asymmetric key builder for {@link CmceKeyGenSpec} (generation), X.509 + * public key import, and PKCS#8 private key import.
  • + *
+ * + *

+ * The algorithm id is {@code "CMCE"}, the display name is + * {@code "Classic McEliece (CMCE)"}, and the provider name is taken from the + * Bouncy Castle PQC provider. + *

+ */ + public CmceAlgorithm() { + super("CMCE", "Classic McEliece (CMCE)", BouncyCastlePQCProvider.PROVIDER_NAME); + + capability(AlgorithmFamily.KEM, KeyUsage.ENCAPSULATE, KemContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> new CmceKemContext(this, k), () -> VoidSpec.INSTANCE); + capability(AlgorithmFamily.KEM, KeyUsage.DECAPSULATE, KemContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> new CmceKemContext(this, k), () -> VoidSpec.INSTANCE); + + // AGREEMENT (initiator): Alice has Bob's public key → encapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← return your + // existing KemContext + PublicKey.class, // ← initiator uses recipient's public key + VoidSpec.class, // ← must implement ContextSpec + (PublicKey recipient, VoidSpec spec) -> { + // create a context bound to recipient public key for encapsulation + return KemMessageAgreementAdapter.builder().upon(new CmceKemContext(this, recipient)).asInitiator() + .build(); + }, () -> VoidSpec.INSTANCE // default + ); + + // AGREEMENT (responder): Bob has his private key → decapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← same KemContext + // type + PrivateKey.class, // ← responder uses their private key + VoidSpec.class, (PrivateKey myPriv, VoidSpec spec) -> { + return KemMessageAgreementAdapter.builder().upon(new CmceKemContext(this, myPriv)).asResponder() + .build(); + }, () -> VoidSpec.INSTANCE); + + registerAsymmetricKeyBuilder(CmceKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(CmceKeyGenSpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("CMCE", providerName()); + CMCEParameterSpec params = switch (spec.variant()) { + case MCELIECE_348864 -> CMCEParameterSpec.mceliece348864; + case MCELIECE_348864F -> CMCEParameterSpec.mceliece348864f; + case MCELIECE_460896 -> CMCEParameterSpec.mceliece460896; + case MCELIECE_460896F -> CMCEParameterSpec.mceliece460896f; + case MCELIECE_6688128 -> CMCEParameterSpec.mceliece6688128; + case MCELIECE_6688128F -> CMCEParameterSpec.mceliece6688128f; + case MCELIECE_6960119 -> CMCEParameterSpec.mceliece6960119; + case MCELIECE_6960119F -> CMCEParameterSpec.mceliece6960119f; + case MCELIECE_8192128 -> CMCEParameterSpec.mceliece8192128; + case MCELIECE_8192128F -> CMCEParameterSpec.mceliece8192128f; + }; + kpg.initialize(params, new SecureRandom()); + return kpg.generateKeyPair(); + } + + @Override + public PublicKey importPublic(CmceKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(CmceKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + }, CmceKeyGenSpec::mceliece8192128f); + + registerAsymmetricKeyBuilder(CmcePublicKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(CmcePublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(CmcePublicKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("CMCE", providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); + } + + @Override + public PrivateKey importPrivate(CmcePublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + }, null); + + registerAsymmetricKeyBuilder(CmcePrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(CmcePrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(CmcePrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(CmcePrivateKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("CMCE", providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + } + }, null); + } + + private static void ensureProvider() throws NoSuchProviderException { + Provider p = Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME); + if (p == null) { + throw new NoSuchProviderException("BCPQC provider not registered"); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmceKemContext.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmceKemContext.java new file mode 100644 index 0000000..8527489 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmceKemContext.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * 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.core.alg.cmce; + +import java.io.IOException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Objects; + +import javax.security.auth.DestroyFailedException; + +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.pqc.crypto.cmce.CMCEKEMExtractor; +import org.bouncycastle.pqc.crypto.cmce.CMCEKEMGenerator; +import org.bouncycastle.pqc.crypto.cmce.CMCEPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.cmce.CMCEPublicKeyParameters; +import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory; +import org.bouncycastle.pqc.crypto.util.PublicKeyFactory; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.KemContext; + +/** + *

Classic McEliece (CMCE) KEM context

+ * + *

+ * Holds the state required to perform CMCE key encapsulation or decapsulation. + * The operational mode is determined by the constructor used: + *

+ * + *
    + *
  • PublicKey constructor - encapsulate mode
  • + *
  • PrivateKey constructor - decapsulate mode
  • + *
+ * + *

+ * Usage: + *

+ *
{@code
+ * CryptoAlgorithm alg = ...;
+ * PublicKey recipient = ...;
+ *
+ * // Encapsulation
+ * try (CmceKemContext ctx = new CmceKemContext(alg, recipient)) {
+ *   KemResult kem = ctx.encapsulate();
+ *   byte[] ct = kem.ciphertext();
+ *   byte[] secret = kem.secret();
+ *   // send ct to recipient; use secret for key derivation
+ * }
+ *
+ * // Decapsulation
+ * PrivateKey myPriv = ...;
+ * byte[] ct = ...;
+ * try (CmceKemContext ctx = new CmceKemContext(alg, myPriv)) {
+ *   byte[] secret = ctx.decapsulate(ct);
+ * }
+ * }
+ * + *

+ * Notes: + *

+ *
    + *
  • Encapsulation requires a CMCE public key; decapsulation requires a CMCE + * private key.
  • + *
  • Returned arrays are owned by the caller; callers should clear secrets + * when no longer needed.
  • + *
  • This class holds no external resources and is safe to close + * repeatedly.
  • + *
+ * + * @since 1.0 + */ +public final class CmceKemContext implements KemContext { + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean encapsulate; + + /** + * Creates an encapsulation context bound to a recipient public key. + * + * @param algorithm parent algorithm metadata (for diagnostics) + * @param k CMCE public key + * @throws NullPointerException if any argument is null + */ + public CmceKemContext(CryptoAlgorithm algorithm, PublicKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = true; + } + + /** + * Creates a decapsulation context bound to a private key. + * + * @param algorithm parent algorithm metadata (for diagnostics) + * @param k CMCE private key + * @throws NullPointerException if any argument is null + */ + public CmceKemContext(CryptoAlgorithm algorithm, PrivateKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = false; + } + + /** + * Returns the parent algorithm descriptor for this context. + * + * @return algorithm descriptor; never null + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + *

+ * In encapsulate mode this is a {@link java.security.PublicKey}; in decapsulate + * mode it is a {@link java.security.PrivateKey}. + *

+ * + * @return key used by this context; never null + */ + @Override + public Key key() { + return key; + } + + /** + * Releases resources held by this context. + * + *

+ * This implementation holds no resources and performs no action. It is safe to + * call multiple times. + *

+ */ + @Override + public void close() { + // empty + } + + /** + * Generates a CMCE ciphertext and shared secret using the stored public key. + * + * @return result containing ciphertext and secret + * @throws IllegalStateException if this context is not in encapsulate mode + * @throws IOException if encapsulation fails + */ + @Override + public KemResult encapsulate() throws IOException { + if (!encapsulate) { + throw new IllegalStateException("Not initialized for ENCAPSULATE"); + } + try { + final CMCEPublicKeyParameters keyParam = (CMCEPublicKeyParameters) PublicKeyFactory + .createKey(key.getEncoded()); + CMCEKEMGenerator gen = new CMCEKEMGenerator(new SecureRandom()); + SecretWithEncapsulation res = gen.generateEncapsulated(keyParam); + byte[] secret = res.getSecret(); + byte[] ct = res.getEncapsulation(); + res.destroy(); + return new KemResult(ct, secret); + } catch (DestroyFailedException e) { + throw new IOException("CMCE encapsulate failed", e); + } + } + + /** + * Extracts the shared secret from the given ciphertext using the stored private + * key. + * + * @param ciphertext CMCE ciphertext (must be non-null and non-empty) + * @return shared secret bytes + * @throws IllegalStateException if this context is not in decapsulate mode + * @throws IllegalArgumentException if {@code ciphertext} is null or empty + * @throws IOException if decapsulation fails + */ + @Override + public byte[] decapsulate(byte[] ciphertext) throws IOException { + if (encapsulate) { + throw new IllegalStateException("Not initialized for DECAPSULATE"); + } + try { + final CMCEPrivateKeyParameters keyParam = (CMCEPrivateKeyParameters) PrivateKeyFactory + .createKey(key.getEncoded()); + CMCEKEMExtractor ex = new CMCEKEMExtractor(keyParam); + return ex.extractSecret(ciphertext); + } catch (Exception e) { // NOPMD + throw new IOException("CMCE decapsulate failed", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java new file mode 100644 index 0000000..7d9036d --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java @@ -0,0 +1,243 @@ +/******************************************************************************* + * 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.core.alg.cmce; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

CMCE key generation specification

+ * + *

+ * Encapsulates the choice of Classic McEliece parameter set (variant) to be + * used when generating new key pairs. Each variant corresponds to a + * standardized security level and key size trade-off as defined in the + * post-quantum KEM standardization process. + *

+ * + *

+ * Usage example: + *

+ *
{@code
+ * // Generate a key pair for McEliece 8192128F (256-bit security, fast)
+ * CmceKeyGenSpec spec = CmceKeyGenSpec.mceliece8192128f();
+ * KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class).generateKeyPair(spec);
+ * }
+ * + * @since 1.0 + */ +public final class CmceKeyGenSpec implements AlgorithmKeySpec, Describable { + /** + * Enumeration of supported CMCE parameter set variants. + * + *

+ * Each value corresponds to a named parameter set from the Classic McEliece + * post-quantum KEM standardization. + *

+ */ + public enum Variant { + /** + * McEliece 348864, standard parameter set (128-bit security). + */ + MCELIECE_348864, + /** + * McEliece 348864, fast parameter set (128-bit security). + */ + MCELIECE_348864F, + /** + * McEliece 460896, standard parameter set (128-bit security, larger keys). + */ + MCELIECE_460896, + /** + * McEliece 460896, fast parameter set (128-bit security, larger keys). + */ + MCELIECE_460896F, + /** + * McEliece 6688128, standard parameter set (192-bit security). + */ + MCELIECE_6688128, + /** + * McEliece 6688128, fast parameter set (192-bit security). + */ + MCELIECE_6688128F, + /** + * McEliece 6960119, standard parameter set (192-bit security, alternative + * form). + */ + MCELIECE_6960119, + /** + * McEliece 6960119, fast parameter set (192-bit security, alternative form). + */ + MCELIECE_6960119F, + /** + * McEliece 8192128, standard parameter set (256-bit security). + */ + MCELIECE_8192128, + /** + * McEliece 8192128, fast parameter set (256-bit security). + */ + MCELIECE_8192128F + } + + private final Variant variant; + + private CmceKeyGenSpec(Variant v) { + this.variant = v; + } + + /** + * Creates a new key generation spec bound to a specific variant. + * + * @param v variant to use + * @return new specification for the given variant + * @throws NullPointerException if {@code v} is null + */ + public static CmceKeyGenSpec of(Variant v) { + return new CmceKeyGenSpec(v); + } + + /** + * Convenience factory for {@link Variant#MCELIECE_348864}. + * + * @return a new specification for {@link Variant#MCELIECE_348864} + */ + public static CmceKeyGenSpec mceliece348864() { + return new CmceKeyGenSpec(Variant.MCELIECE_348864); + } + + /** + * Convenience factory for {@link Variant#MCELIECE_348864F}. + * + * @return a new specification for {@link Variant#MCELIECE_348864F} + */ + public static CmceKeyGenSpec mceliece348864f() { + return new CmceKeyGenSpec(Variant.MCELIECE_348864F); + } + + /** + * Convenience factory for {@link Variant#MCELIECE_460896}. + * + * @return a new specification for {@link Variant#MCELIECE_460896} + */ + public static CmceKeyGenSpec mceliece460896() { + return new CmceKeyGenSpec(Variant.MCELIECE_460896); + } + + /** + * Convenience factory for {@link Variant#MCELIECE_460896F}. + * + * @return a new specification for {@link Variant#MCELIECE_460896F} + */ + public static CmceKeyGenSpec mceliece460896f() { + return new CmceKeyGenSpec(Variant.MCELIECE_460896F); + } + + /** + * Convenience factory for {@link Variant#MCELIECE_6688128}. + * + * @return a new specification for {@link Variant#MCELIECE_6688128} + */ + public static CmceKeyGenSpec mceliece6688128() { + return new CmceKeyGenSpec(Variant.MCELIECE_6688128); + } + + /** + * Convenience factory for {@link Variant#MCELIECE_6688128F}. + * + * @return a new specification for {@link Variant#MCELIECE_6688128F} + */ + public static CmceKeyGenSpec mceliece6688128f() { + return new CmceKeyGenSpec(Variant.MCELIECE_6688128F); + } + + /** + * Convenience factory for {@link Variant#MCELIECE_6960119}. + * + * @return a new specification for {@link Variant#MCELIECE_6960119} + */ + public static CmceKeyGenSpec mceliece6960119() { + return new CmceKeyGenSpec(Variant.MCELIECE_6960119); + } + + /** + * Convenience factory for {@link Variant#MCELIECE_6960119F}. + * + * @return a new specification for {@link Variant#MCELIECE_6960119F} + */ + public static CmceKeyGenSpec mceliece6960119f() { + return new CmceKeyGenSpec(Variant.MCELIECE_6960119F); + } + + /** + * Convenience factory for {@link Variant#MCELIECE_8192128}. + * + * @return a new specification for {@link Variant#MCELIECE_8192128} + */ + public static CmceKeyGenSpec mceliece8192128() { + return new CmceKeyGenSpec(Variant.MCELIECE_8192128); + } + + /** + * Convenience factory for {@link Variant#MCELIECE_8192128F}. + * + * @return a new specification for {@link Variant#MCELIECE_8192128F} + */ + public static CmceKeyGenSpec mceliece8192128f() { + return new CmceKeyGenSpec(Variant.MCELIECE_8192128F); + } + + /** + * Returns the selected variant for this specification. + * + * @return non-null variant + */ + public Variant variant() { + return variant; + } + + /** + * Returns a human-readable description of this specification. + * + *

+ * The value is simply the {@link Variant#toString()} of the selected variant. + *

+ * + * @return string description of the variant + */ + @Override + public String description() { + return variant.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java new file mode 100644 index 0000000..e1107f7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * 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.core.alg.cmce; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Classic McEliece (CMCE) private key specification

+ * + *

+ * Wraps a CMCE private key in PKCS#8 (DER) encoding. This spec is used to + * import or serialize private keys into the ZeroEcho SPI. + *

+ * + *

+ * Instances are immutable. The internal byte array is cloned on construction + * and on every accessor to prevent accidental mutation. + *

+ * + *

Marshalling

+ *
    + *
  • {@link #marshal(CmcePrivateKeySpec)} produces a {@link PairSeq} with + * Base64-encoded PKCS#8.
  • + *
  • {@link #unmarshal(PairSeq)} reconstructs a spec from that format.
  • + *
+ * + *

+ * Example: + *

+ *
{@code
+ * // Wrap an existing PKCS#8 byte array
+ * CmcePrivateKeySpec spec = new CmcePrivateKeySpec(pkcs8Bytes);
+ *
+ * // Serialize to PairSeq for storage or transport
+ * PairSeq encoded = CmcePrivateKeySpec.marshal(spec);
+ *
+ * // Reconstruct later
+ * CmcePrivateKeySpec restored = CmcePrivateKeySpec.unmarshal(encoded);
+ * }
+ * + * @since 1.0 + */ +public final class CmcePrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Creates a new specification from a PKCS#8-encoded CMCE private key. + * + *

+ * The input is defensively copied. + *

+ * + * @param pkcs8Der DER-encoded PKCS#8 private key + * @throws NullPointerException if {@code pkcs8Der} is null + */ + public CmcePrivateKeySpec(byte[] pkcs8Der) { + this.pkcs8 = Objects.requireNonNull(pkcs8Der).clone(); + } + + /** + * Returns a defensive copy of the PKCS#8 bytes. + * + * @return a fresh copy of the underlying PKCS#8 encoding + */ + public byte[] pkcs8() { + return pkcs8.clone(); + } + + /** + * Serializes the given private key spec into a {@link PairSeq}. + * + *

+ * The PKCS#8 bytes are Base64-encoded (without padding) and stored under the + * key {@code "pkcs8.b64"}. The type tag {@code "CmcePrivateKeySpec"} is also + * included. + *

+ * + * @param spec the spec to serialize + * @return a PairSeq containing type and Base64-encoded key + * @throws NullPointerException if {@code spec} is null + */ + public static PairSeq marshal(CmcePrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "CmcePrivateKeySpec", PKCS8_B64, b64); + } + + /** + * Deserializes a {@link CmcePrivateKeySpec} from a {@link PairSeq}. + * + *

+ * The method scans for a key named {@code "pkcs8.b64"}, decodes its value from + * Base64, and reconstructs the spec. + *

+ * + * @param p PairSeq containing serialized fields + * @return reconstructed {@code CmcePrivateKeySpec} + * @throws IllegalArgumentException if no {@code "pkcs8.b64"} field is found + */ + public static CmcePrivateKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (PKCS8_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("CmcePrivateKeySpec: missing pkcs8.b64"); + } + return new CmcePrivateKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string with the length of the encoded key. + * + *

+ * The output is safe to log; it does not include key material. + *

+ * + * @return a string in the form {@code CmcePrivateKeySpec[len=N]} + */ + @Override + public String toString() { + return "CmcePrivateKeySpec[len=" + pkcs8.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/CmcePublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/cmce/CmcePublicKeySpec.java new file mode 100644 index 0000000..80d00b4 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/cmce/CmcePublicKeySpec.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * 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.core.alg.cmce; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Classic McEliece (CMCE) public key specification

+ * + *

+ * Wraps a CMCE public key in X.509 (DER) encoding. This spec is used to import + * or serialize public keys into the ZeroEcho SPI. + *

+ * + *

+ * Instances are immutable. The internal byte array is cloned on construction + * and on every accessor to prevent accidental mutation. + *

+ * + *

Marshalling

+ *
    + *
  • {@link #marshal(CmcePublicKeySpec)} produces a {@link PairSeq} with + * Base64-encoded X.509 data.
  • + *
  • {@link #unmarshal(PairSeq)} reconstructs a spec from that format.
  • + *
+ * + *

+ * Example: + *

+ *
{@code
+ * // Wrap an existing X.509-encoded public key
+ * CmcePublicKeySpec spec = new CmcePublicKeySpec(x509Bytes);
+ *
+ * // Serialize to PairSeq for storage or transport
+ * PairSeq encoded = CmcePublicKeySpec.marshal(spec);
+ *
+ * // Reconstruct later
+ * CmcePublicKeySpec restored = CmcePublicKeySpec.unmarshal(encoded);
+ * }
+ * + * @since 1.0 + */ +public final class CmcePublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Creates a new specification from an X.509-encoded CMCE public key. + * + *

+ * The input is defensively copied. + *

+ * + * @param x509Der DER-encoded X.509 public key + * @throws NullPointerException if {@code x509Der} is null + */ + public CmcePublicKeySpec(byte[] x509Der) { + this.x509 = Objects.requireNonNull(x509Der).clone(); + } + + /** + * Returns a defensive copy of the X.509 bytes. + * + * @return a fresh copy of the underlying X.509 encoding + */ + public byte[] x509() { + return x509.clone(); + } + + /** + * Serializes the given public key spec into a {@link PairSeq}. + * + *

+ * The X.509 bytes are Base64-encoded (without padding) and stored under the key + * {@code "x509.b64"}. The type tag {@code "CmcePublicKeySpec"} is also + * included. + *

+ * + * @param spec the spec to serialize + * @return a PairSeq containing type and Base64-encoded key + * @throws NullPointerException if {@code spec} is null + */ + public static PairSeq marshal(CmcePublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "CmcePublicKeySpec", X509_B64, b64); + } + + /** + * Deserializes a {@link CmcePublicKeySpec} from a {@link PairSeq}. + * + *

+ * The method scans for a key named {@code "x509.b64"}, decodes its value from + * Base64, and reconstructs the spec. + *

+ * + * @param p PairSeq containing serialized fields + * @return reconstructed {@code CmcePublicKeySpec} + * @throws IllegalArgumentException if no {@code "x509.b64"} field is found + */ + public static CmcePublicKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (X509_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("CmcePublicKeySpec: missing x509.b64"); + } + return new CmcePublicKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string with the length of the encoded key. + * + *

+ * The output is safe to log; it does not include key material. + *

+ * + * @return a string in the form {@code CmcePublicKeySpec[len=N]} + */ + @Override + public String toString() { + return "CmcePublicKeySpec[len=" + x509.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/cmce/package-info.java b/lib/src/main/java/zeroecho/core/alg/cmce/package-info.java new file mode 100644 index 0000000..e1491cf --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/cmce/package-info.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + *

Classic McEliece (CMCE)

+ * + *

+ * This package integrates the Classic McEliece cryptosystem, one of the oldest + * and most studied code-based public-key cryptosystems. Originally proposed by + * Robert McEliece in 1978, it is based on the hardness of decoding random + * binary Goppa codes. Despite large public key sizes, the scheme has withstood + * decades of cryptanalysis and remains unbroken by both classical and quantum + * computers. + *

+ * + *

Post-quantum KEM

+ * + *

+ * Classic McEliece has been selected by NIST in the post-quantum cryptography + * standardization process for key encapsulation. Its primary appeal is + * long-term confidence: no efficient attacks are known even in the quantum + * setting. It provides IND-CCA2 security through a well-studied transform and + * is especially suited for use cases where large public keys are acceptable but + * extremely strong security margins are desired. + *

+ * + *

Contents

+ *
    + *
  • {@link zeroecho.core.alg.cmce.CmceAlgorithm} – algorithm adapter exposing + * CMCE as a KEM and agreement primitive.
  • + *
  • {@link zeroecho.core.alg.cmce.CmceKemContext} – runtime context for + * encapsulation and decapsulation.
  • + *
  • {@link zeroecho.core.alg.cmce.CmceKeyGenSpec} – enumeration of + * standardized CMCE parameter sets (variants).
  • + *
  • {@link zeroecho.core.alg.cmce.CmcePublicKeySpec} – wrapper for + * X.509-encoded public keys.
  • + *
  • {@link zeroecho.core.alg.cmce.CmcePrivateKeySpec} – wrapper for + * PKCS#8-encoded private keys.
  • + *
+ * + *

Security properties

+ *
    + *
  • Underlying assumption: hardness of decoding binary Goppa codes.
  • + *
  • Selected as a NIST post-quantum KEM standard (2022).
  • + *
  • Public keys are large (hundreds of kilobytes), but ciphertexts and + * secrets are compact.
  • + *
  • Considered quantum-resistant and secure against known attacks.
  • + *
+ * + *

Usage

{@code
+ * // Select a variant (e.g., 8192128F for 256-bit security)
+ * CmceKeyGenSpec spec = CmceKeyGenSpec.mceliece8192128f();
+ * CmceAlgorithm alg = new CmceAlgorithm();
+ * KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class).generateKeyPair(spec);
+ *
+ * // Encapsulation (sender)
+ * try (CmceKemContext ctx = new CmceKemContext(alg, kp.getPublic())) {
+ *   KemResult kem = ctx.encapsulate();
+ *   byte[] ct = kem.ciphertext();
+ *   byte[] secret = kem.secret();
+ * }
+ *
+ * // Decapsulation (recipient)
+ * try (CmceKemContext ctx = new CmceKemContext(alg, kp.getPrivate())) {
+ *   byte[] secret = ctx.decapsulate(ct);
+ * }
+ * }
+ * + * @since 1.0 + */ +package zeroecho.core.alg.cmce; diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java new file mode 100644 index 0000000..5faa5dc --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/GenericJcaAgreementContext.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * 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.core.alg.common.agreement; + +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; + +import javax.crypto.KeyAgreement; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.AgreementContext; + +/** + *

Generic JCA-based Key Agreement Context

+ * + * An {@link AgreementContext} backed by the standard JCA {@link KeyAgreement} + * API. This class supports elliptic-curve and modern Diffie-Hellman variants + * such as ECDH, XDH (X25519, X448), and others provided by the runtime or + * configured provider. + * + *

+ * Instances of this context are created with a local {@link PrivateKey}, and + * require the peer’s {@link PublicKey} to be provided later via + * {@link #setPeerPublic(PublicKey)} before deriving the shared secret. + *

+ * + *

Lifecycle

+ *
    + *
  1. Construct with local private key and algorithm name.
  2. + *
  3. Call {@link #setPeerPublic(PublicKey)} with the remote party’s key.
  4. + *
  5. Invoke {@link #deriveSecret()} to compute the raw shared secret.
  6. + *
  7. Optionally call {@link #close()} (no resources are held here).
  8. + *
+ * + *

Notes

+ *
    + *
  • The derived secret is the raw key agreement output; protocols should + * apply a KDF before using it as a symmetric key.
  • + *
  • If {@code provider} is {@code null}, the default JCA provider lookup is + * used; otherwise, the specific provider is requested.
  • + *
+ * + * @since 1.0 + */ +public final class GenericJcaAgreementContext implements AgreementContext { + private final CryptoAlgorithm algorithm; + private final PrivateKey privateKey; + private final String jcaName; // e.g., "ECDH" or "XDH" (or "X25519"/"X448") + private final String provider; // null => default + private PublicKey peer; + + /** + * Creates a new JCA-based agreement context. + * + * @param alg the enclosing {@link CryptoAlgorithm} definition + * @param priv the local private key used in the key agreement + * @param jcaName the JCA algorithm name (e.g., {@code "ECDH"}, + * {@code "X25519"}) + * @param provider optional JCA provider name, or {@code null} to use the + * default + * @throws NullPointerException if {@code alg}, {@code priv}, or {@code jcaName} + * is {@code null} + */ + public GenericJcaAgreementContext(CryptoAlgorithm alg, PrivateKey priv, String jcaName, String provider) { + this.algorithm = alg; + this.privateKey = priv; + this.jcaName = jcaName; + this.provider = provider; + } + + /** + * Returns the {@link CryptoAlgorithm} that created this context. + * + * @return the parent algorithm definition + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the local private key bound to this agreement context. + * + * @return the private {@link Key} used in the key agreement + */ + @Override + public Key key() { + return privateKey; + } + + /** + * Assigns the peer’s public key for the key agreement. + * + *

+ * This must be called before {@link #deriveSecret()}, otherwise the context + * cannot complete the protocol. + *

+ * + * @param peer the remote party’s public key + */ + @Override + public void setPeerPublic(PublicKey peer) { + this.peer = peer; + } + + /** + * Computes the raw shared secret using the configured local private key and the + * previously assigned peer public key. + * + *

+ * Internally this delegates to the JCA {@link KeyAgreement} API with the given + * {@code jcaName} and optional provider. + *

+ * + * @return the raw shared secret as a byte array + * @throws IllegalStateException if the peer key has not been set + * @throws IllegalArgumentException if key agreement fails due to invalid keys, + * unsupported parameters, or provider errors + */ + @Override + public byte[] deriveSecret() { + if (peer == null) { + throw new IllegalStateException("Peer public key not set"); + } + try { + KeyAgreement ka = (provider == null) ? KeyAgreement.getInstance(jcaName) + : KeyAgreement.getInstance(jcaName, provider); + ka.init(privateKey); + ka.doPhase(peer, true); + return ka.generateSecret(); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("KeyAgreement failed for " + jcaName, e); + } + } + + /** + * Closes this context. + * + *

+ * For this implementation, there are no system resources to release, so the + * method is a no-op. It exists to satisfy the {@link AgreementContext} contract + * and for future compatibility. + *

+ */ + @Override + public void close() { + /* nothing to release */ + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/KemMessageAgreementAdapter.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/KemMessageAgreementAdapter.java new file mode 100644 index 0000000..4da5f8c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/KemMessageAgreementAdapter.java @@ -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.core.alg.common.agreement; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.Key; +import java.security.PublicKey; +import java.util.Objects; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MessageAgreementContext; + +/** + *

Adapter: using a KEM as a message-based agreement primitive

+ * + * {@code KemMessageAgreementAdapter} adapts a {@link KemContext} into a + * {@link MessageAgreementContext}, making KEMs usable in higher-level protocols + * that expect a two-party message agreement API. + * + *

Roles

+ *
    + *
  • {@link Role#INITIATOR} - encapsulates to a peer’s public key, producing a + * ciphertext (peer message) and shared secret.
  • + *
  • {@link Role#RESPONDER} - receives a peer message (ciphertext), + * decapsulates with their private key, and derives the shared secret.
  • + *
+ * + *

Lifecycle

+ *
    + *
  1. Create via {@link Builder} with a bound {@link KemContext}.
  2. + *
  3. Initiator calls {@link #getPeerMessage()} to obtain ciphertext to + * transmit.
  4. + *
  5. Responder calls {@link #setPeerMessage(byte[])} with received + * ciphertext.
  6. + *
  7. Both parties call {@link #deriveSecret()} to obtain the agreed + * secret.
  8. + *
+ * + *

Thread-safety

Instances are not thread-safe; synchronize externally + * if sharing across threads. + * + * @since 1.0 + */ +public final class KemMessageAgreementAdapter implements MessageAgreementContext { + /** + * Role of the adapter: initiator or responder. + */ + public enum Role { + /** Initiator: produces a peer message via encapsulation. */ + INITIATOR, + /** Responder: consumes a peer message via decapsulation. */ + RESPONDER + } + + private final KemContext kem; + private final Role role; + + private byte[] producedMessage; // initiator: ciphertext (encapsulation) + private byte[] receivedMessage; // responder: ciphertext to decapsulate + private byte[] derivedSecret; // memoized deriveSecret() + + private KemMessageAgreementAdapter(KemContext kem, Role role) { + this.kem = Objects.requireNonNull(kem, "kem must not be null"); + this.role = Objects.requireNonNull(role, "role must not be null"); + } + + /** + * Returns a new builder for constructing a {@code KemMessageAgreementAdapter}. + * + * @return builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for {@link KemMessageAgreementAdapter}. + */ + public static final class Builder { + private KemContext kem; + private Role role; + + /** + * Binds this adapter to a KEM context. + * + * @param kem underlying KEM context + * @return this builder + */ + public Builder upon(KemContext kem) { + this.kem = kem; + return this; + } + + /** + * Configures the adapter as an initiator. + * + * @return this builder + */ + public Builder asInitiator() { + this.role = Role.INITIATOR; + return this; + } + + /** + * Configures the adapter as a responder. + * + * @return this builder + */ + public Builder asResponder() { + this.role = Role.RESPONDER; + return this; + } + + /** + * Builds the adapter. + * + * @return new adapter instance + * @throws NullPointerException if no KEM context or role is set + */ + public KemMessageAgreementAdapter build() { + return new KemMessageAgreementAdapter(kem, role); + } + } + + /** + * Stores the peer’s ciphertext for decapsulation. + * + * @param message ciphertext received from initiator + * @throws IllegalStateException if called in initiator mode + */ + @Override + public void setPeerMessage(byte[] message) { + if (role != Role.RESPONDER) { + throw new IllegalStateException("setPeerMessage only valid for RESPONDER"); + } + this.receivedMessage = (message == null ? null : message.clone()); + } + + /** + * Returns the ciphertext produced by encapsulation. + * + * @return defensive copy of ciphertext to send + * @throws IllegalStateException if called in responder mode + */ + @Override + public byte[] getPeerMessage() { + if (role != Role.INITIATOR) { + throw new IllegalStateException("getPeerMessage only valid for INITIATOR"); + } + ensureEncapsulated(); + return producedMessage.clone(); + } + + /** + * No-op for KEM-based contexts. + * + *

+ * Unlike Diffie–Hellman, KEMs are already bound to the correct key at + * construction. This method exists for interface symmetry. + *

+ * + * @param peer ignored + */ + @Override + public void setPeerPublic(PublicKey peer) { + // KEM already bound to the correct key at construction; nothing to do. + // Provided for API symmetry; ignore or validate if you wish. + } + + /** + * Derives the shared secret from this exchange. + * + * @return defensive copy of the derived secret + * @throws UncheckedIOException if encapsulation/decapsulation fails + */ + @Override + public byte[] deriveSecret() { + if (derivedSecret != null) { + return derivedSecret.clone(); + } + + try { + if (role == Role.INITIATOR) { + ensureEncapsulated(); // fills producedMessage + derivedSecret + } else { + if (receivedMessage == null) { + throw new IllegalStateException("Responder missing peer encapsulation message"); + } + byte[] ss = kem.decapsulate(receivedMessage); + derivedSecret = (ss == null ? new byte[0] : ss.clone()); + } + return derivedSecret.clone(); + } catch (IOException e) { + throw new UncheckedIOException("KEM deriveSecret failed", e); + } + } + + private void ensureEncapsulated() { + if (producedMessage != null && derivedSecret != null) { + return; + } + try { + KemContext.KemResult res = kem.encapsulate(); + this.producedMessage = res.ciphertext().clone(); + this.derivedSecret = res.sharedSecret().clone(); + } catch (IOException e) { + throw new UncheckedIOException("KEM encapsulate failed", e); + } + } + + /** + * Returns the underlying algorithm descriptor. + * + * @return algorithm bound to this adapter + */ + @Override + public CryptoAlgorithm algorithm() { + return kem.algorithm(); + } + + /** + * Returns the key bound to the underlying KEM context. + * + * @return encapsulation (public) or decapsulation (private) key + */ + @Override + public Key key() { + return kem.key(); + } + + /** + * Closes the underlying KEM context if it is closeable. + * + * @throws IOException if the wrapped context fails to close + */ + @Override + public void close() throws IOException { + kem.close(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/common/agreement/package-info.java b/lib/src/main/java/zeroecho/core/alg/common/agreement/package-info.java new file mode 100644 index 0000000..8f6cfcf --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/agreement/package-info.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Adapters and generic contexts for key agreement built on the core SPI. + * + *

+ * This package provides a generic JCA-backed agreement context and a thin + * adapter that exposes a KEM as a message-based agreement primitive. The goal + * is to keep provider-specific details encapsulated while presenting clear + * roles and lifecycles that higher layers can compose. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Expose a generic agreement context that delegates to the JCA + * {@code KeyAgreement} API for algorithms such as ECDH and XDH.
  • + *
  • Adapt KEM contexts to a two-message agreement API suitable for initiator/ + * responder protocols.
  • + *
  • Preserve clear separation between algorithm descriptors, runtime + * contexts, and higher-level composition utilities.
  • + *
+ * + *

Components

+ *
    + *
  • GenericJcaAgreementContext: an + * {@link zeroecho.core.context.AgreementContext} backed by + * {@link javax.crypto.KeyAgreement}; constructed with a local private key and + * configured using a JCA algorithm name and optional provider.
  • + *
  • KemMessageAgreementAdapter: a + * {@link zeroecho.core.context.MessageAgreementContext} built on a + * {@link zeroecho.core.context.KemContext}, modeling initiator/responder roles + * and exchanging a single peer message (ciphertext) when required.
  • + *
+ * + *

Lifecycle and usage notes

+ *
    + *
  • Agreement contexts should be created with the correct local key and + * configured before deriving secrets; KDF application remains the caller's + * responsibility.
  • + *
  • KEM-based adapters encapsulate or decapsulate depending on role and + * memoize results for repeated reads within a single exchange.
  • + *
  • Instances are not thread-safe; synchronize externally if they are + * shared.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.common.agreement; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java new file mode 100644 index 0000000..b7aa650 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEdDSAKeyGenBuilder.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * 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.core.alg.common.eddsa; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; + +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

Abstract EdDSA Key-Pair Builder

+ * + * Base class for key generation builders targeting Edwards-curve Digital + * Signature Algorithm (EdDSA) variants such as Ed25519 and Ed448. + * + *

+ * This class integrates with the JCA {@link KeyPairGenerator} by exposing the + * correct algorithm name (e.g., {@code "Ed25519"} or {@code "Ed448"}). Concrete + * subclasses provide this algorithm identifier via {@link #jcaKeyPairAlg()}. + *

+ * + *

Responsibilities

+ *
    + *
  • Expose a template method {@link #jcaKeyPairAlg()} to return the canonical + * JCA algorithm identifier.
  • + *
  • Generate key pairs using JCA without requiring extra parameters.
  • + *
  • Intentionally reject public and private key imports, as those are + * delegated to the corresponding {@code *KeySpec} builders.
  • + *
+ * + *

Thread-safety

Instances are stateless. Each call to + * {@link #generateKeyPair(AlgorithmKeySpec)} acquires a new + * {@link KeyPairGenerator}, so builders are safe for concurrent use. + * + * @param the algorithm-specific {@link AlgorithmKeySpec} subtype + * + * @since 1.0 + */ +public abstract class AbstractEdDSAKeyGenBuilder implements AsymmetricKeyBuilder { + /** + * Returns the JCA algorithm name understood by {@link KeyPairGenerator}. + * + *

+ * Implementations must return the canonical algorithm string supported by the + * JDK, e.g.: + *

+ *
    + *
  • {@code "Ed25519"}
  • + *
  • {@code "Ed448"}
  • + *
+ * + * @return the JCA algorithm identifier string + */ + protected abstract String jcaKeyPairAlg(); // e.g., "Ed25519", "Ed448" + + /** + * Generates a new EdDSA key pair using JCA defaults. + * + *

+ * The provided {@code spec} is not inspected in this base implementation, but + * it satisfies the {@link AsymmetricKeyBuilder} contract. Subclasses may extend + * this behavior to interpret spec parameters. + *

+ * + * @param spec algorithm-specific key specification (currently unused) + * @return a fresh {@link KeyPair} for the chosen EdDSA variant + * @throws GeneralSecurityException if the JCA provider does not support the + * specified EdDSA algorithm + */ + @Override + public KeyPair generateKeyPair(S spec) throws GeneralSecurityException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(jcaKeyPairAlg()); + return kpg.generateKeyPair(); + } + + /** + * Always throws, as this builder does not support public key import. + * + *

+ * Importing encoded EdDSA public keys must be done through the corresponding + * {@code *PublicKeySpec} builder class. + *

+ * + * @param spec algorithm-specific key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public PublicKey importPublic(S spec) { + throw new UnsupportedOperationException("Use the corresponding PublicKeySpec to import a public key."); + } + + /** + * Always throws, as this builder does not support private key import. + * + *

+ * Importing encoded EdDSA private keys must be done through the corresponding + * {@code *PrivateKeySpec} builder class. + *

+ * + * @param spec algorithm-specific key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public PrivateKey importPrivate(S spec) { + throw new UnsupportedOperationException("Use the corresponding PrivateKeySpec to import a private key."); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java new file mode 100644 index 0000000..5818be2 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPrivateKeyBuilder.java @@ -0,0 +1,150 @@ +/******************************************************************************* + * 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.core.alg.common.eddsa; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; + +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

Abstract EdDSA Encoded Private Key Builder

+ * + * Base class for reconstructing EdDSA private keys (e.g., Ed25519, Ed448) from + * PKCS#8-encoded specifications. + * + *

+ * Unlike {@link AbstractEdDSAKeyGenBuilder}, which is responsible for + * generating fresh key pairs, this class focuses on importing existing + * private keys from their encoded representation. Public key import and key + * pair generation are deliberately unsupported here. + *

+ * + *

Responsibilities

+ *
    + *
  • Define {@link #jcaKeyFactoryAlg()} to specify the canonical JCA algorithm + * name (e.g., {@code "Ed25519"} or {@code "Ed448"}).
  • + *
  • Define {@link #encodedPkcs8(AlgorithmKeySpec)} to extract the raw + * PKCS#8-encoded private key material from the given spec.
  • + *
  • Provide an {@link #importPrivate(AlgorithmKeySpec)} implementation that + * rebuilds a {@link PrivateKey} using {@link KeyFactory} and the PKCS#8-encoded + * material.
  • + *
+ * + *

Thread-safety

Stateless and safe for concurrent use. Each import + * operation creates a new {@link KeyFactory} instance internally. + * + * @param the algorithm-specific key specification carrying PKCS#8 data + * + * @since 1.0 + */ +public abstract class AbstractEncodedPrivateKeyBuilder implements AsymmetricKeyBuilder { + /** + * Returns the canonical JCA algorithm identifier used by + * {@link KeyFactory#getInstance(String)}. + * + *

+ * Must be one of the algorithm names recognized by the JDK (e.g., + * {@code "Ed25519"}, {@code "Ed448"}). + *

+ * + * @return the JCA algorithm identifier string + */ + protected abstract String jcaKeyFactoryAlg(); // e.g., "Ed25519", "Ed448" + + /** + * Extracts the raw PKCS#8-encoded private key bytes from the given spec. + * + *

+ * Subclasses must implement this to pull the encoded material from their + * {@link AlgorithmKeySpec} representation. + *

+ * + * @param spec algorithm-specific key specification + * @return PKCS#8-encoded private key bytes + */ + protected abstract byte[] encodedPkcs8(S spec); + + /** + * Unsupported in this builder, since generation is handled by the + * {@link AbstractEdDSAKeyGenBuilder}. + * + * @param spec algorithm-specific key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public KeyPair generateKeyPair(S spec) { + throw new UnsupportedOperationException("Generation not supported by this spec."); + } + + /** + * Unsupported in this builder, since public key import is delegated to the + * matching {@code *PublicKeySpec} builder. + * + * @param spec algorithm-specific key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public PublicKey importPublic(S spec) { + throw new UnsupportedOperationException("Use the corresponding PublicKeySpec."); + } + + /** + * Imports an EdDSA private key from its PKCS#8-encoded form. + * + *

+ * This method uses {@link KeyFactory} initialized with the algorithm returned + * by {@link #jcaKeyFactoryAlg()} to parse the bytes provided by + * {@link #encodedPkcs8(AlgorithmKeySpec)}. + *

+ * + * @param spec algorithm-specific key specification containing PKCS#8 bytes + * @return a reconstructed {@link PrivateKey} instance + * @throws GeneralSecurityException if the key material is invalid or the JCA + * provider does not support the algorithm + */ + @Override + public PrivateKey importPrivate(S spec) throws GeneralSecurityException { + KeyFactory kf = KeyFactory.getInstance(jcaKeyFactoryAlg()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(encodedPkcs8(spec))); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPublicKeyBuilder.java new file mode 100644 index 0000000..c3c4e90 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/eddsa/AbstractEncodedPublicKeyBuilder.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * 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.core.alg.common.eddsa; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; + +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

Abstract EdDSA Encoded Public Key Builder

+ * + * Base class for reconstructing EdDSA public keys (e.g., Ed25519, Ed448) from + * X.509-encoded specifications. + * + *

+ * Unlike {@link AbstractEdDSAKeyGenBuilder}, which generates new key pairs, and + * {@link AbstractEncodedPrivateKeyBuilder}, which restores private keys, this + * class focuses exclusively on importing existing public keys from their + * encoded form. Key pair generation and private key import are intentionally + * unsupported here. + *

+ * + *

Responsibilities

+ *
    + *
  • Define {@link #jcaKeyFactoryAlg()} to return the canonical JCA algorithm + * name (e.g., {@code "Ed25519"}, {@code "Ed448"}).
  • + *
  • Define {@link #encodedX509(AlgorithmKeySpec)} to extract the raw + * X.509-encoded public key bytes from the given spec.
  • + *
  • Provide an {@link #importPublic(AlgorithmKeySpec)} implementation that + * rebuilds a {@link PublicKey} using {@link KeyFactory} and the encoded X.509 + * material.
  • + *
+ * + *

Thread-safety

Stateless and safe for concurrent use. Each call to + * {@link #importPublic(AlgorithmKeySpec)} creates a new {@link KeyFactory}. + * + * @param the algorithm-specific key specification carrying X.509 data + * + * @since 1.0 + */ +public abstract class AbstractEncodedPublicKeyBuilder implements AsymmetricKeyBuilder { + + /** + * Returns the canonical JCA algorithm identifier used by + * {@link KeyFactory#getInstance(String)}. + * + *

+ * Must be one of the algorithm names recognized by the JDK (e.g., + * {@code "Ed25519"}, {@code "Ed448"}). + *

+ * + * @return the JCA algorithm identifier string + */ + protected abstract String jcaKeyFactoryAlg(); // e.g., "Ed25519", "Ed448" + + /** + * Extracts the raw X.509-encoded public key bytes from the given spec. + * + *

+ * Subclasses must implement this to pull the encoded material from their + * {@link AlgorithmKeySpec} representation. + *

+ * + * @param spec algorithm-specific key specification + * @return X.509-encoded public key bytes + */ + protected abstract byte[] encodedX509(S spec); + + /** + * Unsupported in this builder, since key pair generation is handled by + * {@link AbstractEdDSAKeyGenBuilder}. + * + * @param spec algorithm-specific key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public KeyPair generateKeyPair(S spec) { + throw new UnsupportedOperationException("Generation not supported by this spec."); + } + + /** + * Imports an EdDSA public key from its X.509-encoded form. + * + *

+ * This method uses {@link KeyFactory} initialized with the algorithm returned + * by {@link #jcaKeyFactoryAlg()} to parse the bytes provided by + * {@link #encodedX509(AlgorithmKeySpec)}. + *

+ * + * @param spec algorithm-specific key specification containing X.509 bytes + * @return a reconstructed {@link PublicKey} instance + * @throws GeneralSecurityException if the key material is invalid or the JCA + * provider does not support the algorithm + */ + @Override + public PublicKey importPublic(S spec) throws GeneralSecurityException { + KeyFactory kf = KeyFactory.getInstance(jcaKeyFactoryAlg()); + return kf.generatePublic(new X509EncodedKeySpec(encodedX509(spec))); + } + + /** + * Unsupported in this builder, since private key import is delegated to the + * matching {@code *PrivateKeySpec} builder. + * + * @param spec algorithm-specific key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public PrivateKey importPrivate(S spec) { + throw new UnsupportedOperationException("Use the corresponding PrivateKeySpec."); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/common/eddsa/CommonEdDSASignatureContext.java b/lib/src/main/java/zeroecho/core/alg/common/eddsa/CommonEdDSASignatureContext.java new file mode 100644 index 0000000..d8ec3d2 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/eddsa/CommonEdDSASignatureContext.java @@ -0,0 +1,246 @@ +/******************************************************************************* + * 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.core.alg.common.eddsa; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.alg.common.sig.GenericJcaSignatureContext; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.tag.TagEngine; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; + +/** + * Base class for EdDSA signature contexts that adapts a JCA {@code Signature} + * for streaming sign and verify. + * + *

+ * This class is a thin adapter over {@link GenericJcaSignatureContext}: it + * wires the JCA signature name (for example, {@code "Ed25519"} or + * {@code "Ed448"}) and a fixed tag length, then delegates all + * {@link SignatureContext} operations to the internal delegate. Concrete + * subclasses such as {@code Ed25519SignatureContext} and + * {@code Ed448SignatureContext} expose role-specific contexts for signing or + * verifying. + *

+ * + *

Responsibilities

+ *
    + *
  • Bind an EdDSA variant to a {@link CryptoAlgorithm} and a key + * ({@link PrivateKey} for signing, {@link PublicKey} for verifying).
  • + *
  • Delegate {@link SignatureContext} and {@link TagEngine} behavior to the + * {@link GenericJcaSignatureContext} instance.
  • + *
  • Enforce a fixed tag length appropriate for the algorithm (64 bytes for + * Ed25519, 114 bytes for Ed448).
  • + *
+ * + *

Thread-safety

+ *

+ * Instances are stateful and not guaranteed to be thread-safe. Use one context + * per signing or verification pipeline. + *

+ * + * @since 1.0 + */ +public class CommonEdDSASignatureContext implements SignatureContext { + private final GenericJcaSignatureContext delegate; + + /** + * Constructs a signing context for the given EdDSA algorithm. + * + *

+ * The created context operates in SIGN mode. The JCA engine is obtained using + * the supplied {@code jcaSignatureName}, and the produced signature length is + * fixed to {@code tagLength}. + *

+ * + * @param algorithm associated algorithm descriptor; must not be + * {@code null} + * @param privateKey private key used for signing; must not be + * {@code null} + * @param jcaSignatureName JCA signature name (for example, {@code "Ed25519"} or + * {@code "Ed448"}); must not be {@code null} + * @param tagLength fixed signature length in bytes (64 for Ed25519, 114 + * for Ed448) + * @throws GeneralSecurityException if the JCA provider cannot initialize the + * signature engine + * @throws NullPointerException if any argument is {@code null} + */ + protected CommonEdDSASignatureContext(final CryptoAlgorithm algorithm, final PrivateKey privateKey, + final String jcaSignatureName, final int tagLength) throws GeneralSecurityException { + this.delegate = new GenericJcaSignatureContext(algorithm, privateKey, + GenericJcaSignatureContext.jcaFactory(jcaSignatureName, null), + GenericJcaSignatureContext.SignLengthResolver.fixed(tagLength)); + } + + /** + * Constructs a verification context for the given EdDSA algorithm. + * + *

+ * The created context operates in VERIFY mode. The JCA engine is obtained using + * the supplied {@code jcaSignatureName}, and the expected signature length is + * fixed to {@code tagLength}. + *

+ * + * @param algorithm associated algorithm descriptor; must not be + * {@code null} + * @param publicKey public key used for verification; must not be + * {@code null} + * @param jcaSignatureName JCA signature name (for example, {@code "Ed25519"} or + * {@code "Ed448"}); must not be {@code null} + * @param tagLength fixed signature length in bytes (64 for Ed25519, 114 + * for Ed448) + * @throws GeneralSecurityException if the JCA provider cannot initialize the + * signature engine + * @throws NullPointerException if any argument is {@code null} + */ + protected CommonEdDSASignatureContext(final CryptoAlgorithm algorithm, final PublicKey publicKey, + final String jcaSignatureName, final int tagLength) throws GeneralSecurityException { + this.delegate = new GenericJcaSignatureContext(algorithm, publicKey, + GenericJcaSignatureContext.jcaFactory(jcaSignatureName, null), + GenericJcaSignatureContext.VerifyLengthResolver.fixed(tagLength)); + } + + /** + * Returns the algorithm associated with this context. + * + * @return the {@link CryptoAlgorithm} descriptor + */ + @Override + public CryptoAlgorithm algorithm() { + return delegate.algorithm(); + } + + /** + * Returns the signing or verification key bound to this context. + * + * @return the {@link java.security.Key} in use + */ + @Override + public java.security.Key key() { + return delegate.key(); + } + + /** + * Releases resources associated with this context. + * + *

+ * After calling {@code close()}, further use of this context is undefined. + *

+ */ + @Override + public void close() { + delegate.close(); + } + + /** + * Wraps an upstream {@link InputStream} so that all data read from it is + * processed by the underlying signature engine. + * + *

+ * In SIGN mode the wrapped stream emits the original data followed by a + * detached signature trailer at end-of-stream. In VERIFY mode the wrapped + * stream emits only the body and performs verification at end-of-stream against + * the expected tag. + *

+ * + * @param upstream input stream supplying data to be signed or verified; must + * not be {@code null} + * @return a wrapped stream that updates the signature engine on read + * @throws IOException if stream wrapping fails + */ + @Override + public InputStream wrap(InputStream upstream) throws IOException { + return delegate.wrap(upstream); + } + + /** + * Returns the fixed tag (signature) length in bytes for this algorithm. + * + * @return the signature length in bytes + */ + @Override + public int tagLength() { + return delegate.tagLength(); + } + + /** + * Sets the expected signature (tag) used in VERIFY mode. + * + *

+ * Passing {@code null} clears the expected tag. + *

+ * + * @param expected the signature bytes to verify against, or {@code null} to + * clear + */ + @Override + public void setExpectedTag(byte[] expected) { + delegate.setExpectedTag(expected); + } + + /** + * Sets the verification approach used to compare the computed and expected + * signatures. + * + * @param strategy verification predicate to apply in VERIFY mode; may be + * decorated to throw or flag + */ + @Override + public void setVerificationApproach(VerificationBiPredicate strategy) { + delegate.setVerificationApproach(strategy); + } + + /** + * Returns the core verification predicate used by this context. + * + *

+ * The returned predicate typically delegates to + * {@link Signature#verify(byte[])}. + *

+ * + * @return the base verification predicate + */ + @Override + public VerificationBiPredicate getVerificationCore() { + return delegate.getVerificationCore(); + } + +} diff --git a/lib/src/main/java/zeroecho/core/alg/common/eddsa/package-info.java b/lib/src/main/java/zeroecho/core/alg/common/eddsa/package-info.java new file mode 100644 index 0000000..6c471e8 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/eddsa/package-info.java @@ -0,0 +1,86 @@ +/** + * 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. + */ +/** + * EdDSA (Edwards-curve Digital Signature Algorithm) key builders and contexts. + * + *

+ * This package provides common infrastructure for Ed25519 and Ed448 algorithm + * support. It focuses on key-pair generation, importing encoded keys, and + * wiring EdDSA variants into the generic signature context API. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Provide abstract builders for EdDSA key-pair generation and encoded key + * import.
  • + *
  • Offer an abstract signature context that binds an EdDSA variant to the + * generic JCA-based signature context, with fixed tag lengths.
  • + *
  • Keep provider-specific concerns encapsulated in small, composable + * building blocks.
  • + *
+ * + *

Components

+ *
    + *
  • Key generation: + * {@link zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder} integrates + * with {@link java.security.KeyPairGenerator} to produce new Ed25519/Ed448 key + * pairs.
  • + *
  • Private key import: + * {@link zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder} + * reconstructs private keys from PKCS#8 encodings via + * {@link java.security.KeyFactory}.
  • + *
  • Public key import: + * {@link zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder} + * reconstructs public keys from X.509 encodings via + * {@link java.security.KeyFactory}.
  • + *
  • Signature contexts: + * {@link zeroecho.core.alg.common.eddsa.CommonEdDSASignatureContext} + * delegates all operations to a generic JCA-backed signature adapter, enforcing + * a fixed tag length for the selected EdDSA variant.
  • + *
+ * + *

Design notes

+ *
    + *
  • Builders are stateless and safe for concurrent use; each import or + * generation creates a fresh JCA engine internally.
  • + *
  • Import methods intentionally throw for unsupported directions (e.g., + * public import in the private builder) to keep responsibilities clear.
  • + *
  • Signature contexts are not thread-safe; they are expected to be used for + * a single signing or verification stream at a time.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.common.eddsa; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/common/package-info.java b/lib/src/main/java/zeroecho/core/alg/common/package-info.java new file mode 100644 index 0000000..93e6d90 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/package-info.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Common algorithm infrastructure shared across multiple cryptographic + * families. + * + *

+ * This package contains reusable building blocks and adapters that are not tied + * to a single primitive family but are needed by several of them. It provides + * generic JCA wrappers, abstract base classes for key builders, adapters to map + * KEM into agreement workflows, and streaming helpers for signature engines. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Expose abstract base classes for asymmetric key generation and encoded + * key import so that concrete algorithms can implement only variant-specific + * details.
  • + *
  • Provide generic JCA adapters for key agreement and signature processing + * that integrate with the core streaming context model.
  • + *
  • Offer thin adapters to reinterpret existing primitives (for example, KEM + * as a two-message agreement) for higher-level composition layers.
  • + *
+ * + *

Subpackages

+ *
    + *
  • {@link zeroecho.core.alg.common.agreement} – generic JCA-based agreement + * contexts and KEM-to-agreement adapters.
  • + *
  • {@link zeroecho.core.alg.common.eddsa} – EdDSA infrastructure: abstract + * key builders, encoded key importers, and signature contexts for Ed25519 and + * Ed448.
  • + *
  • {@link zeroecho.core.alg.common.sig} – streaming JCA-backed signature + * contexts and internal stream helpers for sign/verify pipelines.
  • + *
+ * + *

Design notes

+ *
    + *
  • Abstract builders separate generation from import paths, and unsupported + * operations fail fast with clear exceptions.
  • + *
  • Contexts adapt JCA primitives into the core’s streaming model, handling + * resource lifecycles and fixed tag lengths where applicable.
  • + *
  • All components are designed to be composable and reusable across multiple + * algorithms, reducing duplication and ensuring consistent behavior.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.common; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/common/sig/GenericJcaSignatureContext.java b/lib/src/main/java/zeroecho/core/alg/common/sig/GenericJcaSignatureContext.java new file mode 100644 index 0000000..0169378 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/sig/GenericJcaSignatureContext.java @@ -0,0 +1,531 @@ +/******************************************************************************* + * 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.core.alg.common.sig; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.util.Arrays; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.tag.SignatureVerificationStrategy; +import zeroecho.core.tag.TagEngine; +import zeroecho.core.tag.ThrowingBiPredicate; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; + +/** + * Adapts a JCA {@link java.security.Signature} to the streaming + * {@link SignatureContext}/{@link TagEngine} model. + * + *

+ * {@code GenericJcaSignatureContext} binds a key and a + * {@link java.security.Signature} engine to a pull-based pipeline: the wrapped + * stream forwards bytes unchanged while updating the engine. In SIGN mode a + * fixed-length trailer containing the signature is appended at end-of-stream; + * in VERIFY mode the computed signature is compared at end-of-stream to an + * expected tag supplied by the caller. The trailer length is determined up + * front so pipelines can append or strip trailers without buffering the whole + * stream. + *

+ * + *

Modes and usage

+ *
    + *
  • SIGN: consume body bytes; on EOF call + * {@link java.security.Signature#sign()} and append a trailer of + * {@link #tagLength()} bytes.
  • + *
  • VERIFY: consume body bytes; on EOF compare against the expected + * tag set via {@link #setExpectedTag(byte[])} using the verification approach + * configured with + * {@link #setVerificationApproach(ThrowingBiPredicate.VerificationBiPredicate)}.
  • + *
+ * + *

Length resolvers

+ *

+ * A fixed tag length is required in both modes. For SIGN, a + * {@link SignLengthResolver} supplies the produced length (either a constant or + * by probing a provider). For VERIFY, a {@link VerifyLengthResolver} supplies + * the expected length derived from key parameters or a known constant. + *

+ * + *

Examples

+ *

Sign with RSA-PSS

+ * {@code
+ * GenericJcaSignatureContext ctx = new GenericJcaSignatureContext(
+ *     algorithm,
+ *     privateKey,
+ *     GenericJcaSignatureContext.jcaFactory("RSASSA-PSS", null),
+ *     GenericJcaSignatureContext.SignLengthResolver.probeWith("RSASSA-PSS", null));
+ *
+ * try (InputStream in = ctx.wrap(sourceStream)) {
+ *     in.transferTo(out); // body, then trailer of ctx.tagLength() bytes
+ * }
+ * }
+ * 
+ * + *

Verify detached RSA signature

+ * {@code
+ * GenericJcaSignatureContext vctx = new GenericJcaSignatureContext(
+ *     algorithm,
+ *     publicKey,
+ *     GenericJcaSignatureContext.jcaFactory("SHA256withRSA", null),
+ *     GenericJcaSignatureContext.VerifyLengthResolver.fixed(256)); // 2048-bit modulus
+ *
+ * vctx.setVerificationApproach(vctx.getVerificationCore().getThrowOnMismatch());
+ * vctx.setExpectedTag(signatureBytes);
+ *
+ * try (InputStream verified = vctx.wrap(bodyWithoutTrailer)) {
+ *     verified.transferTo(java.io.OutputStream.nullOutputStream()); // throws on mismatch at EOF
+ * }
+ * }
+ * 
+ * + *

Thread-safety

+ *

+ * Instances are stateful, single-use, and not thread-safe. Call + * {@link #wrap(InputStream)} at most once per instance. + *

+ */ +public final class GenericJcaSignatureContext implements SignatureContext { + private static final Logger LOG = Logger.getLogger(GenericJcaSignatureContext.class.getName()); + + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean signMode; + private final Signature engine; + /** + * Declared tag length used by {@link TagEngine#tagLength()} in both modes. + * + *

+ * Determined during construction by the provided resolver and constant for this + * context's lifetime. + *

+ */ + private final int declaredTagLen; + private byte[] expectedTag; + private VerificationBiPredicate verificationStrategy; + + // lifecycle + private boolean wrapped; // = false; + private Stream activeStream; + private boolean autoCloseActiveStream; + + /** + * Factory of initialized {@link java.security.Signature} engines. + * + *

+ * Implementations must return a ready-to-use {@code Signature} configured for + * signing or verifying with the given key. + *

+ */ + @FunctionalInterface + public interface EngineFactory { + /** + * Creates and initializes a {@link java.security.Signature} for the given key + * and mode. + * + * @param key key to initialize the engine with; + * {@link java.security.PrivateKey} for sign mode, + * {@link java.security.PublicKey} for verify mode + * @param signMode {@code true} for signing, {@code false} for verifying + * @return initialized {@code Signature} ready for incremental + * {@code update(...)} calls + * @throws GeneralSecurityException if engine creation or initialization fails + */ + Signature create(Key key, boolean signMode) throws GeneralSecurityException; + } + + /** + * Strategy for determining the signature trailer length in SIGN mode. + * + *

+ * The resolver is evaluated before the signing engine is created and may probe + * a provider when the length is not fixed by specification. + *

+ */ + @FunctionalInterface + public interface SignLengthResolver { + /** + * Resolves the signature length for a given private key in SIGN mode. + * + * @param privateKey private key used for signing + * @return exact number of bytes {@link java.security.Signature#sign()} will + * produce + * @throws GeneralSecurityException if the length cannot be determined + */ + int resolve(PrivateKey privateKey) throws GeneralSecurityException; + + /** + * Returns a resolver that always reports a fixed length. + * + * @param len positive length in bytes + * @return resolver returning {@code len} + * @throws IllegalArgumentException if {@code len} <= 0 + */ + static SignLengthResolver fixed(int len) { + LOG.log(Level.FINE, "SignLengthResolver.len={0}", len); + + if (len <= 0) { + throw new IllegalArgumentException("fixed signature length must be > 0"); + } + return pk -> len; + } + + /** + * Returns a resolver that probes a JCA {@link java.security.Signature} by + * signing an empty message. + * + *

+ * Useful for algorithms/providers where the produced length is not trivially + * known from parameters. + *

+ * + * @param jcaAlg JCA signature name (for example, {@code "SHA256withRSA"}, + * {@code "Ed25519"}) + * @param providerName optional provider name; {@code null} selects the + * highest-priority provider + * @return resolver that initializes a {@code Signature} for signing and returns + * {@code sign().length} + */ + static SignLengthResolver probeWith(final String jcaAlg, final String providerName) { + return privateKey -> { + final Signature s = (providerName == null) ? Signature.getInstance(jcaAlg) + : Signature.getInstance(jcaAlg, providerName); + s.initSign(privateKey); + return s.sign().length; // empty-message probe + }; + } + } + + /** + * Strategy for determining the expected signature length in VERIFY mode. + * + *

+ * The resolver is evaluated during construction and should return the fixed tag + * length for verification. + *

+ */ + @FunctionalInterface + public interface VerifyLengthResolver { + /** + * Resolves the expected signature length for a given public key in VERIFY mode. + * + * @param publicKey public key used for verification + * @return exact number of bytes expected in the verification tag + * @throws GeneralSecurityException if the length cannot be determined + */ + int resolve(PublicKey publicKey) throws GeneralSecurityException; + + /** + * Returns a resolver that always reports a fixed length. + * + * @param len positive length in bytes + * @return resolver returning {@code len} + * @throws IllegalArgumentException if {@code len} <= 0 + */ + static VerifyLengthResolver fixed(int len) { + LOG.log(Level.FINE, "VerifyLengthResolver.len={0}", len); + + if (len <= 0) { + throw new IllegalArgumentException("fixed signature length must be > 0"); + } + return pk -> len; + } + } + + /** + * Convenience factory that produces a JCA {@link java.security.Signature} + * initialized for sign or verify. + * + *

+ * The returned factory performs {@link Signature#getInstance(String)} + * (optionally with a provider) and then calls + * {@link Signature#initSign(PrivateKey)} or + * {@link Signature#initVerify(PublicKey)}. + *

+ * + *
+     * {@code
+     * EngineFactory f = GenericJcaSignatureContext.jcaFactory("SHA256withRSA", null);
+     * Signature signer = f.create(privateKey, true);
+     * }
+     * 
+ * + * @param jcaAlg JCA signature algorithm name; must not be {@code null} + * @param provider optional provider name; {@code null} selects the + * highest-priority provider + * @return factory creating initialized {@code Signature} engines for the + * specified algorithm/provider + */ + public static EngineFactory jcaFactory(final String jcaAlg, final String provider) { + return (key, signMode) -> { + final Signature s = (provider == null) ? Signature.getInstance(jcaAlg) + : Signature.getInstance(jcaAlg, provider); + if (signMode) { + s.initSign((PrivateKey) key); + } else { + s.initVerify((PublicKey) key); + } + return s; + }; + } + + /** + * Constructs a SIGN-mode context. + * + *

+ * First resolves the produced signature length via {@code lengthResolver}, then + * creates and initializes the signing engine via {@code engineFactory}. + *

+ * + * @param algorithm logical algorithm descriptor associated with this + * context; must not be {@code null} + * @param privateKey private key used for signing; must not be {@code null} + * @param engineFactory factory creating an initialized + * {@link java.security.Signature} in sign mode; must not + * be {@code null} + * @param lengthResolver strategy to resolve the produced signature length up + * front; must not be {@code null} + * @throws GeneralSecurityException if length resolution or engine + * initialization fails + * @throws NullPointerException if any required argument is {@code null} + */ + public GenericJcaSignatureContext(final CryptoAlgorithm algorithm, final PrivateKey privateKey, + final EngineFactory engineFactory, final SignLengthResolver lengthResolver) + throws GeneralSecurityException { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm"); + this.key = Objects.requireNonNull(privateKey, "privateKey"); + Objects.requireNonNull(engineFactory, "engineFactory"); + Objects.requireNonNull(lengthResolver, "lengthResolver"); + // compute trailer length first (with an independent probe if needed) + this.declaredTagLen = lengthResolver.resolve(privateKey); + this.engine = engineFactory.create(privateKey, true); + this.signMode = true; + } + + /** + * Constructs a VERIFY-mode context. + * + *

+ * First resolves the expected tag length via {@code verifyLengthResolver}, then + * creates and initializes the verifying engine via {@code engineFactory}. + *

+ * + * @param algorithm logical algorithm descriptor associated with this + * context; must not be {@code null} + * @param publicKey public key used for verification; must not be + * {@code null} + * @param engineFactory factory creating an initialized + * {@link java.security.Signature} in verify mode; + * must not be {@code null} + * @param verifyLengthResolver strategy to resolve the expected signature length + * up front; must not be {@code null} + * @throws GeneralSecurityException if length resolution or engine + * initialization fails + * @throws NullPointerException if any required argument is {@code null} + */ + public GenericJcaSignatureContext(final CryptoAlgorithm algorithm, final PublicKey publicKey, + final EngineFactory engineFactory, final VerifyLengthResolver verifyLengthResolver) + throws GeneralSecurityException { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm"); + this.key = Objects.requireNonNull(publicKey, "publicKey"); + Objects.requireNonNull(engineFactory, "engineFactory"); + Objects.requireNonNull(verifyLengthResolver, "verifyLengthResolver"); + this.engine = engineFactory.create(publicKey, false); + this.signMode = false; + this.declaredTagLen = verifyLengthResolver.resolve(publicKey); + } + + /** + * Returns the logical algorithm associated with this context. + * + * @return the algorithm descriptor + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + * @return the signing key in SIGN mode or the verification key in VERIFY mode + */ + @Override + public Key key() { + return key; + } + + /** + * Closes the active wrapped stream, if any, suppressing + * {@link java.io.IOException}. + * + *

+ * Keeping {@code close()} non-throwing simplifies pipeline cleanup. + *

+ */ + @Override + public void close() { + LOG.log(Level.FINE, "close"); + + if (autoCloseActiveStream) { + try { + if (activeStream != null) { + activeStream.close(); + } + } catch (IOException ignore) { + LOG.log(Level.INFO, "exception ignored on close", ignore); + } + } + } + + /** + * Wraps the supplied upstream stream so the underlying + * {@link java.security.Signature} is updated as data flows. + * + *

+ * This method may be called only once per instance. In SIGN mode the returned + * stream appends a trailer of {@link #tagLength()} bytes when the upstream + * finishes. In VERIFY mode the returned stream performs verification at EOF + * against the expected tag configured via {@link #setExpectedTag(byte[])} and + * surfaces the outcome using the verification approach set via + * {@link #setVerificationApproach(ThrowingBiPredicate.VerificationBiPredicate)}. + *

+ * + * @param upstream source stream whose bytes will be fed into the signature; + * must not be {@code null} + * @return stream that must be fully consumed to trigger signing or verification + * @throws NullPointerException if {@code upstream} is {@code null} + * @throws IllegalStateException if this context has already wrapped a stream + * @throws IOException if signing or verification fails during + * processing or finalization + */ + @Override + public InputStream wrap(final InputStream upstream) throws IOException { + Objects.requireNonNull(upstream, "upstream"); + if (wrapped) { + throw new IllegalStateException( + "This SignatureContext instance was already used; create a new one per stream."); + } + wrapped = true; + + LOG.log(Level.INFO, "wrap for signing, tagLength={0}", declaredTagLen); + + Stream s = new Stream(engine, signMode, upstream, tagLength(), expectedTag, verifier()); + this.activeStream = s; + return s; + } + + /** + * Returns the declared tag length in bytes. + * + *

+ * Computed during construction by the configured resolver and constant for the + * lifetime of this context. + *

+ * + * @return fixed signature trailer length + */ + @Override + public int tagLength() { + // Always advertise a concrete length so callers (like TagTrailer) can strip + // trailers. + return declaredTagLen; + } + + /** + * Sets the expected verification tag to be checked when the wrapped stream + * finishes. + * + *

+ * Applicable only in VERIFY mode. Passing {@code null} clears the tag. The + * array is defensively copied. If a wrapped stream is already active, its + * expected tag is updated as well. + *

+ * + * @param expected expected signature bytes or {@code null} to clear + * @throws UnsupportedOperationException if called in SIGN mode + */ + @Override + public void setExpectedTag(final byte[] expected) { + if (signMode) { + throw new UnsupportedOperationException("setExpectedTag is only applicable in VERIFY mode"); + } + this.expectedTag = (expected == null) ? null : Arrays.copyOf(expected, expected.length); + + if (activeStream != null) { + activeStream.setExpectedTag(expected); + } + } + + /** + * Sets the verification approach used in VERIFY mode to compare the expected + * and computed signatures. + * + * @param strategy verification predicate; may be decorated to throw or to flag + * into a context; {@code null} keeps the default + */ + @Override + public void setVerificationApproach(VerificationBiPredicate strategy) { + verificationStrategy = strategy; + } + + /** + * Returns the core verification predicate for signature comparison. + * + * @return predicate that delegates to {@link Signature#verify(byte[])} + */ + @Override + public VerificationBiPredicate getVerificationCore() { + return new SignatureVerificationStrategy(); + } + + /** + * Selects the effective verification predicate: the user-supplied strategy if + * present, otherwise the default core with throw-on-mismatch decoration. + * + * @return effective verification predicate + */ + private VerificationBiPredicate verifier() { + return verificationStrategy == null ? getVerificationCore().getThrowOnMismatch() : verificationStrategy; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/common/sig/Stream.java b/lib/src/main/java/zeroecho/core/alg/common/sig/Stream.java new file mode 100644 index 0000000..de4ab8b --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/sig/Stream.java @@ -0,0 +1,225 @@ +/******************************************************************************* + * 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.core.alg.common.sig; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.Signature; +import java.security.SignatureException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.core.err.VerificationException; +import zeroecho.core.io.AbstractPassthroughInputStream; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; +import zeroecho.core.util.Strings; + +/** + * Passthrough stream that feeds a {@link java.security.Signature} for streaming + * sign or verify. + * + *

+ * All bytes read from the wrapped upstream are forwarded unchanged to the + * caller and are also passed to the signature engine via + * {@link Signature#update(byte[], int, int)}. At EOF the behavior depends on + * mode: + *

+ *
    + *
  • Sign mode: compute the signature once via {@link Signature#sign()} + * and append it as a trailer by returning it from + * {@link #produceTrailer(byte[])}.
  • + *
  • Verify mode: compute and compare the signature at completion using + * the provided {@link VerificationBiPredicate}, surfacing the result according + * to that strategy.
  • + *
+ * + *

Lifecycle

+ *
    + *
  • {@link #update(byte[], int, int)} - feed each chunk into the engine.
  • + *
  • {@link #produceTrailer(byte[])} - sign mode only: emit the computed + * signature once; verify mode: return 0 (no trailer).
  • + *
  • {@link #onCompleted()} - verify mode only: invoke the verification + * strategy and translate any {@link VerificationException} to + * {@link java.io.IOException}.
  • + *
+ * + *

Thread-safety

+ *

+ * Not thread-safe. Bound to a single {@link java.security.Signature} engine and + * upstream stream. + *

+ */ +final class Stream extends AbstractPassthroughInputStream { + private static final Logger LOG = Logger.getLogger(Stream.class.getName()); + + /** Cached trailer in sign mode: computed once from {@link Signature#sign()}. */ + private byte[] signature; + /** Underlying JCA signature engine used for streaming updates. */ + private final Signature engine; + /** True if this stream signs, false if it verifies. */ + private final boolean signMode; + /** Expected signature to verify against (verify mode only). */ + private byte[] expectedTag; + /** Verification strategy controlling how results are surfaced. */ + private final VerificationBiPredicate strategy; + + /** + * Creates a streaming signature passthrough. + * + * @param engine initialized {@link Signature} engine; must not be + * {@code null} + * @param signMode {@code true} for signing, {@code false} for verifying + * @param upstream upstream input to wrap; must not be {@code null} + * @param bodyBufSize body buffer size for passthrough + * @param expectedTag expected signature in verify mode; may be {@code null} to + * disable verification + * @param strategy verification predicate used in verify mode; ignored in + * sign mode; must not be {@code null} in verify mode + */ + /* package */ Stream(final Signature engine, final boolean signMode, final InputStream upstream, + final int bodyBufSize, final byte[] expectedTag, final VerificationBiPredicate strategy) { + super(upstream, bodyBufSize); + this.engine = engine; + this.signMode = signMode; + this.expectedTag = expectedTag; + this.strategy = strategy; + } + + /** + * Feeds a chunk of bytes into the underlying signature engine. + * + * @param buf input buffer + * @param off start offset within {@code buf} + * @param len number of bytes to process + * @throws IOException if {@link Signature#update(byte[], int, int)} fails + */ + @Override + protected void update(final byte[] buf, final int off, final int len) throws IOException { + try { + LOG.log(Level.FINEST, "update with {0} bytes block", len); + + engine.update(buf, off, len); + } catch (SignatureException e) { + throw new IOException("Signature.update failed", e); + } + } + + /** + * Emits the signature trailer exactly once in sign mode; emits nothing in + * verify mode. + * + *

+ * In sign mode this computes the signature if needed, copies it to {@code buf}, + * and returns its length. If the signature does not fit, an {@link IOException} + * is thrown. In verify mode the method returns {@code 0}. + *

+ * + * @param buf destination buffer + * @return number of bytes written, or {@code 0} if no trailer is emitted + * @throws IOException if signature computation fails or the trailer does not + * fit in {@code buf} + */ + @Override + protected int produceTrailer(byte[] buf) throws IOException { + LOG.log(Level.FINE, "trailer (length={0}) production started", buf.length); + + if (!signMode) { + LOG.log(Level.FINE, "signature will not be appended to the stream: not in the signing mode"); + return 0; // VERIFY mode never emits a trailer + } + + if (signature == null) { + try { + signature = engine.sign(); + } catch (GeneralSecurityException e) { + throw new IOException("Signature finalize failed", e); + } + } + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "signature produced: length={0} signature={1}", + new Object[] { signature.length, Strings.toShortString(signature) }); + } + + if (signature.length == 0) { + return 0; + } + if (signature.length > buf.length) { + throw new IOException( + "Trailer does not fit into buffer have: " + buf.length + " but need: " + signature.length); + } + System.arraycopy(signature, 0, buf, 0, signature.length); + return signature.length; + } + + /** + * Finalizes verification on stream completion (verify mode only). + * + *

+ * Compares the computed signature against {@code expectedTag} using + * {@link #strategy}. Any {@link VerificationException} raised by the strategy + * is translated to {@link IOException}. In sign mode this method does nothing. + *

+ * + * @throws IOException if the verification strategy signals failure + */ + @Override + protected void onCompleted() throws IOException { + if (signMode) { + LOG.log(Level.FINE, "Signature verification is not executed during signing"); + return; // nothing to do for signing + } + try { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "verification {0}", Strings.toShortString(expectedTag)); + } + strategy.verify(engine, expectedTag); + } catch (VerificationException e) { + throw new IOException(e); + } + } + + /** + * Replaces the expected verification tag. + * + * @param expectedTag new expected tag; may be {@code null} to clear + */ + /* default */ void setExpectedTag(byte[] expectedTag) { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "resetting expectedTag to {0}", Strings.toShortString(expectedTag)); + } + this.expectedTag = expectedTag; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/common/sig/package-info.java b/lib/src/main/java/zeroecho/core/alg/common/sig/package-info.java new file mode 100644 index 0000000..8d07fb7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/common/sig/package-info.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Streaming signature contexts and helpers that adapt JCA + * {@link java.security.Signature} to a pull-based pipeline. + * + *

+ * This package provides a generic signature context that wraps an + * {@link java.io.InputStream}, updates a JCA engine as bytes flow, and either + * appends a fixed-length trailer (sign) or verifies against a caller-supplied + * tag (verify). A small internal passthrough stream performs byte forwarding + * and end-of-stream finalization. + *

+ * + *

Design goals

+ *
    + *
  • Single-use pipeline integration: create a context for SIGN or + * VERIFY and call {@code wrap(InputStream)} once.
  • + *
  • Known tag length up front: produced/expected signature length is + * resolved at construction so downstream components can append or strip + * trailers without buffering the whole stream.
  • + *
  • Provider encapsulation: JCA provider details are hidden behind + * factories that create initialized engines.
  • + *
+ * + *

Components

+ *
    + *
  • GenericJcaSignatureContext - streaming context that uses a + * configured {@link java.security.Signature}, resolves a fixed tag length (via + * resolvers), and exposes a one-shot {@code wrap(InputStream)} API. + * Verification behavior is controlled by a pluggable comparison approach.
  • + *
  • Stream - internal passthrough input stream that feeds chunks to + * the signature engine, emits the trailer in SIGN mode, and performs final + * verification in VERIFY mode.
  • + *
+ * + *

Length resolution

+ *

+ * Tag length is determined by resolvers supplied at construction time: + *

+ *
    + *
  • SignLengthResolver - returns the produced length (fixed or by + * probing a provider with an empty-message sign).
  • + *
  • VerifyLengthResolver - returns the expected length (fixed or + * derived from key parameters).
  • + *
+ * + *

Verification approach

+ *

+ * Verification is delegated to a {@code VerificationBiPredicate} strategy. The + * default core delegates to {@link java.security.Signature#verify(byte[])}, and + * callers may decorate it to throw on mismatch or to record results externally. + *

+ * + *

Usage sketch

+ *

Sign

+ * {@code
+ * GenericJcaSignatureContext ctx = new GenericJcaSignatureContext(
+ *     algorithm,
+ *     privateKey,
+ *     GenericJcaSignatureContext.jcaFactory("SHA256withRSA", null),
+ *     GenericJcaSignatureContext.SignLengthResolver.probeWith("SHA256withRSA", null));
+ *
+ * try (InputStream in = ctx.wrap(upstream)) {
+ *     in.transferTo(out); // body, then signature trailer of ctx.tagLength() bytes
+ * }
+ * }
+ * 
+ * + *

Verify

+ * {@code
+ * GenericJcaSignatureContext vctx = new GenericJcaSignatureContext(
+ *     algorithm,
+ *     publicKey,
+ *     GenericJcaSignatureContext.jcaFactory("SHA256withRSA", null),
+ *     GenericJcaSignatureContext.VerifyLengthResolver.fixed(256));
+ *
+ * vctx.setVerificationApproach(vctx.getVerificationCore().getThrowOnMismatch());
+ * vctx.setExpectedTag(signatureBytes);
+ *
+ * try (InputStream verified = vctx.wrap(bodyWithoutTrailer)) {
+ *     verified.transferTo(java.io.OutputStream.nullOutputStream()); // throws on mismatch at EOF
+ * }
+ * }
+ * 
+ * + *

Thread-safety

+ *
    + *
  • Contexts and streams are stateful, not thread-safe, and intended for + * single use.
  • + *
  • Create a new context per wrapped stream.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.common.sig; diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java new file mode 100644 index 0000000..6df5525 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/dh/DhAlgorithm.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * 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.core.alg.dh; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext; +import zeroecho.core.context.AgreementContext; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * Diffie-Hellman algorithm registration for use in the pluggable cryptography + * catalog. + * + *

+ * This class registers a Diffie-Hellman (DH) algorithm under the identifier + * {@code "DH"} with sane defaults and JCA compatibility. It declares the + * algorithm’s capabilities, default parameters, and key builders so that higher + * layers can use DH for key agreement in a consistent and type-safe way. + *

+ * + *

Features

+ *
    + *
  • Algorithm identifier and display name: {@code "DH"}.
  • + *
  • Agreement capability producing {@link AgreementContext} instances backed + * by the JCA algorithm name {@code "DiffieHellman"}.
  • + *
  • Default parameter specification: {@link DhSpec#ffdhe2048()}.
  • + *
  • Asymmetric key builder for {@link DhSpec}-based key pair generation via + * {@link DhKeyGenBuilder}.
  • + *
  • Support for importing keys from {@link DhPublicKeySpec} and + * {@link DhPrivateKeySpec} using standard JCA key factories.
  • + *
+ * + *

Thread-safety

Instances of {@code DhAlgorithm} are immutable after + * construction. They are typically created once at application startup and + * safely reused across threads as shared registrations in a + * {@link zeroecho.core.CryptoCatalog}. + * + *

Usage example

{@code
+ * // Create and register the algorithm
+ * DhAlgorithm dh = new DhAlgorithm();
+ * CryptoCatalog catalog = CryptoCatalog.load();
+ *
+ * // Create a DH spec (or rely on the default ffdhe2048)
+ * DhSpec spec = DhSpec.ffdhe3072();
+ *
+ * // Generate a key pair using the registered builder
+ * KeyPair kp = CryptoAlgorithms.keyPair("DH", spec);
+ *
+ * // Obtain an agreement context for DH key agreement
+ * AgreementContext ctx = CryptoAlgorithms.create("DH", KeyUsage.AGREEMENT, kp.getPrivate(), spec);
+ *
+ * // Use the context with a peer public key to derive a shared secret
+ * ctx.setPeerPublic(peerPublicKey);
+ * byte[] secret = ctx.deriveSecret();
+ * }
+ * + * @see AlgorithmFamily + * @see KeyUsage + * @see AgreementContext + * @see DhSpec + * @see DhKeyGenBuilder + * @see AbstractCryptoAlgorithm + * @since 1.0 + */ +public final class DhAlgorithm extends AbstractCryptoAlgorithm { + /** + * Creates a Diffie-Hellman algorithm registration with standard defaults. + * + *

+ * This constructor: + *

+ *
    + *
  • Registers the algorithm under the identifier {@code "DH"} and display + * name {@code "DH"}.
  • + *
  • Declares an {@link AlgorithmFamily#AGREEMENT} capability for + * {@link KeyUsage#AGREEMENT}, producing {@link GenericJcaAgreementContext} + * instances bound to the JCA name {@code "DiffieHellman"}.
  • + *
  • Sets {@link DhSpec#ffdhe2048()} as the default parameter + * specification.
  • + *
  • Registers {@link DhKeyGenBuilder} as the asymmetric key builder for + * {@link DhSpec}.
  • + *
  • Registers asymmetric key builders for importing keys via + * {@link DhPublicKeySpec} and {@link DhPrivateKeySpec} using JCA + * {@link java.security.KeyFactory}.
  • + *
+ */ + public DhAlgorithm() { + super("DH", "DH"); + + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, AgreementContext.class, PrivateKey.class, + DhSpec.class, + (PrivateKey k, DhSpec s) -> new GenericJcaAgreementContext(this, k, "DiffieHellman", null), + DhSpec::ffdhe2048); + + registerAsymmetricKeyBuilder(DhSpec.class, new DhKeyGenBuilder(), DhSpec::ffdhe2048); + registerAsymmetricKeyBuilder(DhPublicKeySpec.class, new AsymmetricKeyBuilder<>() { + + @Override + public KeyPair generateKeyPair(DhPublicKeySpec spec) throws GeneralSecurityException { + throw new UnsupportedOperationException("Use DhKeyGenBuilder for keypair generation."); + } + + @Override + public PublicKey importPublic(DhPublicKeySpec spec) throws GeneralSecurityException { + KeyFactory kf = KeyFactory.getInstance("DH"); + return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); + } + + @Override + public PrivateKey importPrivate(DhPublicKeySpec spec) throws GeneralSecurityException { + throw new UnsupportedOperationException("Use DhPrivateKeySpec for private key import."); + } + }, null); + registerAsymmetricKeyBuilder(DhPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + + @Override + public KeyPair generateKeyPair(DhPrivateKeySpec spec) throws GeneralSecurityException { + throw new UnsupportedOperationException("Use DhKeyGenBuilder for keypair generation."); + } + + @Override + public PublicKey importPublic(DhPrivateKeySpec spec) throws GeneralSecurityException { + throw new UnsupportedOperationException("Use DhPrivateKeySpec for public key import."); + } + + @Override + public PrivateKey importPrivate(DhPrivateKeySpec spec) throws GeneralSecurityException { + KeyFactory kf = KeyFactory.getInstance("DH"); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + } + }, null); + + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/dh/DhKeyGenBuilder.java new file mode 100644 index 0000000..f89fd38 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/dh/DhKeyGenBuilder.java @@ -0,0 +1,194 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.dh; + +import java.security.AlgorithmParameterGenerator; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; + +import javax.crypto.spec.DHParameterSpec; + +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

DH key pair builder

+ * + * Builds Diffie-Hellman key pairs using the JCA providers available at runtime. + * This builder targets the JCA algorithm name {@code "DiffieHellman"} and + * accepts a {@link DhSpec} describing the desired parameter set. + * + *

+ * How it works + *

+ *
    + *
  • If {@link DhSpec#params()} is non-null, those DH parameters are + * used.
  • + *
  • Otherwise, the builder creates parameters with + * {@link java.security.AlgorithmParameterGenerator} for the requested bit + * length, then initializes a {@link java.security.KeyPairGenerator} to produce + * the key pair.
  • + *
+ * + *

+ * Security note: Prefer named, vetted groups such as the RFC + * 7919 FFDHE sets exposed by {@code DhSpec} (for example + * {@code DhSpec.ffdhe2048()}) over ad-hoc parameter generation. + *

+ * + *

+ * Thread-safety: Instances are stateless and can be shared + * across threads. + *

+ * + *

+ * Example + *

+ *
{@code
+ * DhKeyGenBuilder builder = new DhKeyGenBuilder();
+ *
+ * // Use a standard FFDHE group
+ * KeyPair kp1 = builder.generateKeyPair(DhSpec.ffdhe2048());
+ *
+ * // Or request parameters by size when DhSpec.params() is null
+ * DhSpec sized = DhSpec.ofBits(3072); // example factory returning size-only spec
+ * KeyPair kp2 = builder.generateKeyPair(sized);
+ * }
+ */ +public final class DhKeyGenBuilder implements AsymmetricKeyBuilder { + /** + * Generates a Diffie-Hellman key pair for the given specification. + * + *

+ * If {@code spec.params()} is non-null, the contained + * {@link javax.crypto.spec.DHParameterSpec} is used directly. Otherwise, + * parameters are generated with + * {@link java.security.AlgorithmParameterGenerator} using + * {@code spec.sizeBits()}, and the resulting parameters are applied to a + * {@link java.security.KeyPairGenerator} for {@code "DiffieHellman"}. + *

+ * + *

+ * Performance note: On some providers, ad-hoc parameter + * generation can be slow for large sizes. When possible, pass a {@code DhSpec} + * that supplies a known-good {@code DHParameterSpec}. + *

+ * + *

+ * Example + *

+ *
{@code
+     * KeyPair kp = new DhKeyGenBuilder().generateKeyPair(DhSpec.ffdhe3072());
+     * }
+ * + * @param spec the DH specification indicating either a concrete + * {@link DHParameterSpec} or the target bit length for parameter + * generation; must not be null + * @return a newly generated DH {@link KeyPair} + * @throws GeneralSecurityException if the JCA provider does not support + * Diffie-Hellman, parameter generation fails, + * or key pair generation cannot be completed + */ + @Override + public KeyPair generateKeyPair(DhSpec spec) throws GeneralSecurityException { + // Simplest path: AlgorithmParameterGenerator to get params, then init KPG. + AlgorithmParameterGenerator apg = AlgorithmParameterGenerator.getInstance("DiffieHellman"); + apg.init(spec.sizeBits()); + DHParameterSpec dh = spec.params(); + if (dh == null) { + AlgorithmParameters params = apg.generateParameters(); + dh = params.getParameterSpec(DHParameterSpec.class); + } + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("DiffieHellman"); + kpg.initialize(dh); + return kpg.generateKeyPair(); + } + + /** + * Unsupported for DH in this builder. + * + *

+ * Raw public key import is not implemented because this builder focuses on key + * generation from DH parameters. Use higher-level catalog or codec facilities + * to parse or construct {@link PublicKey} instances if needed. + *

+ * + *

+ * Example + *

+ *
{@code
+     * // This will throw UnsupportedOperationException
+     * new DhKeyGenBuilder().importPublic(DhSpec.ffdhe2048());
+     * }
+ * + * @param spec the DH specification (ignored) + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public PublicKey importPublic(DhSpec spec) { + throw new UnsupportedOperationException(); + } + + /** + * Unsupported for DH in this builder. + * + *

+ * Raw private key import is not implemented because this builder focuses on key + * generation from DH parameters. Use higher-level catalog or codec facilities + * to parse or construct {@link PrivateKey} instances if needed. + *

+ * + *

+ * Example + *

+ *
{@code
+     * // This will throw UnsupportedOperationException
+     * new DhKeyGenBuilder().importPrivate(DhSpec.ffdhe2048());
+     * }
+ * + * @param spec the DH specification (ignored) + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public PrivateKey importPrivate(DhSpec spec) { + throw new UnsupportedOperationException(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java new file mode 100644 index 0000000..66733b5 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/dh/DhPrivateKeySpec.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * 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.core.alg.dh; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Key specification for a Diffie-Hellman private key encoded in PKCS#8 format. + * + *

+ * This spec encapsulates the raw encoded private key bytes following the + * standard PKCS#8 structure. It is used with algorithms in the + * {@link zeroecho.core.AlgorithmFamily#AGREEMENT} family, allowing safe + * import/export of DH private keys across contexts. + *

+ * + *

Design

+ *
    + *
  • Immutable: the internal byte array is defensively copied at construction + * and when returned by {@link #encoded()}.
  • + *
  • Encodable: supports marshaling to/from a + * {@link zeroecho.core.marshal.PairSeq} so keys can be serialized in + * human-readable or protocol-friendly formats.
  • + *
  • Type-safe: used as a binding for asymmetric key builders within + * {@code CryptoAlgorithm} definitions.
  • + *
+ * + *

Example

{@code
+ * // Import a DH private key from encoded bytes
+ * DhPrivateKeySpec spec = new DhPrivateKeySpec(pkcs8Bytes);
+ * PrivateKey priv = CryptoAlgorithms.privateKey("DH", spec);
+ *
+ * // Marshal to a text-friendly representation
+ * PairSeq ps = DhPrivateKeySpec.marshal(spec);
+ *
+ * // Unmarshal back to spec
+ * DhPrivateKeySpec parsed = DhPrivateKeySpec.unmarshal(ps);
+ * }
+ * + * @since 1.0 + */ +public class DhPrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Creates a new specification from a PKCS#8 encoded DH private key. + * + * @param pkcs8 the encoded private key bytes; must not be {@code null} + * @throws IllegalArgumentException if {@code pkcs8} is {@code null} + */ + public DhPrivateKeySpec(byte[] pkcs8) { + if (pkcs8 == null) { + throw new IllegalArgumentException("pkcs8 must not be null"); + } + this.pkcs8 = pkcs8.clone(); + } + + /** + * Returns a clone of the encoded PKCS#8 bytes. + * + * @return a defensive copy of the PKCS#8 encoded DH private key + */ + public byte[] encoded() { + return pkcs8.clone(); + } + + /** + * Marshals the given DH private key spec into a {@link PairSeq}. + * + *

+ * The sequence contains a {@code type} field with value {@code "DH-PRIV"} and a + * {@code pkcs8.b64} field with the Base64 encoding of the key. + *

+ * + * @param spec the spec to encode + * @return a {@code PairSeq} representation of the spec + * @throws NullPointerException if {@code spec} is {@code null} + */ + public static PairSeq marshal(DhPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "DH-PRIV", PKCS8_B64, b64); + } + + /** + * Reconstructs a {@code DhPrivateKeySpec} from a {@link PairSeq}. + * + *

+ * The sequence must contain a {@code pkcs8.b64} field with the Base64-encoded + * private key. If this field is missing, an {@link IllegalArgumentException} is + * thrown. + *

+ * + * @param p the sequence to decode + * @return a new spec containing the decoded PKCS#8 bytes + * @throws IllegalArgumentException if required fields are missing or malformed + */ + public static DhPrivateKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (PKCS8_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for DH private key"); + } + return new DhPrivateKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java new file mode 100644 index 0000000..f71db89 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/dh/DhPublicKeySpec.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * 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.core.alg.dh; + +import java.util.Arrays; +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Key specification for a Diffie-Hellman public key encoded in X.509 format. + * + *

+ * This spec carries the raw encoded public key bytes in the standard X.509 + * SubjectPublicKeyInfo form. It is intended for use with algorithms in the + * {@link zeroecho.core.AlgorithmFamily#AGREEMENT} family, allowing safe + * import/export of DH public keys across contexts. + *

+ * + *

Design

+ *
    + *
  • Immutable: the internal byte array is defensively copied at construction + * and when returned by {@link #encoded()}.
  • + *
  • Encodable: supports marshaling to/from a + * {@link zeroecho.core.marshal.PairSeq} so keys can be serialized in + * human-readable or protocol-friendly formats.
  • + *
  • Type-safe: used as a binding for asymmetric key builders within + * {@code CryptoAlgorithm} definitions.
  • + *
+ * + *

Example

{@code
+ * // Import a DH public key from encoded bytes
+ * DhPublicKeySpec spec = new DhPublicKeySpec(x509Bytes);
+ * PublicKey pub = CryptoAlgorithms.publicKey("DH", spec);
+ *
+ * // Marshal to a text-friendly representation
+ * PairSeq ps = DhPublicKeySpec.marshal(spec);
+ *
+ * // Unmarshal back to spec
+ * DhPublicKeySpec parsed = DhPublicKeySpec.unmarshal(ps);
+ * }
+ * + * @since 1.0 + */ +public class DhPublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Creates a new specification from an X.509 encoded DH public key. + * + * @param key the encoded public key bytes; must not be {@code null} + * @throws NullPointerException if {@code key} is {@code null} + */ + public DhPublicKeySpec(byte[] key) { + Objects.requireNonNull(key, "key must not be null"); + this.x509 = Arrays.copyOf(key, key.length); + } + + /** + * Returns a clone of the encoded X.509 bytes. + * + * @return a defensive copy of the X.509 encoded DH public key + */ + public byte[] encoded() { + return x509.clone(); + } + + /** + * Marshals the given DH public key spec into a {@link PairSeq}. + * + *

+ * The sequence contains a {@code type} field with value {@code "DH-PUB"} and an + * {@code x509.b64} field with the Base64 encoding of the key. + *

+ * + * @param spec the spec to encode + * @return a {@code PairSeq} representation of the spec + * @throws NullPointerException if {@code spec} is {@code null} + */ + public static PairSeq marshal(DhPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "DH-PUB", X509_B64, b64); + } + + /** + * Reconstructs a {@code DhPublicKeySpec} from a {@link PairSeq}. + * + *

+ * The sequence must contain an {@code x509.b64} field with the Base64-encoded + * public key. If this field is missing, an {@link IllegalArgumentException} is + * thrown. + *

+ * + * @param p the sequence to decode + * @return a new spec containing the decoded X.509 bytes + * @throws IllegalArgumentException if required fields are missing or malformed + */ + public static DhPublicKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (X509_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("x509.b64 missing for DH public key"); + } + return new DhPublicKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java b/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java new file mode 100644 index 0000000..8f846b7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/dh/DhSpec.java @@ -0,0 +1,215 @@ +/******************************************************************************* + * 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.core.alg.dh; + +import javax.crypto.spec.DHParameterSpec; + +import org.bouncycastle.crypto.agreement.DHStandardGroups; +import org.bouncycastle.crypto.params.DHParameters; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spec.ContextSpec; + +/** + *

Diffie-Hellman parameter specification

+ * + * Immutable container for Diffie-Hellman (DH) group parameters and their + * effective key size. + * + *

+ * A {@code DhSpec} acts both as a {@link zeroecho.core.spec.ContextSpec} for + * agreement contexts and as an {@link zeroecho.core.spec.AlgorithmKeySpec} for + * key generation. Instances wrap a standard + * {@link javax.crypto.spec.DHParameterSpec} with a declared bit length. They + * are typically constructed via the provided factory methods for the RFC 7919 + * finite-field DH (FFDHE) groups. + *

+ * + *

Predefined groups

+ *
    + *
  • {@link #ffdhe2048()} - ~112-bit security, suitable minimum baseline.
  • + *
  • {@link #ffdhe3072()} - ~128-bit security, recommended + * general-purpose.
  • + *
  • {@link #ffdhe4096()} - ~150-bit security.
  • + *
  • {@link #ffdhe6144()} - ~176-bit security.
  • + *
  • {@link #ffdhe8192()} - ~192-bit security, high-assurance + * environments.
  • + *
+ * + *

+ * These correspond exactly to the safe-prime groups defined in + * RFC 7919. Using + * well-standardized groups avoids parameter negotiation pitfalls and ensures + * interoperability. + *

+ * + *

Thread-safety

{@code DhSpec} instances are immutable and can be + * shared freely. + * + *

Example

{@code
+ * // Create a key pair in the FFDHE-3072 group
+ * KeyPair kp = CryptoAlgorithms.keyPair("DH", DhSpec.ffdhe3072());
+ *
+ * // Establish an agreement context
+ * AgreementContext ctx = CryptoAlgorithms.create(
+ *     "DH", KeyUsage.AGREEMENT, kp.getPrivate(), DhSpec.ffdhe3072());
+ * }
+ * + * @since 1.0 + */ +public final class DhSpec implements ContextSpec, AlgorithmKeySpec, Describable { + private final int sizeBits; // e.g., 2048 + private final DHParameterSpec params; + private final String desc; + + private DhSpec(int sizeBits, DHParameterSpec params, String desc) { + this.sizeBits = sizeBits; + this.params = params; + this.desc = desc; + } + + /** + * Returns the standard FFDHE-2048 group. + * + *

+ * Provides ~112-bit classical security, the minimum acceptable baseline for + * modern use per RFC 7919. + *

+ * + * @return a {@code DhSpec} wrapping FFDHE-2048 parameters + */ + public static DhSpec ffdhe2048() { + DHParameters p = DHStandardGroups.rfc7919_ffdhe2048; + return new DhSpec(2048, new DHParameterSpec(p.getP(), p.getG()), "rfc7919_ffdhe2048"); + } + + /** + * Returns the standard FFDHE-3072 group. + * + *

+ * Provides ~128-bit classical security, recommended for general-purpose + * long-term deployments. + *

+ * + * @return a {@code DhSpec} wrapping FFDHE-3072 parameters + */ + public static DhSpec ffdhe3072() { + DHParameters p = DHStandardGroups.rfc7919_ffdhe3072; + return new DhSpec(3072, new DHParameterSpec(p.getP(), p.getG()), "rfc7919_ffdhe3072"); + } + + /** + * Returns the standard FFDHE-4096 group. + * + *

+ * Provides ~150-bit classical security. Chosen when a moderate increase over + * 128-bit strength is required. + *

+ * + * @return a {@code DhSpec} wrapping FFDHE-4096 parameters + */ + public static DhSpec ffdhe4096() { + DHParameters p = DHStandardGroups.rfc7919_ffdhe4096; + return new DhSpec(4096, new DHParameterSpec(p.getP(), p.getG()), "rfc7919_ffdhe4096"); + } + + /** + * Returns the standard FFDHE-6144 group. + * + *

+ * Provides ~176-bit classical security, suitable for high-value environments + * requiring extra margin. + *

+ * + * @return a {@code DhSpec} wrapping FFDHE-6144 parameters + */ + public static DhSpec ffdhe6144() { + DHParameters p = DHStandardGroups.rfc7919_ffdhe6144; + return new DhSpec(6144, new DHParameterSpec(p.getP(), p.getG()), "rfc7919_ffdhe6144"); + } + + /** + * Returns the standard FFDHE-8192 group. + * + *

+ * Provides ~192-bit classical security, rarely used but suitable for + * maximum-assurance deployments. + *

+ * + * @return a {@code DhSpec} wrapping FFDHE-8192 parameters + */ + public static DhSpec ffdhe8192() { + DHParameters p = DHStandardGroups.rfc7919_ffdhe8192; + return new DhSpec(8192, new DHParameterSpec(p.getP(), p.getG()), "rfc7919_ffdhe8192"); + } + + /** + * Returns the underlying JCA {@link DHParameterSpec}. + * + * @return the DH parameters (prime modulus and generator) + */ + public DHParameterSpec params() { + return params; + } + + /** + * Returns the effective bit length of this group. + * + *

+ * This value reflects the size of the prime modulus {@code p}. + *

+ * + * @return size of the group modulus in bits + */ + public int sizeBits() { + return sizeBits; + } + + /** + * Returns a short, human-readable description of this object. + * + *

+ * The description should be concise and stable enough for logging or display + * purposes, while avoiding exposure of any sensitive information. + *

+ * + * @return non-null descriptive string + */ + @Override + public String description() { + return desc; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/dh/package-info.java b/lib/src/main/java/zeroecho/core/alg/dh/package-info.java new file mode 100644 index 0000000..337d9ca --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/dh/package-info.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Diffie-Hellman (DH) algorithm integration. + * + *

+ * This package provides the core support for Diffie-Hellman key agreement + * within the ZeroEcho framework. It includes the algorithm descriptor, a + * builder for generating key pairs, immutable parameter specifications, and + * encoded key specs for import/export. The implementation favors use of + * standardized RFC 7919 finite-field DH groups and encapsulates JCA + * interoperability. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register the DH algorithm with a canonical identifier and declare its + * {@link zeroecho.core.KeyUsage#AGREEMENT} capability.
  • + *
  • Provide a builder for generating key pairs from known parameter sets or + * ad-hoc parameter generation.
  • + *
  • Expose predefined RFC 7919 FFDHE groups for safe parameter selection. + *
  • + *
  • Allow import/export of encoded keys via immutable key specs supporting + * PKCS#8 and X.509.
  • + *
+ * + *

Components

+ *
    + *
  • DhAlgorithm: registers the algorithm under id {@code "DH"}, binds + * agreement contexts to the JCA {@code "DiffieHellman"} implementation, and + * wires key builders.
  • + *
  • DhKeyGenBuilder: generates key pairs using + * {@link java.security.AlgorithmParameterGenerator} or supplied + * {@link javax.crypto.spec.DHParameterSpec} instances.
  • + *
  • DhSpec: immutable container for DH parameters; provides static + * factories for FFDHE groups (2048–8192 bits).
  • + *
  • DhPublicKeySpec and DhPrivateKeySpec: immutable encoded key + * specs for importing/exporting X.509 and PKCS#8 encodings, with + * {@link zeroecho.core.marshal.PairSeq} marshalling support.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and thread-safe for reuse.
  • + *
  • Builders are stateless; contexts produced for key agreement are not + * thread-safe and should be used per operation.
  • + *
  • Use of standardized groups is strongly recommended over ad-hoc parameter + * generation for interoperability and safety.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.dh; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/digest/DigestSpec.java b/lib/src/main/java/zeroecho/core/alg/digest/DigestSpec.java new file mode 100644 index 0000000..e588cbd --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/digest/DigestSpec.java @@ -0,0 +1,262 @@ +/******************************************************************************* + * 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.core.alg.digest; + +import java.util.Objects; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.annotation.DisplayName; +import zeroecho.core.spec.ContextSpec; + +/** + *

Digest parameters for SHA-2, SHA-3, and SHAKE

+ * + * {@code DigestSpec} is a context specification for the + * {@link Sha2Sha3Algorithm}, selecting which digest algorithm variant to use + * and, for extendable-output functions (XOFs), the desired output length. + * + *

Algorithm selection

The nested {@link Algorithm} enum defines the + * supported digest families: + *
    + *
  • {@link Algorithm#SHA_256}, {@link Algorithm#SHA_384}, + * {@link Algorithm#SHA_512} - fixed-length SHA-2 digests.
  • + *
  • {@link Algorithm#SHA3_256}, {@link Algorithm#SHA3_512} - fixed-length + * SHA-3 digests.
  • + *
  • {@link Algorithm#SHAKE128}, {@link Algorithm#SHAKE256} - + * extendable-output functions (XOFs).
  • + *
+ * + *

+ * For fixed-length digests, the {@link #outputLenBytes()} is always {@code 0} + * (the digest length is implicit). For XOFs, a positive output length must be + * supplied at construction time. + *

+ * + *

Construction

Factory methods are provided for common cases: + *
    + *
  • {@link #sha256()}, {@link #sha384()}, {@link #sha512()}
  • + *
  • {@link #sha3_256()}, {@link #sha3_512()}
  • + *
  • {@link #shake128(int)}, {@link #shake256(int)} (require explicit output + * length)
  • + *
+ * + *

Usage example

{@code
+ * // Create a spec for SHAKE256 with 64-byte output
+ * DigestSpec spec = DigestSpec.shake256(64);
+ *
+ * // Use in context creation
+ * DigestContext ctx = CryptoAlgorithms.create(
+ *         "DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, spec);
+ *
+ * byte[] digest = ctx.doFinal(data);
+ * }
+ * + *

Thread-safety

Instances are immutable and can be safely reused + * across threads. + * + * @see Sha2Sha3Algorithm + * @see zeroecho.core.context.DigestContext + * @since 1.0 + */ +@DisplayName("Digest parameters") +public final class DigestSpec implements ContextSpec, Describable { + + /** + * Enumeration of supported digest algorithms. + * + *

+ * Each constant defines the JCA algorithm name and whether the variant is an + * extendable-output function (XOF). + *

+ */ + public enum Algorithm { + /** SHA-256 digest from the SHA-2 family. */ + SHA_256("SHA-256", false), + /** SHA-384 digest from the SHA-2 family. */ + SHA_384("SHA-384", false), + /** SHA-512 digest from the SHA-2 family. */ + SHA_512("SHA-512", false), + /** SHA3-256 digest from the SHA-3 family. */ + SHA3_256("SHA3-256", false), + /** SHA3-512 digest from the SHA-3 family. */ + SHA3_512("SHA3-512", false), + /** SHAKE128 extendable-output function (XOF). */ + SHAKE128("SHAKE128", true), + /** SHAKE256 extendable-output function (XOF). */ + SHAKE256("SHAKE256", true); + + private final String jca; + private final boolean xof; + + Algorithm(String jca, boolean xof) { + this.jca = jca; + this.xof = xof; + } + + /** + * Returns the canonical JCA algorithm name. + * + * @return JCA name string (e.g., {@code "SHA-256"} or {@code "SHAKE256"}) + */ + public String jca() { + return jca; + } + + /** + * Indicates whether this algorithm is an XOF (extendable-output function). + * + * @return {@code true} if this algorithm is an XOF, {@code false} otherwise + */ + public boolean isXof() { + return xof; + } + } + + private final Algorithm algorithm; + private final int outputLenBytes; // for XOFs; 0 for fixed-length digests + + private DigestSpec(Algorithm algorithm, int outputLenBytes) { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm must not be null"); + if (algorithm.isXof() && outputLenBytes <= 0) { + throw new IllegalArgumentException("XOF output length must be > 0"); + } + this.outputLenBytes = algorithm.isXof() ? outputLenBytes : 0; + } + + /** + * Returns a {@code DigestSpec} for SHA-256. + * + * @return spec for SHA-256 + */ + public static DigestSpec sha256() { + return new DigestSpec(Algorithm.SHA_256, 0); + } + + /** + * Returns a {@code DigestSpec} for SHA-384. + * + * @return spec for SHA-384 + */ + public static DigestSpec sha384() { + return new DigestSpec(Algorithm.SHA_384, 0); + } + + /** + * Returns a {@code DigestSpec} for SHA-512. + * + * @return spec for SHA-512 + */ + public static DigestSpec sha512() { + return new DigestSpec(Algorithm.SHA_512, 0); + } + + /** + * Returns a {@code DigestSpec} for SHA3-256. + * + * @return spec for SHA3-256 + */ + public static DigestSpec sha3_256() { + return new DigestSpec(Algorithm.SHA3_256, 0); + } + + /** + * Returns a {@code DigestSpec} for SHA3-512. + * + * @return spec for SHA3-512 + */ + public static DigestSpec sha3_512() { + return new DigestSpec(Algorithm.SHA3_512, 0); + } + + /** + * Returns a {@code DigestSpec} for SHAKE128 with the given output length. + * + * @param outLen desired output length in bytes (must be > 0) + * @return spec for SHAKE128 with the specified output length + * @throws IllegalArgumentException if {@code outLen <= 0} + */ + public static DigestSpec shake128(int outLen) { + return new DigestSpec(Algorithm.SHAKE128, outLen); + } + + /** + * Returns a {@code DigestSpec} for SHAKE256 with the given output length. + * + * @param outLen desired output length in bytes (must be > 0) + * @return spec for SHAKE256 with the specified output length + * @throws IllegalArgumentException if {@code outLen <= 0} + */ + public static DigestSpec shake256(int outLen) { + return new DigestSpec(Algorithm.SHAKE256, outLen); + } + + /** + * Returns the digest algorithm. + * + * @return algorithm enum constant + */ + public Algorithm algorithm() { + return algorithm; + } + + /** + * Returns the requested output length in bytes. + * + *

+ * For fixed-length digests, this value is {@code 0}. For XOFs, this is the + * explicit length requested at construction. + *

+ * + * @return output length in bytes (0 if not applicable) + */ + public int outputLenBytes() { + return outputLenBytes; + } + + /** + * Returns a human-readable description of this specification. + * + *

+ * For XOFs, the description includes the chosen output length, e.g., + * {@code "SHAKE128(64B)"}. + *

+ * + * @return description string + */ + @Override + public String description() { + return algorithm.isXof() ? algorithm.jca() + "(" + outputLenBytes + "B)" : algorithm.jca(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/digest/JcaDigestContext.java b/lib/src/main/java/zeroecho/core/alg/digest/JcaDigestContext.java new file mode 100644 index 0000000..97f3f3f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/digest/JcaDigestContext.java @@ -0,0 +1,452 @@ +/******************************************************************************* + * 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.core.alg.digest; + +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.DigestContext; +import zeroecho.core.err.VerificationException; +import zeroecho.core.io.AbstractPassthroughInputStream; +import zeroecho.core.tag.ByteVerificationStrategy; +import zeroecho.core.tag.TagEngine; +import zeroecho.core.tag.ThrowingBiPredicate; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; + +/** + * Digest context that adapts a JCA {@link java.security.MessageDigest} to a + * pull-based streaming pipeline. + * + *

+ * {@code JcaDigestContext} implements {@link DigestContext} for + * {@link Sha2Sha3Algorithm} by delegating to a JCA + * {@link java.security.MessageDigest} and exposing the {@link TagEngine} + * contract. As data is read from the wrapped stream the digest state is + * updated; at end-of-stream the digest is finalized and either appended as a + * trailer (produce mode) or compared against an expected tag (verify mode). + *

+ * + *

Features

+ *
    + *
  • Supports SHA-2, SHA-3, and SHAKE digests as defined by + * {@link DigestSpec}.
  • + *
  • Provides pull-stream semantics via + * {@link #wrap(java.io.InputStream)}.
  • + *
  • Two modes: + *
      + *
    • Produce mode: appends the computed digest as a trailer.
    • + *
    • Verify mode: computes the digest and compares it to an expected + * tag using a pluggable verification approach (see + * {@link #setVerificationApproach(ThrowingBiPredicate.VerificationBiPredicate)}).
    • + *
    + *
  • + *
  • For XOFs (SHAKE), the output length is explicitly controlled by + * {@link DigestSpec}.
  • + *
+ * + *

Verification approach

+ *

+ * Verification is delegated to a + * {@link ThrowingBiPredicate.VerificationBiPredicate} over {@code byte[]}. The + * default core returned by {@link #getVerificationCore()} performs + * constant-time byte comparison ({@link ByteVerificationStrategy}). If no + * strategy is set explicitly, the effective verifier defaults to + * {@code getVerificationCore().getThrowOnMismatch()}, causing a mismatch to be + * signaled as an I/O error at end-of-stream. + *

+ * + *

Thread-safety

+ *

+ * Instances are stateful and not thread-safe. Each context may wrap exactly one + * stream; subsequent calls to {@link #wrap(java.io.InputStream)} are rejected. + *

+ * + * @since 1.0 + */ +public final class JcaDigestContext implements DigestContext { + private static final Logger LOG = Logger.getLogger(JcaDigestContext.class.getName()); + + /** Algorithm descriptor that owns this context. */ + private final CryptoAlgorithm algorithm; + /** JCA message digest engine used for computation. */ + private final MessageDigest md; // NOPMD + /** Digest parameters (algorithm choice, output length for XOFs). */ + private final DigestSpec spec; + /** JCA algorithm name string, cached for diagnostics. */ + private final String algorithmName; + + /** Trailer length in bytes (digest size or requested XOF length). */ + private final int outLen; + + /** Whether this context is operating in verification mode. */ + private boolean verifyMode; // = false; + /** Expected digest/tag value provided by caller. */ + private byte[] expectedTag; + /** Verification failure handling policy. */ + private VerificationBiPredicate strategy; + + /** Whether {@link #wrap(InputStream)} has already been called. */ + private boolean wrapped; // = false; + /** Active digesting stream, if {@link #wrap(InputStream)} was invoked. */ + private Stream activeStream; + private boolean autoCloseActiveStream; + + /** + * Creates a new digest context bound to a JCA {@link MessageDigest}. + * + *

+ * This constructor is normally invoked by {@link Sha2Sha3Algorithm}: + *

+ * + *
{@code
+     * MessageDigest md = MessageDigest.getInstance(spec.algorithm().jca());
+     * return new JcaDigestContext(algo, md, spec);
+     * }
+ * + * @param algorithm the owning algorithm descriptor + * @param md JCA message digest implementation + * @param spec digest parameters (algorithm, output length for XOFs) + * @throws NullPointerException if any parameter is null + * @throws IllegalArgumentException if the output length for an XOF is invalid + */ + public JcaDigestContext(final CryptoAlgorithm algorithm, final MessageDigest md, final DigestSpec spec) { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm"); + this.md = Objects.requireNonNull(md, "md"); + this.spec = Objects.requireNonNull(spec, "spec"); + this.algorithmName = md.getAlgorithm(); + this.outLen = resolveOutputLength(md, spec); + } + + /** + * Returns the {@link CryptoAlgorithm} definition that created this context. + * + * @return algorithm descriptor + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key associated with this context. + * + *

+ * Digests are unkeyed; this method always returns {@code null}. + *

+ * + * @return always {@code null} + */ + @Override + public java.security.Key key() { + return null; // digest is keyless + } + + /** + * Closes the active digest stream if one was opened. + * + *

+ * The close operation is non-throwing; any underlying {@link IOException} is + * suppressed to preserve pipeline robustness. + *

+ */ + @Override + public void close() { + LOG.log(Level.INFO, "close()"); + + if (autoCloseActiveStream) { + try { + if (activeStream != null) { + activeStream.close(); + } + } catch (IOException ignore) { + LOG.log(Level.INFO, "exception ignored on close", ignore); + } + } + } + + /** + * Wraps an upstream {@link InputStream} in a digesting pipeline stage. + * + *

+ * Data read from the returned stream is passed through unchanged while being + * accumulated into the underlying {@link MessageDigest}. At end-of-stream the + * digest is finalized and either emitted as a trailer (produce mode) or + * compared to the expected tag (verify mode). + *

+ * + *

+ * This method may be called at most once per context instance. + *

+ * + * @param upstream the input stream to wrap; must not be {@code null} + * @return a stream that transparently digests data + * @throws IOException if stream initialization fails + * @throws IllegalStateException if {@code wrap} was already invoked on this + * instance + * @throws NullPointerException if {@code upstream} is {@code null} + */ + @Override + public InputStream wrap(final InputStream upstream) throws IOException { + Objects.requireNonNull(upstream, "upstream"); + if (wrapped) { + throw new IllegalStateException( + "This JcaDigestContext instance was already used; create a new one per stream."); + } + wrapped = true; + + md.reset(); + + Stream s = new Stream(upstream); + this.activeStream = s; + return s; + } + + /** + * Returns the digest length in bytes. + * + * @return output length of the digest or XOF + */ + @Override + public int tagLength() { + return outLen; + } + + /** + * Installs an expected digest tag for verification mode. + * + *

+ * When provided, the context operates in verification mode: at end-of-stream + * the computed digest is compared against this tag using the effective + * verification strategy. Passing {@code null} clears the tag and leaves the + * mode unchanged. + *

+ * + * @param expected expected digest bytes; copied defensively (may be + * {@code null} to clear) + */ + @Override + public void setExpectedTag(final byte[] expected) { + this.expectedTag = (expected == null) ? null : Arrays.copyOf(expected, expected.length); + this.verifyMode = true; + } + + /** + * Internal pass-through stream that updates the digest as data flows and + * finalizes at EOF. + * + *

+ * In produce mode, the finalized digest is emitted as a trailer. In verify + * mode, the finalized digest is compared to the expected tag using the + * configured verification strategy. + *

+ */ + private final class Stream extends AbstractPassthroughInputStream { + /** Produce mode: cached digest to emit as a trailer. */ + private byte[] trailer; // NOPMD + + /** + * Creates a new digesting stream. + * + * @param upstream underlying input stream to wrap + */ + private Stream(final InputStream upstream) { + super(upstream, 8192); + } + + /** + * Updates the digest with newly read bytes. + * + * @param buf buffer containing input + * @param off offset into buffer + * @param len number of bytes to process + */ + @Override + protected void update(final byte[] buf, final int off, final int len) { + md.update(buf, off, len); + } + + /** + * Produces the digest trailer once in produce mode; emits nothing in verify + * mode. + * + *

+ * In produce mode this finalizes the digest and copies it into {@code buf}. For + * XOFs or when the provider's reported digest length differs from the + * configured output length, exactly {@code outLen} bytes are requested. In + * verify mode the method returns {@code 0}. + *

+ * + * @param buf destination buffer to receive the trailer + * @return number of bytes written, or {@code 0} if no trailer is emitted + * @throws IOException if digest finalization fails or the trailer does not fit + * in {@code buf} + */ + @Override + protected int produceTrailer(byte[] buf) throws IOException { + if (verifyMode) { + return 0; // verification path handled in onCompleted() + } + + // Compute the digest output once for trailer emission. + final byte[] out; + try { + // For XOFs or unknown/size-mismatch digests, request exactly outLen bytes into + // a fresh array. + if (spec.algorithm().isXof() || md.getDigestLength() == 0 || md.getDigestLength() != outLen) { + out = new byte[outLen]; + md.digest(out, 0, outLen); + } else { + out = md.digest(); + } + } catch (java.security.DigestException e) { + throw new IOException("Digest finalize failed (" + algorithmName + ")", e); + } + + trailer = out; + if (trailer.length == 0) { + return 0; + } + if (trailer.length > buf.length) { + throw new IOException("Trailer does not fit into buffer"); + } + System.arraycopy(trailer, 0, buf, 0, trailer.length); + return trailer.length; + } + + /** + * Completes verification in verify mode; no-op in produce mode. + * + *

+ * Finalizes the digest and compares it to {@code expectedTag} using the + * configured verification strategy. Any {@link VerificationException} is + * translated to {@link IOException}. + *

+ * + * @throws IOException if verification fails and the strategy signals failure + */ + @Override + protected void onCompleted() throws IOException { + if (!verifyMode) { + return; // nothing to do for produce mode; trailer already emitted (if any) + } + try { + // Compute the digest output for verification. + final byte[] out; + if (spec.algorithm().isXof() || md.getDigestLength() == 0 || md.getDigestLength() != outLen) { + out = new byte[outLen]; + md.digest(out, 0, outLen); + } else { + out = md.digest(); + } + + strategy.verify(expectedTag, out); + } catch (java.security.DigestException e) { + throw new IllegalStateException("Digest finalize failed (" + algorithmName + ")", e); + } catch (VerificationException e) { + throw new IOException(e); + } + } + } + + /** + * Resolves the digest output length for the given algorithm. + * + *

+ * For XOFs, uses the explicit length from {@link DigestSpec}. For fixed-length + * digests, queries the provider via {@link MessageDigest#getDigestLength()}. + * Falls back to the hint in {@link DigestSpec} or 32 bytes if the provider + * reports zero. + *

+ * + * @param md JCA message digest; must not be {@code null} + * @param spec digest specification; must not be {@code null} + * @return output length in bytes + * @throws IllegalArgumentException if an XOF output length is non-positive + */ + private static int resolveOutputLength(final MessageDigest md, final DigestSpec spec) { + Objects.requireNonNull(md, "md"); + Objects.requireNonNull(spec, "spec"); + if (spec.algorithm().isXof()) { + int out = spec.outputLenBytes(); + if (out < 1) { // NOPMD + throw new IllegalArgumentException("XOF requires positive output length"); + } + return out; + } + int dl = md.getDigestLength(); + if (dl > 0) { + return dl; + } + int hinted = spec.outputLenBytes(); + return hinted > 0 ? hinted : 32; // conservative default if provider doesn't report length + } + + /** + * Sets the verification approach used to compare computed and expected digests. + * + *

+ * If no strategy is set explicitly, the effective verifier defaults to + * {@code getVerificationCore().getThrowOnMismatch()}. + *

+ * + * @param strategy verification predicate; may be {@code null} to keep the + * default behavior + */ + @Override + public void setVerificationApproach(VerificationBiPredicate strategy) { + this.strategy = strategy; + } + + /** + * Returns the core verification predicate for digest tags. + * + *

+ * The default core is a constant-time byte-array comparison implemented by + * {@link ByteVerificationStrategy}. + *

+ * + * @return base verification predicate (never {@code null}) + */ + @Override + public VerificationBiPredicate getVerificationCore() { + return new ByteVerificationStrategy(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/digest/Sha2Sha3Algorithm.java b/lib/src/main/java/zeroecho/core/alg/digest/Sha2Sha3Algorithm.java new file mode 100644 index 0000000..9992fc0 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/digest/Sha2Sha3Algorithm.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.digest; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.KeyUsage; +import zeroecho.core.NullKey; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.context.DigestContext; + +/** + *

SHA-2, SHA-3, and SHAKE digest algorithms

+ * + * Implementation of the {@link CryptoAlgorithm} abstraction for unkeyed hash + * functions from the SHA-2 and SHA-3 families, including extendable-output + * functions (XOFs) such as SHAKE128 and SHAKE256. + * + *

+ * This algorithm is registered under the canonical identifier {@code "DIGEST"} + * with the display name {@code "SHA-2/SHA-3/SHAKE"}. + *

+ * + *

Capabilities

The algorithm declares a single capability: + *
    + *
  • Family: {@link AlgorithmFamily#DIGEST}
  • + *
  • Role: {@link KeyUsage#DIGEST}
  • + *
  • Context type: {@link zeroecho.core.context.DigestContext}
  • + *
  • Key type: {@link NullKey} (no cryptographic key required)
  • + *
  • Spec type: {@link DigestSpec} (algorithm selection)
  • + *
  • Default spec: {@link DigestSpec#sha256()}
  • + *
+ * + *

Provider model

Internally, this class delegates to the JCA + * {@link java.security.MessageDigest} implementation corresponding to the + * chosen digest variant. The constructor performs lookup via + * {@link MessageDigest#getInstance(String)}, and wraps the result in a + * {@link JcaDigestContext}. + * + *

Usage example

{@code
+ * // Acquire algorithm via static registry
+ * CryptoAlgorithm algo = CryptoAlgorithms.require("DIGEST");
+ *
+ * // Create a digest context for SHA3-512
+ * DigestContext ctx = CryptoAlgorithms.create(
+ *         "DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, DigestSpec.sha3_512());
+ *
+ * // Stream data into the digest
+ * ctx.update(inputStream);
+ *
+ * byte[] hash = ctx.doFinal();
+ * }
+ * + *

+ * The {@link NullKey} sentinel must always be provided for the key argument + * since digests are unkeyed functions. The {@link DigestSpec} determines the + * exact digest variant (SHA-256, SHA-512, SHA3-256, SHAKE128, etc.). + *

+ * + *

Thread-safety

The {@code Sha2Sha3Algorithm} instance is immutable + * and can be shared across threads. The created {@link DigestContext} objects + * are not thread-safe and must not be used concurrently. + * + * @since 1.0 + */ +public final class Sha2Sha3Algorithm extends AbstractCryptoAlgorithm { + /** + * Constructs the SHA-2/SHA-3/SHAKE digest algorithm definition and registers + * its {@link zeroecho.core.Capability}. + * + *

+ * The default digest specification is {@link DigestSpec#sha256()}, ensuring + * compatibility with common tests and catalog listings. + *

+ */ + public Sha2Sha3Algorithm() { + super("DIGEST", "SHA-2/SHA-3/SHAKE"); + + capability(AlgorithmFamily.DIGEST, KeyUsage.DIGEST, DigestContext.class, NullKey.class, DigestSpec.class, + (NullKey k, DigestSpec s) -> { + try { + MessageDigest md = MessageDigest.getInstance(s.algorithm().jca()); + return new JcaDigestContext(this, md, s); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to init MessageDigest: " + s.algorithm().jca(), e); + } + }, DigestSpec::sha256 // default for catalog/tests + ); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/digest/package-info.java b/lib/src/main/java/zeroecho/core/alg/digest/package-info.java new file mode 100644 index 0000000..2ff512d --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/digest/package-info.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Digest algorithms (SHA-2, SHA-3, SHAKE) and their streaming contexts. + * + *

+ * This package provides the core unkeyed hash functions of the ZeroEcho + * library. It integrates JCA {@link java.security.MessageDigest} engines into + * the framework’s streaming context model, allowing digests to be applied as + * pipeline stages with produce/verify modes. It also defines immutable + * specifications for selecting digest variants and output lengths for + * extendable-output functions. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Expose a single algorithm descriptor under id {@code "DIGEST"} that + * covers SHA-2, SHA-3, and SHAKE variants.
  • + *
  • Provide a JCA-backed {@link zeroecho.core.context.DigestContext} for + * streaming input through a {@link java.security.MessageDigest} engine.
  • + *
  • Support both fixed-length digests and extendable-output functions (XOFs) + * with configurable output size.
  • + *
  • Ensure interoperability with JCA provider implementations.
  • + *
+ * + *

Components

+ *
    + *
  • Sha2Sha3Algorithm: registers the algorithm, declares its digest + * capability, and creates contexts bound to the appropriate JCA + * {@link java.security.MessageDigest}.
  • + *
  • DigestSpec: immutable specification of digest variant and output + * length (for XOFs); provides static factories for common cases.
  • + *
  • JcaDigestContext: streaming digest context that adapts + * {@link java.security.MessageDigest} to the core SPI, supporting produce + * (append digest) and verify (check expected tag) modes.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm instances are immutable and thread-safe for reuse.
  • + *
  • Context instances are stateful and not thread-safe; create a new context + * for each pipeline.
  • + *
  • Verification policies integrate with the tagging engine to either throw + * on mismatch or set a flag in the context.
  • + *
  • Extendable-output functions require explicit output lengths; fixed + * digests derive their size from the algorithm.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.digest; diff --git a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java new file mode 100644 index 0000000..9e0457d --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhAlgorithm.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * 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.core.alg.ecdh; + +import java.security.PrivateKey; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext; +import zeroecho.core.alg.ecdsa.EcdsaCurveSpec; +import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder; +import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec; +import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder; +import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec; +import zeroecho.core.context.AgreementContext; + +/** + *

Elliptic Curve Diffie-Hellman (ECDH) Algorithm

+ * + * Implementation of the Elliptic Curve Diffie-Hellman key agreement scheme + * within the ZeroEcho framework. + * + *

+ * ECDH allows two parties, each possessing an elliptic-curve key pair, to + * derive a common shared secret over an insecure channel. Only the private key + * of one party and the public key of the other are required to compute the same + * shared value. This shared secret can then be expanded into symmetric session + * keys. + *

+ * + *

Capabilities

+ *
    + *
  • {@link AlgorithmFamily#AGREEMENT} with {@link KeyUsage#AGREEMENT}: + * creates an {@link zeroecho.core.context.AgreementContext} from a local + * {@link java.security.PrivateKey} and a peer's public key supplied through the + * context.
  • + *
+ * + *

Key builders

+ *
    + *
  • {@link EcdhCurveSpec}: used to generate EC key pairs for ECDH; default is + * {@code P-256}.
  • + *
  • {@link EcdsaPublicKeySpec} and {@link EcdsaPrivateKeySpec}: reused for + * importing elliptic-curve keys since ECDH and ECDSA share curve parameters and + * encoding formats.
  • + *
+ * + *

Notes

+ *
    + *
  • The {@link EcdsaCurveSpec} parameter passed in the agreement capability + * is currently ignored in this implementation (defaulting always to the curve + * of the key itself). This is retained for API uniformity and may be enforced + * in future versions.
  • + *
+ * + *

Example

{@code
+ * // Generate a key pair for Alice
+ * KeyPair aliceKeys = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
+ *
+ * // Generate a key pair for Bob
+ * KeyPair bobKeys = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
+ *
+ * // Alice computes shared secret using her private key
+ * AgreementContext aliceCtx = CryptoAlgorithms.create("ECDH",
+ *         KeyUsage.AGREEMENT, aliceKeys.getPrivate(), EcdsaCurveSpec.P256);
+ * byte[] aliceSecret = aliceCtx.derive(bobKeys.getPublic());
+ *
+ * // Bob computes shared secret using his private key
+ * AgreementContext bobCtx = CryptoAlgorithms.create("ECDH",
+ *         KeyUsage.AGREEMENT, bobKeys.getPrivate(), EcdsaCurveSpec.P256);
+ * byte[] bobSecret = bobCtx.derive(aliceKeys.getPublic());
+ *
+ * // aliceSecret and bobSecret will be identical
+ * }
+ * + * @since 1.0 + */ +public final class EcdhAlgorithm extends AbstractCryptoAlgorithm { + /** + * Constructs a new ECDH algorithm definition. + * + *

+ * This constructor registers the capabilities and key builders required for + * Elliptic Curve Diffie-Hellman (ECDH) key agreement: + *

+ * + *
    + *
  • Capability: {@link AlgorithmFamily#AGREEMENT} with + * {@link KeyUsage#AGREEMENT}, producing an + * {@link zeroecho.core.context.AgreementContext} from a local + * {@link java.security.PrivateKey} and a peer’s public key (supplied at + * runtime).
  • + * + *
  • Asymmetric key builders: + *
      + *
    • {@link EcdhCurveSpec} - generates EC key pairs suitable for ECDH + * (default: {@code P-256}).
    • + *
    • {@link EcdsaPublicKeySpec} - imports existing EC public keys.
    • + *
    • {@link EcdsaPrivateKeySpec} - imports existing EC private keys.
    • + *
    + *
  • + *
+ * + *

+ * Note: although an {@link EcdsaCurveSpec} is accepted as a context parameter, + * the current implementation does not use it when constructing the + * {@link zeroecho.core.context.AgreementContext}. The curve is inferred from + * the provided key. This placeholder remains for API uniformity and may be + * enforced in later revisions. + *

+ * + * @since 1.0 + */ + public EcdhAlgorithm() { + super("ECDH", "ECDH"); + + // AGREEMENT (our private key + peer public provided via context) + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, AgreementContext.class, PrivateKey.class, + EcdsaCurveSpec.class, + (PrivateKey k, EcdsaCurveSpec s) -> new GenericJcaAgreementContext(this, k, "ECDH", null), + () -> EcdsaCurveSpec.P256); // XXX spec is not used at all ?!?! + + // Reuse EC builders/importers + registerAsymmetricKeyBuilder(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), () -> EcdhCurveSpec.P256); + registerAsymmetricKeyBuilder(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder(), null); + registerAsymmetricKeyBuilder(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder(), null); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java new file mode 100644 index 0000000..cdbbd37 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhCurveSpec.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * 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.core.alg.ecdh; + +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spec.ContextSpec; + +/** + *

Standardized ECDH Curve Specifications

+ * + * Enumeration of commonly used NIST-recommended elliptic curves for ECDH + * (Elliptic Curve Diffie-Hellman) and related algorithms. + * + *

+ * Each constant provides: + *

+ *
    + *
  • Curve name - the JCA-standardized name, suitable for use with + * {@link java.security.spec.ECGenParameterSpec}.
  • + *
  • Signature algorithm name - a JCA/JCE identifier combining the hash + * function and ECDSA variant in P1363 encoding.
  • + *
  • Fixed signature length - the expected byte length of deterministic + * ECDSA signatures for the curve, following the P1363 format.
  • + *
+ * + *

Curves

+ *
    + *
  • {@link #P256} - secp256r1, 128-bit security level.
  • + *
  • {@link #P384} - secp384r1, 192-bit security level.
  • + *
  • {@link #P512} - secp521r1, ~256-bit security level.
  • + *
+ * + *

Usage

{@code
+ * // Generate a key pair on P-256
+ * KeyPair kp = CryptoAlgorithms.generateKeyPair("ECDH", EcdhCurveSpec.P256);
+ *
+ * // Use curve metadata
+ * String jcaName = EcdhCurveSpec.P256.curveName(); // "secp256r1"
+ * int sigLen = EcdhCurveSpec.P256.signFixedLength(); // 64 bytes
+ * }
+ * + * @since 1.0 + */ +public enum EcdhCurveSpec implements ContextSpec, AlgorithmKeySpec { + /** NIST P-256 (secp256r1), approx. 128-bit security. */ + P256("secp256r1", "SHA256withECDSAinP1363Format", 64), + /** NIST P-384 (secp384r1), approx. 192-bit security. */ + P384("secp384r1", "SHA384withECDSAinP1363Format", 96), + /** NIST P-521 (secp521r1), approx. 256-bit security. */ + P512("secp521r1", "SHA512withECDSAinP1363Format", 132); + + private final String curveName; + private final String jca; + private final int tagLen; + + EcdhCurveSpec(String curveName, String jca, int tagLen) { + this.curveName = curveName; + this.jca = jca; + this.tagLen = tagLen; + } + + /** + * Returns the JCA-standardized name of the elliptic curve. + * + * @return curve name, e.g., {@code "secp256r1"} + */ + public String curveName() { + return curveName; + } + + /** + * Returns the JCA factory name of the corresponding signature algorithm (ECDSA + * with hash, P1363 encoding). + * + * @return JCA algorithm name, e.g., {@code "SHA256withECDSAinP1363Format"} + */ + public String jcaFactory() { + return jca; + } + + /** + * Returns the fixed byte length of deterministic ECDSA signatures for this + * curve in P1363 format. + * + * @return fixed signature length in bytes + */ + public int signFixedLength() { + return tagLen; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java new file mode 100644 index 0000000..1fe3f72 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdh/EcdhKeyGenBuilder.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * 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.core.alg.ecdh; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.spec.ECGenParameterSpec; + +import zeroecho.core.alg.ecdsa.EcdsaPrivateKeyBuilder; +import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec; +import zeroecho.core.alg.ecdsa.EcdsaPublicKeyBuilder; +import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

ECDH Key Pair Generator

+ * + * Implementation of {@link AsymmetricKeyBuilder} for elliptic curve + * Diffie-Hellman (ECDH) key pairs. + * + *

+ * This builder generates fresh EC key pairs suitable for ECDH key agreement. It + * accepts an {@link EcdhCurveSpec} that defines the named curve to use (e.g., + * {@code P-256}, {@code P-384}, {@code P-521}). + *

+ * + *

Usage

{@code
+ * // Generate a key pair on P-256
+ * KeyPair kp = new EcdhKeyGenBuilder().generateKeyPair(EcdhCurveSpec.P256);
+ * }
+ * + *

+ * Import of existing keys is not supported by this builder. Instead, use + * {@link EcdsaPublicKeyBuilder} and {@link EcdsaPrivateKeyBuilder} together + * with {@code EcdsaPublicKeySpec} or {@code EcdsaPrivateKeySpec} when importing + * encoded elliptic curve keys. + *

+ * + * @since 1.0 + */ +public final class EcdhKeyGenBuilder implements AsymmetricKeyBuilder { + /** + * Generates a new elliptic curve key pair for use in ECDH key agreement. + * + *

+ * The curve is determined from the provided {@link EcdhCurveSpec}. Internally, + * a standard JCA {@link KeyPairGenerator} for {@code "EC"} is initialized with + * the corresponding {@link ECGenParameterSpec}. + *

+ * + * @param spec curve specification selecting the named curve + * @return a freshly generated {@link KeyPair} suitable for ECDH + * @throws GeneralSecurityException if the curve is not supported by the + * underlying JCA provider or if key generation + * fails + */ + @Override + public KeyPair generateKeyPair(EcdhCurveSpec spec) throws GeneralSecurityException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(new ECGenParameterSpec(spec.curveName())); + return kpg.generateKeyPair(); + } + + /** + * Unsupported operation for this builder. + * + *

+ * Importing existing ECDH public keys should be performed via + * {@link EcdsaPublicKeyBuilder} with an {@link EcdsaPublicKeySpec}. This method + * will always throw an {@link UnsupportedOperationException}. + *

+ * + * @param spec ignored + * @return never returns normally + * @throws UnsupportedOperationException always + */ + @Override + public java.security.PublicKey importPublic(EcdhCurveSpec spec) { + throw new UnsupportedOperationException("Use EcdhPublicKeySpec with EcdsaPublicKeyBuilder."); + } + + /** + * Unsupported operation for this builder. + * + *

+ * Importing existing ECDH private keys should be performed via + * {@link EcdsaPrivateKeyBuilder} with an {@link EcdsaPrivateKeySpec}. This + * method will always throw an {@link UnsupportedOperationException}. + *

+ * + * @param spec ignored + * @return never returns normally + * @throws UnsupportedOperationException always + */ + @Override + public java.security.PrivateKey importPrivate(EcdhCurveSpec spec) { + throw new UnsupportedOperationException("Use EcdhPrivateKeySpec with EcdsaPrivateKeyBuilder."); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ecdh/package-info.java b/lib/src/main/java/zeroecho/core/alg/ecdh/package-info.java new file mode 100644 index 0000000..75dbd4f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdh/package-info.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Elliptic Curve Diffie-Hellman (ECDH) key agreement integration. + * + *

+ * This package provides the ECDH algorithm descriptor, named-curve selection, + * and a key-pair builder backed by the JCA. It wires ECDH into the core + * agreement SPI through a generic JCA-based agreement context and reuses EC key + * importers shared with ECDSA where appropriate. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register a canonical ECDH algorithm and declare its agreement + * capability.
  • + *
  • Expose a curve specification for standard NIST curves suitable for + * ECDH.
  • + *
  • Provide a key-pair builder that generates EC keys on the selected curve + * using JCA primitives.
  • + *
  • Reuse EC public/private key importers to avoid duplication.
  • + *
+ * + *

Components

+ *
    + *
  • EcdhAlgorithm: registers the {@code "ECDH"} algorithm, binds the + * agreement capability to a JCA-backed context, and wires EC key + * builders/importers.
  • + *
  • EcdhCurveSpec: immutable enum of named curves providing the JCA + * curve name and auxiliary metadata.
  • + *
  • EcdhKeyGenBuilder: generates EC key pairs for ECDH on the + * requested curve.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and thread-safe; agreement contexts + * they produce are stateful and not thread-safe.
  • + *
  • Key import paths are shared between ECDH and ECDSA, as the encoding and + * curve parameters are identical at the key level.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.ecdh; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java new file mode 100644 index 0000000..b6c6c48 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaAlgorithm.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * 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.core.alg.ecdsa; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.CryptoCatalog; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.sig.GenericJcaSignatureContext; +import zeroecho.core.context.SignatureContext; + +/** + *

Elliptic Curve Digital Signature Algorithm (ECDSA)

+ * + * Implementation of the ECDSA digital signature scheme within the ZeroEcho + * cryptographic framework. This algorithm supports signing and verification + * using elliptic curve keys defined by {@link EcdsaCurveSpec}. + * + *

Roles

+ *
    + *
  • {@link KeyUsage#SIGN} - Create digital signatures using a + * {@link PrivateKey} on a selected curve.
  • + *
  • {@link KeyUsage#VERIFY} - Verify digital signatures using a + * {@link PublicKey} on the same curve.
  • + *
+ * + *

Key builders

+ *
    + *
  • {@link EcdsaCurveSpec} - Used to generate a new key pair for a chosen + * curve.
  • + *
  • {@link EcdsaPublicKeySpec} - Used to import an existing ECDSA public + * key.
  • + *
  • {@link EcdsaPrivateKeySpec} - Used to import an existing ECDSA private + * key.
  • + *
+ * + *

Provider model

This implementation leverages the Java Cryptography + * Architecture (JCA) with algorithm identifiers supplied by + * {@link EcdsaCurveSpec#jcaFactory()}. The {@link GenericJcaSignatureContext} + * is used internally to provide streaming-friendly signing and verification + * with strict enforcement of fixed-length signatures. + * + *

Default configuration

If no curve specification is explicitly + * provided, the default is {@link EcdsaCurveSpec#P256}. + * + *
{@code
+ * // Example: Sign and verify with ECDSA/P-256
+ * KeyPair kp = CryptoAlgorithms.keyPair("ECDSA", EcdsaCurveSpec.P256);
+ * SignatureContext signer = CryptoAlgorithms.create("ECDSA", KeyUsage.SIGN, kp.getPrivate(), EcdsaCurveSpec.P256);
+ * SignatureContext verifier = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), EcdsaCurveSpec.P256);
+ * }
+ * + * @since 1.0 + */ +public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm { + /** + * Constructs a new ECDSA algorithm instance and registers its capabilities: + *
    + *
  • {@link KeyUsage#SIGN} with {@link PrivateKey} and + * {@link EcdsaCurveSpec}
  • + *
  • {@link KeyUsage#VERIFY} with {@link PublicKey} and + * {@link EcdsaCurveSpec}
  • + *
  • Asymmetric key builders for {@link EcdsaCurveSpec}, + * {@link EcdsaPublicKeySpec}, and {@link EcdsaPrivateKeySpec}
  • + *
+ * + *

+ * On construction, the algorithm declares its supported roles and registers + * builders with the {@link CryptoAlgorithm} infrastructure so they can be + * discovered by the {@link CryptoCatalog} or invoked through + * {@link CryptoAlgorithms} convenience methods. + *

+ */ + public EcdsaAlgorithm() { + super("ECDSA", "ECDSA"); + + // SIGN + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.SIGN, SignatureContext.class, PrivateKey.class, + EcdsaCurveSpec.class, (PrivateKey k, EcdsaCurveSpec s) -> { + try { + return new GenericJcaSignatureContext(this, k, + GenericJcaSignatureContext.jcaFactory(s.jcaFactory(), null), + GenericJcaSignatureContext.SignLengthResolver.fixed(s.signFixedLength())); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Cannot init " + s.jcaFactory() + " signer", e); + } + }, () -> EcdsaCurveSpec.P256); + + // VERIFY + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.VERIFY, SignatureContext.class, PublicKey.class, + EcdsaCurveSpec.class, (PublicKey k, EcdsaCurveSpec s) -> { + try { + return new GenericJcaSignatureContext(this, k, + GenericJcaSignatureContext.jcaFactory(s.jcaFactory(), null), + GenericJcaSignatureContext.VerifyLengthResolver.fixed(s.signFixedLength())); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Cannot init " + s.jcaFactory() + " verifier", e); + } + }, () -> EcdsaCurveSpec.P256); + + registerAsymmetricKeyBuilder(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(), () -> EcdsaCurveSpec.P256); + registerAsymmetricKeyBuilder(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder(), null); + registerAsymmetricKeyBuilder(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder(), null); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaCurveSpec.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaCurveSpec.java new file mode 100644 index 0000000..14d236a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaCurveSpec.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ecdsa; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spec.ContextSpec; + +/** + *

ECDSA Curve Specifications

+ * + * Enumeration of supported elliptic curve domain parameters for the + * {@link EcdsaAlgorithm}. Each value binds a named curve, its associated JCA + * signature algorithm string, and the fixed-length of produced signatures in + * bytes (P1363 format). + * + *

Curves

+ *
    + *
  • {@link #P256} - NIST P-256 (secp256r1), 64-byte signatures, SHA-256 + * digest binding.
  • + *
  • {@link #P384} - NIST P-384 (secp384r1), 96-byte signatures, SHA-384 + * digest binding.
  • + *
  • {@link #P512} - NIST P-521 (secp521r1), 132-byte signatures, SHA-512 + * digest binding.
  • + *
+ * + *

Usage

The specification is used both as a {@link ContextSpec} for + * signature contexts and as an {@link AlgorithmKeySpec} for key pair generation + * or import. It ensures consistent algorithm selection and enforces + * deterministic signature lengths. + * + *
{@code
+ * // Example: select curve specification
+ * EcdsaCurveSpec spec = EcdsaCurveSpec.P256;
+ * String curve = spec.curveName(); // "secp256r1"
+ * String jcaAlg = spec.jcaFactory(); // "SHA256withECDSAinP1363Format"
+ * int sigLen = spec.signFixedLength(); // 64
+ * }
+ * + * @since 1.0 + */ +public enum EcdsaCurveSpec implements ContextSpec, AlgorithmKeySpec, Describable { + /** NIST P-256 (secp256r1), SHA-256 binding, 64-byte signatures. */ + P256("secp256r1", "SHA256withECDSAinP1363Format", 64), + /** NIST P-384 (secp384r1), SHA-384 binding, 96-byte signatures. */ + P384("secp384r1", "SHA384withECDSAinP1363Format", 96), + /** NIST P-521 (secp521r1), SHA-512 binding, 132-byte signatures. */ + P512("secp521r1", "SHA512withECDSAinP1363Format", 132); + + private final String curveName; + private final String jca; + private final int tagLen; + + /** + * Constructs a curve specification. + * + * @param curveName canonical JCA curve name (e.g., "secp256r1") + * @param jca JCA algorithm identifier for signature operations + * @param tagLen fixed signature length in bytes (P1363 format) + */ + EcdsaCurveSpec(String curveName, String jca, int tagLen) { + this.curveName = curveName; + this.jca = jca; + this.tagLen = tagLen; + } + + /** + * Returns the canonical JCA curve name. + * + * @return curve name string (e.g., {@code "secp256r1"}) + */ + public String curveName() { + return curveName; + } + + /** + * Returns the JCA algorithm identifier for this curve. + * + * @return JCA signature algorithm string + */ + public String jcaFactory() { + return jca; + } + + /** + * Returns the fixed signature length for this curve in bytes. + * + *

+ * Lengths follow the IEEE P1363 format (raw concatenation of R and S). + *

+ * + * @return signature length in bytes + */ + public int signFixedLength() { + return tagLen; + } + + /** + * Returns a short, human-readable description of this object. + * + *

+ * The description should be concise and stable enough for logging or display + * purposes, while avoiding exposure of any sensitive information. + *

+ * + * @return non-null descriptive string + */ + @Override + public String description() { + return curveName; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java new file mode 100644 index 0000000..192f757 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaKeyGenBuilder.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ecdsa; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.spec.ECGenParameterSpec; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

ECDSA Key Pair Generator

+ * + * Implementation of {@link AsymmetricKeyBuilder} for {@link EcdsaCurveSpec}. + * This builder is responsible for generating new elliptic curve key pairs for + * use with the {@link EcdsaAlgorithm}. + * + *

Supported operations

+ *
    + *
  • {@link #generateKeyPair(EcdsaCurveSpec)} - create a fresh key pair for + * the given named curve.
  • + *
  • {@link #importPublic(EcdsaCurveSpec)} - unsupported; use + * {@link EcdsaPublicKeySpec} with {@link EcdsaPublicKeyBuilder} instead.
  • + *
  • {@link #importPrivate(EcdsaCurveSpec)} - unsupported; use + * {@link EcdsaPrivateKeySpec} with {@link EcdsaPrivateKeyBuilder} instead.
  • + *
+ * + *

Usage

Typically accessed indirectly through + * {@link CryptoAlgorithms#keyPair(String, zeroecho.core.spec.AlgorithmKeySpec)} + * or + * {@link CryptoAlgorithm#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec)}. + * + *
{@code
+ * // Example: Generate an ECDSA P-256 key pair
+ * EcdsaCurveSpec spec = EcdsaCurveSpec.P256;
+ * KeyPair kp = new EcdsaKeyGenBuilder().generateKeyPair(spec);
+ * }
+ * + * @since 1.0 + */ +public final class EcdsaKeyGenBuilder implements AsymmetricKeyBuilder { + /** + * Generates a new elliptic curve key pair for the given curve specification. + * + *

+ * Internally uses the JCA {@link KeyPairGenerator} with the {@code "EC"} + * algorithm and initializes it with an {@link ECGenParameterSpec} corresponding + * to the curve name (e.g., {@code "secp256r1"}). + *

+ * + * @param spec the curve specification to use + * @return newly generated EC {@link KeyPair} + * @throws GeneralSecurityException if the curve is not supported by the + * provider + */ + @Override + public KeyPair generateKeyPair(EcdsaCurveSpec spec) throws GeneralSecurityException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(new ECGenParameterSpec(spec.curveName())); + return kpg.generateKeyPair(); + } + + /** + * Unsupported operation for this builder. + * + *

+ * Public key import should be performed using {@link EcdsaPublicKeySpec} and + * {@link EcdsaPublicKeyBuilder}. + *

+ * + * @param spec unused curve specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public java.security.PublicKey importPublic(EcdsaCurveSpec spec) { + throw new UnsupportedOperationException("Use EcdsaPublicKeySpec with EcdsaPublicKeyBuilder."); + } + + /** + * Unsupported operation for this builder. + * + *

+ * Private key import should be performed using {@link EcdsaPrivateKeySpec} and + * {@link EcdsaPrivateKeyBuilder}. + *

+ * + * @param spec unused curve specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public java.security.PrivateKey importPrivate(EcdsaCurveSpec spec) { + throw new UnsupportedOperationException("Use EcdsaPrivateKeySpec with EcdsaPrivateKeyBuilder."); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java new file mode 100644 index 0000000..4f17cbc --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeyBuilder.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * 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.core.alg.ecdsa; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

ECDSA Private Key Builder

+ * + * Implementation of {@link AsymmetricKeyBuilder} for + * {@link EcdsaPrivateKeySpec}. This builder is responsible for importing ECDSA + * private keys from encoded representations. + * + *

Supported operations

+ *
    + *
  • {@link #importPrivate(EcdsaPrivateKeySpec)} - construct a + * {@link PrivateKey} instance from a PKCS#8 encoded key.
  • + *
  • {@link #generateKeyPair(EcdsaPrivateKeySpec)} - unsupported; use + * {@link EcdsaKeyGenBuilder} instead.
  • + *
  • {@link #importPublic(EcdsaPrivateKeySpec)} - unsupported; use + * {@link EcdsaPublicKeySpec} with {@link EcdsaPublicKeyBuilder} instead.
  • + *
+ * + *

Encoding

The {@link EcdsaPrivateKeySpec} stores the private key in + * PKCS#8 DER format. This builder delegates to a JCA {@link KeyFactory} for the + * {@code "EC"} algorithm to reconstruct a usable {@link PrivateKey}. + * + *

Usage

Typically accessed indirectly through + * {@link CryptoAlgorithms#privateKey(String, zeroecho.core.spec.AlgorithmKeySpec)} + * or + * {@link CryptoAlgorithm#importPrivate(zeroecho.core.spec.AlgorithmKeySpec)}. + * + *
{@code
+ * // Example: Import an ECDSA private key
+ * byte[] pkcs8 = ...; // load PKCS#8 DER data
+ * EcdsaPrivateKeySpec spec = new EcdsaPrivateKeySpec(pkcs8);
+ * PrivateKey priv = new EcdsaPrivateKeyBuilder().importPrivate(spec);
+ * }
+ * + * @since 1.0 + */ +public final class EcdsaPrivateKeyBuilder implements AsymmetricKeyBuilder { + /** + * Unsupported operation for this builder. + * + *

+ * ECDSA key pair generation should be performed using + * {@link EcdsaKeyGenBuilder}, not from a private key specification. + *

+ * + * @param spec unused private key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public java.security.KeyPair generateKeyPair(EcdsaPrivateKeySpec spec) { + throw new UnsupportedOperationException("Use EcdsaKeyGenBuilder for keypair generation."); + } + + /** + * Unsupported operation for this builder. + * + *

+ * Public key import should be performed using {@link EcdsaPublicKeySpec} with + * {@link EcdsaPublicKeyBuilder}. + *

+ * + * @param spec unused private key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public java.security.PublicKey importPublic(EcdsaPrivateKeySpec spec) { + throw new UnsupportedOperationException("Use EcdsaPublicKeySpec with EcdsaPublicKeyBuilder."); + } + + /** + * Imports a private key from a PKCS#8 encoded specification. + * + *

+ * Uses a JCA {@link KeyFactory} instance for {@code "EC"} to parse the given + * {@link EcdsaPrivateKeySpec} and construct a {@link PrivateKey}. + *

+ * + * @param spec private key specification containing PKCS#8 DER encoding + * @return reconstructed {@link PrivateKey} instance + * @throws GeneralSecurityException if the encoding is invalid or the key cannot + * be reconstructed + */ + @Override + public PrivateKey importPrivate(EcdsaPrivateKeySpec spec) throws GeneralSecurityException { + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeySpec.java new file mode 100644 index 0000000..a793677 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPrivateKeySpec.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * 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.core.alg.ecdsa; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

ECDSA Private Key Specification

+ * + * An immutable wrapper around a PKCS#8-encoded ECDSA private key. This + * specification is used by {@link EcdsaPrivateKeyBuilder} to import keys into + * the JCA {@link java.security.PrivateKey} representation. + * + *

Encoding

+ *
    + *
  • Keys are stored internally in PKCS#8 DER format.
  • + *
  • Instances are defensive: the internal byte array is cloned on + * construction and on every access.
  • + *
  • For serialization, {@link #marshal(EcdsaPrivateKeySpec)} encodes the + * PKCS#8 bytes in Base64 without padding.
  • + *
+ * + *

Usage

{@code
+ * // Construct from PKCS#8 DER
+ * byte[] pkcs8 = ...; // load from file or keystore
+ * EcdsaPrivateKeySpec spec = new EcdsaPrivateKeySpec(pkcs8);
+ *
+ * // Import into a PrivateKey
+ * PrivateKey priv = new EcdsaPrivateKeyBuilder().importPrivate(spec);
+ *
+ * // Marshal/unmarshal for transport or storage
+ * PairSeq seq = EcdsaPrivateKeySpec.marshal(spec);
+ * EcdsaPrivateKeySpec restored = EcdsaPrivateKeySpec.unmarshal(seq);
+ * }
+ * + * @since 1.0 + */ +public final class EcdsaPrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Creates a new private key specification from a PKCS#8 encoded byte array. + * + * @param pkcs8 PKCS#8 DER-encoded private key (must not be {@code null}) + * @throws IllegalArgumentException if {@code pkcs8} is {@code null} + */ + public EcdsaPrivateKeySpec(byte[] pkcs8) { + if (pkcs8 == null) { + throw new IllegalArgumentException("pkcs8 must not be null"); + } + this.pkcs8 = pkcs8.clone(); + } + + /** + * Returns a defensive copy of the encoded PKCS#8 data. + * + * @return cloned PKCS#8 byte array + */ + public byte[] encoded() { + return pkcs8.clone(); + } + + /** + * Serializes this specification into a {@link PairSeq}. + * + *

+ * The PKCS#8 data is Base64-encoded (without padding) and emitted with the key + * {@code "pkcs8.b64"}. + *

+ * + * @param spec private key spec to marshal + * @return serialized representation in key-value form + */ + public static PairSeq marshal(EcdsaPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "ECDSA-PRIV", PKCS8_B64, b64); + } + + /** + * Reconstructs a private key specification from a {@link PairSeq}. + * + *

+ * The sequence must contain a {@code "pkcs8.b64"} field with Base64-encoded + * PKCS#8 DER data. If missing, an {@link IllegalArgumentException} is thrown. + *

+ * + * @param p serialized key-value sequence + * @return reconstructed {@code EcdsaPrivateKeySpec} + * @throws IllegalArgumentException if {@code pkcs8.b64} is absent + */ + public static EcdsaPrivateKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (PKCS8_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for ECDSA private key"); + } + return new EcdsaPrivateKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java new file mode 100644 index 0000000..7912c42 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeyBuilder.java @@ -0,0 +1,133 @@ +/******************************************************************************* + * 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.core.alg.ecdsa; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

ECDSA Public Key Builder

+ * + * Implementation of {@link AsymmetricKeyBuilder} for + * {@link EcdsaPublicKeySpec}. This builder is responsible for importing ECDSA + * public keys from X.509 SubjectPublicKeyInfo encodings. + * + *

Supported operations

+ *
    + *
  • {@link #importPublic(EcdsaPublicKeySpec)} - construct a {@link PublicKey} + * instance from an X.509-encoded key.
  • + *
  • {@link #generateKeyPair(EcdsaPublicKeySpec)} - unsupported; use + * {@link EcdsaKeyGenBuilder} instead.
  • + *
  • {@link #importPrivate(EcdsaPublicKeySpec)} - unsupported; use + * {@link EcdsaPrivateKeySpec} with {@link EcdsaPrivateKeyBuilder} instead.
  • + *
+ * + *

Encoding

The {@link EcdsaPublicKeySpec} stores the public key in + * standard X.509 DER format. This builder delegates to a JCA {@link KeyFactory} + * for the {@code "EC"} algorithm to reconstruct a usable {@link PublicKey}. + * + *

Usage

Typically accessed indirectly through + * {@link CryptoAlgorithms#publicKey(String, zeroecho.core.spec.AlgorithmKeySpec)} + * or {@link CryptoAlgorithm#importPublic(zeroecho.core.spec.AlgorithmKeySpec)}. + * + *
{@code
+ * // Example: Import an ECDSA public key
+ * byte[] x509 = ...; // load SubjectPublicKeyInfo DER data
+ * EcdsaPublicKeySpec spec = new EcdsaPublicKeySpec(x509);
+ * PublicKey pub = new EcdsaPublicKeyBuilder().importPublic(spec);
+ * }
+ * + * @since 1.0 + */ +public final class EcdsaPublicKeyBuilder implements AsymmetricKeyBuilder { + /** + * Unsupported operation for this builder. + * + *

+ * ECDSA key pair generation should be performed using + * {@link EcdsaKeyGenBuilder}, not from a public key specification. + *

+ * + * @param spec unused public key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public java.security.KeyPair generateKeyPair(EcdsaPublicKeySpec spec) { + throw new UnsupportedOperationException("Use EcdsaKeyGenBuilder for keypair generation."); + } + + /** + * Imports a public key from an X.509 SubjectPublicKeyInfo specification. + * + *

+ * Uses a JCA {@link KeyFactory} instance for {@code "EC"} to parse the given + * {@link EcdsaPublicKeySpec} and construct a {@link PublicKey}. + *

+ * + * @param spec public key specification containing X.509 DER encoding + * @return reconstructed {@link PublicKey} instance + * @throws GeneralSecurityException if the encoding is invalid or the key cannot + * be reconstructed + */ + @Override + public PublicKey importPublic(EcdsaPublicKeySpec spec) throws GeneralSecurityException { + KeyFactory kf = KeyFactory.getInstance("EC"); + return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); + } + + /** + * Unsupported operation for this builder. + * + *

+ * Private key import should be performed using {@link EcdsaPrivateKeySpec} with + * {@link EcdsaPrivateKeyBuilder}. + *

+ * + * @param spec unused public key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown + */ + @Override + public java.security.PrivateKey importPrivate(EcdsaPublicKeySpec spec) { + throw new UnsupportedOperationException("Use EcdsaPrivateKeySpec with EcdsaPrivateKeyBuilder."); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeySpec.java new file mode 100644 index 0000000..d0c297a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/EcdsaPublicKeySpec.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ecdsa; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

ECDSA Public Key Specification

+ * + * An immutable wrapper around an X.509-encoded ECDSA public key. This + * specification is used by {@link EcdsaPublicKeyBuilder} to import keys into + * the JCA {@link java.security.PublicKey} representation. + * + *

Encoding

+ *
    + *
  • Keys are stored internally in X.509 SubjectPublicKeyInfo DER format.
  • + *
  • Instances are defensive: the internal byte array is cloned on + * construction and on every access.
  • + *
  • For serialization, {@link #marshal(EcdsaPublicKeySpec)} encodes the X.509 + * bytes in Base64 without padding.
  • + *
+ * + *

Usage

{@code
+ * // Construct from X.509 DER
+ * byte[] x509 = ...; // load from certificate or key file
+ * EcdsaPublicKeySpec spec = new EcdsaPublicKeySpec(x509);
+ *
+ * // Import into a PublicKey
+ * PublicKey pub = new EcdsaPublicKeyBuilder().importPublic(spec);
+ *
+ * // Marshal/unmarshal for transport or storage
+ * PairSeq seq = EcdsaPublicKeySpec.marshal(spec);
+ * EcdsaPublicKeySpec restored = EcdsaPublicKeySpec.unmarshal(seq);
+ * }
+ * + * @since 1.0 + */ +public final class EcdsaPublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Creates a new public key specification from an X.509 encoded byte array. + * + * @param x509 X.509 SubjectPublicKeyInfo DER-encoded public key (must not be + * {@code null}) + * @throws IllegalArgumentException if {@code x509} is {@code null} + */ + public EcdsaPublicKeySpec(byte[] x509) { + if (x509 == null) { + throw new IllegalArgumentException("x509 must not be null"); + } + this.x509 = x509.clone(); + } + + /** + * Returns a defensive copy of the encoded X.509 data. + * + * @return cloned X.509 byte array + */ + public byte[] encoded() { + return x509.clone(); + } + + /** + * Serializes this specification into a {@link PairSeq}. + * + *

+ * The X.509 data is Base64-encoded (without padding) and emitted with the key + * {@code "x509.b64"}. + *

+ * + * @param spec public key spec to marshal + * @return serialized representation in key-value form + */ + public static PairSeq marshal(EcdsaPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "ECDSA-PUB", X509_B64, b64); + } + + /** + * Reconstructs a public key specification from a {@link PairSeq}. + * + *

+ * The sequence must contain a {@code "x509.b64"} field with Base64-encoded + * X.509 DER data. If missing, an {@link IllegalArgumentException} is thrown. + *

+ * + * @param p serialized key-value sequence + * @return reconstructed {@code EcdsaPublicKeySpec} + * @throws IllegalArgumentException if {@code x509.b64} is absent + */ + public static EcdsaPublicKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (X509_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("x509.b64 missing for ECDSA public key"); + } + return new EcdsaPublicKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java b/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java new file mode 100644 index 0000000..e71dca6 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ecdsa/package-info.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Elliptic Curve Digital Signature Algorithm (ECDSA) integration. + * + *

+ * This package provides the ECDSA algorithm descriptor, curve specifications, + * key builders for generation and import, and immutable encoded key specs. It + * wires ECDSA into the core signature SPI through a JCA-backed streaming + * signature context that enforces fixed signature lengths. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register ECDSA under a canonical identifier and declare its + * {@link zeroecho.core.KeyUsage#SIGN} and {@link zeroecho.core.KeyUsage#VERIFY} + * roles.
  • + *
  • Expose curve specifications for NIST P-256, P-384, and P-521 with + * canonical curve names, JCA algorithm identifiers, and fixed tag lengths.
  • + *
  • Provide key builders for generating EC key pairs and importing + * X.509/PKCS#8 encodings.
  • + *
  • Adapt JCA {@link java.security.Signature} engines to the core streaming + * context model with strict length enforcement.
  • + *
+ * + *

Components

+ *
    + *
  • EcdsaAlgorithm: registers the algorithm, declares sign/verify + * capabilities, and wires EC key builders and specs.
  • + *
  • EcdsaCurveSpec: enum of supported curves (P-256, P-384, P-521) + * with curve names, JCA identifiers, and deterministic signature lengths.
  • + *
  • EcdsaKeyGenBuilder: generates EC key pairs for the chosen curve + * using {@link java.security.KeyPairGenerator} with + * {@link java.security.spec.ECGenParameterSpec}.
  • + *
  • EcdsaPublicKeyBuilder and EcdsaPrivateKeyBuilder: import + * keys from X.509 and PKCS#8 encodings via + * {@link java.security.KeyFactory}.
  • + *
  • EcdsaPublicKeySpec and EcdsaPrivateKeySpec: immutable + * wrappers around encoded keys with marshalling support.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and thread-safe; signature contexts + * are stateful and not thread-safe.
  • + *
  • Unsupported directions in key builders (e.g., generating from an import + * spec) fail fast with clear exceptions.
  • + *
  • Signatures are always encoded in IEEE P1363 format (R and S concatenated) + * with deterministic lengths.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.ecdsa; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java new file mode 100644 index 0000000..5d0986f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519Algorithm.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ed25519; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.spec.VoidSpec; + +/** + *

Ed25519 Digital Signature Algorithm

+ * + * Implementation of the Edwards-curve Digital Signature Algorithm (Ed25519). + * This class integrates Ed25519 into the ZeroEcho cryptographic framework as an + * {@link AbstractCryptoAlgorithm}. + * + *

+ * Ed25519 is a modern, high-security, high-performance signature scheme with + * fixed-size keys and signatures. It is resistant to a wide class of + * implementation pitfalls and side-channel attacks, making it a recommended + * choice for new protocols. + *

+ * + *

Capabilities

+ *
    + *
  • {@link KeyUsage#SIGN}: Creates a {@link SignatureContext} bound to a + * {@link PrivateKey}. This context produces 64-byte Ed25519 signatures.
  • + *
  • {@link KeyUsage#VERIFY}: Creates a {@link SignatureContext} bound to a + * {@link PublicKey}. This context verifies 64-byte Ed25519 signatures.
  • + *
+ * + *

Key material

+ *
    + *
  • Generation via {@link Ed25519KeyGenSpec} and + * {@link Ed25519KeyGenBuilder}.
  • + *
  • Import of public keys via {@link Ed25519PublicKeySpec} and + * {@link Ed25519PublicKeyBuilder}.
  • + *
  • Import of private keys via {@link Ed25519PrivateKeySpec} and + * {@link Ed25519PrivateKeyBuilder}.
  • + *
+ * + *

Thread-safety

This algorithm definition is immutable and safe to + * share across threads. The created {@link SignatureContext} instances are not + * guaranteed to be thread-safe. + * + * @since 1.0 + */ +public final class Ed25519Algorithm extends AbstractCryptoAlgorithm { + /** + * Constructs the Ed25519 algorithm definition and registers its roles and key + * builders. + * + *

+ * This includes: + *

+ *
    + *
  • Binding SIGN (with {@link PrivateKey}) to + * {@link Ed25519SignatureContext}.
  • + *
  • Binding VERIFY (with {@link PublicKey}) to + * {@link Ed25519SignatureContext}.
  • + *
  • Registering key builders for generation and import of Ed25519 key + * material.
  • + *
+ */ + public Ed25519Algorithm() { + super("Ed25519", "Ed25519"); + + // SIGN (private key) + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.SIGN, SignatureContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> { + try { + return new Ed25519SignatureContext(this, k); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Cannot init Ed25519 signer", e); + } + }, () -> VoidSpec.INSTANCE); + + // VERIFY (public key) + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.VERIFY, SignatureContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> { + try { + return new Ed25519SignatureContext(this, k); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Cannot init Ed25519 verifier", e); + } + }, () -> VoidSpec.INSTANCE); + + // Key builders + registerAsymmetricKeyBuilder(Ed25519KeyGenSpec.class, new Ed25519KeyGenBuilder(), + Ed25519KeyGenSpec::defaultSpec); + registerAsymmetricKeyBuilder(Ed25519PublicKeySpec.class, new Ed25519PublicKeyBuilder(), null); + registerAsymmetricKeyBuilder(Ed25519PrivateKeySpec.class, new Ed25519PrivateKeyBuilder(), null); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java new file mode 100644 index 0000000..6e526c5 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenBuilder.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ed25519; + +import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder; + +/** + *

Key-pair builder for Ed25519

+ * + * Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} implementation for + * generating Ed25519 key pairs. + * + *

+ * This builder delegates to the JCA provider under the canonical algorithm name + * {@code "Ed25519"}. It is registered by {@link Ed25519Algorithm} to support + * key generation from an {@link Ed25519KeyGenSpec}. + *

+ * + *

Usage example

{@code
+ * // Generate a new Ed25519 key pair with default parameters
+ * Ed25519KeyGenSpec spec = Ed25519KeyGenSpec.defaultSpec();
+ * KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", spec);
+ * }
+ * + *

Thread-safety

Instances of this builder are stateless and may be + * reused safely across threads. + * + * @since 1.0 + */ +public final class Ed25519KeyGenBuilder extends AbstractEdDSAKeyGenBuilder { + + /** + * Returns the canonical JCA algorithm name for Ed25519 key-pair generation. + * + *

+ * This method is invoked by the parent {@link AbstractEdDSAKeyGenBuilder} to + * construct a {@code KeyPairGenerator}. + *

+ * + * @return the string {@code "Ed25519"} + */ + @Override + protected String jcaKeyPairAlg() { + return "Ed25519"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java new file mode 100644 index 0000000..cf66744 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519KeyGenSpec.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ed25519; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Specification for Ed25519 key-pair generation

+ * + * Marker {@link zeroecho.core.spec.AlgorithmKeySpec} used to request generation + * of Ed25519 key pairs. + * + *

+ * Ed25519 has no tunable domain parameters (e.g., key size or curve options). + * Therefore, this spec acts as a simple token to identify the algorithm when + * invoking key generation. All instances are equivalent; applications should + * typically use {@link #defaultSpec()}. + *

+ * + *

Usage example

{@code
+ * // Generate a new Ed25519 key pair using the default spec
+ * KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec());
+ * }
+ * + *

Thread-safety

The default spec instance is immutable and safe to + * reuse across threads. + * + * @since 1.0 + */ +public final class Ed25519KeyGenSpec implements AlgorithmKeySpec { + private static final Ed25519KeyGenSpec DEFAULT = new Ed25519KeyGenSpec(); + + /** + * Returns the canonical, shared default spec instance for Ed25519 key + * generation. + * + *

+ * Since Ed25519 has no configurable parameters, this singleton should be used + * for all generation requests. Applications may still construct additional + * instances, but they are functionally identical. + *

+ * + * @return the default Ed25519 key generation spec + */ + public static Ed25519KeyGenSpec defaultSpec() { + return DEFAULT; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java new file mode 100644 index 0000000..b997f88 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeyBuilder.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * 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.core.alg.ed25519; + +import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder; + +/** + *

Private key builder for Ed25519

+ * + * Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} for importing and + * wrapping Ed25519 private keys. + * + *

+ * This builder integrates with the JCA under the canonical key factory + * algorithm name {@code "Ed25519"}. It consumes an + * {@link Ed25519PrivateKeySpec}, which provides the encoded PKCS#8 + * representation of the private key. + *

+ * + *

Responsibilities

+ *
    + *
  • Expose the JCA key factory name {@code "Ed25519"} for + * interoperability.
  • + *
  • Provide the encoded PKCS#8 bytes via + * {@link Ed25519PrivateKeySpec#encoded()} for key material import.
  • + *
+ * + *

Usage example

{@code
+ * // Import a private key from its encoded PKCS#8 form
+ * Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes);
+ * PrivateKey privateKey = CryptoAlgorithms.privateKey("Ed25519", spec);
+ * }
+ * + *

Thread-safety

Instances of this builder are stateless and may be + * reused safely across threads. + * + * @since 1.0 + */ +public final class Ed25519PrivateKeyBuilder extends AbstractEncodedPrivateKeyBuilder { + /** + * Returns the canonical JCA algorithm name for Ed25519 key factories. + * + *

+ * This value is used by the parent {@link AbstractEncodedPrivateKeyBuilder} to + * obtain a {@code KeyFactory}. + *

+ * + * @return the string {@code "Ed25519"} + */ + @Override + protected String jcaKeyFactoryAlg() { + return "Ed25519"; + } + + /** + * Returns the PKCS#8 encoded bytes from the given key spec. + * + *

+ * This encoding is passed to the JCA {@code KeyFactory} for parsing into a + * {@link java.security.PrivateKey} instance. + *

+ * + * @param spec the private key specification holding the PKCS#8 encoding + * @return a defensive copy of the PKCS#8-encoded key bytes + */ + @Override + protected byte[] encodedPkcs8(final Ed25519PrivateKeySpec spec) { + return spec.encoded(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java new file mode 100644 index 0000000..41daf55 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PrivateKeySpec.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * 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.core.alg.ed25519; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Specification for Ed25519 private keys

+ * + * {@link zeroecho.core.spec.AlgorithmKeySpec} representing an Ed25519 private + * key in its encoded PKCS#8 form. + * + *

+ * This spec is used by {@link Ed25519PrivateKeyBuilder} and related APIs to + * import and wrap Ed25519 private keys. The encoding is expected to conform to + * the PKCS#8 standard as produced by standard JCA key factories or external + * tooling. + *

+ * + *

Responsibilities

+ *
    + *
  • Encapsulates the PKCS#8-encoded private key bytes.
  • + *
  • Provides safe cloning to avoid exposing mutable internal state.
  • + *
  • Supports serialization to and from a compact base64-based {@link PairSeq} + * representation via {@link #marshal(Ed25519PrivateKeySpec)} and + * {@link #unmarshal(PairSeq)}.
  • + *
+ * + *

Usage example

{@code
+ * // Wrap PKCS#8 bytes in a spec
+ * Ed25519PrivateKeySpec spec = new Ed25519PrivateKeySpec(pkcs8Bytes);
+ *
+ * // Import into a PrivateKey using ZeroEcho
+ * PrivateKey priv = CryptoAlgorithms.privateKey("Ed25519", spec);
+ *
+ * // Serialize to PairSeq (e.g., for configuration or transport)
+ * PairSeq seq = Ed25519PrivateKeySpec.marshal(spec);
+ *
+ * // Reconstruct from serialized form
+ * Ed25519PrivateKeySpec restored = Ed25519PrivateKeySpec.unmarshal(seq);
+ * }
+ * + *

Thread-safety

Instances are immutable. The internal key bytes are + * defensively copied on construction and retrieval, making this class safe to + * share across threads. + * + * @since 1.0 + */ +public final class Ed25519PrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] encodedPkcs8; + + /** + * Creates a new Ed25519 private key specification from its PKCS#8 encoding. + * + * @param encodedPkcs8 PKCS#8-encoded private key bytes; must not be null + * @throws IllegalArgumentException if {@code encodedPkcs8} is null + */ + public Ed25519PrivateKeySpec(byte[] encodedPkcs8) { + if (encodedPkcs8 == null) { + throw new IllegalArgumentException("encodedPkcs8 must not be null"); + } + this.encodedPkcs8 = encodedPkcs8.clone(); + } + + /** + * Returns a defensive copy of the PKCS#8-encoded private key bytes. + * + * @return clone of the PKCS#8 encoding + */ + public byte[] encoded() { + return encodedPkcs8.clone(); + } + + /** + * Serializes the given private key spec to a base64-encoded {@link PairSeq}. + * + *

+ * The output includes a type marker ({@code "Ed25519-PRIV"}) and the field + * {@code "pkcs8.b64"} containing the base64 representation of the key. + *

+ * + * @param spec private key spec to marshal + * @return serialized representation as a {@link PairSeq} + */ + public static PairSeq marshal(Ed25519PrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8); + return PairSeq.of("type", "Ed25519-PRIV", PKCS8_B64, b64); + } + + /** + * Reconstructs a private key spec from a base64-encoded {@link PairSeq}. + * + *

+ * The sequence must contain the field {@code "pkcs8.b64"} with the base64 + * encoding of the PKCS#8 key. + *

+ * + * @param p serialized key data + * @return a new {@code Ed25519PrivateKeySpec} instance + * @throws IllegalArgumentException if the sequence does not contain + * {@code "pkcs8.b64"} + */ + public static Ed25519PrivateKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (PKCS8_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for Ed25519 private key"); + } + return new Ed25519PrivateKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java new file mode 100644 index 0000000..90ab18a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeyBuilder.java @@ -0,0 +1,102 @@ +/******************************************************************************* + * 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.core.alg.ed25519; + +import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder; + +/** + *

Public key builder for Ed25519

+ * + * Concrete {@link zeroecho.core.spi.AsymmetricKeyBuilder} for importing and + * wrapping Ed25519 public keys. + * + *

+ * This builder integrates with the JCA under the canonical key factory + * algorithm name {@code "Ed25519"}. It consumes an + * {@link Ed25519PublicKeySpec}, which provides the encoded X.509 representation + * of the public key. + *

+ * + *

Responsibilities

+ *
    + *
  • Expose the JCA key factory name {@code "Ed25519"} for + * interoperability.
  • + *
  • Provide the encoded X.509 bytes via + * {@link Ed25519PublicKeySpec#encoded()} for key material import.
  • + *
+ * + *

Usage example

{@code
+ * // Import a public key from its encoded X.509 form
+ * Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes);
+ * PublicKey publicKey = CryptoAlgorithms.publicKey("Ed25519", spec);
+ * }
+ * + *

Thread-safety

Instances of this builder are stateless and may be + * reused safely across threads. + * + * @since 1.0 + */ +public final class Ed25519PublicKeyBuilder extends AbstractEncodedPublicKeyBuilder { + /** + * Returns the canonical JCA algorithm name for Ed25519 key factories. + * + *

+ * This value is used by the parent {@link AbstractEncodedPublicKeyBuilder} to + * obtain a {@code KeyFactory}. + *

+ * + * @return the string {@code "Ed25519"} + */ + @Override + protected String jcaKeyFactoryAlg() { + return "Ed25519"; + } + + /** + * Returns the X.509 encoded bytes from the given key spec. + * + *

+ * This encoding is passed to the JCA {@code KeyFactory} for parsing into a + * {@link java.security.PublicKey} instance. + *

+ * + * @param spec the public key specification holding the X.509 encoding + * @return a defensive copy of the X.509-encoded key bytes + */ + @Override + protected byte[] encodedX509(final Ed25519PublicKeySpec spec) { + return spec.encoded(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java new file mode 100644 index 0000000..7b34125 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519PublicKeySpec.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * 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.core.alg.ed25519; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Specification for Ed25519 public keys

+ * + * {@link zeroecho.core.spec.AlgorithmKeySpec} representing an Ed25519 public + * key in its encoded X.509 form. + * + *

+ * This spec is used by {@link Ed25519PublicKeyBuilder} and related APIs to + * import and wrap Ed25519 public keys. The encoding is expected to conform to + * the X.509 SubjectPublicKeyInfo structure as produced by standard JCA key + * factories or external tooling. + *

+ * + *

Responsibilities

+ *
    + *
  • Encapsulates the X.509-encoded public key bytes.
  • + *
  • Provides safe cloning to avoid exposing mutable internal state.
  • + *
  • Supports serialization to and from a compact base64-based {@link PairSeq} + * representation via {@link #marshal(Ed25519PublicKeySpec)} and + * {@link #unmarshal(PairSeq)}.
  • + *
+ * + *

Usage example

{@code
+ * // Wrap X.509 bytes in a spec
+ * Ed25519PublicKeySpec spec = new Ed25519PublicKeySpec(x509Bytes);
+ *
+ * // Import into a PublicKey using ZeroEcho
+ * PublicKey pub = CryptoAlgorithms.publicKey("Ed25519", spec);
+ *
+ * // Serialize to PairSeq (e.g., for configuration or transport)
+ * PairSeq seq = Ed25519PublicKeySpec.marshal(spec);
+ *
+ * // Reconstruct from serialized form
+ * Ed25519PublicKeySpec restored = Ed25519PublicKeySpec.unmarshal(seq);
+ * }
+ * + *

Thread-safety

Instances are immutable. The internal key bytes are + * defensively copied on construction and retrieval, making this class safe to + * share across threads. + * + * @since 1.0 + */ +public final class Ed25519PublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] encodedX509; + + /** + * Creates a new Ed25519 public key specification from its X.509 encoding. + * + * @param encodedX509 X.509-encoded public key bytes; must not be null + * @throws IllegalArgumentException if {@code encodedX509} is null + */ + public Ed25519PublicKeySpec(byte[] encodedX509) { + if (encodedX509 == null) { + throw new IllegalArgumentException("encodedX509 must not be null"); + } + this.encodedX509 = encodedX509.clone(); + } + + /** + * Returns a defensive copy of the X.509-encoded public key bytes. + * + * @return clone of the X.509 encoding + */ + public byte[] encoded() { + return encodedX509.clone(); + } + + /** + * Serializes the given public key spec to a base64-encoded {@link PairSeq}. + * + *

+ * The output includes a type marker ({@code "Ed25519-PUB"}) and the field + * {@code "x509.b64"} containing the base64 representation of the key. + *

+ * + * @param spec public key spec to marshal + * @return serialized representation as a {@link PairSeq} + */ + public static PairSeq marshal(Ed25519PublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedX509); + return PairSeq.of("type", "Ed25519-PUB", X509_B64, b64); + } + + /** + * Reconstructs a public key spec from a base64-encoded {@link PairSeq}. + * + *

+ * The sequence must contain the field {@code "x509.b64"} with the base64 + * encoding of the X.509 key. + *

+ * + * @param p serialized key data + * @return a new {@code Ed25519PublicKeySpec} instance + * @throws IllegalArgumentException if the sequence does not contain + * {@code "x509.b64"} + */ + public static Ed25519PublicKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (X509_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("x509.b64 missing for Ed25519 public key"); + } + return new Ed25519PublicKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519SignatureContext.java b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519SignatureContext.java new file mode 100644 index 0000000..080e97b --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/Ed25519SignatureContext.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ed25519; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.alg.common.eddsa.CommonEdDSASignatureContext; +import zeroecho.core.context.SignatureContext; + +/** + * Signature context for Ed25519 with a fixed 64-byte tag length. + * + *

+ * This implementation binds the Ed25519 algorithm to {@link SignatureContext} + * via {@link CommonEdDSASignatureContext}. Internally it configures a JCA + * {@code Signature} with the {@code "Ed25519"} name and delegates all streaming + * and verification behavior to the common adapter. + *

+ * + *

Algorithm characteristics

+ *
    + *
  • JCA signature name: {@code "Ed25519"}.
  • + *
  • Fixed signature length: 64 bytes (reported by {@link #tagLength()}).
  • + *
+ * + *

Usage

+ *

One-shot API

+ * {@code
+ * // Signing
+ * PrivateKey priv = ...;
+ * SignatureContext signer =
+ *     new Ed25519SignatureContext(CryptoAlgorithms.require("Ed25519"), priv);
+ * signer.update(message);
+ * byte[] sig = signer.sign();
+ *
+ * // Verifying
+ * PublicKey pub = ...;
+ * SignatureContext verifier =
+ *     new Ed25519SignatureContext(CryptoAlgorithms.require("Ed25519"), pub);
+ * verifier.update(message);
+ * boolean ok = verifier.verify(sig);
+ * }
+ * 
+ * + *

Streaming pipeline (wrap)

+ * {@code
+ * // SIGN mode: body bytes followed by 64-byte signature trailer
+ * try (SignatureContext ctx =
+ *          new Ed25519SignatureContext(CryptoAlgorithms.require("Ed25519"), priv);
+ *      InputStream in = ctx.wrap(upstream)) {
+ *     in.transferTo(out);
+ * }
+ *
+ * // VERIFY mode: supply expected signature; verification occurs at EOF
+ * try (SignatureContext ctx =
+ *          new Ed25519SignatureContext(CryptoAlgorithms.require("Ed25519"), pub)) {
+ *     ctx.setExpectedTag(expectedSig);
+ *     try (InputStream in = ctx.wrap(bodyWithoutTrailer)) {
+ *         in.transferTo(java.io.OutputStream.nullOutputStream());
+ *     }
+ * }
+ * }
+ * 
+ * + *

Thread-safety

+ *

+ * Instances are stateful and not thread-safe. Use one context per signing or + * verification operation, and call {@code wrap(...)} at most once per instance. + *

+ * + * @since 1.0 + */ +public final class Ed25519SignatureContext extends CommonEdDSASignatureContext { + private static final String SIG_NAME = "Ed25519"; + private static final int TAG_LEN = 64; + + /** + * Constructs a signing context bound to the given private key. + * + * @param algorithm parent algorithm descriptor; must not be {@code null} + * @param privateKey Ed25519 private key; must not be {@code null} + * @throws GeneralSecurityException if the underlying JCA engine cannot be + * initialized + * @throws NullPointerException if any argument is {@code null} + */ + public Ed25519SignatureContext(final CryptoAlgorithm algorithm, final PrivateKey privateKey) + throws GeneralSecurityException { + super(algorithm, privateKey, SIG_NAME, TAG_LEN); + } + + /** + * Constructs a verification context bound to the given public key. + * + * @param algorithm parent algorithm descriptor; must not be {@code null} + * @param publicKey Ed25519 public key; must not be {@code null} + * @throws GeneralSecurityException if the underlying JCA engine cannot be + * initialized + * @throws NullPointerException if any argument is {@code null} + */ + public Ed25519SignatureContext(final CryptoAlgorithm algorithm, final PublicKey publicKey) + throws GeneralSecurityException { + super(algorithm, publicKey, SIG_NAME, TAG_LEN); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java b/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java new file mode 100644 index 0000000..eab7a99 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed25519/package-info.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Ed25519 digital signature integration. + * + *

+ * This package wires the Ed25519 signature scheme into the core layer. It + * provides the algorithm descriptor, a streaming signature context with a fixed + * tag length, builders for generating and importing keys, and compact key + * specifications for marshalling and transport. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register the Ed25519 algorithm and declare SIGN and VERIFY roles with + * fixed-length signatures.
  • + *
  • Provide a streaming signature context that adapts JCA engines and + * enforces the 64-byte tag size.
  • + *
  • Expose builders for key-pair generation and for importing encoded + * public/private keys.
  • + *
  • Define immutable key specifications suitable for safe cloning and simple + * marshalling.
  • + *
+ * + *

Components

+ *
    + *
  • Ed25519Algorithm: algorithm descriptor that binds roles to a + * signature context and registers builders for key generation and import.
  • + *
  • Ed25519SignatureContext: streaming context for signing and + * verification with a fixed 64-byte tag.
  • + *
  • Ed25519KeyGenBuilder and Ed25519KeyGenSpec: generator and + * marker spec for producing key pairs.
  • + *
  • Ed25519PublicKeyBuilder / Ed25519PrivateKeyBuilder: + * importers backed by JCA key factories.
  • + *
  • Ed25519PublicKeySpec / Ed25519PrivateKeySpec: immutable + * wrappers over X.509 and PKCS#8 encodings, with defensive copying and simple + * base64 marshalling helpers.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and thread-safe.
  • + *
  • Signature contexts are stateful and not thread-safe; create a new + * instance per operation.
  • + *
  • Key specification classes never expose internal byte arrays; cloning is + * used on input and output.
  • + *
  • Marshalling helpers use a compact key-value form intended for + * configuration, transport, and tests.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.ed25519; diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java new file mode 100644 index 0000000..01577bf --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448Algorithm.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * 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.core.alg.ed448; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.spec.VoidSpec; + +/** + *

Ed448 Digital Signature Algorithm

+ * + * Implementation of the Edwards-curve Digital Signature Algorithm over the + * Curve448 curve, commonly referred to as Ed448. + * + *

+ * Ed448 is a modern elliptic curve signature scheme standardized in + * RFC 8032. It provides + * high security margins (224-bit classical strength) and is designed for + * simplicity, determinism, and resilience against common implementation + * pitfalls such as malleability. + *

+ * + *

Roles

+ *
    + *
  • {@link KeyUsage#SIGN}: Contexts that sign messages using a private key, + * producing fixed-length Ed448 signatures.
  • + *
  • {@link KeyUsage#VERIFY}: Contexts that verify Ed448 signatures using the + * corresponding public key.
  • + *
+ * + *

Key material

+ *
    + *
  • {@link Ed448KeyGenSpec}: Generation of new key pairs with defined + * parameters.
  • + *
  • {@link Ed448PublicKeySpec}: Encoded form of Ed448 public keys (X.509 + * format).
  • + *
  • {@link Ed448PrivateKeySpec}: Encoded form of Ed448 private keys (PKCS#8 + * format).
  • + *
+ * + *

Thread-safety

Instances of this class are immutable and safe to + * reuse across threads. Context objects created through capabilities are not + * necessarily thread-safe. + * + *
{@code
+ * // Example: generate a key pair and sign data
+ * CryptoAlgorithm ed448 = new Ed448Algorithm();
+ * KeyPair kp = ed448.generateKeyPair(Ed448KeyGenSpec.defaultSpec());
+ *
+ * SignatureContext signer = ed448.create(KeyUsage.SIGN, kp.getPrivate(), null);
+ * byte[] sig = signer.sign(data);
+ *
+ * SignatureContext verifier = ed448.create(KeyUsage.VERIFY, kp.getPublic(), null);
+ * boolean ok = verifier.verify(data, sig);
+ * }
+ * + * @since 1.0 + */ +public final class Ed448Algorithm extends AbstractCryptoAlgorithm { + /** + * Constructs and wires the Ed448 algorithm definition. + * + *

+ * The constructor registers: + *

+ *
    + *
  • Roles: + *
      + *
    • {@link KeyUsage#SIGN} using {@link SignatureContext} with a + * {@link PrivateKey} and {@link VoidSpec} (no additional parameters).
    • + *
    • {@link KeyUsage#VERIFY} using {@link SignatureContext} with a + * {@link PublicKey} and {@link VoidSpec}.
    • + *
    + *
  • + *
  • Asymmetric key builders: + *
      + *
    • {@link Ed448KeyGenSpec} via {@link Ed448KeyGenBuilder}, defaulting to + * {@link Ed448KeyGenSpec#defaultSpec()}.
    • + *
    • {@link Ed448PublicKeySpec} via {@link Ed448PublicKeyBuilder}.
    • + *
    • {@link Ed448PrivateKeySpec} via {@link Ed448PrivateKeyBuilder}.
    • + *
    + *
  • + *
+ * + *

+ * No exceptions are thrown by this constructor. Any provider initialization + * needed for signing or verification is deferred to the creation of + * {@link SignatureContext} instances. + *

+ * + *
{@code
+     * // Example: instantiate and obtain a signer
+     * CryptoAlgorithm ed448 = new Ed448Algorithm();
+     * SignatureContext signer = ed448.create(KeyUsage.SIGN, privateKey, null);
+     * }
+ */ + public Ed448Algorithm() { + super("Ed448", "Ed448"); + + // SIGN (private key) + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.SIGN, SignatureContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> { + try { + return new Ed448SignatureContext(this, k); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Cannot init Ed448 signer", e); + } + }, () -> VoidSpec.INSTANCE); + + // VERIFY (public key) + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.VERIFY, SignatureContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> { + try { + return new Ed448SignatureContext(this, k); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Cannot init Ed448 verifier", e); + } + }, () -> VoidSpec.INSTANCE); + + // Key builders + registerAsymmetricKeyBuilder(Ed448KeyGenSpec.class, new Ed448KeyGenBuilder(), Ed448KeyGenSpec::defaultSpec); + registerAsymmetricKeyBuilder(Ed448PublicKeySpec.class, new Ed448PublicKeyBuilder(), null); + registerAsymmetricKeyBuilder(Ed448PrivateKeySpec.class, new Ed448PrivateKeyBuilder(), null); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448KeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448KeyGenBuilder.java new file mode 100644 index 0000000..dc58dcc --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448KeyGenBuilder.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ed448; + +import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder; + +/** + *

Ed448 Key-Pair Builder

+ * + * Concrete key generation builder for the Ed448 Edwards-curve Digital Signature + * Algorithm, standardized in + * RFC 8032. + * + *

+ * This builder integrates with the JCA {@link java.security.KeyPairGenerator} + * under the canonical algorithm name {@code "Ed448"}. It is responsible for + * producing fresh {@link java.security.KeyPair} instances; key import is + * delegated to {@link Ed448PublicKeyBuilder} and + * {@link Ed448PrivateKeyBuilder}. + *

+ * + *

Usage

{@code
+ * // Generate a new Ed448 key pair
+ * Ed448KeyGenBuilder builder = new Ed448KeyGenBuilder();
+ * KeyPair kp = builder.generateKeyPair(Ed448KeyGenSpec.defaultSpec());
+ * }
+ * + *

Thread-safety

Instances are stateless and may be reused across + * threads. Each call creates a new {@link java.security.KeyPairGenerator}. + * + * @since 1.0 + */ +public final class Ed448KeyGenBuilder extends AbstractEdDSAKeyGenBuilder { + /** + * Returns the canonical JCA algorithm identifier for Ed448 key pair generation. + * + * @return the string {@code "Ed448"} + */ + @Override + protected String jcaKeyPairAlg() { + return "Ed448"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448KeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448KeyGenSpec.java new file mode 100644 index 0000000..fed1144 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448KeyGenSpec.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ed448; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Ed448 Key Generation Specification

+ * + * Marker specification for generating Ed448 key pairs. + * + *

+ * Unlike parameterized algorithms (e.g., RSA with modulus size), Ed448 has + * fixed domain parameters defined in + * RFC 8032. As a result, + * this spec carries no configuration fields - it serves only as a typed handle + * linking {@link Ed448KeyGenBuilder} with the surrounding framework. + *

+ * + *

Usage

{@code
+ * // Generate a new Ed448 key pair using the default spec
+ * KeyPair kp = new Ed448KeyGenBuilder().generateKeyPair(Ed448KeyGenSpec.defaultSpec());
+ * }
+ * + *

Thread-safety

The {@link #defaultSpec()} instance is immutable and + * safe to reuse across threads. + * + * @since 1.0 + */ +public final class Ed448KeyGenSpec implements AlgorithmKeySpec { + private static final Ed448KeyGenSpec DEFAULT = new Ed448KeyGenSpec(); + + /** + * Returns the shared default specification instance for Ed448 key generation. + * + *

+ * Since Ed448 has no configurable generation parameters, all key generation + * uses this singleton. This avoids unnecessary object allocation and clarifies + * intent. + *

+ * + * @return the singleton {@code Ed448KeyGenSpec} instance + */ + public static Ed448KeyGenSpec defaultSpec() { + return DEFAULT; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java new file mode 100644 index 0000000..828d18c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeyBuilder.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * 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.core.alg.ed448; + +import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

Ed448 Private Key Builder

+ * + * Concrete builder for importing Ed448 private keys from their PKCS#8-encoded + * representation. + * + *

+ * This class extends {@link AbstractEncodedPrivateKeyBuilder} and specifies the + * Ed448 algorithm. It reconstructs {@link java.security.PrivateKey} instances + * from {@link Ed448PrivateKeySpec}, which carries the raw encoded form. + *

+ * + *

Responsibilities

+ *
    + *
  • Provide the canonical JCA key factory algorithm name + * ({@code "Ed448"}).
  • + *
  • Extract the PKCS#8-encoded key bytes from + * {@link Ed448PrivateKeySpec}.
  • + *
  • Delegate actual key reconstruction to + * {@link java.security.KeyFactory}.
  • + *
+ * + *

Usage

{@code
+ * // Import an Ed448 private key from encoded bytes
+ * Ed448PrivateKeySpec spec = new Ed448PrivateKeySpec(encodedPkcs8Bytes);
+ * PrivateKey key = new Ed448PrivateKeyBuilder().importPrivate(spec);
+ * }
+ * + *

Thread-safety

Stateless and safe for concurrent use. Each call to + * {@link AsymmetricKeyBuilder#importPrivate(AlgorithmKeySpec) + * importPrivate(Ed448PrivateKeySpec)} creates a new + * {@link java.security.KeyFactory}. + * + * @since 1.0 + */ +public final class Ed448PrivateKeyBuilder extends AbstractEncodedPrivateKeyBuilder { + /** + * Returns the canonical JCA key factory algorithm name for Ed448. + * + * @return the string {@code "Ed448"} + */ + @Override + protected String jcaKeyFactoryAlg() { + return "Ed448"; + } + + /** + * Returns the PKCS#8-encoded private key bytes from the given specification. + * + * @param spec the {@link Ed448PrivateKeySpec} holding encoded private key data + * @return raw PKCS#8-encoded private key bytes + */ + @Override + protected byte[] encodedPkcs8(Ed448PrivateKeySpec spec) { + return spec.encoded(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java new file mode 100644 index 0000000..84a00a5 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PrivateKeySpec.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * 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.core.alg.ed448; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Ed448 Private Key Specification

+ * + * Immutable specification for an Ed448 private key in PKCS#8 encoding. + * + *

+ * This class acts as a typed carrier for encoded private key material, + * typically used with {@link Ed448PrivateKeyBuilder} to reconstruct a usable + * {@link java.security.PrivateKey}. It also provides marshal/unmarshal helpers + * to serialize the key into a portable {@link PairSeq} representation. + *

+ * + *

Encoding

+ *
    + *
  • The byte array is expected to contain a valid PKCS#8-encoded Ed448 + * private key.
  • + *
  • Internally, the array is defensively cloned on construction and when + * returned by {@link #encoded()}.
  • + *
  • The {@link #marshal(Ed448PrivateKeySpec)} and {@link #unmarshal(PairSeq)} + * methods wrap and unwrap the PKCS#8 bytes using base64 without padding.
  • + *
+ * + *

Usage

{@code
+ * // Wrap existing PKCS#8-encoded bytes
+ * Ed448PrivateKeySpec spec = new Ed448PrivateKeySpec(pkcs8Bytes);
+ *
+ * // Import into JCA PrivateKey via builder
+ * PrivateKey key = new Ed448PrivateKeyBuilder().importPrivate(spec);
+ *
+ * // Serialize to PairSeq (e.g., JSON transport)
+ * PairSeq p = Ed448PrivateKeySpec.marshal(spec);
+ *
+ * // Deserialize from PairSeq
+ * Ed448PrivateKeySpec restored = Ed448PrivateKeySpec.unmarshal(p);
+ * }
+ * + *

Thread-safety

Instances are immutable and safe to share across + * threads. + * + * @since 1.0 + */ +public final class Ed448PrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] encodedPkcs8; + + /** + * Constructs a new Ed448 private key spec from the given PKCS#8-encoded bytes. + * + * @param encodedPkcs8 PKCS#8-encoded Ed448 private key (non-null) + * @throws IllegalArgumentException if {@code encodedPkcs8} is {@code null} + */ + public Ed448PrivateKeySpec(byte[] encodedPkcs8) { + if (encodedPkcs8 == null) { + throw new IllegalArgumentException("encodedPkcs8 must not be null"); + } + this.encodedPkcs8 = encodedPkcs8.clone(); + } + + /** + * Returns a defensive copy of the PKCS#8-encoded private key bytes. + * + * @return cloned PKCS#8-encoded key bytes + */ + public byte[] encoded() { + return encodedPkcs8.clone(); + } + + /** + * Serializes this key spec into a {@link PairSeq} record. + * + *

+ * The encoding is base64 without padding. The {@code type} field is set to + * {@code "Ed448-PRIV"}, and the PKCS#8 bytes are stored under the key + * {@code "pkcs8.b64"}. + *

+ * + * @param spec the key spec to serialize + * @return a {@link PairSeq} containing the type and base64 data + */ + public static PairSeq marshal(Ed448PrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8); + return PairSeq.of("type", "Ed448-PRIV", PKCS8_B64, b64); + } + + /** + * Deserializes a {@link PairSeq} into a new {@link Ed448PrivateKeySpec}. + * + *

+ * Expects a field {@code "pkcs8.b64"} containing the base64-encoded PKCS#8 + * private key bytes. Other fields are ignored. + *

+ * + * @param p the serialized pair sequence + * @return a reconstructed {@link Ed448PrivateKeySpec} + * @throws IllegalArgumentException if {@code pkcs8.b64} is missing + */ + public static Ed448PrivateKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (PKCS8_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for Ed448 private key"); + } + return new Ed448PrivateKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PublicKeyBuilder.java new file mode 100644 index 0000000..74200d4 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PublicKeyBuilder.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * 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.core.alg.ed448; + +import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

Ed448 Public Key Builder

+ * + * Concrete builder for importing Ed448 public keys from their X.509-encoded + * representation. + * + *

+ * This class extends {@link AbstractEncodedPublicKeyBuilder} and specifies the + * Ed448 algorithm. It reconstructs {@link java.security.PublicKey} instances + * from {@link Ed448PublicKeySpec}, which carries the raw encoded form. + *

+ * + *

Responsibilities

+ *
    + *
  • Provide the canonical JCA key factory algorithm name + * ({@code "Ed448"}).
  • + *
  • Extract the X.509-encoded key bytes from {@link Ed448PublicKeySpec}.
  • + *
  • Delegate actual key reconstruction to + * {@link java.security.KeyFactory}.
  • + *
+ * + *

Usage

{@code
+ * // Import an Ed448 public key from encoded bytes
+ * Ed448PublicKeySpec spec = new Ed448PublicKeySpec(x509Bytes);
+ * PublicKey key = new Ed448PublicKeyBuilder().importPublic(spec);
+ * }
+ * + *

Thread-safety

Stateless and safe for concurrent use. Each call to + * {@link AsymmetricKeyBuilder#importPublic(AlgorithmKeySpec) + * importPublic(Ed448PublicKeySpec)} creates a new + * {@link java.security.KeyFactory}. + * + * @since 1.0 + */ +public final class Ed448PublicKeyBuilder extends AbstractEncodedPublicKeyBuilder { + /** + * Returns the canonical JCA key factory algorithm name for Ed448. + * + * @return the string {@code "Ed448"} + */ + @Override + protected String jcaKeyFactoryAlg() { + return "Ed448"; + } + + /** + * Returns the X.509-encoded public key bytes from the given specification. + * + * @param spec the {@link Ed448PublicKeySpec} holding encoded public key data + * @return raw X.509-encoded public key bytes + */ + @Override + protected byte[] encodedX509(Ed448PublicKeySpec spec) { + return spec.encoded(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PublicKeySpec.java new file mode 100644 index 0000000..4a3f51e --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448PublicKeySpec.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * 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.core.alg.ed448; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Ed448 Public Key Specification

+ * + * Immutable specification for an Ed448 public key in X.509 encoding. + * + *

+ * This class acts as a typed carrier for encoded public key material, typically + * used with {@link Ed448PublicKeyBuilder} to reconstruct a usable + * {@link java.security.PublicKey}. It also provides marshal/unmarshal helpers + * to serialize the key into a portable {@link PairSeq} representation. + *

+ * + *

Encoding

+ *
    + *
  • The byte array is expected to contain a valid X.509-encoded Ed448 public + * key.
  • + *
  • The constructor defensively clones the provided array, and + * {@link #encoded()} returns a fresh copy on each call.
  • + *
  • The {@link #marshal(Ed448PublicKeySpec)} and {@link #unmarshal(PairSeq)} + * methods wrap and unwrap the X.509 bytes using base64 without padding.
  • + *
+ * + *

Usage

{@code
+ * // Wrap existing X.509-encoded bytes
+ * Ed448PublicKeySpec spec = new Ed448PublicKeySpec(x509Bytes);
+ *
+ * // Import into JCA PublicKey via builder
+ * PublicKey key = new Ed448PublicKeyBuilder().importPublic(spec);
+ *
+ * // Serialize to PairSeq (e.g., for transport)
+ * PairSeq p = Ed448PublicKeySpec.marshal(spec);
+ *
+ * // Deserialize from PairSeq
+ * Ed448PublicKeySpec restored = Ed448PublicKeySpec.unmarshal(p);
+ * }
+ * + *

Thread-safety

Instances are immutable and safe to share across + * threads. + * + * @since 1.0 + */ +public final class Ed448PublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] encodedX509; + + /** + * Constructs a new Ed448 public key spec from the given X.509-encoded bytes. + * + * @param encodedX509 X.509-encoded Ed448 public key (non-null) + * @throws IllegalArgumentException if {@code encodedX509} is {@code null} + */ + public Ed448PublicKeySpec(byte[] encodedX509) { + if (encodedX509 == null) { + throw new IllegalArgumentException("encodedX509 must not be null"); + } + this.encodedX509 = encodedX509.clone(); + } + + /** + * Returns a defensive copy of the X.509-encoded public key bytes. + * + * @return cloned X.509-encoded key bytes + */ + public byte[] encoded() { + return encodedX509.clone(); + } + + /** + * Serializes this key spec into a {@link PairSeq} record. + * + *

+ * The encoding is base64 without padding. The {@code type} field is set to + * {@code "Ed448-PUB"}, and the X.509 bytes are stored under the key + * {@code "x509.b64"}. + *

+ * + * @param spec the key spec to serialize + * @return a {@link PairSeq} containing the type and base64 data + */ + public static PairSeq marshal(Ed448PublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedX509); + return PairSeq.of("type", "Ed448-PUB", X509_B64, b64); + } + + /** + * Deserializes a {@link PairSeq} into a new {@link Ed448PublicKeySpec}. + * + *

+ * Expects a field {@code "x509.b64"} containing the base64-encoded X.509 public + * key bytes. Other fields are ignored. + *

+ * + * @param p the serialized pair sequence + * @return a reconstructed {@link Ed448PublicKeySpec} + * @throws IllegalArgumentException if {@code x509.b64} is missing + */ + public static Ed448PublicKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (X509_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("x509.b64 missing for Ed448 public key"); + } + return new Ed448PublicKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/Ed448SignatureContext.java b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448SignatureContext.java new file mode 100644 index 0000000..155b7fc --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed448/Ed448SignatureContext.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ed448; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.alg.common.eddsa.CommonEdDSASignatureContext; +import zeroecho.core.context.SignatureContext; + +/** + * Signature context for Ed448 with a fixed 114-byte tag length. + * + *

+ * This class binds the Ed448 algorithm to {@link SignatureContext} via + * {@link CommonEdDSASignatureContext}. Internally it configures a JCA + * {@code Signature} with the name {@code "Ed448"} and delegates all streaming + * and verification behavior to the common adapter. + *

+ * + *

Algorithm characteristics

+ *
    + *
  • JCA signature name: {@code "Ed448"}.
  • + *
  • Fixed signature length: 114 bytes (reported by + * {@link #tagLength()}).
  • + *
  • Edwards-curve Digital Signature Algorithm as specified in RFC 8032.
  • + *
+ * + *

Usage

+ *

One-shot API

+ * {@code
+ * // Sign
+ * PrivateKey priv = ...;
+ * SignatureContext signer =
+ *     new Ed448SignatureContext(CryptoAlgorithms.require("Ed448"), priv);
+ * signer.update(message);
+ * byte[] sig = signer.sign();
+ *
+ * // Verify
+ * PublicKey pub = ...;
+ * SignatureContext verifier =
+ *     new Ed448SignatureContext(CryptoAlgorithms.require("Ed448"), pub);
+ * verifier.update(message);
+ * boolean ok = verifier.verify(sig);
+ * }
+ * 
+ * + *

Streaming pipeline (wrap)

+ * {@code
+ * // SIGN mode: body bytes followed by 114-byte signature trailer
+ * try (SignatureContext ctx =
+ *          new Ed448SignatureContext(CryptoAlgorithms.require("Ed448"), priv);
+ *      InputStream in = ctx.wrap(upstream)) {
+ *     in.transferTo(out);
+ * }
+ *
+ * // VERIFY mode: supply expected signature; verification occurs at EOF
+ * try (SignatureContext ctx =
+ *          new Ed448SignatureContext(CryptoAlgorithms.require("Ed448"), pub)) {
+ *     ctx.setExpectedTag(expectedSig);
+ *     try (InputStream in = ctx.wrap(bodyWithoutTrailer)) {
+ *         in.transferTo(java.io.OutputStream.nullOutputStream());
+ *     }
+ * }
+ * }
+ * 
+ * + *

Thread-safety

+ *

+ * Instances are stateful and not thread-safe. Use one context per signing or + * verification operation, and call {@code wrap(...)} at most once per instance. + *

+ * + * @since 1.0 + */ +public final class Ed448SignatureContext extends CommonEdDSASignatureContext { + private static final String SIG_NAME = "Ed448"; + private static final int TAG_LEN = 114; + + /** + * Constructs a signing context bound to the given private key. + * + * @param algorithm parent algorithm descriptor; must not be {@code null} + * @param privateKey Ed448 private key; must not be {@code null} + * @throws GeneralSecurityException if the underlying JCA engine cannot be + * initialized + * @throws NullPointerException if any argument is {@code null} + */ + public Ed448SignatureContext(final CryptoAlgorithm algorithm, final PrivateKey privateKey) + throws GeneralSecurityException { + super(algorithm, privateKey, SIG_NAME, TAG_LEN); + } + + /** + * Constructs a verification context bound to the given public key. + * + * @param algorithm parent algorithm descriptor; must not be {@code null} + * @param publicKey Ed448 public key; must not be {@code null} + * @throws GeneralSecurityException if the underlying JCA engine cannot be + * initialized + * @throws NullPointerException if any argument is {@code null} + */ + public Ed448SignatureContext(final CryptoAlgorithm algorithm, final PublicKey publicKey) + throws GeneralSecurityException { + super(algorithm, publicKey, SIG_NAME, TAG_LEN); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ed448/package-info.java b/lib/src/main/java/zeroecho/core/alg/ed448/package-info.java new file mode 100644 index 0000000..80aca36 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ed448/package-info.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Ed448 digital signature integration. + * + *

+ * This package wires the Ed448 Edwards-curve Digital Signature Algorithm into + * the core layer. It provides the algorithm descriptor, a streaming signature + * context with a fixed 114-byte tag length, builders for generating and + * importing keys, and immutable key specifications with marshalling helpers. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register the Ed448 algorithm and declare SIGN and VERIFY roles with + * fixed-length signatures.
  • + *
  • Provide a streaming signature context that adapts JCA engines and + * enforces the 114-byte tag size.
  • + *
  • Expose builders for key-pair generation and for importing encoded + * keys.
  • + *
  • Define immutable key specifications suitable for safe cloning and simple + * marshalling.
  • + *
+ * + *

Components

+ *
    + *
  • Ed448Algorithm: algorithm descriptor that binds roles to a + * signature context and registers builders for key generation and import.
  • + *
  • Ed448SignatureContext: streaming context for signing and + * verification with a fixed 114-byte tag.
  • + *
  • Ed448KeyGenBuilder and Ed448KeyGenSpec: generator and + * marker spec for producing key pairs.
  • + *
  • Ed448PublicKeyBuilder / Ed448PrivateKeyBuilder: importers + * backed by JCA key factories.
  • + *
  • Ed448PublicKeySpec / Ed448PrivateKeySpec: immutable + * wrappers over X.509 and PKCS#8 encodings, with defensive copying and base64 + * marshalling helpers.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and thread-safe.
  • + *
  • Signature contexts are stateful and not thread-safe; create a new + * instance per operation.
  • + *
  • Key specification classes never expose internal byte arrays; cloning is + * used on input and output.
  • + *
  • Marshalling helpers use compact key-value sequences intended for + * configuration, transport, and testing.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.ed448; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalAlgorithm.java new file mode 100644 index 0000000..873180f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalAlgorithm.java @@ -0,0 +1,250 @@ +/******************************************************************************* + * 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.core.alg.elgamal; + +import java.security.AlgorithmParameterGenerator; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.jce.spec.ElGamalParameterSpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

ElGamal Asymmetric Encryption Algorithm

+ * + * Concrete {@link zeroecho.core.CryptoAlgorithm} for the ElGamal public-key + * encryption scheme. + * + *

+ * ElGamal is an asymmetric cryptosystem based on the hardness of the discrete + * logarithm problem in a finite cyclic group. It provides semantic security + * under chosen-plaintext attacks when combined with proper padding (e.g., + * PKCS#1 style). This implementation delegates to the Bouncy Castle provider + * ({@code "BC"}) for underlying primitives and parameter generation. + *

+ * + *

+ * Note: OAEP is not offered for ElGamal; for modern IND-CCA security + * prefer KEM or KEM-DEM constructions such as DHIES, ECIES, or Cramer-Shoup, or + * use a KEM provided elsewhere in the catalog. + *

+ * + *

Declared capabilities

+ *
    + *
  • {@link zeroecho.core.KeyUsage#ENCRYPT} using a + * {@link java.security.PublicKey} and producing an + * {@link zeroecho.core.context.EncryptionContext}.
  • + *
  • {@link zeroecho.core.KeyUsage#DECRYPT} using a + * {@link java.security.PrivateKey} and producing an + * {@link zeroecho.core.context.EncryptionContext}.
  • + *
+ * + *

Key material

+ *
    + *
  • {@link ElgamalParamSpec} - wrapper for explicit ElGamal group/domain + * parameters. Registered with a default of + * {@link ElgamalParamSpec#ffdhe2048()}.
  • + *
  • {@link ElgamalPublicKeySpec} - X.509-encoded public key import.
  • + *
  • {@link ElgamalPrivateKeySpec} - PKCS#8-encoded private key import.
  • + *
  • {@code ElgamalKeyGenSpec} is supported but disabled by default because + * parameter generation is slow; prefer reusing standardized + * {@link ElgamalParamSpec} instances.
  • + *
+ * + *

Provider requirements

This implementation requires the Bouncy Castle + * JCE provider to be registered under the name {@code "BC"}. If unavailable, + * {@link #ensureBC()} will throw a + * {@link java.security.NoSuchProviderException}. + * + *

Thread-safety

Instances of {@code ElgamalAlgorithm} are immutable + * and may be safely shared across threads. Generated + * {@link zeroecho.core.context.CryptoContext} instances are not necessarily + * thread-safe. + * + *

Example

{@code
+ * CryptoAlgorithm algo = new ElgamalAlgorithm();
+ * KeyPair kp = algo.generateKeyPair(ElgamalParamSpec.ffdhe2048());
+ *
+ * EncryptionContext enc = algo.create(KeyUsage.ENCRYPT, kp.getPublic(),
+ *                                     ElgamalEncSpec.pkcs1());
+ * EncryptionContext dec = algo.create(KeyUsage.DECRYPT, kp.getPrivate(),
+ *                                     ElgamalEncSpec.pkcs1());
+ * }
+ * + * @since 1.0 + */ +public final class ElgamalAlgorithm extends AbstractCryptoAlgorithm { + private static final String EL_GAMAL = "ElGamal"; + + /** + * Constructs a new ElGamal algorithm instance bound to the {@code "ElGamal"} + * identifier and backed by the Bouncy Castle provider. + * + *

+ * During construction, the algorithm registers: + *

    + *
  • Encryption and decryption capabilities,
  • + *
  • Key builders for parameter-based, public, and private key specs,
  • + *
  • Default parameter spec suppliers (e.g., ffdhe2048).
  • + *
+ */ + @SuppressWarnings("unused") + public ElgamalAlgorithm() { + super(EL_GAMAL, EL_GAMAL, "BC"); + + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, PublicKey.class, + ElgamalEncSpec.class, (PublicKey k, ElgamalEncSpec s) -> new ElgamalCipherContext(this, k, s, true), + ElgamalEncSpec::pkcs1); + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, PrivateKey.class, + ElgamalEncSpec.class, (PrivateKey k, ElgamalEncSpec s) -> new ElgamalCipherContext(this, k, s, false), + ElgamalEncSpec::pkcs1); + + if (false) { // NOPMD + // this key generation is slow + registerAsymmetricKeyBuilder(ElgamalKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(ElgamalKeyGenSpec spec) throws GeneralSecurityException { + ensureBC(); + AlgorithmParameterGenerator apg = AlgorithmParameterGenerator.getInstance(EL_GAMAL, providerName()); + apg.init(spec.keySize(), new SecureRandom()); + AlgorithmParameters ap = apg.generateParameters(); + ElGamalParameterSpec eg = ap.getParameterSpec(ElGamalParameterSpec.class); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance(EL_GAMAL, providerName()); + kpg.initialize(eg, new SecureRandom()); + return kpg.generateKeyPair(); + } + + @Override + public PublicKey importPublic(ElgamalKeyGenSpec spec) { + throw new UnsupportedOperationException("Use ElgamalPublicKeySpec to import a public key."); + } + + @Override + public PrivateKey importPrivate(ElgamalKeyGenSpec spec) { + throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec to import a private key."); + } + }, ElgamalKeyGenSpec::elgamal2048); + } + + registerAsymmetricKeyBuilder(ElgamalParamSpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(ElgamalParamSpec spec) throws GeneralSecurityException { + ensureBC(); + ElGamalParameterSpec eg = spec.parameters(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(EL_GAMAL, providerName()); + kpg.initialize(eg, new SecureRandom()); + return kpg.generateKeyPair(); + } + + @Override + public PublicKey importPublic(ElgamalParamSpec spec) { + throw new UnsupportedOperationException("Use ElgamalPublicKeySpec to import a public key."); + } + + @Override + public PrivateKey importPrivate(ElgamalParamSpec spec) { + throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec to import a private key."); + } + }, ElgamalParamSpec::ffdhe2048); + + registerAsymmetricKeyBuilder(ElgamalPublicKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(ElgamalPublicKeySpec spec) { + throw new UnsupportedOperationException("Generation not supported for encoded spec."); + } + + @Override + public PublicKey importPublic(ElgamalPublicKeySpec spec) throws GeneralSecurityException { + ensureBC(); + KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); + } + + @Override + public PrivateKey importPrivate(ElgamalPublicKeySpec spec) { + throw new UnsupportedOperationException("Use ElgamalPrivateKeySpec for private keys."); + } + }, null); + + registerAsymmetricKeyBuilder(ElgamalPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(ElgamalPrivateKeySpec spec) { + throw new UnsupportedOperationException("Generation not supported for encoded spec."); + } + + @Override + public PublicKey importPublic(ElgamalPrivateKeySpec spec) { + throw new UnsupportedOperationException("Use ElgamalPublicKeySpec for public keys."); + } + + @Override + public PrivateKey importPrivate(ElgamalPrivateKeySpec spec) throws GeneralSecurityException { + ensureBC(); + KeyFactory kf = KeyFactory.getInstance(EL_GAMAL, providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + } + }, null); + } + + /** + * Ensures that the Bouncy Castle provider is available. + * + * @throws java.security.NoSuchProviderException if the provider {@code "BC"} is + * not registered with the JCA + */ + private static void ensureBC() throws NoSuchProviderException { + Provider p = Security.getProvider("BC"); + if (p == null) { + throw new NoSuchProviderException("Bouncy Castle provider (BC) not registered"); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java new file mode 100644 index 0000000..cf83671 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalCipherContext.java @@ -0,0 +1,245 @@ +/******************************************************************************* + * 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.core.alg.elgamal; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.Key; + +import javax.crypto.Cipher; +import javax.crypto.interfaces.DHKey; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.io.CipherTransformInputStreamBuilder; + +/** + * Streaming ElGamal cipher context that adapts an upstream stream into an + * encrypted or decrypted stream. + * + *

+ * The context applies ElGamal block-by-block over a pull pipeline. For a + * modulus of size {@code pBytes}, each ciphertext block is fixed at + * {@code 2 * pBytes} bytes (two group elements), while the plaintext-per-block + * depends on the padding: + *

+ * + *
    + *
  • NOPADDING: {@code ptBlock = pBytes - 1}
  • + *
  • PKCS1: {@code ptBlock = pBytes - 11}
  • + *
+ * + *

Padding subtleties

+ *
    + *
  • NOPADDING: some providers encode integers with a leading + * {@code 0x00} byte for positive sign. This implementation trims a leading sign + * byte and enforces a fixed plaintext length for non-last blocks only. + * The last block is emitted as-is.
  • + *
  • PKCS1: the last plaintext block may be shorter by design; it is + * never padded on output.
  • + *
+ * + *

EOF rules

+ *
    + *
  • Encrypt: a short final plaintext block is allowed and produces a + * single ciphertext block.
  • + *
  • Decrypt: ciphertext must be an integral number of full ciphertext + * blocks; a partial tail is rejected.
  • + *
+ * + *

Thread-safety

Instances are not thread-safe. Create one context per + * pipeline. + */ +public final class ElgamalCipherContext implements EncryptionContext { + private final CryptoAlgorithm algorithm; + private final Key key; + private final ElgamalEncSpec spec; + private final boolean encrypt; + + /** + * Creates a new ElGamal cipher context. + * + *

+ * The transformation used is {@code "ElGamal/None/NOPADDING"} for + * {@link ElgamalEncSpec.Padding#NOPADDING} and + * {@code "ElGamal/None/PKCS1Padding"} for {@link ElgamalEncSpec.Padding#PKCS1}. + * The JCE provider must support the chosen transformation and the provided + * {@code key} for the requested direction. + *

+ * + * @param algorithm the owning algorithm for metadata; must not be {@code null} + * @param key public key for encrypt or private key for decrypt; must not + * be {@code null} + * @param spec ElGamal encoding/padding specification; must not be + * {@code null} + * @param encrypt {@code true} for encryption, {@code false} for decryption + * @throws NullPointerException if any argument is {@code null} + */ + public ElgamalCipherContext(CryptoAlgorithm algorithm, Key key, ElgamalEncSpec spec, boolean encrypt) { + this.algorithm = java.util.Objects.requireNonNull(algorithm, "algorithm"); + this.key = java.util.Objects.requireNonNull(key, "key"); + this.spec = java.util.Objects.requireNonNull(spec, "spec"); + this.encrypt = encrypt; + } + + /** + * Returns an input stream that transforms bytes on-the-fly using ElGamal. + * + *

+ * A fresh {@link Cipher} is created and initialized per call. The returned + * stream implements the block model and EOF rules described in the class + * documentation. Callers must close the returned stream to finalize the + * transformation and release resources. + *

+ * + * @param upstream the upstream source to transform; must not be {@code null} + * @return a transforming input stream + * @throws IOException if the cipher cannot be created or initialized + * @throws NullPointerException if {@code upstream} is {@code null} + */ + @Override + public InputStream attach(InputStream upstream) throws IOException { + java.util.Objects.requireNonNull(upstream, "upstream"); + final Cipher cipher = newCipher(spec.padding(), encrypt, key); + final BlockGeometry g = new BlockGeometry(modulusBytes(key), spec.padding(), encrypt); + + return CipherTransformInputStreamBuilder.builder().withCipher(cipher).withUpstream(upstream) + .withInputBlockSize(g.inputBlockSize()).withOutputBlockSize(g.perBlockOutput()) + .withLeftZeroPadding(g.noPadding).build(); + } + + /** + * Returns the associated algorithm object for metadata and auditing. + * + * @return the algorithm that created this context + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + * @return the key used for encryption or decryption + */ + @Override + public Key key() { + return key; + } + + /** + * Closes this context. + * + *

+ * The returned transforming streams are independent of the context object, so + * closing the context has no effect on previously created streams. This method + * is provided to satisfy the {@link zeroecho.core.context.CryptoContext} + * contract and may be used by higher layers for lifecycle hooks. + *

+ */ + @Override + public void close() throws IOException { + // No resources to release here. Streams manage their own lifetimes. + } + + private static Cipher newCipher(ElgamalEncSpec.Padding padding, boolean encrypt, Key key) throws IOException { + final String transformation = switch (padding) { + case NOPADDING -> "ElGamal/None/NOPADDING"; + case PKCS1 -> "ElGamal/None/PKCS1Padding"; + }; + try { + Cipher c = Cipher.getInstance(transformation); + c.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key); + return c; + } catch (GeneralSecurityException e) { + throw new IOException("ElGamal cipher init failed: " + e.getMessage(), e); + } + } + + /** + * Derives the modulus size in bytes from a DH-like key (FFDHE/ElGamal). + */ + private static int modulusBytes(Key key) { + if (key instanceof DHKey dh) { + // FFDHE: p bit length is a multiple of 64; convert to exact bytes. + final int bits = dh.getParams().getP().bitLength(); + return (bits + 7) >>> 3; + } + // If your keys expose p through a different interface, add handling here. + throw new IllegalArgumentException("Unsupported key type for ElGamal modulus: " + key.getClass().getName()); + } + + /** + * Immutable description of the ElGamal block layout for a given modulus size, + * padding, and direction. + */ + private static final class BlockGeometry { + private final int pBytes; // modulus size in bytes + private final int ptBlock; // plaintext-per-block + private final int ctBlock; // ciphertext-per-block (always 2*pBytes) + private final boolean encrypt; + private final boolean noPadding; + + private BlockGeometry(int pBytes, ElgamalEncSpec.Padding padding, boolean encrypt) { + this.pBytes = pBytes; + this.ctBlock = 2 * pBytes; + this.encrypt = encrypt; + this.noPadding = (padding == ElgamalEncSpec.Padding.NOPADDING); + this.ptBlock = switch (padding) { + case NOPADDING -> pBytes - 1; + case PKCS1 -> pBytes - 11; + }; + } + + private int inputBlockSize() { + // How many upstream bytes constitute one cipher operation + return encrypt ? ptBlock : ctBlock; + } + + private int perBlockOutput() { + // Upper-bound for bytes produced by one operation (for buffer sizing) + return encrypt ? ctBlock : ptBlock; + } + + @Override + public String toString() { + return "BlockGeometry [pBytes=" + pBytes + ", ptBlock=" + ptBlock + ", ctBlock=" + ctBlock + ", encrypt=" + + encrypt + ", noPadding=" + noPadding + ", inputBlockSize()=" + inputBlockSize() + + ", perBlockOutput()=" + perBlockOutput() + "]"; + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java new file mode 100644 index 0000000..45adc08 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalEncSpec.java @@ -0,0 +1,160 @@ +/******************************************************************************* + * 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.core.alg.elgamal; + +import java.util.Objects; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.annotation.DisplayName; +import zeroecho.core.spec.ContextSpec; + +/** + *

ElGamal Encryption Parameters

+ * + * Specification for configuring ElGamal encryption contexts. + * + *

+ * {@code ElgamalEncSpec} selects the padding mode to apply when transforming + * plaintext into group elements for ElGamal encryption. This affects both + * security properties and maximum plaintext block size. + *

+ * + *

+ * Note: OAEP is not offered for ElGamal; for modern IND-CCA security + * prefer KEM or KEM-DEM constructions such as DHIES, ECIES, or Cramer-Shoup, or + * use a KEM provided elsewhere in the catalog. + *

+ * + *

Padding modes

+ *
    + *
  • {@link Padding#NOPADDING} - raw ElGamal encryption of messages less than + * the modulus {@code p}. Provides minimal security (plaintexts are only + * randomized by ephemeral exponent) and must not be used directly in modern + * protocols without additional safeguards (e.g., hybrid encryption or KEM + * construction).
  • + *
  • {@link Padding#PKCS1} - PKCS#1 v1.5-style padding applied to the + * plaintext before encryption. Adds structural redundancy and prevents trivial + * small-subgroup attacks. Still considered outdated for new protocols but + * available for compatibility.
  • + *
+ * + *

Usage

Instances are created via the static factories:
{@code
+ * ElgamalEncSpec spec = ElgamalEncSpec.pkcs1();
+ * EncryptionContext ctx = algo.create(KeyUsage.ENCRYPT, pubKey, spec);
+ * }
+ * + *

Thread-safety

{@code ElgamalEncSpec} is immutable and safe to share + * across threads. + * + * @since 1.0 + */ +@DisplayName("ElGamal encryption parameters") +public final class ElgamalEncSpec implements ContextSpec, Describable { + /** + * Supported padding modes for ElGamal encryption. + */ + public enum Padding { + /** + * No padding applied. + * + *

+ * Plaintext must be strictly less than modulus {@code p}. Provides only + * semantic security under chosen-plaintext attack if used in combination with + * fresh random exponents. Not recommended for use outside compatibility + * scenarios. + *

+ */ + NOPADDING, + /** + * PKCS#1 v1.5-style padding. + * + *

+ * Adds structured redundancy to plaintext before encryption. Historically used + * in RSA and adapted here for ElGamal. Provides limited protection against + * certain classes of attacks but should be replaced by modern OAEP-style + * constructions where available. + *

+ */ + PKCS1 + } + + private final Padding padding; + + private ElgamalEncSpec(Padding padding) { + this.padding = Objects.requireNonNull(padding, "padding"); + } + + /** + * Returns an {@code ElgamalEncSpec} with no padding. + * + * @return specification with {@link Padding#NOPADDING} + */ + public static ElgamalEncSpec noPadding() { + return new ElgamalEncSpec(Padding.NOPADDING); + } + + /** + * Returns an {@code ElgamalEncSpec} with PKCS#1 v1.5-style padding. + * + * @return specification with {@link Padding#PKCS1} + */ + public static ElgamalEncSpec pkcs1() { + return new ElgamalEncSpec(Padding.PKCS1); + } + + /** + * Returns the selected padding mode. + * + * @return padding enumeration value + */ + public Padding padding() { + return padding; + } + + /** + * Human-readable description of this spec. + * + *

+ * Matches the transformation string used by the JCA provider (e.g., + * {@code "NOPADDING"} or {@code "PKCS1Padding"}). + *

+ * + * @return descriptive string for this padding mode + */ + @Override + public String description() { + return (padding == Padding.NOPADDING) ? "NOPADDING" : "PKCS1Padding"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalKeyGenSpec.java new file mode 100644 index 0000000..65e443e --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalKeyGenSpec.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.elgamal; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

ElGamal Key Generation Specification

+ * + * Parameters controlling ElGamal key pair generation. This specification + * provides the modulus size and the certainty parameter for primality testing. + * + *

+ * When generating a new ElGamal key pair, the modulus {@code p} must be chosen + * as a large safe prime. The {@code keySize} determines the bit length of + * {@code p}, and {@code certainty} controls the statistical strength of + * Miller–Rabin primality checks. + *

+ * + *

Validation rules

+ *
    + *
  • {@code keySize} must be at least 1024 bits.
  • + *
  • {@code certainty} must be at least 64, which ensures negligible error + * probability in primality testing.
  • + *
+ * + *

Factory

+ *
    + *
  • {@link #elgamal2048()} returns a commonly used default spec with a + * 2048-bit modulus and 128 Miller–Rabin iterations.
  • + *
+ * + *

Usage

{@code
+ * ElgamalKeyGenSpec spec = ElgamalKeyGenSpec.elgamal2048();
+ * KeyPair kp = algo.generateKeyPair(spec);
+ * }
+ * + *

Thread-safety

Instances are immutable and can be freely shared + * between threads. + * + * @since 1.0 + */ +public final class ElgamalKeyGenSpec implements AlgorithmKeySpec, Describable { + private final int keySize; // bits for p + private final int certainty; // Miller-Rabin certainty + + /** + * Constructs a new ElGamal key generation spec. + * + * @param keySize desired modulus size in bits (minimum 1024) + * @param certainty number of Miller–Rabin iterations for primality testing + * (minimum 64) + * @throws IllegalArgumentException if {@code keySize} < 1024 or + * {@code certainty} < 64 + */ + public ElgamalKeyGenSpec(int keySize, int certainty) { + if (keySize < 1024) { // NOPMD + throw new IllegalArgumentException("keySize too small"); + } + if (certainty < 64) { // NOPMD + throw new IllegalArgumentException("certainty too small"); + } + this.keySize = keySize; + this.certainty = certainty; + } + + /** + * Returns the modulus size in bits. + * + * @return number of bits for the prime modulus p + */ + public int keySize() { + return keySize; + } + + /** + * Returns the Miller–Rabin certainty parameter. + * + * @return number of iterations used in primality testing + */ + public int certainty() { + return certainty; + } + + /** + * Returns a default ElGamal spec with a 2048-bit modulus and 128 iterations for + * primality testing. + * + * @return default ElGamal key generation specification + */ + public static ElgamalKeyGenSpec elgamal2048() { + return new ElgamalKeyGenSpec(2048, 128); + } + + /** + * Human-readable description of this spec. + * + *

+ * Example: {@code "p=2048, certainty=128"}. + *

+ * + * @return descriptive string including modulus size and certainty + */ + @Override + public String description() { + return "p=" + keySize + ", certainty=" + certainty; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalParamSpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalParamSpec.java new file mode 100644 index 0000000..7639754 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalParamSpec.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * 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.core.alg.elgamal; + +import java.math.BigInteger; +import java.util.Objects; + +import org.bouncycastle.jce.spec.ElGamalParameterSpec; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

ElGamal Parameter Specification

+ * + * Wrapper for ElGamal domain parameters {@code (p, g)}. Unlike + * {@link ElgamalKeyGenSpec}, which requests new random primes, + * {@code ElgamalParamSpec} directly encapsulates an explicit safe prime modulus + * and generator. + * + *

+ * An {@link ElGamalParameterSpec} instance defines the cyclic subgroup of + * integers modulo {@code p}, generated by {@code g}, in which all ElGamal + * operations occur. The modulus {@code p} should be a large safe prime to + * resist discrete logarithm attacks. + *

+ * + *

Contents

+ *
    + *
  • {@link #parameters()} exposes the wrapped BouncyCastle + * {@link org.bouncycastle.jce.spec.ElGamalParameterSpec} object.
  • + *
  • {@link #keySize()} reports the bit length of the modulus {@code p}.
  • + *
+ * + *

Predefined parameters

+ *
    + *
  • {@link #ffdhe2048()} returns the 2048-bit safe prime from + * RFC 7919 + * (FFDHE2048), with generator {@code g = 2}. This provides a widely accepted + * interoperable parameter set.
  • + *
+ * + *

Usage

{@code
+ * ElgamalParamSpec spec = ElgamalParamSpec.ffdhe2048();
+ * KeyPair kp = algo.generateKeyPair(spec);
+ * }
+ * + *

Thread-safety

Instances are immutable and safe to share between + * threads. + * + * @since 1.0 + */ +public final class ElgamalParamSpec implements AlgorithmKeySpec, Describable { + private final ElGamalParameterSpec params; + private final int bits; + + /** + * Creates a new ElGamal parameter spec with modulus and generator. + * + * @param p safe prime modulus (must not be {@code null}) + * @param g generator of the subgroup (must not be {@code null}) + * @throws NullPointerException if either argument is {@code null} + */ + public ElgamalParamSpec(BigInteger p, BigInteger g) { + Objects.requireNonNull(p, "p"); + Objects.requireNonNull(g, "g"); + this.params = new ElGamalParameterSpec(p, g); + this.bits = p.bitLength(); + } + + /** + * Returns the underlying ElGamal parameters. + * + * @return {@link ElGamalParameterSpec} wrapping {@code (p, g)} + */ + public ElGamalParameterSpec parameters() { + return params; + } + + /** + * Returns the modulus size in bits. + * + * @return number of bits of {@code p} + */ + public int keySize() { + return bits; + } + + /** + * Returns the RFC 7919 FFDHE2048 safe prime with generator 2. + * + *

+ * This parameter set is standardized and widely supported for Diffie–Hellman + * and ElGamal constructions. + *

+ * + * @return predefined ElGamal parameter spec (2048-bit, g=2) + */ + public static ElgamalParamSpec ffdhe2048() { + final String pHex = """ + FFFFFFFF FFFFFFFF ADF85458 A2BB4A9A AFDC5620 273D3CF1 + D8B9C583 CE2D3695 A9E13641 146433FB CC939DCE 249B3EF9 + 7D2FE363 630C75D8 F681B202 AEC4617A D3DF1ED5 D5FD6561 + 2433F51F 5F066ED0 85636555 3DED1AF3 B557135E 7F57C935 + 984F0C70 E0E68B77 E2A689DA F3EFE872 1DF158A1 36ADE735 + 30ACCA4F 483A797A BC0AB182 B324FB61 D108A94B B2C8E3FB + B96ADAB7 60D7F468 1D4F42A3 DE394DF4 AE56EDE7 6372BB19 + 0B07A7C8 EE0A6D70 9E02FCE1 CDF7E2EC C03404CD 28342F61 + 9172FE9C E98583FF 8E4F1232 EEF28183 C3FE3B1B 4C6FAD73 + 3BB5FCBC 2EC22005 C58EF183 7D1683B2 C6F34A26 C1B2EFFA + 886B4238 61285C97 FFFFFFFF FFFFFFFF + """.replaceAll("\\s+", ""); + BigInteger p = new BigInteger(pHex, 16); + BigInteger g = BigInteger.valueOf(2L); + return new ElgamalParamSpec(p, g); + } + + /** + * Human-readable description of this spec. + * + *

+ * Example: {@code "predefined(p=2048, g=2)"}. + *

+ * + * @return descriptive string including modulus size and generator + */ + @Override + public String description() { + return "predefined(p=" + bits + ", g=2)"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPrivateKeySpec.java new file mode 100644 index 0000000..40258ca --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPrivateKeySpec.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * 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.core.alg.elgamal; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

ElGamal Private Key Specification

+ * + * Wrapper for a PKCS#8-encoded ElGamal private key. + * + *

+ * This specification serves as the input to key-import routines, providing a + * portable representation of private key material. The encoding format is + * assumed to be PKCS#8 DER, as produced by standard JCA/JCE providers. + *

+ * + *

Contents

+ *
    + *
  • {@link #encoded()} returns a defensive copy of the raw PKCS#8 bytes.
  • + *
  • {@link #marshal(ElgamalPrivateKeySpec)} encodes the key into a + * {@link PairSeq} structure for transport or logging.
  • + *
  • {@link #unmarshal(PairSeq)} reconstructs the spec from a marshalled + * representation.
  • + *
+ * + *

Usage

{@code
+ * // Import from PKCS#8 DER
+ * byte[] der = Files.readAllBytes(Path.of("elgamal-priv.der"));
+ * ElgamalPrivateKeySpec spec = new ElgamalPrivateKeySpec(der);
+ * PrivateKey priv = algo.importPrivate(spec);
+ *
+ * // Marshal for serialization
+ * PairSeq ps = ElgamalPrivateKeySpec.marshal(spec);
+ *
+ * // Unmarshal back
+ * ElgamalPrivateKeySpec restored = ElgamalPrivateKeySpec.unmarshal(ps);
+ * }
+ * + *

Security considerations

+ *
    + *
  • Private key material must be protected in memory and at rest. The spec + * itself does not attempt to zeroize its contents.
  • + *
  • Always prefer secure storage (e.g., a hardware module) over raw byte + * handling when possible.
  • + *
+ * + *

Thread-safety

Instances are immutable and safe to share between + * threads. + * + * @since 1.0 + */ +public final class ElgamalPrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Constructs a new private key spec from PKCS#8-encoded bytes. + * + * @param pkcs8 PKCS#8 DER encoding of the private key; must not be {@code null} + */ + public ElgamalPrivateKeySpec(byte[] pkcs8) { + this.pkcs8 = pkcs8.clone(); + } + + /** + * Returns a defensive copy of the PKCS#8-encoded private key. + * + * @return PKCS#8 DER encoding + */ + public byte[] encoded() { + return pkcs8.clone(); + } + + /** + * Serializes this spec into a {@link PairSeq}. + * + *

+ * The output includes: + *

    + *
  • {@code type} = {@code "ELGAMAL-PRIV"}
  • + *
  • {@code pkcs8.b64} = Base64 encoding of the DER bytes (without + * padding)
  • + *
+ * + * @param spec ElGamal private key spec + * @return marshalled key as {@link PairSeq} + */ + public static PairSeq marshal(ElgamalPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "ELGAMAL-PRIV", PKCS8_B64, b64); + } + + /** + * Parses a {@link PairSeq} and reconstructs an {@code ElgamalPrivateKeySpec}. + * + * @param p encoded pair sequence + * @return new private key spec + * @throws IllegalArgumentException if required field {@code pkcs8.b64} is + * missing + */ + public static ElgamalPrivateKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (PKCS8_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for ElGamal private key"); + } + return new ElgamalPrivateKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPublicKeySpec.java new file mode 100644 index 0000000..c5c84d1 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/ElgamalPublicKeySpec.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * 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.core.alg.elgamal; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

ElGamal Public Key Specification

+ * + * Wrapper for an X.509-encoded ElGamal public key. + * + *

+ * This specification serves as the input to key-import routines, providing a + * portable representation of public key material. The encoding format is + * assumed to be X.509 DER, as produced by standard JCA/JCE providers. + *

+ * + *

Contents

+ *
    + *
  • {@link #encoded()} returns a defensive copy of the raw X.509 bytes.
  • + *
  • {@link #marshal(ElgamalPublicKeySpec)} encodes the key into a + * {@link PairSeq} structure for transport or logging.
  • + *
  • {@link #unmarshal(PairSeq)} reconstructs the spec from a marshalled + * representation.
  • + *
+ * + *

Usage

{@code
+ * // Import from X.509 DER
+ * byte[] der = Files.readAllBytes(Path.of("elgamal-pub.der"));
+ * ElgamalPublicKeySpec spec = new ElgamalPublicKeySpec(der);
+ * PublicKey pub = algo.importPublic(spec);
+ *
+ * // Marshal for serialization
+ * PairSeq ps = ElgamalPublicKeySpec.marshal(spec);
+ *
+ * // Unmarshal back
+ * ElgamalPublicKeySpec restored = ElgamalPublicKeySpec.unmarshal(ps);
+ * }
+ * + *

Security considerations

+ *
    + *
  • Public keys are not secret, but they must be bound to the correct + * parameters (modulus and generator) to avoid substitution attacks.
  • + *
  • Always validate public key encoding against trusted parameter sets before + * using in protocols.
  • + *
+ * + *

Thread-safety

Instances are immutable and safe to share between + * threads. + * + * @since 1.0 + */ +public final class ElgamalPublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Constructs a new public key spec from X.509-encoded bytes. + * + * @param x509 X.509 DER encoding of the public key; must not be {@code null} + */ + public ElgamalPublicKeySpec(byte[] x509) { + this.x509 = x509.clone(); + } + + /** + * Returns a defensive copy of the X.509-encoded public key. + * + * @return X.509 DER encoding + */ + public byte[] encoded() { + return x509.clone(); + } + + /** + * Serializes this spec into a {@link PairSeq}. + * + *

+ * The output includes: + *

    + *
  • {@code type} = {@code "ELGAMAL-PUB"}
  • + *
  • {@code x509.b64} = Base64 encoding of the DER bytes (without + * padding)
  • + *
+ * + * @param spec ElGamal public key spec + * @return marshalled key as {@link PairSeq} + */ + public static PairSeq marshal(ElgamalPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "ELGAMAL-PUB", X509_B64, b64); + } + + /** + * Parses a {@link PairSeq} and reconstructs an {@code ElgamalPublicKeySpec}. + * + * @param p encoded pair sequence + * @return new public key spec + * @throws IllegalArgumentException if required field {@code x509.b64} is + * missing + */ + public static ElgamalPublicKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (X509_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("x509.b64 missing for ElGamal public key"); + } + return new ElgamalPublicKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/elgamal/package-info.java b/lib/src/main/java/zeroecho/core/alg/elgamal/package-info.java new file mode 100644 index 0000000..20ed157 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/elgamal/package-info.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * ElGamal asymmetric encryption integration. + * + *

+ * This package provides the ElGamal algorithm descriptor, a streaming cipher + * context, configuration specifications for padding and parameters, and encoded + * key specifications for import/export. Provider-specific details are kept + * behind small factories while roles and metadata remain explicit to higher + * layers. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register ElGamal under a canonical identifier and declare + * {@link zeroecho.core.KeyUsage#ENCRYPT} and + * {@link zeroecho.core.KeyUsage#DECRYPT} roles.
  • + *
  • Offer a streaming {@link zeroecho.core.context.EncryptionContext} that + * performs block-wise ElGamal transforms with selectable padding.
  • + *
  • Expose specifications for padding modes and for explicit domain + * parameters, plus utility specs for encoded key import.
  • + *
  • Encapsulate JCA/JCE interop and provider checks inside the algorithm + * descriptor and builders.
  • + *
+ * + *

Components

+ *
    + *
  • ElgamalAlgorithm: algorithm descriptor that wires roles to the + * streaming cipher context and registers key builders for explicit parameter + * sets and encoded keys.
  • + *
  • ElgamalCipherContext: streaming context that encrypts or decrypts + * data using ElGamal with block geometry derived from the modulus size and + * selected padding.
  • + *
  • ElgamalEncSpec: immutable padding specification selecting either + * no padding or PKCS#1 v1.5-style padding.
  • + *
  • ElgamalParamSpec: immutable wrapper for explicit domain parameters + * (modulus and generator), including a predefined RFC 7919 FFDHE2048 set.
  • + *
  • ElgamalKeyGenSpec: parameters for generating fresh domain + * parameters and key pairs; typically disabled in favor of predefined parameter + * sets.
  • + *
  • ElgamalPublicKeySpec / ElgamalPrivateKeySpec: immutable + * encoded key specifications (X.509 and PKCS#8) with defensive copying and + * compact marshalling helpers.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and thread-safe; cipher contexts are + * stateful and not thread-safe.
  • + *
  • Padding choice affects plaintext-per-block and security properties; + * modern protocols should prefer KEM-based constructions for IND-CCA + * security.
  • + *
  • Encoded key specs clone input and output arrays to avoid leaking internal + * state.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.elgamal; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoAlgorithm.java new file mode 100644 index 0000000..3623845 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoAlgorithm.java @@ -0,0 +1,278 @@ +/******************************************************************************* + * 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.core.alg.frodo; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.bouncycastle.pqc.jcajce.spec.FrodoParameterSpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

Frodo Key Encapsulation Mechanism (KEM)

+ * + * Implementation of the FrodoKEM post-quantum algorithm, registered as a + * {@link zeroecho.core.CryptoAlgorithm} within ZeroEcho. + * + *

+ * FrodoKEM is a lattice-based key encapsulation mechanism designed to resist + * quantum adversaries. It is based on the hardness of the Learning With Errors + * (LWE) problem, avoiding reliance on structured lattices. This provides + * conservative security at the cost of relatively large key sizes compared to + * schemes such as Kyber. + *

+ * + *

Capabilities

+ *
    + *
  • {@link zeroecho.core.AlgorithmFamily#KEM}: + *
      + *
    • {@link zeroecho.core.KeyUsage#ENCAPSULATE} with a + * {@link java.security.PublicKey} to create a + * {@link zeroecho.core.context.KemContext}.
    • + *
    • {@link zeroecho.core.KeyUsage#DECAPSULATE} with a + * {@link java.security.PrivateKey} to recover the shared secret.
    • + *
    + *
  • + *
  • {@link zeroecho.core.AlgorithmFamily#AGREEMENT}: + *
      + *
    • As initiator (Alice) using a recipient {@link java.security.PublicKey}, + * the KEM encapsulation is adapted into a + * {@link zeroecho.core.context.MessageAgreementContext}.
    • + *
    • As responder (Bob) using his {@link java.security.PrivateKey}, the + * decapsulation is similarly adapted into a + * {@link zeroecho.core.context.MessageAgreementContext}.
    • + *
    + *
  • + *
+ * + *

Key Management

+ *
    + *
  • Key generation is parameterized by {@link FrodoKeyGenSpec}, which selects + * a concrete variant such as {@code FRODO_640_AES} or + * {@code FRODO_1344_SHAKE}.
  • + *
  • Public keys may be imported from {@link FrodoPublicKeySpec} using an + * X.509 encoding.
  • + *
  • Private keys may be imported from {@link FrodoPrivateKeySpec} using a + * PKCS#8 encoding.
  • + *
  • Direct import of key specs via {@code generateKeyPair} in the spec-based + * builders is not supported and will throw + * {@link UnsupportedOperationException}.
  • + *
+ * + *

Provider requirements

+ *

+ * FrodoKEM operations are delegated to the BouncyCastle PQC provider + * ({@code BCPQC}). The static method {@link #ensureProvider()} verifies that + * the provider is installed; otherwise, a + * {@link java.security.NoSuchProviderException} is thrown. + *

+ * + *

Example usage

{@code
+ * // Generate a Frodo keypair
+ * CryptoAlgorithm frodo = CryptoAlgorithms.require("Frodo");
+ * KeyPair kp = frodo.generateKeyPair(FrodoKeyGenSpec.frodo1344aes());
+ *
+ * // Encapsulate using the recipient's public key
+ * KemContext enc = frodo.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
+ * 
+ * // Decapsulate using the recipient's private key
+ * KemContext dec = frodo.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
+ * }
+ * + *

Thread-safety

+ *

+ * Instances of {@code FrodoAlgorithm} are immutable and may be shared safely + * across threads. Contexts created via capabilities are not guaranteed to be + * thread-safe. + *

+ * + * @since 1.0 + */ +public final class FrodoAlgorithm extends AbstractCryptoAlgorithm { + /** + * Constructs and registers the Frodo post-quantum algorithm. + * + *

+ * The constructor wires all roles and key builders supported by FrodoKEM: + *

+ *
    + *
  • Binds {@link zeroecho.core.AlgorithmFamily#KEM} roles for + * {@link zeroecho.core.KeyUsage#ENCAPSULATE} (public key) and + * {@link zeroecho.core.KeyUsage#DECAPSULATE} (private key) to + * {@link FrodoKemContext}.
  • + *
  • Binds {@link zeroecho.core.AlgorithmFamily#AGREEMENT} roles for initiator + * and responder, adapting the KEM context to a + * {@link zeroecho.core.context.MessageAgreementContext} via + * {@link zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter}.
  • + *
  • Registers asymmetric key builders for: + *
      + *
    • {@link FrodoKeyGenSpec} - key pair generation with a selectable Frodo + * variant.
    • + *
    • {@link FrodoPublicKeySpec} - importing X.509-encoded public keys.
    • + *
    • {@link FrodoPrivateKeySpec} - importing PKCS#8-encoded private keys.
    • + *
    + *
  • + *
+ * + *

+ * The default algorithm identifier is {@code "Frodo"} with display name + * {@code "FrodoKEM"}, bound to the {@code BCPQC} provider. + *

+ * + * @throws java.lang.IllegalStateException if registration of capabilities or + * builders fails + */ + public FrodoAlgorithm() { + super("Frodo", "FrodoKEM", BouncyCastlePQCProvider.PROVIDER_NAME); + + capability(AlgorithmFamily.KEM, KeyUsage.ENCAPSULATE, KemContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> new FrodoKemContext(this, k), () -> VoidSpec.INSTANCE); + capability(AlgorithmFamily.KEM, KeyUsage.DECAPSULATE, KemContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> new FrodoKemContext(this, k), () -> VoidSpec.INSTANCE); + + // AGREEMENT (initiator): Alice has Bob's public key → encapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← return your + // existing KemContext + PublicKey.class, // ← initiator uses recipient's public key + VoidSpec.class, // ← must implement ContextSpec + (PublicKey recipient, VoidSpec spec) -> { + // create a context bound to recipient public key for encapsulation + return KemMessageAgreementAdapter.builder().upon(new FrodoKemContext(this, recipient)).asInitiator() + .build(); + }, () -> VoidSpec.INSTANCE // default + ); + + // AGREEMENT (responder): Bob has his private key → decapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← same KemContext + // type + PrivateKey.class, // ← responder uses their private key + VoidSpec.class, (PrivateKey myPriv, VoidSpec spec) -> { + return KemMessageAgreementAdapter.builder().upon(new FrodoKemContext(this, myPriv)).asResponder() + .build(); + }, () -> VoidSpec.INSTANCE); + + registerAsymmetricKeyBuilder(FrodoKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(FrodoKeyGenSpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("Frodo", providerName()); + FrodoParameterSpec params = switch (spec.variant()) { + case FRODO_640_AES -> FrodoParameterSpec.frodokem640aes; + case FRODO_640_SHAKE -> FrodoParameterSpec.frodokem640shake; + case FRODO_976_AES -> FrodoParameterSpec.frodokem976aes; + case FRODO_976_SHAKE -> FrodoParameterSpec.frodokem976shake; + case FRODO_1344_AES -> FrodoParameterSpec.frodokem1344aes; + case FRODO_1344_SHAKE -> FrodoParameterSpec.frodokem1344shake; + }; + kpg.initialize(params, new SecureRandom()); + return kpg.generateKeyPair(); + } + + @Override + public PublicKey importPublic(FrodoKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(FrodoKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + }, FrodoKeyGenSpec::frodo1344aes); + + registerAsymmetricKeyBuilder(FrodoPublicKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(FrodoPublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(FrodoPublicKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("Frodo", providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); + } + + @Override + public PrivateKey importPrivate(FrodoPublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + }, null); + + registerAsymmetricKeyBuilder(FrodoPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(FrodoPrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(FrodoPrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(FrodoPrivateKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("Frodo", providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + } + }, null); + } + + private static void ensureProvider() throws NoSuchProviderException { + Provider p = Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME); + if (p == null) { + throw new NoSuchProviderException("BCPQC provider not registered"); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoKemContext.java b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoKemContext.java new file mode 100644 index 0000000..796eef6 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoKemContext.java @@ -0,0 +1,219 @@ +/******************************************************************************* + * 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.core.alg.frodo; + +import java.io.IOException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Objects; + +import javax.security.auth.DestroyFailedException; + +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.pqc.crypto.frodo.FrodoKEMExtractor; +import org.bouncycastle.pqc.crypto.frodo.FrodoKEMGenerator; +import org.bouncycastle.pqc.crypto.frodo.FrodoPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.frodo.FrodoPublicKeyParameters; +import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory; +import org.bouncycastle.pqc.crypto.util.PublicKeyFactory; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.KemContext; + +/** + *

FrodoKEM runtime context

+ * + * Implementation of {@link zeroecho.core.context.KemContext} for the Frodo + * post-quantum key encapsulation mechanism (KEM). + * + *

+ * A {@code FrodoKemContext} is a lightweight binding of: + *

+ *
    + *
  • a {@link zeroecho.core.CryptoAlgorithm} descriptor,
  • + *
  • a key (public for encapsulation or private for decapsulation), and
  • + *
  • a role indicator (encapsulate or decapsulate).
  • + *
+ * + *

+ * Contexts are created by {@link FrodoAlgorithm} when binding roles such as + * {@link zeroecho.core.KeyUsage#ENCAPSULATE} or + * {@link zeroecho.core.KeyUsage#DECAPSULATE}. They provide the runtime entry + * points to encapsulate a secret or decapsulate a received ciphertext. + *

+ * + *

Thread-safety

+ *

+ * Instances are not thread-safe. A context is intended for single-operation or + * short-lived use within a single thread. + *

+ * + * @since 1.0 + */ +public final class FrodoKemContext implements KemContext { + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean encapsulate; + + /** + * Constructs a context bound to a public key for the + * {@link zeroecho.core.KeyUsage#ENCAPSULATE} role. + * + * @param algorithm algorithm descriptor, must not be {@code null} + * @param k Frodo public key used to encapsulate the shared secret + * @throws NullPointerException if any argument is {@code null} + */ + public FrodoKemContext(CryptoAlgorithm algorithm, PublicKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = true; + } + + /** + * Constructs a context bound to a private key for the + * {@link zeroecho.core.KeyUsage#DECAPSULATE} role. + * + * @param algorithm algorithm descriptor, must not be {@code null} + * @param k Frodo private key used to decapsulate ciphertext + * @throws NullPointerException if any argument is {@code null} + */ + public FrodoKemContext(CryptoAlgorithm algorithm, PrivateKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = false; + } + + /** + * Returns the associated algorithm descriptor. + * + * @return algorithm instance that created this context + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + * @return public key (for encapsulate) or private key (for decapsulate) + */ + @Override + public Key key() { + return key; + } + + /** + * Closes the context and releases any held resources. + * + *

+ * For FrodoKEM this is a no-op, but the method is present for consistency with + * other context types. + *

+ */ + @Override + public void close() { + // empty + } + + /** + * Encapsulates a fresh shared secret to the recipient’s public key. + * + *

+ * This method is only valid if the context was constructed with a public key + * (encapsulation role). Otherwise an {@link IllegalStateException} is thrown. + *

+ * + *

+ * A new random secret and ciphertext are generated using BouncyCastle’s + * FrodoKEM implementation. The ciphertext is transmitted to the recipient, + * while the secret is retained locally as the agreed keying material. + *

+ * + * @return result containing the encapsulated ciphertext and shared secret + * @throws IllegalStateException if this context is in decapsulation mode + * @throws IOException if encapsulation fails in the underlying + * provider + */ + @Override + public KemResult encapsulate() throws IOException { + if (!encapsulate) { + throw new IllegalStateException("Not initialized for ENCAPSULATE"); + } + try { + final FrodoPublicKeyParameters keyParam = (FrodoPublicKeyParameters) PublicKeyFactory + .createKey(key.getEncoded()); + FrodoKEMGenerator gen = new FrodoKEMGenerator(new SecureRandom()); + SecretWithEncapsulation res = gen.generateEncapsulated(keyParam); + byte[] secret = res.getSecret(); + byte[] ct = res.getEncapsulation(); + res.destroy(); + return new KemResult(ct, secret); + } catch (DestroyFailedException e) { + throw new IOException("Frodo encapsulate failed", e); + } + } + + /** + * Decapsulates a ciphertext to recover the shared secret. + * + *

+ * This method is only valid if the context was constructed with a private key + * (decapsulation role). Otherwise an {@link IllegalStateException} is thrown. + *

+ * + * @param ciphertext encapsulated keying material received from the initiator + * @return the recovered shared secret + * @throws IllegalStateException if this context is in encapsulation mode + * @throws IOException if decapsulation fails in the underlying + * provider + */ + @Override + public byte[] decapsulate(byte[] ciphertext) throws IOException { + if (encapsulate) { + throw new IllegalStateException("Not initialized for DECAPSULATE"); + } + try { + final FrodoPrivateKeyParameters keyParam = (FrodoPrivateKeyParameters) PrivateKeyFactory + .createKey(key.getEncoded()); + FrodoKEMExtractor ex = new FrodoKEMExtractor(keyParam); + return ex.extractSecret(ciphertext); + } catch (Exception e) { // NOPMD + throw new IOException("Frodo decapsulate failed", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoKeyGenSpec.java new file mode 100644 index 0000000..1cfd9dd --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoKeyGenSpec.java @@ -0,0 +1,195 @@ +/******************************************************************************* + * 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.core.alg.frodo; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Key generation specification for FrodoKEM

+ * + * {@code FrodoKeyGenSpec} defines the parameters required to generate a + * FrodoKEM key pair. It encapsulates the choice of Frodo variant, which + * determines security level, performance, and key sizes. + * + *

Variants

Each {@link Variant} corresponds to a standardized FrodoKEM + * parameter set: + *
    + *
  • {@link Variant#FRODO_640_AES} - 128-bit security level using AES-based + * pseudorandomness.
  • + *
  • {@link Variant#FRODO_640_SHAKE} - 128-bit security level using SHAKE + * instead of AES.
  • + *
  • {@link Variant#FRODO_976_AES} - ~192-bit security level with AES-based + * pseudorandomness.
  • + *
  • {@link Variant#FRODO_976_SHAKE} - ~192-bit security level with + * SHAKE.
  • + *
  • {@link Variant#FRODO_1344_AES} - ~256-bit security level with AES-based + * pseudorandomness.
  • + *
  • {@link Variant#FRODO_1344_SHAKE} - ~256-bit security level with + * SHAKE.
  • + *
+ * + *

+ * AES-based and SHAKE-based variants differ only in the pseudorandom generator + * used internally. Security levels are defined against both classical and + * quantum adversaries under the Learning With Errors (LWE) assumption. + *

+ * + *

Thread-safety

+ *

+ * This class is immutable and therefore safe to share across threads. + *

+ * + * @since 1.0 + */ +public final class FrodoKeyGenSpec implements AlgorithmKeySpec, Describable { + /** + * Enumerates the standardized FrodoKEM parameter sets. + * + *

+ * Each variant fixes matrix dimensions, modulus, and error distribution + * parameters. They balance security and performance at three target NIST + * security levels (128, 192, 256 bits). + *

+ */ + public enum Variant { + /** 128-bit security, AES-based pseudorandomness. */ + FRODO_640_AES, + /** 128-bit security, SHAKE-based pseudorandomness. */ + FRODO_640_SHAKE, + /** ~192-bit security, AES-based pseudorandomness. */ + FRODO_976_AES, + /** ~192-bit security, SHAKE-based pseudorandomness. */ + FRODO_976_SHAKE, + /** ~256-bit security, AES-based pseudorandomness. */ + FRODO_1344_AES, + /** ~256-bit security, SHAKE-based pseudorandomness. */ + FRODO_1344_SHAKE + } + + private final Variant variant; + + private FrodoKeyGenSpec(Variant v) { + this.variant = v; + } + + /** + * Creates a spec from a given variant. + * + * @param v Frodo variant to select + * @return new key generation spec + */ + public static FrodoKeyGenSpec of(Variant v) { + return new FrodoKeyGenSpec(v); + } + + /** + * Convenience factory for the {@link Variant#FRODO_640_AES} parameter set. + * + * @return spec targeting 128-bit security with AES PRG + */ + public static FrodoKeyGenSpec frodo640aes() { + return new FrodoKeyGenSpec(Variant.FRODO_640_AES); + } + + /** + * Convenience factory for the {@link Variant#FRODO_640_SHAKE} parameter set. + * + * @return spec targeting 128-bit security with SHAKE PRG + */ + public static FrodoKeyGenSpec frodo640shake() { + return new FrodoKeyGenSpec(Variant.FRODO_640_SHAKE); + } + + /** + * Convenience factory for the {@link Variant#FRODO_976_AES} parameter set. + * + * @return spec targeting ~192-bit security with AES PRG + */ + public static FrodoKeyGenSpec frodo976aes() { + return new FrodoKeyGenSpec(Variant.FRODO_976_AES); + } + + /** + * Convenience factory for the {@link Variant#FRODO_976_SHAKE} parameter set. + * + * @return spec targeting ~192-bit security with SHAKE PRG + */ + public static FrodoKeyGenSpec frodo976shake() { + return new FrodoKeyGenSpec(Variant.FRODO_976_SHAKE); + } + + /** + * Convenience factory for the {@link Variant#FRODO_1344_AES} parameter set. + * + * @return spec targeting ~256-bit security with AES PRG + */ + public static FrodoKeyGenSpec frodo1344aes() { + return new FrodoKeyGenSpec(Variant.FRODO_1344_AES); + } + + /** + * Convenience factory for the {@link Variant#FRODO_1344_SHAKE} parameter set. + * + * @return spec targeting ~256-bit security with SHAKE PRG + */ + public static FrodoKeyGenSpec frodo1344shake() { + return new FrodoKeyGenSpec(Variant.FRODO_1344_SHAKE); + } + + /** + * Returns the Frodo variant. + * + * @return selected parameter set + */ + public Variant variant() { + return variant; + } + + /** + * Human-readable description of this spec. + * + *

+ * The value is simply the {@link Variant#toString()} of the chosen parameter + * set (e.g., {@code "FRODO_640_AES"}). + *

+ * + * @return string description of the variant + */ + @Override + public String description() { + return variant.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPrivateKeySpec.java new file mode 100644 index 0000000..df052de --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPrivateKeySpec.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * 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.core.alg.frodo; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Specification for importing a Frodo private key

+ * + * {@code FrodoPrivateKeySpec} is a simple wrapper around a PKCS#8-encoded + * FrodoKEM private key. It provides immutable access to the raw encoding and + * utilities for serialization. + * + *

Encoding format

+ *
    + *
  • Keys are stored in the standard PKCS#8 DER format as produced by + * providers such as BouncyCastle PQC.
  • + *
  • The encoding is preserved exactly as given; no internal parsing or + * validation occurs in this class.
  • + *
+ * + *

Usage

+ *
    + *
  • Instances of this spec can be passed to + * {@link CryptoAlgorithm#importPrivate(AlgorithmKeySpec) + * importPrivate(FrodoPrivateKeySpec)} to construct a usable + * {@link java.security.PrivateKey} object.
  • + *
  • The {@link #marshal(FrodoPrivateKeySpec)} and {@link #unmarshal(PairSeq)} + * helpers allow safe conversion to/from structured textual form for persistence + * or transmission.
  • + *
+ * + *
{@code
+ * // Import an existing Frodo private key
+ * byte[] encoded = Files.readAllBytes(Paths.get("frodo.key"));
+ * FrodoPrivateKeySpec spec = new FrodoPrivateKeySpec(encoded);
+ * PrivateKey priv = frodo.importPrivate(spec);
+ * }
+ * + *

Thread-safety

+ *

+ * This class is immutable; the internal byte array is cloned on construction + * and when accessed via {@link #pkcs8()}. + *

+ * + * @since 1.0 + */ +public final class FrodoPrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Creates a new specification from a PKCS#8 DER-encoded key. + * + * @param pkcs8Der the encoded private key bytes (will be cloned) + * @throws NullPointerException if {@code pkcs8Der} is {@code null} + */ + public FrodoPrivateKeySpec(byte[] pkcs8Der) { + this.pkcs8 = Objects.requireNonNull(pkcs8Der).clone(); + } + + /** + * Returns a clone of the underlying PKCS#8 encoding. + * + * @return defensive copy of the PKCS#8-encoded private key + */ + public byte[] pkcs8() { + return pkcs8.clone(); + } + + /** + * Serializes this spec into a {@link PairSeq}, encoding the key as Base64 + * without padding. + * + * @param spec Frodo private key specification to serialize + * @return a {@code PairSeq} with type and Base64-encoded key + */ + public static PairSeq marshal(FrodoPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "FrodoPrivateKeySpec", PKCS8_B64, b64); + } + + /** + * Parses a {@link PairSeq} previously produced by + * {@link #marshal(FrodoPrivateKeySpec)}. + * + * @param p input sequence containing a {@code pkcs8.b64} entry + * @return a new private key spec initialized with the decoded bytes + * @throws IllegalArgumentException if {@code pkcs8.b64} entry is missing + */ + public static FrodoPrivateKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (PKCS8_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("FrodoPrivateKeySpec: missing pkcs8.b64"); + } + return new FrodoPrivateKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string containing the encoded length. + * + * @return string in the form {@code FrodoPrivateKeySpec[len=N]} + */ + @Override + public String toString() { + return "FrodoPrivateKeySpec[len=" + pkcs8.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java new file mode 100644 index 0000000..7e8243a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/frodo/FrodoPublicKeySpec.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * 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.core.alg.frodo; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Specification for importing a Frodo public key

+ * + * {@code FrodoPublicKeySpec} is a wrapper around an X.509-encoded FrodoKEM + * public key. It provides immutable access to the raw encoding and utility + * methods for serialization. + * + *

Encoding format

+ *
    + *
  • Keys are stored in the standard X.509 SubjectPublicKeyInfo format as + * produced by providers such as BouncyCastle PQC.
  • + *
  • The encoding is preserved exactly as provided; no parsing or validation + * is performed by this class.
  • + *
+ * + *

Usage

+ *
    + *
  • Instances of this spec can be passed to + * {@link CryptoAlgorithm#importPublic(AlgorithmKeySpec) + * importPublic(FrodoPublicKeySpec)} to construct a usable + * {@link java.security.PublicKey}.
  • + *
  • The {@link #marshal(FrodoPublicKeySpec)} and {@link #unmarshal(PairSeq)} + * methods allow safe serialization into and recovery from structured textual + * form.
  • + *
+ * + *
{@code
+ * // Import an existing Frodo public key
+ * byte[] encoded = Files.readAllBytes(Paths.get("frodo.pub"));
+ * FrodoPublicKeySpec spec = new FrodoPublicKeySpec(encoded);
+ * PublicKey pub = frodo.importPublic(spec);
+ * }
+ * + *

Thread-safety

+ *

+ * This class is immutable; the internal byte array is cloned on construction + * and when accessed via {@link #x509()}. + *

+ * + * @since 1.0 + */ +public final class FrodoPublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Creates a new specification from an X.509 DER-encoded public key. + * + * @param x509Der the encoded public key bytes (will be cloned) + * @throws NullPointerException if {@code x509Der} is {@code null} + */ + public FrodoPublicKeySpec(byte[] x509Der) { + this.x509 = Objects.requireNonNull(x509Der).clone(); + } + + /** + * Returns a clone of the underlying X.509 encoding. + * + * @return defensive copy of the X.509-encoded public key + */ + public byte[] x509() { + return x509.clone(); + } + + /** + * Serializes this spec into a {@link PairSeq}, encoding the key in Base64 + * without padding. + * + * @param spec Frodo public key specification to serialize + * @return a {@code PairSeq} with type and Base64-encoded key + */ + public static PairSeq marshal(FrodoPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "FrodoPublicKeySpec", X509_B64, b64); + } + + /** + * Parses a {@link PairSeq} previously produced by + * {@link #marshal(FrodoPublicKeySpec)}. + * + * @param p input sequence containing a {@code x509.b64} entry + * @return a new public key spec initialized with the decoded bytes + * @throws IllegalArgumentException if {@code x509.b64} entry is missing + */ + public static FrodoPublicKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (X509_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("FrodoPublicKeySpec: missing x509.b64"); + } + return new FrodoPublicKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string containing the encoded length. + * + * @return string in the form {@code FrodoPublicKeySpec[len=N]} + */ + @Override + public String toString() { + return "FrodoPublicKeySpec[len=" + x509.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/frodo/package-info.java b/lib/src/main/java/zeroecho/core/alg/frodo/package-info.java new file mode 100644 index 0000000..5a14d51 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/frodo/package-info.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * FrodoKEM post-quantum key encapsulation integration. + * + *

+ * This package integrates the Frodo lattice-based key encapsulation mechanism + * (KEM) into the core layer. It provides the algorithm descriptor, runtime KEM + * context, key generation specifications, and encoded key specs for import and + * export. Frodo is based on the hardness of the Learning With Errors (LWE) + * problem, avoiding structured lattices and offering conservative security + * margins. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register Frodo under a canonical identifier and declare + * {@link zeroecho.core.KeyUsage#ENCAPSULATE} and + * {@link zeroecho.core.KeyUsage#DECAPSULATE} roles, plus an agreement adapter + * role for initiator/responder workflows.
  • + *
  • Provide a {@link zeroecho.core.context.KemContext} implementation bound + * to either a public or private key for encapsulation or decapsulation.
  • + *
  • Expose immutable specifications for key generation variants and encoded + * key carriers with marshalling helpers.
  • + *
  • Ensure operations are delegated to a supported PQC provider (BouncyCastle + * PQC) and fail fast if absent.
  • + *
+ * + *

Components

+ *
    + *
  • FrodoAlgorithm: algorithm descriptor that wires KEM and agreement + * roles and registers key builders.
  • + *
  • FrodoKemContext: runtime context implementing encapsulate and + * decapsulate operations using BouncyCastle’s Frodo engine.
  • + *
  • FrodoKeyGenSpec: immutable specification of the Frodo variant + * (640, 976, 1344; AES or SHAKE-based), with convenience factories.
  • + *
  • FrodoPublicKeySpec / FrodoPrivateKeySpec: wrappers over + * X.509 and PKCS#8 encodings with defensive copying and + * {@link zeroecho.core.marshal.PairSeq} marshalling utilities.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and safe to share across + * threads.
  • + *
  • KEM contexts are stateful and not thread-safe; they are intended for + * single encapsulation or decapsulation operations.
  • + *
  • Key specifications preserve encoded form without internal parsing and + * rely on defensive cloning for safety.
  • + *
  • Agreement adapters allow FrodoKEM to be composed into higher-level + * initiator/responder protocols transparently.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.frodo; diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacAlgorithm.java new file mode 100644 index 0000000..036e1ba --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacAlgorithm.java @@ -0,0 +1,170 @@ +/******************************************************************************* + * 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.core.alg.hmac; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.context.MacContext; +import zeroecho.core.spi.SymmetricKeyBuilder; + +/** + *

HMAC Algorithm Integration

+ * + * {@code HmacAlgorithm} registers the Hash-based Message Authentication Code + * (HMAC) family into the ZeroEcho framework. It provides a single role, + * {@link KeyUsage#MAC}, which can both produce and verify authentication tags. + * + *

+ * HMAC is a widely standardized construction that combines a cryptographic hash + * function with a secret key to provide integrity and authenticity for + * arbitrary messages. It is secure under the assumption that the underlying + * hash function is pseudorandom and collision-resistant. + *

+ * + *

Capabilities

+ *
    + *
  • Family: {@link AlgorithmFamily#SYMMETRIC}
  • + *
  • Role: {@link KeyUsage#MAC}
  • + *
  • Context: {@link MacContext}
  • + *
  • Key type: {@link SecretKey}
  • + *
  • Spec type: {@link HmacSpec}
  • + *
+ * + *

Key management

+ *
    + *
  • {@link HmacKeyGenSpec}: for generating fresh keys using a secure random + * source and the specified key length.
  • + *
  • {@link HmacKeyImportSpec}: for wrapping existing raw key material into a + * {@link SecretKey} instance compatible with HMAC.
  • + *
+ * + *

Defaults

+ *
    + *
  • Default MAC spec: {@link HmacSpec#sha256()}
  • + *
  • Default key generation: 256-bit key for SHA-256 HMAC
  • + *
+ * + *

Usage example

{@code
+ * // Generate a fresh key for HMAC-SHA256
+ * SecretKey key = CryptoAlgorithms.generateSecret("HMAC",
+ *     HmacKeyGenSpec.sha256(256));
+ *
+ * // Create a MAC context
+ * MacContext ctx = CryptoAlgorithms.create("HMAC",
+ *     KeyUsage.MAC, key, HmacSpec.sha256());
+ *
+ * ctx.update(data);
+ * byte[] tag = ctx.doFinal();
+ *
+ * // Verification: set expected tag
+ * ctx.setExpectedTag(tag);
+ * boolean valid = ctx.verify(data);
+ * }
+ * + * @since 1.0 + */ +public final class HmacAlgorithm extends AbstractCryptoAlgorithm { + /** + * Constructs and registers the HMAC algorithm. + * + *

+ * The constructor wires the algorithm identifier, declares the MAC capability, + * and registers symmetric key builders for generation and import. It sets + * SHA-256 HMAC as the default spec for both catalog metadata and testing. + *

+ * + *

Initialization steps

+ *
    + *
  1. Registers a {@link KeyUsage#MAC} capability bound to {@link MacContext} + * and {@link HmacSpec}.
  2. + *
  3. Registers a symmetric key builder for {@link HmacKeyGenSpec}, which + * generates keys using the JCA {@link KeyGenerator} API.
  4. + *
  5. Registers a symmetric key builder for {@link HmacKeyImportSpec}, which + * wraps raw byte arrays as {@link SecretKeySpec} instances.
  6. + *
+ */ + public HmacAlgorithm() { + super("HMAC", "HMAC (generic)"); + + // Single capability: use KeyUsage.MAC for both produce and verify. + // Verify is selected by calling setExpectedTag(...) on the returned MacContext. + capability(AlgorithmFamily.SYMMETRIC, KeyUsage.MAC, MacContext.class, SecretKey.class, HmacSpec.class, + (SecretKey k, HmacSpec s) -> { + try { + return new HmacMacContext(this, k, s.macName()); + } catch (GeneralSecurityException e) { + throw new IOException("Init HMAC failed for " + s.macName(), e); + } + }, HmacSpec::sha256 // default for catalog/tests + ); + + // Key builders (generation/import) — both respect macName in the spec. + registerSymmetricKeyBuilder(HmacKeyGenSpec.class, new SymmetricKeyBuilder<>() { + @Override + public SecretKey generateSecret(HmacKeyGenSpec spec) throws GeneralSecurityException { + KeyGenerator kg = KeyGenerator.getInstance(spec.macName()); + kg.init(spec.keySizeBits(), new SecureRandom()); + return kg.generateKey(); + } + + @Override + public SecretKey importSecret(HmacKeyGenSpec spec) { + throw new UnsupportedOperationException("Use HmacKeyImportSpec for import"); + } + }, () -> HmacKeyGenSpec.sha256(256) // default keygen spec + ); + + registerSymmetricKeyBuilder(HmacKeyImportSpec.class, new SymmetricKeyBuilder<>() { + @Override + public SecretKey generateSecret(HmacKeyImportSpec spec) { + throw new UnsupportedOperationException("Use HmacKeyGenSpec for generation"); + } + + @Override + public SecretKey importSecret(HmacKeyImportSpec spec) { + return new SecretKeySpec(spec.key(), spec.macName()); + } + }, null); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java new file mode 100644 index 0000000..9dbc14b --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyGenSpec.java @@ -0,0 +1,172 @@ +/******************************************************************************* + * 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.core.alg.hmac; + +import java.util.Objects; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

Specification for HMAC key generation

+ * + * {@code HmacKeyGenSpec} describes the parameters required to generate a secret + * key for a Hash-based Message Authentication Code (HMAC) algorithm. It defines + * the target MAC algorithm name and the key size in bits. + * + *

Constraints

+ *
    + *
  • The {@code macName} must correspond to a supported HMAC variant such as + * {@code "HmacSHA256"}, {@code "HmacSHA384"}, or {@code "HmacSHA512"}.
  • + *
  • The key size must be a positive multiple of 8 (i.e., full-byte + * lengths).
  • + *
+ * + *

Usage

Typical usage is to construct a spec with a given digest + * family and key size, and then pass it to a registered + * {@code SymmetricKeyBuilder}: + * + *
{@code
+ * // Generate a 256-bit key for HMAC-SHA256
+ * HmacKeyGenSpec spec = HmacKeyGenSpec.sha256(256);
+ * SecretKey key = CryptoAlgorithms.generateSecret("HMAC", spec);
+ * }
+ * + *

Defaults

Convenience static factories are provided for the most + * common variants: + *
    + *
  • {@link #sha256(int)} - HMAC-SHA256 with a caller-specified bit + * length
  • + *
  • {@link #sha384(int)} - HMAC-SHA384 with a caller-specified bit + * length
  • + *
  • {@link #sha512(int)} - HMAC-SHA512 with a caller-specified bit + * length
  • + *
+ * + *

Security considerations

+ *
    + *
  • Recommended key size is at least equal to the output size of the + * underlying hash function (e.g., 256 bits for SHA-256).
  • + *
  • Keys should always be generated with a strong randomness source.
  • + *
  • Do not reuse HMAC keys across different digest families.
  • + *
+ * + * @since 1.0 + */ +public final class HmacKeyGenSpec implements AlgorithmKeySpec, Describable { + private final String macName; + private final int keySizeBits; + + /** + * Constructs a new specification for HMAC key generation. + * + * @param macName canonical name of the target HMAC algorithm (e.g., + * {@code "HmacSHA256"}) + * @param keySizeBits desired key size in bits; must be a positive multiple of 8 + * @throws NullPointerException if {@code macName} is {@code null} + * @throws IllegalArgumentException if {@code keySizeBits} is non-positive or + * not divisible by 8 + */ + public HmacKeyGenSpec(String macName, int keySizeBits) { + this.macName = Objects.requireNonNull(macName, "macName must not be null"); + if (keySizeBits <= 0 || (keySizeBits % 8) != 0) { + throw new IllegalArgumentException("keySizeBits must be a positive multiple of 8"); + } + this.keySizeBits = keySizeBits; + } + + /** + * Returns the JCA algorithm name of the HMAC variant. + * + * @return the algorithm name (e.g., {@code "HmacSHA256"}) + */ + public String macName() { + return macName; + } + + /** + * Returns the key size requested for generation. + * + * @return key size in bits (always a positive multiple of 8) + */ + public int keySizeBits() { + return keySizeBits; + } + + /** + * Convenience factory for HMAC-SHA256 key generation specs. + * + * @param bits desired key size in bits + * @return a new spec targeting {@code HmacSHA256} with the given key size + */ + public static HmacKeyGenSpec sha256(int bits) { + return new HmacKeyGenSpec("HmacSHA256", bits); + } + + /** + * Convenience factory for HMAC-SHA384 key generation specs. + * + * @param bits desired key size in bits + * @return a new spec targeting {@code HmacSHA384} with the given key size + */ + public static HmacKeyGenSpec sha384(int bits) { + return new HmacKeyGenSpec("HmacSHA384", bits); + } + + /** + * Convenience factory for HMAC-SHA512 key generation specs. + * + * @param bits desired key size in bits + * @return a new spec targeting {@code HmacSHA512} with the given key size + */ + public static HmacKeyGenSpec sha512(int bits) { + return new HmacKeyGenSpec("HmacSHA512", bits); + } + + /** + * Returns a short description of this key spec. + * + *

+ * The description is formed as {@code "/"}. Example: + * {@code "HmacSHA256/256"}. + *

+ * + * @return human-readable description string + */ + @Override + public String description() { + return macName + "/" + keySizeBits; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java new file mode 100644 index 0000000..c9fb515 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacKeyImportSpec.java @@ -0,0 +1,237 @@ +/******************************************************************************* + * 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.core.alg.hmac; + +import java.util.Base64; +import java.util.HexFormat; +import java.util.Objects; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Specification for importing raw HMAC keys into a + * {@link javax.crypto.SecretKey}. + * + *

+ * {@code HmacKeyImportSpec} describes how existing key material for an HMAC + * algorithm (for example, {@code "HmacSHA256"}) should be wrapped into a + * {@link javax.crypto.SecretKey}. Unlike {@link HmacKeyGenSpec}, which requests + * generation of fresh random keys, this class is used when the caller already + * possesses key material in raw, hexadecimal, or Base64-encoded form. + *

+ * + *

Fields

+ *
    + *
  • {@code macName}: canonical HMAC algorithm name (for example, + * {@code "HmacSHA256"}).
  • + *
  • {@code key}: the raw key material as a byte array (defensively + * copied).
  • + *
+ * + *

Typical usage

+ *

Import from raw bytes

+ * {@code
+ * byte[] rawKey = Files.readAllBytes(Paths.get("hmac.key"));
+ * HmacKeyImportSpec spec = HmacKeyImportSpec.fromRaw("HmacSHA256", rawKey);
+ * SecretKey key = CryptoAlgorithms.importSecret("HMAC", spec);
+ * }
+ * 
+ * + *

Import from hex/Base64

+ * {@code
+ * HmacKeyImportSpec specHex =
+ *     HmacKeyImportSpec.fromHex("HmacSHA256", "aabbccddeeff00112233");
+ *
+ * HmacKeyImportSpec specB64 =
+ *     HmacKeyImportSpec.fromBase64("HmacSHA256", "Khsz95oU+7H0Ow==");
+ * }
+ * 
+ * + *

Serialization

+ *
    + *
  • {@link #marshal(HmacKeyImportSpec)} encodes a spec into a {@link PairSeq} + * with fields {@code type=HMAC-KEY}, {@code mac}, and {@code k.b64} (Base64 + * without padding).
  • + *
  • {@link #unmarshal(PairSeq)} decodes a spec, accepting {@code k.b64} or + * {@code k.hex}.
  • + *
+ * + *

Security notes

+ *
    + *
  • Imported keys are cloned defensively; callers should erase the original + * buffers if sensitive.
  • + *
  • Validate the key length for the chosen digest family (for example, 256 + * bits for HmacSHA256).
  • + *
  • Do not reuse the same key across different algorithms.
  • + *
+ * + * @since 1.0 + */ +public final class HmacKeyImportSpec implements AlgorithmKeySpec, Describable { + private final String macName; + private final byte[] key; + + /** + * Constructs a new HMAC key import specification. + * + * @param macName algorithm name (e.g., {@code "HmacSHA256"}) + * @param key raw key material; must not be {@code null} + * @throws NullPointerException if {@code macName} or {@code key} is + * {@code null} + */ + public HmacKeyImportSpec(String macName, byte[] key) { + this.macName = Objects.requireNonNull(macName, "macName must not be null"); + this.key = Objects.requireNonNull(key, "key must not be null").clone(); + } + + /** + * Returns the HMAC algorithm name. + * + * @return algorithm name string (e.g., {@code "HmacSHA256"}) + */ + public String macName() { + return macName; + } + + /** + * Returns a defensive copy of the raw key material. + * + * @return cloned key bytes + */ + public byte[] key() { + return key.clone(); + } + + /** + * Creates a spec from raw bytes. + * + * @param macName algorithm name (e.g., {@code "HmacSHA256"}) + * @param key raw key material + * @return a new {@code HmacKeyImportSpec} + */ + public static HmacKeyImportSpec fromRaw(String macName, byte[] key) { + return new HmacKeyImportSpec(macName, key); + } + + /** + * Creates a spec from a hex-encoded key string. + * + * @param macName algorithm name (e.g., {@code "HmacSHA256"}) + * @param hex key material encoded as hexadecimal + * @return a new {@code HmacKeyImportSpec} + * @throws NullPointerException if {@code hex} is {@code null} + */ + public static HmacKeyImportSpec fromHex(String macName, String hex) { + Objects.requireNonNull(hex, "hex must not be null"); + return fromRaw(macName, HexFormat.of().parseHex(hex)); + } + + /** + * Creates a spec from a Base64-encoded key string. + * + * @param macName algorithm name (e.g., {@code "HmacSHA256"}) + * @param b64 key material encoded in Base64 + * @return a new {@code HmacKeyImportSpec} + * @throws NullPointerException if {@code b64} is {@code null} + */ + public static HmacKeyImportSpec fromBase64(String macName, String b64) { + Objects.requireNonNull(b64, "base64 must not be null"); + return fromRaw(macName, Base64.getDecoder().decode(b64)); + } + + /** + * Returns a short human-readable description of this spec. + * + *

+ * The description is the algorithm name (e.g., {@code "HmacSHA256"}). + *

+ * + * @return algorithm name + */ + @Override + public String description() { + return macName; + } + + /** + * Serializes a spec into a {@link PairSeq}. + * + * @param spec the spec to serialize + * @return encoded key spec sequence + */ + public static PairSeq marshal(HmacKeyImportSpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.key); + return PairSeq.of("type", "HMAC-KEY", "mac", spec.macName, "k.b64", b64); + } + + /** + * Deserializes a spec from a {@link PairSeq}. + * + *

+ * Accepts key material encoded as {@code k.b64} (Base64) or {@code k.hex} + * (hexadecimal). + *

+ * + * @param p sequence containing encoded spec fields + * @return a reconstructed {@code HmacKeyImportSpec} + * @throws IllegalArgumentException if {@code mac} or key material is missing + */ + public static HmacKeyImportSpec unmarshal(PairSeq p) { + String mac = null; + byte[] key = null; + + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + switch (k) { + case "mac" -> mac = v; + case "k.b64" -> key = Base64.getDecoder().decode(v); + case "k.hex" -> key = HexFormat.of().parseHex(v); + default -> { + } + } + } + if (mac == null) { + throw new IllegalArgumentException("mac missing for HMAC key"); + } + if (key == null) { + throw new IllegalArgumentException("HMAC key missing (k.b64 or k.hex)"); + } + return new HmacKeyImportSpec(mac, key); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java new file mode 100644 index 0000000..1c15651 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacMacContext.java @@ -0,0 +1,323 @@ +/******************************************************************************* + * 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.core.alg.hmac; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.MacContext; +import zeroecho.core.tag.ByteVerificationStrategy; +import zeroecho.core.tag.TagEngine; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; + +/** + * Streaming HMAC context backed by the JCA {@link javax.crypto.Mac} API. + * + *

+ * {@code HmacMacContext} implements {@link MacContext} and integrates with the + * {@link TagEngine} contract to support both production and verification in a + * pull-based streaming pipeline. One instance is intended for one wrapped + * stream. + *

+ * + *

TagEngine contract

+ *
    + *
  • Produce mode (no expected tag set): {@link #wrap(InputStream)} + * returns a pass-through stream that emits the full body followed by the + * computed MAC as a trailer.
  • + *
  • Verify mode (expected tag set): the stream emits only the body. At + * EOF, the computed MAC is compared with the expected tag using the configured + * verification approach set via + * {@link #setVerificationApproach(VerificationBiPredicate)}. Decorators such as + * {@link VerificationBiPredicate#getThrowOnMismatch()} or + * {@link VerificationBiPredicate#getFlagOkInCtx(conflux.CtxInterface, conflux.Key)} + * may be used to control error signaling.
  • + *
+ * + *

CryptoContext contract

+ *
    + *
  • {@link #algorithm()} returns the owning {@link CryptoAlgorithm}.
  • + *
  • {@link #key()} returns the {@link javax.crypto.SecretKey} used for the + * MAC.
  • + *
  • {@link #close()} closes the active wrapped stream (idempotent).
  • + *
+ * + *

Verification approach

+ *

+ * The core comparison strategy is provided by {@link #getVerificationCore()}, + * which by default returns a constant-time byte-array comparator + * ({@link ByteVerificationStrategy}). If no explicit strategy is set, + * {@link #wrap(InputStream)} applies that core strategy decorated with + * {@code getThrowOnMismatch()} so that mismatches raise an exception at EOF. + *

+ * + *

Thread-safety

+ *

+ * This context is stateful and not thread-safe. After + * {@link #wrap(InputStream)} has been called once, further calls are rejected. + *

+ * + *

Usage example

+ *

Produce HMAC-SHA256 trailer

+ * {@code
+ * SecretKey key = CryptoAlgorithms.generateSecret("HMAC", HmacKeyGenSpec.sha256(256));
+ * HmacMacContext ctx = new HmacMacContext(CryptoAlgorithms.require("HMAC"), key, "HmacSHA256");
+ * try (InputStream in = ctx.wrap(new FileInputStream("data.bin"))) {
+ *     in.transferTo(OutputStream.nullOutputStream()); // body then MAC trailer
+ * }
+ * }
+ * 
+ * + *

Verify with explicit strategy

+ * {@code
+ * HmacMacContext ctx = new HmacMacContext(CryptoAlgorithms.require("HMAC"), key, "HmacSHA256");
+ * ctx.setVerificationApproach(ctx.getVerificationCore().getThrowOnMismatch());
+ * ctx.setExpectedTag(expectedTag);
+ * try (InputStream in = ctx.wrap(new FileInputStream("body.bin"))) {
+ *     in.transferTo(OutputStream.nullOutputStream()); // throws on mismatch at EOF
+ * }
+ * }
+ * 
+ * + * @since 1.0 + */ +public final class HmacMacContext implements MacContext { + private static final Logger LOG = Logger.getLogger(HmacMacContext.class.getName()); + + private final CryptoAlgorithm algorithm; + private final SecretKey key; + private final String macName; + private final int macLen; // length of MAC/tag in bytes + + // verification configuration (used when expectedTag is set) + private byte[] expectedTag; + private VerificationBiPredicate verificationStrategy; + + // lifecycle + private boolean wrapped; // = false; + private Stream activeStream; + private boolean autoCloseActiveStream; + + /** + * Constructs an HMAC context for the given algorithm and key. + * + *

+ * The constructor probes the MAC length by initializing a JCA {@link Mac} + * instance with the given key. + *

+ * + * @param algorithm owning algorithm descriptor + * @param key JCA secret key for HMAC + * @param macName canonical MAC algorithm name (e.g., {@code "HmacSHA256"}) + * @throws NullPointerException if any argument is {@code null} + * @throws GeneralSecurityException if the algorithm cannot be initialized with + * the given key + */ + public HmacMacContext(final CryptoAlgorithm algorithm, final SecretKey key, final String macName) + throws GeneralSecurityException { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm"); + this.key = Objects.requireNonNull(key, "key"); + this.macName = Objects.requireNonNull(macName, "macName"); + Mac probe = Mac.getInstance(macName); + probe.init(key); + this.macLen = probe.getMacLength(); + } + + /** + * Returns the algorithm descriptor associated with this context. + * + * @return owning algorithm + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key used for HMAC operations. + * + * @return JCA secret key + */ + @Override + public java.security.Key key() { + return key; + } + + /** + * Closes the active wrapped stream, if any. + * + *

+ * Idempotent: multiple calls are safe. Any underlying I/O exceptions are + * suppressed. + *

+ */ + @Override + public void close() { + LOG.log(Level.INFO, "close"); + + if (autoCloseActiveStream) { + try { + if (activeStream != null) { + activeStream.close(); + } + } catch (IOException ignore) { + LOG.log(Level.INFO, "exception ignored on close", ignore); + } + } + } + + /** + * Wraps an upstream input stream with HMAC computation. + * + *

+ * In produce mode, the returned stream emits the body followed by the computed + * tag. In verify mode, it emits the body only and validates at EOF using the + * configured verification approach (default: throw on mismatch). + *

+ * + * @param upstream input stream to wrap; must not be {@code null} + * @return wrapped input stream + * @throws IOException if initialization fails + * @throws IllegalStateException if this context has already been used + * @throws NullPointerException if {@code upstream} is {@code null} + */ + @Override + public InputStream wrap(final InputStream upstream) throws IOException { + Objects.requireNonNull(upstream, "upstream"); + if (wrapped) { + throw new IllegalStateException( + "This HmacMacContext instance was already used; create a new one per stream."); + } + wrapped = true; + + final Mac mac; + try { + mac = Mac.getInstance(macName); + mac.init(key); + } catch (GeneralSecurityException e) { + throw new IOException("HMAC init failed for " + macName, e); + } + + Stream s = new Stream(upstream, mac, macName, expectedTag, verifier()); + this.activeStream = s; + return s; + } + + /** + * Returns the MAC tag length in bytes. + * + * @return tag length + */ + @Override + public int tagLength() { + return macLen; + } + + /** + * Sets the expected tag for verification mode. + * + *

+ * Passing {@code null} clears verification and reverts to produce mode. The + * value is copied defensively. If a wrapped stream is already active, the + * expected tag of the underlying stream is updated as well. + *

+ * + * @param expected expected tag, or {@code null} to disable verification + */ + @Override + public void setExpectedTag(final byte[] expected) { + this.expectedTag = (expected == null) ? null : Arrays.copyOf(expected, expected.length); + + if (activeStream != null) { + activeStream.setExpectedTag(expected); + } + } + + /** + * Sets the verification approach used in verify mode to compare the expected + * and computed tags. + * + *

+ * If no strategy is set explicitly, the context will use + * {@link #getVerificationCore()} decorated with + * {@link VerificationBiPredicate#getThrowOnMismatch()} so that mismatches raise + * an exception at EOF. + *

+ * + * @param strategy verification predicate; may be {@code null} to keep the + * default + */ + @Override + public void setVerificationApproach(VerificationBiPredicate strategy) { + verificationStrategy = strategy; + } + + /** + * Returns the core verification predicate for HMAC tags. + * + *

+ * The default core is a constant-time byte-array comparison implemented by + * {@link ByteVerificationStrategy}. Callers may decorate it with + * {@link VerificationBiPredicate#getThrowOnMismatch()} or + * {@link VerificationBiPredicate#getFlagOkInCtx(conflux.CtxInterface, conflux.Key)} + * depending on the desired reporting behavior. + *

+ * + * @return the base verification predicate (never {@code null}) + */ + @Override + public VerificationBiPredicate getVerificationCore() { + return new ByteVerificationStrategy(); + } + + /** + * Selects the effective verification predicate: the user-supplied strategy if + * present, otherwise the default core with throw-on-mismatch decoration. + * + * @return effective verification predicate + */ + private VerificationBiPredicate verifier() { + return verificationStrategy == null ? getVerificationCore().getThrowOnMismatch() : verificationStrategy; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java b/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java new file mode 100644 index 0000000..144ef1f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hmac/HmacSpec.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.hmac; + +import java.util.Objects; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.ContextSpec; + +/** + *

Specification for HMAC operation parameters

+ * + * {@code HmacSpec} identifies the HMAC variant (digest family) to be used in a + * {@link zeroecho.core.context.MacContext}. It does not include key material; + * keys are provided separately via {@link javax.crypto.SecretKey}. + * + *

Fields

+ *
    + *
  • {@code macName}: the canonical algorithm name as understood by the JCA + * (e.g., {@code "HmacSHA256"}, {@code "HmacSHA384"}, + * {@code "HmacSHA512"}).
  • + *
+ * + *

Usage

This spec is passed when creating a new HMAC context: + *
{@code
+ * SecretKey key = CryptoAlgorithms.generateSecret("HMAC",
+ *     HmacKeyGenSpec.sha256(256));
+ *
+ * MacContext ctx = CryptoAlgorithms.create("HMAC",
+ *     KeyUsage.MAC, key, HmacSpec.sha256());
+ *
+ * ctx.update(data);
+ * byte[] tag = ctx.doFinal();
+ * }
+ * + *

Defaults

Convenience factories are provided for the most common + * variants: + *
    + *
  • {@link #sha256()} - HMAC with SHA-256
  • + *
  • {@link #sha384()} - HMAC with SHA-384
  • + *
  • {@link #sha512()} - HMAC with SHA-512
  • + *
+ * + * @since 1.0 + */ +public final class HmacSpec implements ContextSpec, Describable { + private final String macName; // e.g., "HmacSHA256", "HmacSHA384", "HmacSHA512" + + /** + * Constructs a new HMAC spec for the given algorithm. + * + * @param macName canonical algorithm name (e.g., {@code "HmacSHA256"}) + * @throws NullPointerException if {@code macName} is {@code null} + */ + public HmacSpec(String macName) { + this.macName = Objects.requireNonNull(macName, "macName must not be null"); + } + + /** + * Returns the canonical JCA algorithm name of this spec. + * + * @return algorithm name (e.g., {@code "HmacSHA256"}) + */ + public String macName() { + return macName; + } + + /** + * Convenience factory for HMAC-SHA256. + * + * @return new spec targeting {@code HmacSHA256} + */ + public static HmacSpec sha256() { + return new HmacSpec("HmacSHA256"); + } + + /** + * Convenience factory for HMAC-SHA384. + * + * @return new spec targeting {@code HmacSHA384} + */ + public static HmacSpec sha384() { + return new HmacSpec("HmacSHA384"); + } + + /** + * Convenience factory for HMAC-SHA512. + * + * @return new spec targeting {@code HmacSHA512} + */ + public static HmacSpec sha512() { + return new HmacSpec("HmacSHA512"); + } + + /** + * Returns a human-readable description of this spec. + * + *

+ * The description is simply the algorithm name (e.g., {@code "HmacSHA256"}). + *

+ * + * @return algorithm name string + */ + @Override + public String description() { + return macName; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/Stream.java b/lib/src/main/java/zeroecho/core/alg/hmac/Stream.java new file mode 100644 index 0000000..747e2e9 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hmac/Stream.java @@ -0,0 +1,241 @@ +/******************************************************************************* + * 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.core.alg.hmac; + +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.Mac; + +import zeroecho.core.err.VerificationException; +import zeroecho.core.io.AbstractPassthroughInputStream; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; +import zeroecho.core.util.Strings; + +/** + * Passthrough input stream that computes or verifies a MAC while bytes are + * read. + * + *

+ * The stream forwards all bytes from the wrapped {@link InputStream} unchanged + * while updating a running {@link javax.crypto.Mac}. Behavior depends on + * whether an expected tag is provided and on the chosen verification strategy: + *

+ *
    + *
  • Produce mode (no expected tag): the computed MAC is emitted once + * as a trailer via {@link #produceTrailer(byte[])}, after all body bytes have + * been read.
  • + *
  • Verify mode (with expected tag): no trailer is emitted; at + * completion the computed MAC is compared to the expected tag using the + * supplied {@link VerificationBiPredicate}. The strategy may throw, record + * flags elsewhere, or simply return a boolean, depending on its implementation + * or decorators.
  • + *
+ * + *

Lifecycle

+ *
    + *
  • {@link #update(byte[], int, int)} feeds each pulled chunk to the + * MAC.
  • + *
  • {@link #produceTrailer(byte[])} emits the MAC bytes once in produce mode; + * returns 0 in verify mode.
  • + *
  • {@link #onCompleted()} finalizes and applies the verification strategy in + * verify mode.
  • + *
+ * + *

Example

+ *

Produce mode: append HMAC-SHA256 as trailer

+ * {@code
+ * Mac mac = Mac.getInstance("HmacSHA256");
+ * mac.init(secretKey);
+ * try (InputStream in = Files.newInputStream(path);
+ *      InputStream s  = new Stream(in, mac, "HmacSHA256", null,
+ *                                  new ByteVerificationStrategy())) {
+ *     // read from 's' to consume body; trailer is produced automatically
+ * }
+ * }
+ * 
+ * + *

Verify mode: check expected tag and throw on mismatch

+ * {@code
+ * Mac macV = Mac.getInstance("HmacSHA256");
+ * macV.init(secretKey);
+ * byte[] expected = ...; // received/authenticated out-of-band
+ *
+ * VerificationBiPredicate strategy =
+ *     new ByteVerificationStrategy().getThrowOnMismatch();
+ *
+ * try (InputStream in = Files.newInputStream(path);
+ *      InputStream s  = new Stream(in, macV, "HmacSHA256", expected, strategy)) {
+ *     // read from 's'; exception is thrown at EOF if verification fails
+ * }
+ * }
+ * 
+ */ +final class Stream extends AbstractPassthroughInputStream { + private static final Logger LOG = Logger.getLogger(Stream.class.getName()); + + private final Mac mac; + private final String macName; + private byte[] expectedTag; + private final VerificationBiPredicate verificationStrategy; + + /** + * Creates a MAC-enabled passthrough stream with an 8192-byte body buffer. + * + *

+ * If {@code expectedTag} is {@code null}, the instance works in produce mode + * and will emit the computed MAC as a trailer. Otherwise it works in verify + * mode and will not emit a trailer; instead it verifies the computed tag at + * completion by applying {@code verificationStrategy}. + *

+ * + * @param upstream source stream to wrap; must not be {@code null} + * @param mac initialized MAC instance updated by the stream; + * must not be {@code null} + * @param macName display name used for diagnostics (for example, + * {@code "HmacSHA256"}); must not be {@code null} + * @param expectedTag expected MAC bytes for verification; {@code null} + * enables produce mode + * @param verificationStrategy strategy used in verify mode to compare expected + * and computed tags; must not be {@code null} in + * verify mode + * @throws NullPointerException if {@code upstream}, {@code mac}, or + * {@code macName} is {@code null} + */ + /* package */ Stream(final InputStream upstream, final Mac mac, final String macName, final byte[] expectedTag, + final VerificationBiPredicate verificationStrategy) { + super(upstream, 8192); + this.mac = mac; + this.macName = macName; + this.expectedTag = expectedTag; + this.verificationStrategy = verificationStrategy; + } + + /** + * Updates the running MAC with the bytes just read from upstream. + * + * @param buf buffer containing the read bytes + * @param off offset in {@code buf} + * @param len number of valid bytes + */ + @Override + protected void update(final byte[] buf, final int off, final int len) { + mac.update(buf, off, len); + } + + /** + * Produces the MAC trailer once in produce mode; emits nothing in verify mode. + * + *

+ * In produce mode, this method finalizes the MAC, copies it into {@code buf}, + * and returns the number of bytes written. If the finalized tag is empty, zero + * is returned. In verify mode, this method always returns zero. + *

+ * + * @param buf destination buffer to receive the trailer + * @return number of bytes written to {@code buf}, or {@code 0} if no trailer is + * emitted + * @throws IOException if {@code Mac#doFinal()} fails or the tag does not fit + * {@code buf} + */ + @Override + protected int produceTrailer(byte[] buf) throws IOException { + if (expectedTag != null) { + // Verify mode: trailer is never emitted. + return 0; + } + final byte[] tag; + try { + tag = mac.doFinal(); + } catch (RuntimeException ex) { // NOPMD + throw new IOException("HMAC finalize failed (" + macName + ")", ex); + } + if (tag.length == 0) { + return 0; + } + if (tag.length > buf.length) { + throw new IOException("Trailer does not fit into buffer"); + } + System.arraycopy(tag, 0, buf, 0, tag.length); + return tag.length; + } + + /** + * Finalizes the MAC and applies the verification strategy in verify mode. + * + *

+ * When {@code expectedTag} is non-null, this method computes {@code doFinal()}, + * compares it to {@code expectedTag} using {@code verificationStrategy}, and + * propagates any strategy-defined signaling (for example, throwing on + * mismatch). In produce mode, the method returns immediately. + *

+ * + * @throws IOException if the verification strategy signals a failure (for + * example, via {@link VerificationException}) + */ + @Override + protected void onCompleted() throws IOException { + if (expectedTag == null) { + // Produce mode: nothing to verify. + return; + } + try { + final byte[] out = mac.doFinal(); + verificationStrategy.verify(expectedTag, out); + } catch (VerificationException e) { + throw new IOException(e); + } + } + + /** + * Sets or replaces the expected tag for subsequent verification. + * + *

+ * If called in produce mode, this switches the instance to verify mode for the + * remainder of the stream. + *

+ * + * @param expectedTag the expected MAC bytes; may be {@code null} to clear + */ + /* package */ void setExpectedTag(byte[] expectedTag) { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "resetting expectedTag to {0}", Strings.toShortString(expectedTag)); + } + + this.expectedTag = expectedTag; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java b/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java new file mode 100644 index 0000000..a9b8efb --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hmac/package-info.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * HMAC algorithms, streaming MAC contexts, and key specifications. + * + *

+ * This package integrates Hash-based Message Authentication Code (HMAC) into + * the core layer. It provides the algorithm descriptor, a streaming MAC context + * that can produce or verify tags, operation specifications for choosing the + * digest family, and key specifications for generation and import. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register a canonical HMAC algorithm and declare the + * {@link zeroecho.core.KeyUsage#MAC} role.
  • + *
  • Expose a streaming {@link zeroecho.core.context.MacContext} that appends + * tags in produce mode or verifies an expected tag at end of stream in verify + * mode.
  • + *
  • Provide immutable specs for selecting the HMAC variant and for supplying + * keys (generation or import of raw key material).
  • + *
  • Encapsulate JCA/JCE interop and provider checks behind small + * factories.
  • + *
+ * + *

Components

+ *
    + *
  • HmacAlgorithm: algorithm descriptor that wires the MAC role to a + * streaming context and registers symmetric key builders for generation and + * import.
  • + *
  • HmacMacContext: streaming MAC context backed by + * {@link javax.crypto.Mac}; supports produce and verify modes under a clear + * verification policy.
  • + *
  • HmacSpec: operation specification selecting the HMAC variant + * (e.g., SHA-256/384/512).
  • + *
  • HmacKeyGenSpec: parameters for generating secret keys for a + * specific HMAC variant.
  • + *
  • HmacKeyImportSpec: wrapper for importing existing raw keys, with + * Base64/hex helpers.
  • + *
  • Stream: internal passthrough input stream implementing the + * byte-pumping and trailer/verification logic for the MAC context.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and safe to share.
  • + *
  • Streaming contexts are stateful and not thread-safe; use a new instance + * per wrapped stream.
  • + *
  • Key specification classes defensively copy input and output byte + * arrays.
  • + *
  • Verification behavior is explicit: failures either throw or are recorded + * in a caller-provided context.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.hmac; diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java new file mode 100644 index 0000000..07b6527 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcAlgorithm.java @@ -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.core.alg.hqc; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.bouncycastle.pqc.jcajce.spec.HQCParameterSpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + *

HQC (Hamming Quasi-Cyclic) Algorithm Integration

+ * + * Concrete {@link zeroecho.core.CryptoAlgorithm} implementation for the + * post-quantum key encapsulation mechanism (KEM) HQC, as defined in the NIST + * PQC standardization process. + * + *

+ * HQC is based on error-correcting codes and offers IND-CCA2 security against + * quantum adversaries. This implementation delegates to the + * {@code BouncyCastlePQCProvider} for actual cryptographic operations and + * exposes HQC through the unified {@code CryptoAlgorithm} API surface. + *

+ * + *

Declared capabilities

+ *
    + *
  • {@link zeroecho.core.AlgorithmFamily#KEM}: + *
      + *
    • {@link zeroecho.core.KeyUsage#ENCAPSULATE} with a + * {@link java.security.PublicKey}, returning a + * {@link zeroecho.core.context.KemContext}.
    • + *
    • {@link zeroecho.core.KeyUsage#DECAPSULATE} with a + * {@link java.security.PrivateKey}, returning a + * {@link zeroecho.core.context.KemContext}.
    • + *
    + *
  • + *
  • {@link zeroecho.core.AlgorithmFamily#AGREEMENT} (message-based): + *
      + *
    • Initiator: given recipient's {@link java.security.PublicKey}, wraps an + * HQC KEM context in a + * {@link zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter}.
    • + *
    • Responder: given own {@link java.security.PrivateKey}, wraps an HQC KEM + * context in a {@code KemMessageAgreementAdapter}.
    • + *
    + *
  • + *
+ * + *

Key builders

+ *
    + *
  • {@link zeroecho.core.alg.hqc.HqcKeyGenSpec}: supports generation of key + * pairs for the {@code HQC-128}, {@code HQC-192}, and {@code HQC-256} parameter + * sets, via BouncyCastle's {@link java.security.KeyPairGenerator}.
  • + *
  • {@link zeroecho.core.alg.hqc.HqcPublicKeySpec}: supports import of HQC + * public keys from X.509-encoded form.
  • + *
  • {@link zeroecho.core.alg.hqc.HqcPrivateKeySpec}: supports import of HQC + * private keys from PKCS#8-encoded form.
  • + *
+ * + *

Provider dependency

+ *

+ * All operations require the Bouncy Castle PQC provider + * ({@code BouncyCastlePQCProvider}) to be registered in + * {@link java.security.Security}. The private helper {@link #ensureProvider()} + * validates provider availability before key generation or import. + *

+ * + *

Thread-safety

+ *

+ * Instances of {@code HqcAlgorithm} are immutable and safe to share. Contexts + * created via capabilities (e.g., {@code KemContext}, + * {@code MessageAgreementContext}) are not necessarily thread-safe. + *

+ * + *

Usage example

{@code
+ * // Generate an HQC key pair
+ * HqcAlgorithm hqc = new HqcAlgorithm();
+ * KeyPair kp = hqc.generateKeyPair(HqcKeyGenSpec.hqc256());
+ *
+ * // Encapsulation by initiator
+ * KemContext encapsCtx = hqc.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
+ *
+ * // Decapsulation by responder
+ * KemContext decapsCtx = hqc.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
+ * }
+ * + * @since 1.0 + */ +public final class HqcAlgorithm extends AbstractCryptoAlgorithm { + /** + * Constructs and registers the HQC algorithm with the BouncyCastle PQC + * provider. + * + *

+ * During construction this algorithm: + *

+ *
    + *
  • Registers KEM encapsulation/decapsulation capabilities bound to + * {@link java.security.PublicKey} and {@link java.security.PrivateKey} + * respectively.
  • + *
  • Registers message-based agreement roles (initiator/responder) using + * {@link zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter} + * wrappers.
  • + *
  • Registers asymmetric key builders for: + *
      + *
    • {@link zeroecho.core.alg.hqc.HqcKeyGenSpec} (for fresh key + * generation),
    • + *
    • {@link zeroecho.core.alg.hqc.HqcPublicKeySpec} (for importing X.509 + * public keys),
    • + *
    • {@link zeroecho.core.alg.hqc.HqcPrivateKeySpec} (for importing PKCS#8 + * private keys).
    • + *
    + *
  • + *
+ * + *

+ * The default key generation spec is + * {@link zeroecho.core.alg.hqc.HqcKeyGenSpec#hqc256()}, corresponding to the + * HQC-256 parameter set. + *

+ * + * @throws java.lang.IllegalStateException if BouncyCastle PQC provider is + * missing at runtime + */ + public HqcAlgorithm() { + super("HQC", "HQC", BouncyCastlePQCProvider.PROVIDER_NAME); + + capability(AlgorithmFamily.KEM, KeyUsage.ENCAPSULATE, KemContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> new HqcKemContext(this, k), () -> VoidSpec.INSTANCE); + capability(AlgorithmFamily.KEM, KeyUsage.DECAPSULATE, KemContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> new HqcKemContext(this, k), () -> VoidSpec.INSTANCE); + + // AGREEMENT (initiator): Alice has Bob's public key → encapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← return your + // existing KemContext + PublicKey.class, // ← initiator uses recipient's public key + VoidSpec.class, // ← must implement ContextSpec + (PublicKey recipient, VoidSpec spec) -> { + // create a context bound to recipient public key for encapsulation + return KemMessageAgreementAdapter.builder().upon(new HqcKemContext(this, recipient)).asInitiator() + .build(); + }, () -> VoidSpec.INSTANCE // default + ); + + // AGREEMENT (responder): Bob has his private key → decapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← same KemContext + // type + PrivateKey.class, // ← responder uses their private key + VoidSpec.class, (PrivateKey myPriv, VoidSpec spec) -> { + return KemMessageAgreementAdapter.builder().upon(new HqcKemContext(this, myPriv)).asResponder() + .build(); + }, () -> VoidSpec.INSTANCE); + + registerAsymmetricKeyBuilder(HqcKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(HqcKeyGenSpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("HQC", providerName()); + HQCParameterSpec params = switch (spec.variant()) { + case HQC_128 -> HQCParameterSpec.hqc128; + case HQC_192 -> HQCParameterSpec.hqc192; + case HQC_256 -> HQCParameterSpec.hqc256; + }; + kpg.initialize(params, new SecureRandom()); + return kpg.generateKeyPair(); + } + + @Override + public PublicKey importPublic(HqcKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(HqcKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + }, HqcKeyGenSpec::hqc256); + + registerAsymmetricKeyBuilder(HqcPublicKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(HqcPublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(HqcPublicKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("HQC", providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); + } + + @Override + public PrivateKey importPrivate(HqcPublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + }, null); + + registerAsymmetricKeyBuilder(HqcPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(HqcPrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(HqcPrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(HqcPrivateKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("HQC", providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + } + }, null); + } + + private static void ensureProvider() throws NoSuchProviderException { + Provider p = Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME); + if (p == null) { + throw new NoSuchProviderException("BCPQC provider not registered"); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcKemContext.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcKemContext.java new file mode 100644 index 0000000..50610a0 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcKemContext.java @@ -0,0 +1,220 @@ +/******************************************************************************* + * 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.core.alg.hqc; + +import java.io.IOException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Objects; + +import javax.security.auth.DestroyFailedException; + +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.pqc.crypto.hqc.HQCKEMExtractor; +import org.bouncycastle.pqc.crypto.hqc.HQCKEMGenerator; +import org.bouncycastle.pqc.crypto.hqc.HQCPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.hqc.HQCPublicKeyParameters; +import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory; +import org.bouncycastle.pqc.crypto.util.PublicKeyFactory; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.KemContext; + +/** + *

HQC KEM Context

+ * + * Implementation of {@link zeroecho.core.context.KemContext} for the + * post-quantum HQC (Hamming Quasi-Cyclic) key encapsulation mechanism. + * + *

+ * An {@code HqcKemContext} binds an HQC key (public or private) and role + * (encapsulation or decapsulation). It delegates to BouncyCastle's + * {@code HQCKEMGenerator} and {@code HQCKEMExtractor} for cryptographic + * operations. This context is created indirectly via {@link HqcAlgorithm}, not + * by application code. + *

+ * + *

Roles

+ *
    + *
  • Encapsulation: constructed with a {@link java.security.PublicKey}. + * Provides {@link #encapsulate()} to generate a ciphertext and shared + * secret.
  • + *
  • Decapsulation: constructed with a + * {@link java.security.PrivateKey}. Provides {@link #decapsulate(byte[])} to + * recover the shared secret from a ciphertext.
  • + *
+ * + *

Thread-safety

+ *

+ * Instances are not thread-safe. Each context should be used for a single + * encapsulation or decapsulation flow. + *

+ * + * @since 1.0 + */ +public final class HqcKemContext implements KemContext { + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean encapsulate; + + /** + * Constructs an encapsulation context using the recipient's public key. + * + *

+ * This role is for the initiator (Alice), who produces a ciphertext to send to + * the responder. + *

+ * + * @param algorithm parent algorithm instance + * @param k HQC public key of the recipient + * @throws NullPointerException if any argument is {@code null} + */ + public HqcKemContext(CryptoAlgorithm algorithm, PublicKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = true; + } + + /** + * Constructs a decapsulation context using the recipient's private key. + * + *

+ * This role is for the responder (Bob), who receives a ciphertext and recovers + * the shared secret. + *

+ * + * @param algorithm parent algorithm instance + * @param k HQC private key of the recipient + * @throws NullPointerException if any argument is {@code null} + */ + public HqcKemContext(CryptoAlgorithm algorithm, PrivateKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = false; + } + + /** + * Returns the algorithm that created this context. + * + * @return associated {@link CryptoAlgorithm} + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the bound key (public or private) for this context. + * + * @return the underlying {@link Key} instance + */ + @Override + public Key key() { + return key; + } + + /** + * Closes this context. No-op for HQC, since no sensitive state is held beyond + * the key reference. + */ + @Override + public void close() { + // empty + } + + /** + * Performs HQC encapsulation, generating a ciphertext and a shared secret. + * + *

+ * Requires that the context was constructed with a public key. On success, a + * {@link KemResult} containing the ciphertext and secret is returned. + *

+ * + * @return encapsulation result containing ciphertext and shared secret + * @throws IOException if encapsulation fails due to provider error + * @throws IllegalStateException if this context was initialized for + * decapsulation + */ + @Override + public KemResult encapsulate() throws IOException { + if (!encapsulate) { + throw new IllegalStateException("Not initialized for ENCAPSULATE"); + } + try { + final HQCPublicKeyParameters keyParam = (HQCPublicKeyParameters) PublicKeyFactory + .createKey(key.getEncoded()); + HQCKEMGenerator gen = new HQCKEMGenerator(new SecureRandom()); + SecretWithEncapsulation res = gen.generateEncapsulated(keyParam); + byte[] secret = res.getSecret(); + byte[] ct = res.getEncapsulation(); + res.destroy(); + return new KemResult(ct, secret); + } catch (DestroyFailedException e) { + throw new IOException("HQC encapsulate failed", e); + } + } + + /** + * Performs HQC decapsulation, recovering the shared secret from a ciphertext. + * + *

+ * Requires that the context was constructed with a private key. On success, the + * raw shared secret is returned. + *

+ * + * @param ciphertext the encapsulated key material produced by + * {@link #encapsulate()} + * @return the recovered shared secret + * @throws IOException if decapsulation fails due to provider error + * @throws IllegalStateException if this context was initialized for + * encapsulation + */ + @Override + public byte[] decapsulate(byte[] ciphertext) throws IOException { + if (encapsulate) { + throw new IllegalStateException("Not initialized for DECAPSULATE"); + } + try { + final HQCPrivateKeyParameters keyParam = (HQCPrivateKeyParameters) PrivateKeyFactory + .createKey(key.getEncoded()); + HQCKEMExtractor ex = new HQCKEMExtractor(keyParam); + return ex.extractSecret(ciphertext); + } catch (Exception e) { // NOPMD + throw new IOException("HQC decapsulate failed", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java new file mode 100644 index 0000000..fbe396c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcKeyGenSpec.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * 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.core.alg.hqc; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

HQC Key Generation Specification

+ * + * Algorithm-specific {@link zeroecho.core.spec.AlgorithmKeySpec} for generating + * HQC (Hamming Quasi-Cyclic) key pairs. HQC is a post-quantum key encapsulation + * mechanism based on error-correcting codes and provides IND-CCA2 security. + * + *

+ * An {@code HqcKeyGenSpec} selects one of the parameter sets standardized for + * HQC: 128-, 192-, or 256-bit security. The chosen variant determines key + * sizes, ciphertext lengths, and shared secret size. + *

+ * + *

Variants

+ *
    + *
  • {@link Variant#HQC_128} - 128-bit classical / ~64-bit quantum + * security.
  • + *
  • {@link Variant#HQC_192} - 192-bit classical / ~96-bit quantum + * security.
  • + *
  • {@link Variant#HQC_256} - 256-bit classical / ~128-bit quantum + * security.
  • + *
+ * + *

Usage

{@code
+ * // Create a spec for HQC-256 key generation
+ * HqcKeyGenSpec spec = HqcKeyGenSpec.hqc256();
+ *
+ * // Generate a key pair via HqcAlgorithm
+ * HqcAlgorithm hqc = new HqcAlgorithm();
+ * KeyPair kp = hqc.generateKeyPair(spec);
+ * }
+ * + * @since 1.0 + */ +public final class HqcKeyGenSpec implements AlgorithmKeySpec, Describable { + /** + * Enumeration of HQC parameter sets. + * + *

+ * Each variant specifies different code parameters and security levels as + * defined in the HQC specification. + *

+ */ + public enum Variant { + /** HQC-128 parameter set (128-bit classical security). */ + HQC_128, + /** HQC-192 parameter set (192-bit classical security). */ + HQC_192, + /** HQC-256 parameter set (256-bit classical security). */ + HQC_256 + } + + private final Variant variant; + + private HqcKeyGenSpec(Variant v) { + this.variant = v; + } + + /** + * Creates a specification for the given HQC parameter set. + * + * @param v selected variant + * @return new {@code HqcKeyGenSpec} for the chosen variant + */ + public static HqcKeyGenSpec of(Variant v) { + return new HqcKeyGenSpec(v); + } + + /** + * Returns a specification for the HQC-128 parameter set. + * + * @return spec for HQC-128 + */ + public static HqcKeyGenSpec hqc128() { + return new HqcKeyGenSpec(Variant.HQC_128); + } + + /** + * Returns a specification for the HQC-192 parameter set. + * + * @return spec for HQC-192 + */ + public static HqcKeyGenSpec hqc192() { + return new HqcKeyGenSpec(Variant.HQC_192); + } + + /** + * Returns a specification for the HQC-256 parameter set. + * + * @return spec for HQC-256 + */ + public static HqcKeyGenSpec hqc256() { + return new HqcKeyGenSpec(Variant.HQC_256); + } + + /** + * Returns the selected HQC variant. + * + * @return one of {@link Variant#HQC_128}, {@link Variant#HQC_192}, + * {@link Variant#HQC_256} + */ + public Variant variant() { + return variant; + } + + /** + * Returns a textual description of this spec. + * + *

+ * The description is the {@link Variant#toString()} value, e.g. + * {@code "HQC_256"}. + *

+ * + * @return human-readable description of the spec + */ + @Override + public String description() { + return variant.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java new file mode 100644 index 0000000..502ea8a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcPrivateKeySpec.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * 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.core.alg.hqc; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

HQC Private Key Specification

+ * + * {@link zeroecho.core.spec.AlgorithmKeySpec} wrapper for HQC private keys, + * encoded in standard PKCS#8 DER format. + * + *

+ * This class is used to transport and import HQC private keys into the + * {@link HqcAlgorithm}. The encoded form is immutable and defensively copied on + * construction and retrieval. + *

+ * + *

Serialization

+ *

+ * {@link #marshal(HqcPrivateKeySpec)} encodes the key material into a + * {@link PairSeq} with Base64 (unpadded) representation. The inverse + * {@link #unmarshal(PairSeq)} restores the specification from such a sequence. + *

+ * + *

Security note

+ *
    + *
  • The contained byte array represents sensitive key material. Applications + * should minimize retention and ensure secure erasure when possible.
  • + *
  • Use only for import into a trusted {@link java.security.KeyFactory} or + * {@link HqcAlgorithm}. Never log or transmit the raw array.
  • + *
+ * + *

Usage example

{@code
+ * // Wrap an existing PKCS#8 HQC private key
+ * HqcPrivateKeySpec spec = new HqcPrivateKeySpec(pkcs8Bytes);
+ *
+ * // Import into a PrivateKey via HqcAlgorithm
+ * HqcAlgorithm hqc = new HqcAlgorithm();
+ * PrivateKey priv = hqc.importPrivate(spec);
+ *
+ * // Serialize for transport
+ * PairSeq serialized = HqcPrivateKeySpec.marshal(spec);
+ *
+ * // Reconstruct from serialized form
+ * HqcPrivateKeySpec restored = HqcPrivateKeySpec.unmarshal(serialized);
+ * }
+ * + * @since 1.0 + */ +public final class HqcPrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Constructs a new private key spec from a PKCS#8-encoded byte array. + * + * @param pkcs8Der DER-encoded PKCS#8 private key + * @throws NullPointerException if {@code pkcs8Der} is {@code null} + */ + public HqcPrivateKeySpec(byte[] pkcs8Der) { + this.pkcs8 = Objects.requireNonNull(pkcs8Der).clone(); + } + + /** + * Returns a defensive copy of the PKCS#8-encoded key material. + * + * @return cloned PKCS#8 byte array + */ + public byte[] pkcs8() { + return pkcs8.clone(); + } + + /** + * Serializes this spec into a {@link PairSeq} with Base64-encoded key data. + * + *

+ * The resulting sequence has keys: + *

    + *
  • {@code type} = {@code "HqcPrivateKeySpec"}
  • + *
  • {@code pkcs8.b64} = Base64 of the PKCS#8 key
  • + *
+ * + * @param spec the spec to serialize + * @return serialized representation in a {@link PairSeq} + */ + public static PairSeq marshal(HqcPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "HqcPrivateKeySpec", PKCS8_B64, b64); + } + + /** + * Reconstructs a spec from its {@link PairSeq} representation. + * + * @param p sequence containing {@code pkcs8.b64} + * @return reconstructed {@code HqcPrivateKeySpec} + * @throws IllegalArgumentException if required field {@code pkcs8.b64} is + * missing + */ + public static HqcPrivateKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (PKCS8_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("HqcPrivateKeySpec: missing pkcs8.b64"); + } + return new HqcPrivateKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string containing the encoded length. + * + *

+ * The actual key material is not exposed in this representation. + *

+ * + * @return string with the PKCS#8 byte length + */ + @Override + public String toString() { + return "HqcPrivateKeySpec[len=" + pkcs8.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/HqcPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/hqc/HqcPublicKeySpec.java new file mode 100644 index 0000000..1cc0f0e --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hqc/HqcPublicKeySpec.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * 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.core.alg.hqc; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + *

HQC Public Key Specification

+ * + * {@link zeroecho.core.spec.AlgorithmKeySpec} wrapper for HQC public keys, + * encoded in standard X.509 DER format. + * + *

+ * This class is used to transport and import HQC public keys into the + * {@link HqcAlgorithm}. The encoded form is immutable and defensively copied on + * construction and retrieval. + *

+ * + *

Serialization

+ *

+ * {@link #marshal(HqcPublicKeySpec)} encodes the key material into a + * {@link PairSeq} with Base64 (unpadded) representation. The inverse + * {@link #unmarshal(PairSeq)} restores the specification from such a sequence. + *

+ * + *

Security note

+ *
    + *
  • Public keys are not confidential, but integrity is critical. They should + * only be distributed over authenticated or integrity-protected channels.
  • + *
  • The contained byte array is immutable and returned as a defensive copy to + * avoid accidental modification.
  • + *
+ * + *

Usage example

{@code
+ * // Wrap an existing X.509 HQC public key
+ * HqcPublicKeySpec spec = new HqcPublicKeySpec(x509Bytes);
+ *
+ * // Import into a PublicKey via HqcAlgorithm
+ * HqcAlgorithm hqc = new HqcAlgorithm();
+ * PublicKey pub = hqc.importPublic(spec);
+ *
+ * // Serialize for transport
+ * PairSeq serialized = HqcPublicKeySpec.marshal(spec);
+ *
+ * // Reconstruct from serialized form
+ * HqcPublicKeySpec restored = HqcPublicKeySpec.unmarshal(serialized);
+ * }
+ * + * @since 1.0 + */ +public final class HqcPublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Constructs a new public key spec from an X.509-encoded byte array. + * + * @param x509Der DER-encoded X.509 public key + * @throws NullPointerException if {@code x509Der} is {@code null} + */ + public HqcPublicKeySpec(byte[] x509Der) { + this.x509 = Objects.requireNonNull(x509Der).clone(); + } + + /** + * Returns a defensive copy of the X.509-encoded key material. + * + * @return cloned X.509 byte array + */ + public byte[] x509() { + return x509.clone(); + } + + /** + * Serializes this spec into a {@link PairSeq} with Base64-encoded key data. + * + *

+ * The resulting sequence has keys: + *

    + *
  • {@code type} = {@code "HqcPublicKeySpec"}
  • + *
  • {@code x509.b64} = Base64 of the X.509 key
  • + *
+ * + * @param spec the spec to serialize + * @return serialized representation in a {@link PairSeq} + */ + public static PairSeq marshal(HqcPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "HqcPublicKeySpec", X509_B64, b64); + } + + /** + * Reconstructs a spec from its {@link PairSeq} representation. + * + * @param p sequence containing {@code x509.b64} + * @return reconstructed {@code HqcPublicKeySpec} + * @throws IllegalArgumentException if required field {@code x509.b64} is + * missing + */ + public static HqcPublicKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (X509_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("HqcPublicKeySpec: missing x509.b64"); + } + return new HqcPublicKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string containing the encoded length. + * + *

+ * The actual key material is not exposed in this representation. + *

+ * + * @return string with the X.509 byte length + */ + @Override + public String toString() { + return "HqcPublicKeySpec[len=" + x509.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/hqc/package-info.java b/lib/src/main/java/zeroecho/core/alg/hqc/package-info.java new file mode 100644 index 0000000..0d83026 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/hqc/package-info.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * HQC post-quantum key encapsulation integration. + * + *

+ * This package integrates the HQC (Hamming Quasi-Cyclic) KEM into the core + * layer. It provides the algorithm descriptor, a runtime KEM context, and key + * specifications for generation and import. Provider-specific details are + * encapsulated behind small factories, while roles and metadata remain explicit + * to higher layers. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register HQC under a canonical identifier and declare ENCAPSULATE and + * DECAPSULATE roles, plus an agreement adapter for initiator and responder + * workflows.
  • + *
  • Provide a {@link zeroecho.core.context.KemContext} bound to a public or + * private key for encapsulation or decapsulation.
  • + *
  • Expose immutable specifications for key generation variants and encoded + * key carriers with simple marshalling helpers.
  • + *
  • Ensure operations use a supported PQC provider and fail fast if the + * provider is absent.
  • + *
+ * + *

Components

+ *
    + *
  • HqcAlgorithm: algorithm descriptor wiring KEM and agreement roles + * and registering key builders.
  • + *
  • HqcKemContext: runtime context implementing encapsulate and + * decapsulate operations.
  • + *
  • HqcKeyGenSpec: specification of HQC variants (128, 192, 256) with + * convenience factories.
  • + *
  • HqcPublicKeySpec and HqcPrivateKeySpec: wrappers over X.509 + * and PKCS#8 encodings, with defensive copying and compact marshalling + * utilities.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and safe to share across + * threads.
  • + *
  • KEM contexts are stateful and not thread-safe; use one context per + * operation.
  • + *
  • Encoded key specs preserve the provided encodings without internal + * parsing and clone byte arrays on input and output.
  • + *
  • Agreement adapters allow HQC to be composed into message-based exchanges + * where needed.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.hqc; diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberAlgorithm.java new file mode 100644 index 0000000..19f2bda --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberAlgorithm.java @@ -0,0 +1,383 @@ +/******************************************************************************* + * 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.core.alg.kyber; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.bouncycastle.pqc.jcajce.spec.KyberParameterSpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * Concrete CryptoAlgorithm implementation for the post-quantum key + * encapsulation mechanism (KEM) Kyber, standardized as ML-KEM. + * + *

Overview

Kyber provides IND-CCA2 security based on module-LWE. This + * implementation delegates cryptographic operations to the Bouncy Castle PQC + * provider and exposes Kyber through the unified CryptoAlgorithm API surface. + * + *

Declared capabilities

+ *
    + *
  • AlgorithmFamily.KEM: + *
      + *
    • KeyUsage.ENCAPSULATE with a PublicKey, returning a KemContext.
    • + *
    • KeyUsage.DECAPSULATE with a PrivateKey, returning a KemContext.
    • + *
    + *
  • + *
  • AlgorithmFamily.AGREEMENT (message-based): + *
      + *
    • Initiator: given the recipient PublicKey, wraps a Kyber KEM context in a + * KemMessageAgreementAdapter to drive message agreement.
    • + *
    • Responder: given the local PrivateKey, wraps a Kyber KEM context in the + * same adapter to decapsulate and agree the shared secret.
    • + *
    + *
  • + *
+ * + *

Key builders

+ *
    + *
  • KyberKeyGenSpec: key pair generation for variants {@code KYBER512}, + * {@code KYBER768}, and {@code KYBER1024} via KeyPairGenerator configured with + * KyberParameterSpec.
  • + *
  • KyberPublicKeySpec: import of Kyber public keys from X.509 + * SubjectPublicKeyInfo.
  • + *
  • KyberPrivateKeySpec: import of Kyber private keys from PKCS#8 + * PrivateKeyInfo.
  • + *
+ * + *

Provider dependency

All operations require the Bouncy Castle PQC + * provider to be present and registered in java.security.Security under the + * name provided by BouncyCastlePQCProvider. The helper ensureProvider() + * validates availability before key generation or import. + * + *

Thread-safety

Instances of KyberAlgorithm are immutable and safe to + * share. Contexts created via the declared capabilities (KemContext, + * MessageAgreementContext) are not necessarily thread-safe. + * + *

Usage examples

{@code
+ * // Register provider once during bootstrap:
+ * Security.addProvider(new BouncyCastlePQCProvider());
+ *
+ * // Construct the algorithm:
+ * KyberAlgorithm kyber = new KyberAlgorithm();
+ *
+ * // Generate a Kyber-768 key pair:
+ * KeyPair kp = kyber.asymmetricKeyBuilder(KyberKeyGenSpec.class)
+ *                   .generateKeyPair(KyberKeyGenSpec.kyber768());
+ *
+ * // Encapsulation by initiator (recipient public key known):
+ * KemContext enc = kyber.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
+ *
+ * // Decapsulation by responder (own private key):
+ * KemContext dec = kyber.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
+ *
+ * // Message-style agreement (initiator):
+ * MessageAgreementContext initCtx =
+ *     kyber.create(KeyUsage.AGREEMENT, kp.getPublic(), VoidSpec.INSTANCE);
+ *
+ * // Message-style agreement (responder):
+ * MessageAgreementContext respCtx =
+ *     kyber.create(KeyUsage.AGREEMENT, kp.getPrivate(), VoidSpec.INSTANCE);
+ * }
+ */ +public final class KyberAlgorithm extends AbstractCryptoAlgorithm { + /** + * Provides ML-KEM (Kyber) integration backed by the Bouncy Castle PQC provider + * and registers capabilities and key builders required for KEM usage and + * message agreement. + * + *

Overview

+ *

+ * This algorithm instance wires the following into the surrounding crypto + * framework: + *

+ *
    + *
  • KEM roles: ENCAPSULATE with {@link java.security.PublicKey} and + * DECAPSULATE with {@link java.security.PrivateKey}.
  • + *
  • AGREEMENT role via {@code KemMessageAgreementAdapter} for initiator and + * responder flows, reusing the underlying KEM context.
  • + *
  • Asymmetric key builders for {@code KyberKeyGenSpec} generation and + * X.509/PKCS#8 key import paths.
  • + *
+ * + *

Example

{@code
+     * Security.addProvider(new BouncyCastlePQCProvider());
+     * KyberAlgorithm alg = new KyberAlgorithm();
+     *
+     * KeyPair kp = alg.asymmetricKeyBuilder(KyberKeyGenSpec.class)
+     *                 .generateKeyPair(KyberKeyGenSpec.kyber768());
+     * }
+ */ + public KyberAlgorithm() { + super("ML-KEM", "Kyber (ML-KEM)", BouncyCastlePQCProvider.PROVIDER_NAME); + + capability(AlgorithmFamily.KEM, KeyUsage.ENCAPSULATE, KemContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> new KyberKemContext(this, k), () -> VoidSpec.INSTANCE); + + capability(AlgorithmFamily.KEM, KeyUsage.DECAPSULATE, KemContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> new KyberKemContext(this, k), () -> VoidSpec.INSTANCE); + + // AGREEMENT (initiator): Alice has Bob's public key → encapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← return your + // existing KemContext + PublicKey.class, // ← initiator uses recipient's public key + VoidSpec.class, // ← must implement ContextSpec + (PublicKey recipient, VoidSpec spec) -> { + // create a context bound to recipient public key for encapsulation + return KemMessageAgreementAdapter.builder().upon(new KyberKemContext(this, recipient)).asInitiator() + .build(); + }, () -> VoidSpec.INSTANCE // default + ); + + // AGREEMENT (responder): Bob has his private key → decapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← same KemContext + // type + PrivateKey.class, // ← responder uses their private key + VoidSpec.class, (PrivateKey myPriv, VoidSpec spec) -> { + return KemMessageAgreementAdapter.builder().upon(new KyberKemContext(this, myPriv)).asResponder() + .build(); + }, () -> VoidSpec.INSTANCE); + + // Keypair builder via BCPQC + registerAsymmetricKeyBuilder(KyberKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + /** + * Generates a Kyber key pair for the variant defined by the provided spec. + * + * @param spec the key generation spec selecting the Kyber variant. + * @return a newly generated Kyber key pair. + * @throws GeneralSecurityException if the provider is unavailable or key + * generation fails. + */ + @Override + public KeyPair generateKeyPair(KyberKeyGenSpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("Kyber", providerName()); + KyberParameterSpec params = toParams(spec); + kpg.initialize(params, new SecureRandom()); + return kpg.generateKeyPair(); + } + + /** + * Unsupported operation for this builder. Use {@code KyberPublicKeySpec} for + * public key import. + * + * @param spec the key generation spec; not used. + * @return never returns normally. + * @throws UnsupportedOperationException always thrown to indicate that public + * key import uses a dedicated encoded + * spec. + */ + @Override + public PublicKey importPublic(KyberKeyGenSpec spec) { + throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed."); + } + + /** + * Unsupported operation for this builder. Use {@code KyberPrivateKeySpec} for + * private key import. + * + * @param spec the key generation spec; not used. + * @return never returns normally. + * @throws UnsupportedOperationException always thrown to indicate that private + * key import uses a dedicated encoded + * spec. + */ + @Override + public PrivateKey importPrivate(KyberKeyGenSpec spec) { + throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed."); + } + }, KyberKeyGenSpec::kyber768); + + // Public-key import (X.509) + registerAsymmetricKeyBuilder(KyberPublicKeySpec.class, new AsymmetricKeyBuilder<>() { + /** + * Unsupported operation for this builder. Use {@code KyberKeyGenSpec} for + * generation. + * + * @param spec the public key spec; not used. + * @return never returns normally. + * @throws UnsupportedOperationException always thrown to indicate that key + * generation is not supported here. + */ + @Override + public KeyPair generateKeyPair(KyberPublicKeySpec spec) { + throw new UnsupportedOperationException("Use KyberKeyGenSpec for generation"); + } + + /** + * Imports a Kyber public key from an X.509 SubjectPublicKeyInfo encoding. + * + * @param spec the public key spec carrying X.509-encoded bytes. + * @return the decoded Kyber public key. + * @throws GeneralSecurityException if the provider is unavailable or the key + * cannot be decoded. + */ + @Override + public PublicKey importPublic(KyberPublicKeySpec spec) throws GeneralSecurityException { + ensureProvider(); // your existing helper + // Pick the same algorithm/provider you use for keygen + KeyFactory kf = KeyFactory.getInstance("Kyber", providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); + } + + /** + * Unsupported operation for this builder. Use {@code KyberPrivateKeySpec} for + * private key import. + * + * @param spec the public key spec; not used. + * @return never returns normally. + * @throws UnsupportedOperationException always thrown to indicate that private + * key import is not supported here. + */ + @Override + public PrivateKey importPrivate(KyberPublicKeySpec spec) { + throw new UnsupportedOperationException("Use KyberPrivateKeySpec for private key import"); + } + }, null // no default spec + ); + + // Private-key import (PKCS#8) + registerAsymmetricKeyBuilder(KyberPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + /** + * Unsupported operation for this builder. Use {@code KyberKeyGenSpec} for + * generation. + * + * @param spec the private key spec; not used. + * @return never returns normally. + * @throws UnsupportedOperationException always thrown to indicate that key + * generation is not supported here. + */ + @Override + public KeyPair generateKeyPair(KyberPrivateKeySpec spec) { + throw new UnsupportedOperationException("Use KyberKeyGenSpec for generation"); + } + + /** + * Unsupported operation for this builder. Use {@code KyberPublicKeySpec} for + * public key import. + * + * @param spec the private key spec; not used. + * @return never returns normally. + * @throws UnsupportedOperationException always thrown to indicate that public + * key import is not supported here. + */ + @Override + public PublicKey importPublic(KyberPrivateKeySpec spec) { + throw new UnsupportedOperationException("Use KyberPublicKeySpec for public key import"); + } + + /** + * Imports a Kyber private key from a PKCS#8 PrivateKeyInfo encoding. + * + * @param spec the private key spec carrying PKCS#8-encoded bytes. + * @return the decoded Kyber private key. + * @throws GeneralSecurityException if the provider is unavailable or the key + * cannot be decoded. + */ + @Override + public PrivateKey importPrivate(KyberPrivateKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("Kyber", providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + } + }, null); + } + + /** + * Maps a {@code KyberKeyGenSpec} to the corresponding + * {@code KyberParameterSpec} constant. + * + *

Example

{@code
+     * KyberParameterSpec params = KyberAlgorithm.toParams(KyberKeyGenSpec.kyber512());
+     * }
+ * + * @param s key generation spec carrying the chosen Kyber variant. + * @return the provider parameter set that matches the requested variant. + * @throws IllegalArgumentException if the variant is not recognized. + */ + private static KyberParameterSpec toParams(KyberKeyGenSpec s) { + return switch (s.variant()) { + case KYBER512 -> KyberParameterSpec.kyber512; + case KYBER768 -> KyberParameterSpec.kyber768; + case KYBER1024 -> KyberParameterSpec.kyber1024; + }; + } + + /** + * Ensures that the Bouncy Castle PQC provider is registered before accessing + * JCA primitives. + * + *

+ * This method is a defensive check used by key generation and key import + * implementations. It fails fast when the required provider is not available. + *

+ * + *

Example

{@code
+     * Security.addProvider(new BouncyCastlePQCProvider());
+     * KyberAlgorithm.ensureProvider(); // returns silently if available
+     * }
+ * + * @throws NoSuchProviderException if the Bouncy Castle PQC provider is not + * present. + */ + private static void ensureProvider() throws NoSuchProviderException { + // Ensure BCPQC is registered; caller can add Security.addProvider in bootstrap + // if desired. + Provider p = Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME); + if (p == null) { + throw new NoSuchProviderException( + "BouncyCastle PQC provider not found. Add bcpqc-jdk18on to classpath and register provider."); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberKemContext.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberKemContext.java new file mode 100644 index 0000000..8e3a6e5 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberKemContext.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.kyber; + +import java.io.IOException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Objects; + +import javax.security.auth.DestroyFailedException; + +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMExtractor; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMGenerator; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters; +import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory; +import org.bouncycastle.pqc.crypto.util.PublicKeyFactory; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.KemContext; + +/** + * KyberKemContext is a lightweight KEM context used to perform Kyber (ML-KEM) + * encapsulation or decapsulation. + * + *

Overview

A context is created in exactly one role: + *
    + *
  • Encapsulation role when constructed with a {@link PublicKey}. In this + * role {@link #encapsulate()} is enabled and {@link #decapsulate(byte[])} is + * invalid.
  • + *
  • Decapsulation role when constructed with a {@link PrivateKey}. In this + * role {@link #decapsulate(byte[])} is enabled and {@link #encapsulate()} is + * invalid.
  • + *
+ * The implementation delegates to Bouncy Castle PQC primitives for ML-KEM. The + * context retains a reference to the associated {@link CryptoAlgorithm} + * instance to report metadata via {@link #algorithm()}. + * + *

Thread-safety

Instances are not explicitly synchronized. Do not + * share a single context instance across threads while operations are running. + * + *

Usage examples

{@code
+ * // Encapsulation role (sender knows recipient's public key):
+ * KemContext encCtx = new KyberKemContext(alg, recipientPublicKey);
+ * KemResult result = encCtx.encapsulate();
+ * byte[] ciphertext = result.ciphertext();
+ * byte[] sharedSecret = result.sharedSecret();
+ *
+ * // Decapsulation role (recipient uses its private key):
+ * KemContext decCtx = new KyberKemContext(alg, recipientPrivateKey);
+ * byte[] agreedSecret = decCtx.decapsulate(ciphertext);
+ * }
+ */ +public final class KyberKemContext implements KemContext { + + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean encapsulate; + + /** + * Creates a context in encapsulation role bound to the given public key. + * + * @param algorithm the owning algorithm instance that exposes provider name and + * metadata; must not be null. + * @param k the recipient public key for encapsulation; must not be + * null. + * @throws NullPointerException if any argument is null. + */ + public KyberKemContext(CryptoAlgorithm algorithm, PublicKey k) { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm must not be null"); + this.key = Objects.requireNonNull(k, "public key must not be null"); + this.encapsulate = true; + } + + /** + * Creates a context in decapsulation role bound to the given private key. + * + * @param algorithm the owning algorithm instance that exposes provider name and + * metadata; must not be null. + * @param k the local private key for decapsulation; must not be null. + * @throws NullPointerException if any argument is null. + */ + public KyberKemContext(CryptoAlgorithm algorithm, PrivateKey k) { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm must not be null"); + this.key = Objects.requireNonNull(k, "private key must not be null"); + this.encapsulate = false; + } + + /** + * Returns the algorithm descriptor associated with this context. + * + * @return the algorithm that created this context. + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + * @return the public key when in encapsulation role, or the private key when in + * decapsulation role. + */ + @Override + public Key key() { + return key; + } + + /** + * Closes this context. + * + *

+ * This implementation does not hold external resources, so the method is a + * no-op. It is provided for API symmetry and future compatibility. + *

+ */ + @Override + public void close() { + // empty + } + + /** + * Performs Kyber encapsulation using the bound public key and returns the + * ciphertext and shared secret. + * + *

+ * This method is valid only if the context was constructed with a + * {@link PublicKey}. If the context is in decapsulation role, an + * {@link IllegalStateException} is thrown. + *

+ * + * @return a result containing the encapsulated ciphertext and the derived + * shared secret. + * @throws IllegalStateException if the context is not initialized for + * encapsulation. + * @throws IOException if encapsulation fails in the underlying + * provider. + */ + @Override + public KemResult encapsulate() throws IOException { + if (!encapsulate) { + throw new IllegalStateException("Not initialized for ENCAPSULATE"); + } + try { + final MLKEMPublicKeyParameters keyParam = (MLKEMPublicKeyParameters) PublicKeyFactory + .createKey(key.getEncoded()); + MLKEMGenerator gen = new MLKEMGenerator(new SecureRandom()); + SecretWithEncapsulation res = gen.generateEncapsulated(keyParam); + byte[] secret = res.getSecret(); + byte[] ct = res.getEncapsulation(); + res.destroy(); + return new KemResult(ct, secret); + } catch (DestroyFailedException e) { + throw new IOException("Kyber encapsulate failed", e); + } + } + + /** + * Performs Kyber decapsulation using the bound private key and returns the + * shared secret. + * + *

+ * This method is valid only if the context was constructed with a + * {@link PrivateKey}. If the context is in encapsulation role, an + * {@link IllegalStateException} is thrown. + *

+ * + * @param ciphertext the Kyber ciphertext to decapsulate; must be the exact + * bytes produced by the peer's encapsulate call. + * @return the derived shared secret corresponding to the provided ciphertext. + * @throws IllegalStateException if the context is not initialized for + * decapsulation. + * @throws IOException if decapsulation fails in the underlying + * provider. + */ + @Override + public byte[] decapsulate(byte[] ciphertext) throws IOException { + if (encapsulate) { + throw new IllegalStateException("Not initialized for DECAPSULATE"); + } + try { + final MLKEMPrivateKeyParameters keyParam = (MLKEMPrivateKeyParameters) PrivateKeyFactory + .createKey(key.getEncoded()); + MLKEMExtractor gen = new MLKEMExtractor(keyParam); + + return gen.extractSecret(ciphertext); + } catch (Exception e) { // NOPMD + throw new IOException("Kyber decapsulate failed", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberKeyGenSpec.java new file mode 100644 index 0000000..247ada1 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberKeyGenSpec.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * 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.core.alg.kyber; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Key generation specification for the Kyber (ML-KEM) algorithm family. + * + *

+ * {@code KyberKeyGenSpec} captures the choice of Kyber parameter set when + * generating a key pair. Kyber is a post-quantum key encapsulation mechanism + * (KEM) standardized as ML-KEM. The three supported security levels are + * represented by {@link Variant} values: + *

+ *
    + *
  • {@link Variant#KYBER512} - lowest cost, NIST level 1 security,
  • + *
  • {@link Variant#KYBER768} - balanced choice, NIST level 3 security,
  • + *
  • {@link Variant#KYBER1024} - strongest, NIST level 5 security.
  • + *
+ * + *

Usage

Instances of this class are passed to the Kyber key builder to + * select the desired parameter set:
{@code
+ * CryptoAlgorithm kyber = new KyberAlgorithm();
+ * KeyPair kp = kyber.asymmetricKeyBuilder(KyberKeyGenSpec.class)
+ *                   .generateKeyPair(KyberKeyGenSpec.kyber768());
+ * }
+ * + *

+ * The spec is immutable and may be safely shared between threads. + *

+ * + * @see KyberAlgorithm + * @see zeroecho.core.CryptoAlgorithm#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec) + */ +public final class KyberKeyGenSpec implements AlgorithmKeySpec, Describable { + /** + * Enumerates the supported Kyber parameter sets. + */ + public enum Variant { + /** Kyber-512, NIST level 1 security strength. */ + KYBER512, + /** Kyber-768, NIST level 3 security strength. */ + KYBER768, + /** Kyber-1024, NIST level 5 security strength. */ + KYBER1024 + } + + private final Variant variant; + + private KyberKeyGenSpec(Variant v) { + this.variant = v; + } + + /** + * Creates a specification for Kyber-512 key generation. + * + * @return a new spec bound to {@link Variant#KYBER512} + */ + public static KyberKeyGenSpec kyber512() { + return new KyberKeyGenSpec(Variant.KYBER512); + } + + /** + * Creates a specification for Kyber-768 key generation. + * + * @return a new spec bound to {@link Variant#KYBER768} + */ + public static KyberKeyGenSpec kyber768() { + return new KyberKeyGenSpec(Variant.KYBER768); + } + + /** + * Creates a specification for Kyber-1024 key generation. + * + * @return a new spec bound to {@link Variant#KYBER1024} + */ + public static KyberKeyGenSpec kyber1024() { + return new KyberKeyGenSpec(Variant.KYBER1024); + } + + /** + * Returns the variant associated with this specification. + * + * @return the Kyber parameter set + */ + public Variant variant() { + return variant; + } + + /** + * Returns a human-readable description of this spec. + * + *

+ * The description is the variant name, e.g., {@code "KYBER768"}. + *

+ * + * @return string description of the spec + */ + @Override + public String description() { + return variant.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java new file mode 100644 index 0000000..8ff58ab --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPrivateKeySpec.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * 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.core.alg.kyber; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Specification wrapper for a Kyber (ML-KEM) private key encoded in PKCS#8. + * + *

+ * Instances of this class carry an immutable copy of the PKCS#8-encoded private + * key bytes. They are used with {@link zeroecho.core.CryptoAlgorithm} key + * builders to import keys into the provider’s native representation. + *

+ * + *

Encoding

+ *
    + *
  • Format: PKCS#8 DER encoding of a Kyber private key.
  • + *
  • Stored as a defensive clone to ensure immutability.
  • + *
  • Marshalling/unmarshalling supported via {@link PairSeq} with Base64 + * encoding.
  • + *
+ * + *

Usage

{@code
+ * // Import a private key into a CryptoAlgorithm
+ * byte[] pkcs8Bytes = ...; // obtained from storage
+ * KyberPrivateKeySpec spec = new KyberPrivateKeySpec(pkcs8Bytes);
+ * PrivateKey k = kyberAlg.importPrivate(spec);
+ *
+ * // Serialize for persistence
+ * PairSeq seq = KyberPrivateKeySpec.marshal(spec);
+ *
+ * // Deserialize back
+ * KyberPrivateKeySpec restored = KyberPrivateKeySpec.unmarshal(seq);
+ * }
+ * + *

+ * This class is immutable and thread-safe. + *

+ * + * @see KyberPublicKeySpec + * @see KyberKeyGenSpec + */ +public final class KyberPrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Creates a new spec from PKCS#8-encoded private key bytes. + * + * @param pkcs8Der PKCS#8 DER-encoded private key, not null + * @throws NullPointerException if {@code pkcs8Der} is null + */ + public KyberPrivateKeySpec(byte[] pkcs8Der) { + this.pkcs8 = Objects.requireNonNull(pkcs8Der, "pkcs8Der").clone(); + } + + /** + * Returns a defensive copy of the PKCS#8-encoded private key. + * + * @return cloned byte array of PKCS#8 DER encoding + */ + public byte[] pkcs8() { + return pkcs8.clone(); + } + + /** + * Serializes this spec into a {@link PairSeq} using Base64 encoding. + * + *

+ * The sequence contains: + *

    + *
  • {@code type} = {@code KyberPrivateKey}
  • + *
  • {@code pkcs8.b64} = Base64 of PKCS#8 bytes (no padding)
  • + *
+ * + * @param spec the spec to marshal + * @return key-value representation suitable for persistence + */ + public static PairSeq marshal(KyberPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "KyberPrivateKey", PKCS8_B64, b64); + } + + /** + * Reconstructs a {@code KyberPrivateKeySpec} from a {@link PairSeq}. + * + * @param p key-value sequence containing a {@code pkcs8.b64} entry + * @return a new spec with decoded PKCS#8 bytes + * @throws IllegalArgumentException if the required field is missing + */ + public static KyberPrivateKeySpec unmarshal(PairSeq p) { + String pkcs8b64 = null; + + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (PKCS8_B64.equals(k)) { + pkcs8b64 = v; + } + } + if (pkcs8b64 == null) { + throw new IllegalArgumentException("KyberPrivateKeySpec: missing 'pkcs8.b64'"); + } + return new KyberPrivateKeySpec(Base64.getDecoder().decode(pkcs8b64)); + } + + /** + * Returns a diagnostic string for logging and debugging. + * + *

+ * Does not expose key material; only length is shown. + *

+ * + * @return string with length of encoded key + */ + @Override + public String toString() { + return "KyberPrivateKeySpec[len=" + pkcs8.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java new file mode 100644 index 0000000..c0d3e9b --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/kyber/KyberPublicKeySpec.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * 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.core.alg.kyber; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Specification wrapper for a Kyber (ML-KEM) public key encoded in X.509. + * + *

+ * Instances of this class carry an immutable copy of the X.509 + * SubjectPublicKeyInfo-encoded bytes. They are used with + * {@link zeroecho.core.CryptoAlgorithm} key builders to import keys into the + * provider’s native representation. + *

+ * + *

Encoding

+ *
    + *
  • Format: X.509 DER encoding of a Kyber public key + * (SubjectPublicKeyInfo).
  • + *
  • Stored as a defensive clone to ensure immutability.
  • + *
  • Marshalling/unmarshalling supported via {@link PairSeq} with Base64 + * encoding.
  • + *
+ * + *

Usage

{@code
+ * // Import a public key into a CryptoAlgorithm
+ * byte[] x509Bytes = ...; // obtained from storage
+ * KyberPublicKeySpec spec = new KyberPublicKeySpec(x509Bytes);
+ * PublicKey k = kyberAlg.importPublic(spec);
+ *
+ * // Serialize for persistence
+ * PairSeq seq = KyberPublicKeySpec.marshal(spec);
+ *
+ * // Deserialize back
+ * KyberPublicKeySpec restored = KyberPublicKeySpec.unmarshal(seq);
+ * }
+ * + *

+ * This class is immutable and thread-safe. + *

+ * + * @see KyberPrivateKeySpec + * @see KyberKeyGenSpec + */ +public final class KyberPublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Creates a new spec from X.509-encoded public key bytes. + * + * @param x509Der X.509 SubjectPublicKeyInfo DER-encoded public key, not null + * @throws NullPointerException if {@code x509Der} is null + */ + public KyberPublicKeySpec(byte[] x509Der) { + this.x509 = Objects.requireNonNull(x509Der, "x509Der").clone(); + } + + /** + * Returns a defensive copy of the X.509-encoded public key. + * + * @return cloned byte array of X.509 DER encoding + */ + public byte[] x509() { + return x509.clone(); + } + + /** + * Serializes this spec into a {@link PairSeq} using Base64 encoding. + * + *

+ * The sequence contains: + *

    + *
  • {@code type} = {@code KyberPublicKey}
  • + *
  • {@code x509.b64} = Base64 of X.509 bytes (no padding)
  • + *
+ * + * @param spec the spec to marshal + * @return key-value representation suitable for persistence + */ + public static PairSeq marshal(KyberPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "KyberPublicKey", X509_B64, b64); + } + + /** + * Reconstructs a {@code KyberPublicKeySpec} from a {@link PairSeq}. + * + * @param p key-value sequence containing an {@code x509.b64} entry + * @return a new spec with decoded X.509 bytes + * @throws IllegalArgumentException if the required field is missing + */ + public static KyberPublicKeySpec unmarshal(PairSeq p) { + String x509b64 = null; + + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (X509_B64.equals(k)) { + x509b64 = v; + } + } + if (x509b64 == null) { + throw new IllegalArgumentException("KyberPublicKeySpec: missing 'x509.b64'"); + } + return new KyberPublicKeySpec(Base64.getDecoder().decode(x509b64)); + } + + /** + * Returns a diagnostic string for logging and debugging. + * + *

+ * Does not expose key material; only length is shown. + *

+ * + * @return string with length of encoded key + */ + @Override + public String toString() { + return "KyberPublicKeySpec[len=" + x509.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java b/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java new file mode 100644 index 0000000..6626690 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/kyber/package-info.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Kyber (ML-KEM) post-quantum key encapsulation integration. + * + *

+ * This package wires the Kyber key encapsulation mechanism into the core layer. + * It provides the algorithm descriptor, a lightweight runtime KEM context, + * key-generation specifications for the supported Kyber variants, and encoded + * key specs for import and export. Provider-specific details are kept behind + * small factories while roles and metadata remain explicit to higher layers. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register Kyber under a canonical identifier and declare + * {@link zeroecho.core.KeyUsage#ENCAPSULATE} and + * {@link zeroecho.core.KeyUsage#DECAPSULATE} roles, with an optional + * message-style agreement adapter where needed.
  • + *
  • Provide a {@link zeroecho.core.context.KemContext} implementation bound + * to either a public or private key for encapsulation or decapsulation.
  • + *
  • Expose immutable specifications for key generation variants and encoded + * key carriers with compact marshalling helpers.
  • + *
  • Ensure operations are delegated to an available PQC provider and fail + * fast if the provider is absent.
  • + *
+ * + *

Components

+ *
    + *
  • KyberAlgorithm: algorithm descriptor that wires KEM and, when + * required, message-agreement roles and registers asymmetric key builders for + * generation and encoded-key import.
  • + *
  • KyberKemContext: runtime context implementing encapsulate and + * decapsulate operations via provider primitives.
  • + *
  • KyberKeyGenSpec: immutable specification of the Kyber variant + * (512, 768, 1024) with convenience factories.
  • + *
  • KyberPublicKeySpec and KyberPrivateKeySpec: wrappers over + * X.509 and PKCS#8 encodings with defensive copying and simple marshalling + * utilities.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and safe to share across + * threads.
  • + *
  • KEM contexts are stateful and not thread-safe; create one per + * encapsulation or decapsulation operation.
  • + *
  • Key specifications preserve encoded form without internal parsing and + * rely on defensive cloning for safety.
  • + *
  • Agreement adapters allow Kyber to be composed into initiator/responder + * exchanges when a message-based interface is preferred.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.kyber; diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/NtruAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ntru/NtruAlgorithm.java new file mode 100644 index 0000000..bd1a585 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntru/NtruAlgorithm.java @@ -0,0 +1,364 @@ +/******************************************************************************* + * 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.core.alg.ntru; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.bouncycastle.pqc.jcajce.spec.NTRUParameterSpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * NtruAlgorithm exposes the NTRU KEM from the Bouncy Castle PQC provider and + * adapts it for both KEM and message-based agreement roles. + * + *

Capabilities registered

+ *
    + *
  • KEM: + *
      + *
    • {@link KeyUsage#ENCAPSULATE} with {@link PublicKey} producing a + * {@link KemContext}.
    • + *
    • {@link KeyUsage#DECAPSULATE} with {@link PrivateKey} producing a + * {@link KemContext}.
    • + *
    + *
  • + *
  • AGREEMENT: + *
      + *
    • {@link KeyUsage#AGREEMENT} (initiator) using a {@link PublicKey}, exposed + * as {@link MessageAgreementContext} via a KEM-to-agreement adapter.
    • + *
    • {@link KeyUsage#AGREEMENT} (responder) using a {@link PrivateKey}, + * exposed as {@link MessageAgreementContext} via a KEM-to-agreement + * adapter.
    • + *
    + *
  • + *
+ * + *

Usage

+ *

+ * KEM, initiator and responder + *

+ *
{@code
+ * NtruAlgorithm alg = new NtruAlgorithm();
+ * // Generate a key pair (or import one). hrss701 is the default.
+ * KeyPair kp = alg.asymmetricKeyBuilder(NtruKeyGenSpec.class)
+ *                 .generateKeyPair(NtruKeyGenSpec.hrss701());
+ *
+ * // Initiator encapsulates using recipient's public key
+ * KemContext enc = alg.create(KeyUsage.ENCAPSULATE, kp.getPublic(), VoidSpec.INSTANCE);
+ * KemResult kem = enc.encapsulate();
+ *
+ * // Responder decapsulates using their private key
+ * KemContext dec = alg.create(KeyUsage.DECAPSULATE, kp.getPrivate(), VoidSpec.INSTANCE);
+ * byte[] secret = dec.decapsulate(kem.encapsulation());
+ * }
+ * + *

+ * Agreement over messages using the KEM adapter + *

+ *
{@code
+ * NtruAlgorithm alg = new NtruAlgorithm();
+ * KeyPair kpBob = alg.asymmetricKeyBuilder(NtruKeyGenSpec.class)
+ *                    .generateKeyPair(NtruKeyGenSpec.hrss701());
+ *
+ * // Alice (initiator) - has Bob's public key
+ * MessageAgreementContext alice = alg.create(
+ *     KeyUsage.AGREEMENT, kpBob.getPublic(), VoidSpec.INSTANCE);
+ *
+ * // Alice produces the peer message she must send to Bob
+ * byte[] toBob = alice.getPeerMessage();
+ *
+ * // Bob (responder) - has his private key
+ * MessageAgreementContext bob = alg.create(
+ *     KeyUsage.AGREEMENT, kpBob.getPrivate(), VoidSpec.INSTANCE);
+ *
+ * // Bob supplies Alice's message so he can complete decapsulation
+ * bob.setPeerMessage(toBob);
+ *
+ * // Both parties now hold the same derived secret via the AgreementContext API.
+ * }
+ * + *

Key builders

+ *
    + *
  • {@code NtruKeyGenSpec} - generates key pairs with a selected NTRU + * parameter set (default: {@code hrss701}).
  • + *
  • {@code NtruPublicKeySpec} - imports an X.509 SubjectPublicKeyInfo public + * key.
  • + *
  • {@code NtruPrivateKeySpec} - imports a PKCS#8 PrivateKeyInfo private + * key.
  • + *
+ * + *

Provider requirements

+ *

+ * This implementation uses {@code KeyPairGenerator} and {@code KeyFactory} with + * the algorithm string {@code "NTRU"} from the Bouncy Castle PQC provider + * ({@code BouncyCastlePQCProvider.PROVIDER_NAME}). If the provider is not + * present, attempts to generate or import keys will fail with + * {@link java.security.NoSuchProviderException}. + *

+ * + *

Thread-safety

+ *

+ * Instances are stateless after construction and may be safely shared across + * threads. Contexts returned from this algorithm are not guaranteed to be + * thread-safe. + *

+ * + * @since 1.0 + * @see AlgorithmFamily + * @see KeyUsage + * @see KemContext + * @see MessageAgreementContext + */ +public final class NtruAlgorithm extends AbstractCryptoAlgorithm { + /** + * Creates an algorithm instance that registers KEM and AGREEMENT roles and + * wires asymmetric key builders for generation and X.509/PKCS#8 import. + * + *

+ * The default key generation spec is {@code NtruKeyGenSpec.hrss701()}. + *

+ */ + public NtruAlgorithm() { + super("NTRU", "NTRU (KEM)", BouncyCastlePQCProvider.PROVIDER_NAME); + + // KEM capabilities + capability(AlgorithmFamily.KEM, KeyUsage.ENCAPSULATE, KemContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> new NtruKemContext(this, k), () -> VoidSpec.INSTANCE); + + capability(AlgorithmFamily.KEM, KeyUsage.DECAPSULATE, KemContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> new NtruKemContext(this, k), () -> VoidSpec.INSTANCE); + + // AGREEMENT (initiator): Alice has Bob's public key → encapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← return your + // existing KemContext + PublicKey.class, // ← initiator uses recipient's public key + VoidSpec.class, // ← must implement ContextSpec + (PublicKey recipient, VoidSpec spec) -> { + // create a context bound to recipient public key for encapsulation + return KemMessageAgreementAdapter.builder().upon(new NtruKemContext(this, recipient)).asInitiator() + .build(); + }, () -> VoidSpec.INSTANCE // default + ); + + // AGREEMENT (responder): Bob has his private key → decapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← same KemContext + // type + PrivateKey.class, // ← responder uses their private key + VoidSpec.class, (PrivateKey myPriv, VoidSpec spec) -> { + return KemMessageAgreementAdapter.builder().upon(new NtruKemContext(this, myPriv)).asResponder() + .build(); + }, () -> VoidSpec.INSTANCE); + + // Keypair builder via BCPQC + registerAsymmetricKeyBuilder(NtruKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + /** + * Generates a new NTRU key pair using the requested parameter set. + * + * @param spec NTRU key generation parameters (e.g., HRSS701) + * @return freshly generated key pair + * @throws GeneralSecurityException if the provider is missing or initialization + * fails + */ + @Override + public KeyPair generateKeyPair(NtruKeyGenSpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("NTRU", providerName()); + NTRUParameterSpec params = toParams(spec); + kpg.initialize(params, new SecureRandom()); + return kpg.generateKeyPair(); + } + + /** + * Importing a public key is not supported by this spec type. + * + * @param spec unused + * @return never returns + * @throws UnsupportedOperationException always thrown + */ + @Override + public PublicKey importPublic(NtruKeyGenSpec spec) { + throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed."); + } + + /** + * Importing a private key is not supported by this spec type. + * + * @param spec unused + * @return never returns + * @throws UnsupportedOperationException always thrown + */ + @Override + public PrivateKey importPrivate(NtruKeyGenSpec spec) { + throw new UnsupportedOperationException("Import with a dedicated encoded spec, if needed."); + } + }, NtruKeyGenSpec::hrss701); // sensible default + + // Public-key import (X.509) + registerAsymmetricKeyBuilder(NtruPublicKeySpec.class, new AsymmetricKeyBuilder<>() { + /** + * Key generation is not supported for an encoded public-key spec. + * + * @param spec unused + * @return never returns + * @throws UnsupportedOperationException always thrown + */ + @Override + public KeyPair generateKeyPair(NtruPublicKeySpec spec) { + throw new UnsupportedOperationException("Use NtruKeyGenSpec for generation"); + } + + /** + * Imports a public key from an X.509 SubjectPublicKeyInfo blob. + * + * @param spec holder of the X.509 encoded key bytes + * @return imported {@link PublicKey} + * @throws GeneralSecurityException if the provider is missing or decoding fails + */ + @Override + public PublicKey importPublic(NtruPublicKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("NTRU", providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); + } + + /** + * Private key import is not supported by the public-key spec. + * + * @param spec unused + * @return never returns + * @throws UnsupportedOperationException always thrown + */ + @Override + public PrivateKey importPrivate(NtruPublicKeySpec spec) { + throw new UnsupportedOperationException("Use NtruPrivateKeySpec for private key import"); + } + }, null); + + // Private-key import (PKCS#8) + registerAsymmetricKeyBuilder(NtruPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + /** + * Key generation is not supported for an encoded private-key spec. + * + * @param spec unused + * @return never returns + * @throws UnsupportedOperationException always thrown + */ + @Override + public KeyPair generateKeyPair(NtruPrivateKeySpec spec) { + throw new UnsupportedOperationException("Use NtruKeyGenSpec for generation"); + } + + /** + * Public key import is not supported by the private-key spec. + * + * @param spec unused + * @return never returns + * @throws UnsupportedOperationException always thrown + */ + @Override + public PublicKey importPublic(NtruPrivateKeySpec spec) { + throw new UnsupportedOperationException("Use NtruPublicKeySpec for public key import"); + } + + /** + * Imports a private key from a PKCS#8 PrivateKeyInfo blob. + * + * @param spec holder of the PKCS#8 encoded key bytes + * @return imported {@link PrivateKey} + * @throws GeneralSecurityException if the provider is missing or decoding fails + */ + @Override + public PrivateKey importPrivate(NtruPrivateKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("NTRU", providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + } + }, null); + } + + /** + * Maps a high-level NTRU key generation spec to the corresponding Bouncy Castle + * {@link NTRUParameterSpec}. + * + * @param s high-level NTRU key generation spec + * @return provider parameter set matching {@code s} + * @throws IllegalArgumentException if the variant is unknown + */ + private static NTRUParameterSpec toParams(NtruKeyGenSpec s) { + return switch (s.variant()) { + case HPS2048_509 -> NTRUParameterSpec.ntruhps2048509; + case HPS2048_677 -> NTRUParameterSpec.ntruhps2048677; + case HPS4096_821 -> NTRUParameterSpec.ntruhps4096821; + case HPS4096_1229 -> NTRUParameterSpec.ntruhps40961229; + case HRSS701 -> NTRUParameterSpec.ntruhrss701; + case HRSS1373 -> NTRUParameterSpec.ntruhrss1373; + }; + } + + /** + * Ensures the Bouncy Castle PQC provider is available before attempting JCA + * operations. + * + * @throws NoSuchProviderException if + * {@code BouncyCastlePQCProvider.PROVIDER_NAME} + * is not registered + */ + private static void ensureProvider() throws NoSuchProviderException { + Provider p = Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME); + if (p == null) { + throw new NoSuchProviderException( + "BouncyCastle PQC provider not found. Add bcpqc-jdk18on to classpath and register it."); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/NtruKemContext.java b/lib/src/main/java/zeroecho/core/alg/ntru/NtruKemContext.java new file mode 100644 index 0000000..addcbdb --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntru/NtruKemContext.java @@ -0,0 +1,257 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ntru; + +import java.io.IOException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Objects; + +import javax.security.auth.DestroyFailedException; + +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.pqc.crypto.ntru.NTRUKEMExtractor; +import org.bouncycastle.pqc.crypto.ntru.NTRUKEMGenerator; +import org.bouncycastle.pqc.crypto.ntru.NTRUPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.ntru.NTRUPublicKeyParameters; +import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory; +import org.bouncycastle.pqc.crypto.util.PublicKeyFactory; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.KemContext; + +/** + * NtruKemContext performs NTRU key encapsulation and decapsulation against a + * single key and parameter set. + * + *

+ * This context is created either with a public key (encapsulation role) or a + * private key (decapsulation role) and then drives the NTRU KEM using Bouncy + * Castle's lightweight API under the hood. Public keys enable + * {@link #encapsulate()}, private keys enable {@link #decapsulate(byte[])}. + * Calling a method that does not match the role results in + * {@link IllegalStateException}. + *

+ * + *

Usage

{@code
+ * // Key generation and import are outside of this class; shown here for completeness.
+ * KeyPairGenerator kpg = KeyPairGenerator.getInstance("NTRU", "BCPQC");
+ * kpg.initialize(NTRUParameterSpec.ntruhrss701, new SecureRandom());
+ * KeyPair kp = kpg.generateKeyPair();
+ *
+ * // Initiator (encapsulation) uses the recipient's public key
+ * try (NtruKemContext enc = new NtruKemContext(algorithm, kp.getPublic())) {
+ *     KemContext.KemResult r = enc.encapsulate();
+ *     byte[] ct = r.ciphertext();     // message to send to the recipient
+ *     byte[] ssA = r.sharedSecret();  // initiator's shared secret
+ *
+ *     // Responder (decapsulation) uses the matching private key
+ *     try (NtruKemContext dec = new NtruKemContext(algorithm, kp.getPrivate())) {
+ *         byte[] ssB = dec.decapsulate(ct);
+ *         // ssA and ssB are identical
+ *     }
+ * }
+ * }
+ * + *

Key material and formats

+ *

+ * The context consumes standard JCA keys via + * {@link java.security.Key#getEncoded()}: X.509 SubjectPublicKeyInfo for public + * keys and PKCS#8 PrivateKeyInfo for private keys. Internally these are + * converted to Bouncy Castle NTRU key parameters for the KEM operations. + *

+ * + *

Thread-safety

+ *

+ * Instances are not thread-safe. Create a separate context per thread or per + * protocol run. + *

+ * + * @since 1.0 + */ +public final class NtruKemContext implements KemContext { + + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean encapsulate; + + /** + * Creates an encapsulation context bound to the provided public key. + * + *

+ * The resulting instance supports {@link #encapsulate()} and forbids + * {@link #decapsulate(byte[])}. + *

+ * + * @param algorithm the catalog algorithm descriptor this context belongs to, + * must not be null + * @param k the recipient's public key in the NTRU parameter set of + * interest, must not be null + * @throws NullPointerException if any argument is null + */ + public NtruKemContext(CryptoAlgorithm algorithm, PublicKey k) { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm"); + this.key = Objects.requireNonNull(k, "public key"); + this.encapsulate = true; + } + + /** + * Creates a decapsulation context bound to the provided private key. + * + *

+ * The resulting instance supports {@link #decapsulate(byte[])} and forbids + * {@link #encapsulate()}. + *

+ * + * @param algorithm the catalog algorithm descriptor this context belongs to, + * must not be null + * @param k the private key matching the recipient's public key, must + * not be null + * @throws NullPointerException if any argument is null + */ + public NtruKemContext(CryptoAlgorithm algorithm, PrivateKey k) { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm"); + this.key = Objects.requireNonNull(k, "private key"); + this.encapsulate = false; + } + + /** + * Returns the algorithm descriptor that created this context. + * + * @return the algorithm descriptor, never null + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key this context is bound to. + * + *

+ * For encapsulation this is a public key; for decapsulation it is the matching + * private key. + *

+ * + * @return the bound key reference, never null + */ + @Override + public Key key() { + return key; + } + + /** + * Closes this context. + * + *

+ * There are no native resources to release in the current implementation, so + * this method is a no-op. It exists to honor the {@link KemContext} contract + * and to allow future implementations to free resources. + *

+ */ + @Override + public void close() { + // empty + } + + /** + * Performs NTRU key encapsulation using the bound public key. + * + *

+ * On success, returns the encapsulation ciphertext and the derived shared + * secret. The ciphertext must be delivered to the holder of the matching + * private key, who can derive the same secret by calling + * {@link #decapsulate(byte[])}. + *

+ * + * @return the encapsulation result containing ciphertext and shared secret + * @throws IOException if the underlying cryptographic operation fails + * @throws IllegalStateException if this context was constructed for + * decapsulation + */ + @Override + public KemResult encapsulate() throws IOException { + if (!encapsulate) { + throw new IllegalStateException("Not initialized for ENCAPSULATE"); + } + try { + final NTRUPublicKeyParameters keyParam = (NTRUPublicKeyParameters) PublicKeyFactory + .createKey(key.getEncoded()); + NTRUKEMGenerator gen = new NTRUKEMGenerator(new SecureRandom()); + SecretWithEncapsulation res = gen.generateEncapsulated(keyParam); + byte[] secret = res.getSecret(); + byte[] ct = res.getEncapsulation(); + res.destroy(); + return new KemResult(ct, secret); + } catch (DestroyFailedException e) { + throw new IOException("NTRU encapsulate failed", e); + } + } + + /** + * Performs NTRU key decapsulation using the bound private key. + * + *

+ * The input must be a valid encapsulation ciphertext produced for the matching + * public key and parameter set. On success, the returned byte array is the + * shared secret that matches the secret produced during encapsulation. + *

+ * + * @param ciphertext the encapsulation ciphertext received from the initiator, + * must not be null + * @return the derived shared secret + * @throws IOException if the underlying cryptographic operation fails + * or the ciphertext is invalid + * @throws IllegalStateException if this context was constructed for + * encapsulation + * @throws NullPointerException if {@code ciphertext} is null + */ + @Override + public byte[] decapsulate(byte[] ciphertext) throws IOException { + if (encapsulate) { + throw new IllegalStateException("Not initialized for DECAPSULATE"); + } + try { + final NTRUPrivateKeyParameters keyParam = (NTRUPrivateKeyParameters) PrivateKeyFactory + .createKey(key.getEncoded()); + NTRUKEMExtractor ex = new NTRUKEMExtractor(keyParam); + return ex.extractSecret(ciphertext); + } catch (Exception e) { // NOPMD + throw new IOException("NTRU decapsulate failed", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java new file mode 100644 index 0000000..c787350 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntru/NtruKeyGenSpec.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.ntru; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * NtruKeyGenSpec selects an NTRU parameter set for key pair generation. + * + *

+ * This specification is consumed by the asymmetric key builder registered by + * {@code NtruAlgorithm}. Each factory method corresponds to a concrete + * parameter set from the NTRU-HPS or NTRU-HRSS families. Larger parameter sets + * generally trade bigger keys and ciphertexts for higher security margins. + *

+ * + *

Usage

{@code
+ * // Choose a parameter set and generate a key pair
+ * NtruAlgorithm alg = new NtruAlgorithm();
+ * KeyPair kp = alg.asymmetricKeyBuilder(NtruKeyGenSpec.class)
+ *                 .generateKeyPair(NtruKeyGenSpec.hrss701());
+ * }
+ * + *

+ * Instances are immutable and can be safely reused across threads. + *

+ * + * @since 1.0 + */ +public final class NtruKeyGenSpec implements AlgorithmKeySpec, Describable { + + /** + * Variant enumerates supported NTRU parameter sets for key generation. + * + *

+ * The constants reflect the naming used by widely adopted implementations: HPS + * denotes the NTRU-HPS family and HRSS denotes the NTRU-HRSS family. The + * numeric suffixes indicate core polynomial dimensions and related parameters + * used internally by the scheme. + *

+ */ + public enum Variant { + /** + * NTRU-HPS-2048-509 parameter set. + */ + HPS2048_509, + /** + * NTRU-HPS-2048-677 parameter set. + */ + HPS2048_677, + /** + * NTRU-HPS-4096-821 parameter set. + */ + HPS4096_821, + /** + * NTRU-HPS-4096-1229 parameter set. + */ + HPS4096_1229, + /** + * NTRU-HRSS-701 parameter set. Common practical default when using HRSS. + */ + HRSS701, + /** + * NTRU-HRSS-1373 parameter set. + */ + HRSS1373 + } + + private final Variant variant; + + private NtruKeyGenSpec(Variant v) { + this.variant = v; + } + + /** + * Returns a spec for the NTRU-HPS-2048-509 parameter set. + * + * @return a new key generation spec for HPS-2048-509 + */ + public static NtruKeyGenSpec hps2048_509() { + return new NtruKeyGenSpec(Variant.HPS2048_509); + } + + /** + * Returns a spec for the NTRU-HPS-2048-677 parameter set. + * + * @return a new key generation spec for HPS-2048-677 + */ + public static NtruKeyGenSpec hps2048_677() { + return new NtruKeyGenSpec(Variant.HPS2048_677); + } + + /** + * Returns a spec for the NTRU-HPS-4096-821 parameter set. + * + * @return a new key generation spec for HPS-4096-821 + */ + public static NtruKeyGenSpec hps4096_821() { + return new NtruKeyGenSpec(Variant.HPS4096_821); + } + + /** + * Returns a spec for the NTRU-HPS-4096-1229 parameter set. + * + * @return a new key generation spec for HPS-4096-1229 + */ + public static NtruKeyGenSpec hps4096_1229() { + return new NtruKeyGenSpec(Variant.HPS4096_1229); + } + + /** + * Returns a spec for the NTRU-HRSS-701 parameter set. + * + *

+ * This is a sensible default in many deployments when an HRSS choice is + * desired. + *

+ * + * @return a new key generation spec for HRSS-701 + */ + public static NtruKeyGenSpec hrss701() { + return new NtruKeyGenSpec(Variant.HRSS701); + } + + /** + * Returns a spec for the NTRU-HRSS-1373 parameter set. + * + * @return a new key generation spec for HRSS-1373 + */ + public static NtruKeyGenSpec hrss1373() { + return new NtruKeyGenSpec(Variant.HRSS1373); + } + + /** + * Returns the selected NTRU parameter variant. + * + * @return the variant represented by this spec + */ + public Variant variant() { + return variant; + } + + /** + * Returns a short, human-readable description of the selected parameter set. + * + * @return the variant name as a string + */ + @Override + public String description() { + return variant.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java new file mode 100644 index 0000000..f6e3fa1 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntru/NtruPrivateKeySpec.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * 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.core.alg.ntru; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * NtruPrivateKeySpec holds a PKCS#8-encoded NTRU private key and supports a + * simple marshal/unmarshal form. + * + *

+ * Instances are immutable. The byte array provided to the constructor is + * defensively copied and {@link #pkcs8()} returns a fresh clone on each call. + *

+ * + *

Usage

{@code
+ * // Construct from existing PKCS#8 bytes
+ * byte[] pkcs8Der = Files.readAllBytes(Path.of("ntru-private.der"));
+ * NtruPrivateKeySpec spec = new NtruPrivateKeySpec(pkcs8Der);
+ *
+ * // Import into JCA (example provider alias shown)
+ * KeyFactory kf = KeyFactory.getInstance("NTRU", "BCPQC");
+ * PrivateKey sk = kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8()));
+ *
+ * // Marshal to a compact name-value form (Base64 without padding)
+ * PairSeq serialized = NtruPrivateKeySpec.marshal(spec);
+ *
+ * // Later or elsewhere, reconstruct the spec
+ * NtruPrivateKeySpec restored = NtruPrivateKeySpec.unmarshal(serialized);
+ * }
+ * + * @since 1.0 + */ +public final class NtruPrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Creates a new specification from PKCS#8-encoded bytes. + * + *

+ * The input array is not retained. A defensive copy is made and stored. + *

+ * + * @param pkcs8Der PKCS#8 DER bytes of the NTRU private key, must not be null + * @throws NullPointerException if {@code pkcs8Der} is null + */ + public NtruPrivateKeySpec(byte[] pkcs8Der) { + this.pkcs8 = Objects.requireNonNull(pkcs8Der, "pkcs8Der").clone(); + } + + /** + * Returns a copy of the PKCS#8-encoded private key. + * + *

+ * The returned array is a fresh clone. Callers may modify it without affecting + * the spec. + *

+ * + * @return a new byte array containing the PKCS#8 DER encoding + */ + public byte[] pkcs8() { + return pkcs8.clone(); + } + + /** + * Serializes this specification into a simple name-value sequence. + * + *

+ * The output contains: + *

    + *
  • {@code "type"} set to {@code "NtruPrivateKey"} for identification, + * and
  • + *
  • {@code "pkcs8.b64"} containing the PKCS#8 bytes encoded with Base64 + * without padding.
  • + *
+ * This format is suitable for lightweight persistence or interchange where a + * compact textual representation is preferred. + * + * @param spec the specification to serialize, must not be null + * @return a {@code PairSeq} carrying the serialized fields + * @throws NullPointerException if {@code spec} is null + */ + public static PairSeq marshal(NtruPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "NtruPrivateKey", PKCS8_B64, b64); + } + + /** + * Deserializes a specification from a name-value sequence produced by + * {@link #marshal(NtruPrivateKeySpec)}. + * + *

+ * Fields not recognized by this implementation are ignored to allow forward + * compatibility. The field {@code "pkcs8.b64"} is required and must contain + * Base64 (no padding) of the PKCS#8 bytes. + *

+ * + * @param p the sequence to read + * @return a new specification carrying the decoded PKCS#8 bytes + * @throws IllegalArgumentException if the required {@code "pkcs8.b64"} field is + * absent + * @throws NullPointerException if {@code p} is null + */ + public static NtruPrivateKeySpec unmarshal(PairSeq p) { + String pkcs8b64 = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (PKCS8_B64.equals(k)) { + pkcs8b64 = v; + } + } + if (pkcs8b64 == null) { + throw new IllegalArgumentException("NtruPrivateKeySpec: missing 'pkcs8.b64'"); + } + return new NtruPrivateKeySpec(Base64.getDecoder().decode(pkcs8b64)); + } + + /** + * Returns a short diagnostic string including the encoded length. + * + * @return a human-readable form such as {@code NtruPrivateKeySpec[len=...]}} + */ + @Override + public String toString() { + return "NtruPrivateKeySpec[len=" + pkcs8.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/NtruPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntru/NtruPublicKeySpec.java new file mode 100644 index 0000000..be16da2 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntru/NtruPublicKeySpec.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * 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.core.alg.ntru; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * NtruPublicKeySpec holds an X.509 SubjectPublicKeyInfo encoded NTRU public key + * and supports a simple marshal/unmarshal form. + * + *

+ * Instances are immutable. The byte array provided to the constructor is + * defensively copied and {@link #x509()} returns a fresh clone on each call. + *

+ * + *

Usage

{@code
+ * // Construct from existing X.509 bytes
+ * byte[] x509Der = Files.readAllBytes(Path.of("ntru-public.der"));
+ * NtruPublicKeySpec spec = new NtruPublicKeySpec(x509Der);
+ *
+ * // Import into JCA (example provider alias shown)
+ * KeyFactory kf = KeyFactory.getInstance("NTRU", "BCPQC");
+ * PublicKey pk = kf.generatePublic(new X509EncodedKeySpec(spec.x509()));
+ *
+ * // Marshal to a compact name-value form (Base64 without padding)
+ * PairSeq serialized = NtruPublicKeySpec.marshal(spec);
+ *
+ * // Later or elsewhere, reconstruct the spec
+ * NtruPublicKeySpec restored = NtruPublicKeySpec.unmarshal(serialized);
+ * }
+ * + * @since 1.0 + */ +public final class NtruPublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Creates a new specification from X.509 SubjectPublicKeyInfo encoded bytes. + * + *

+ * The input array is not retained. A defensive copy is made and stored. + *

+ * + * @param x509Der X.509 DER bytes of the NTRU public key, must not be null + * @throws NullPointerException if {@code x509Der} is null + */ + public NtruPublicKeySpec(byte[] x509Der) { + this.x509 = Objects.requireNonNull(x509Der, "x509Der").clone(); + } + + /** + * Returns a copy of the X.509-encoded public key. + * + *

+ * The returned array is a fresh clone. Callers may modify it without affecting + * the spec. + *

+ * + * @return a new byte array containing the X.509 DER encoding + */ + public byte[] x509() { + return x509.clone(); + } + + /** + * Serializes this specification into a simple name-value sequence. + * + *

+ * The output contains: + *

    + *
  • {@code "type"} set to {@code "NtruPublicKey"} for identification, + * and
  • + *
  • {@code "x509.b64"} containing the X.509 bytes encoded with Base64 without + * padding.
  • + *
+ * This format is suitable for lightweight persistence or interchange where a + * compact textual representation is preferred. + * + * @param spec the specification to serialize, must not be null + * @return a {@code PairSeq} carrying the serialized fields + * @throws NullPointerException if {@code spec} is null + */ + public static PairSeq marshal(NtruPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "NtruPublicKey", X509_B64, b64); + } + + /** + * Deserializes a specification from a name-value sequence produced by + * {@link #marshal(NtruPublicKeySpec)}. + * + *

+ * Fields not recognized by this implementation are ignored to allow forward + * compatibility. The field {@code "x509.b64"} is required and must contain + * Base64 (no padding) of the X.509 bytes. + *

+ * + * @param p the sequence to read + * @return a new specification carrying the decoded X.509 bytes + * @throws IllegalArgumentException if the required {@code "x509.b64"} field is + * absent + * @throws NullPointerException if {@code p} is null + */ + public static NtruPublicKeySpec unmarshal(PairSeq p) { + String x509b64 = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (X509_B64.equals(k)) { + x509b64 = v; + } + } + if (x509b64 == null) { + throw new IllegalArgumentException("NtruPublicKeySpec: missing 'x509.b64'"); + } + return new NtruPublicKeySpec(Base64.getDecoder().decode(x509b64)); + } + + /** + * Returns a short diagnostic string including the encoded length. + * + * @return a human-readable form such as {@code NtruPublicKeySpec[len=...]} + */ + @Override + public String toString() { + return "NtruPublicKeySpec[len=" + x509.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java b/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java new file mode 100644 index 0000000..da7c57c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntru/package-info.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * NTRU post-quantum key encapsulation integration. + * + *

+ * This package wires the NTRU KEM into the core layer. It provides the + * algorithm descriptor, a runtime KEM context, a key generation specification + * selecting parameter sets, and encoded key specifications for import and + * export. Provider specifics are kept behind small factories while roles and + * metadata remain explicit for higher layers. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register NTRU under a canonical identifier and declare ENCAPSULATE and + * DECAPSULATE roles, with an optional message-style agreement adapter.
  • + *
  • Provide a {@link zeroecho.core.context.KemContext} bound to either a + * public key (encapsulation) or a private key (decapsulation).
  • + *
  • Expose immutable specifications for key generation variants and encoded + * key carriers with simple marshalling helpers.
  • + *
  • Validate the presence of a suitable PQC provider before performing JCA + * operations.
  • + *
+ * + *

Components

+ *
    + *
  • NtruAlgorithm: algorithm descriptor that wires KEM and optional + * agreement roles and registers asymmetric key builders for generation and + * encoded-key import.
  • + *
  • NtruKemContext: runtime context that performs encapsulation and + * decapsulation using provider primitives.
  • + *
  • NtruKeyGenSpec: immutable selection of NTRU variants from HPS and + * HRSS families, with convenience factories.
  • + *
  • NtruPublicKeySpec and NtruPrivateKeySpec: wrappers over + * X.509 and PKCS#8 encodings with defensive copying and compact marshalling + * utilities.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and safe to share across + * threads.
  • + *
  • KEM contexts are stateful and not thread-safe; create one per + * operation.
  • + *
  • Encoded key specs preserve the provided encodings without internal + * parsing and clone byte arrays on input and output.
  • + *
  • An agreement adapter allows NTRU KEM to participate in initiator and + * responder exchanges through a message-based interface when needed.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.ntru; diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeAlgorithm.java new file mode 100644 index 0000000..8c8c79f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeAlgorithm.java @@ -0,0 +1,243 @@ +/******************************************************************************* + * 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.core.alg.ntruprime; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.bouncycastle.pqc.jcajce.spec.NTRULPRimeParameterSpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * Configures and exposes the NTRU LPRime post-quantum KEM and a + * message-agreement adapter backed by the Bouncy Castle PQC provider. + * + *

Overview

This algorithm wrapper wires Bouncy Castle's "NTRULPRime" + * implementation into the ZeroEcho capability model. It registers two KEM + * capabilities (encapsulation and decapsulation) and maps them to an + * agreement-style API via a lightweight adapter so that callers can use a + * single {@code MessageAgreementContext} for both initiator and responder + * roles. + * + *

Capabilities

+ *
    + *
  • {@code AlgorithmFamily.KEM / KeyUsage.ENCAPSULATE} - creates a + * {@code KemContext} bound to a recipient public key.
  • + *
  • {@code AlgorithmFamily.KEM / KeyUsage.DECAPSULATE} - creates a + * {@code KemContext} bound to a holder's private key.
  • + *
  • {@code AlgorithmFamily.AGREEMENT / KeyUsage.AGREEMENT} - provides a + * {@code MessageAgreementContext} that delegates to a {@code KemContext} for + * initiator and responder roles.
  • + *
+ * + *

Key material

A default asymmetric key builder is registered for + * {@code NtrulPrimeKeyGenSpec} with {@code ntrulpr1277} as the default variant. + * Additional import builders are provided for X.509-encoded public keys and + * PKCS#8-encoded private keys of the same algorithm. + * + *

Provider requirements

All key operations rely on + * {@code BouncyCastlePQCProvider}. The provider must be installed in the + * current JVM before use, otherwise key generation and import will fail with a + * {@code NoSuchProviderException}. See {@link #ensureProvider()}. + * + *

Example

{@code
+ * // Initialize the algorithm and generate a key pair
+ * NtrulPrimeAlgorithm alg = new NtrulPrimeAlgorithm();
+ * KeyPair kp = alg.keys(NtrulPrimeKeyGenSpec.ntrulpr761()).generateKeyPair(NtrulPrimeKeyGenSpec.ntrulpr761());
+ *
+ * // Initiator (Alice) encapsulates to Bob's public key
+ * MessageAgreementContext alice = alg
+ *     .context(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class,
+ *              kp.getPublic(), VoidSpec.INSTANCE);
+ *
+ * // Responder (Bob) decapsulates with his private key
+ * MessageAgreementContext bob = alg
+ *     .context(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class,
+ *              kp.getPrivate(), VoidSpec.INSTANCE);
+ * }
+ * + * @see KemContext + * @see MessageAgreementContext + * @see NtrulPrimeKeyGenSpec + * @see VoidSpec + */ +public final class NtrulPrimeAlgorithm extends AbstractCryptoAlgorithm { + /** + * Creates a new algorithm wrapper for NTRU LPRime and registers its KEM and + * agreement capabilities, together with key builders for generation and import. + * + *

+ * The constructor: + *

    + *
  • Declares the algorithm names used by the provider.
  • + *
  • Registers KEM encapsulation and decapsulation capabilities.
  • + *
  • Registers agreement capabilities that delegate to a KEM-based + * adapter.
  • + *
  • Registers an asymmetric key builder for {@code NtrulPrimeKeyGenSpec} + * including the default variant.
  • + *
  • Registers import builders for X.509 public keys and PKCS#8 private + * keys.
  • + *
+ */ + public NtrulPrimeAlgorithm() { + super("NTRULPRime", "NTRU LPRime", BouncyCastlePQCProvider.PROVIDER_NAME); + + capability(AlgorithmFamily.KEM, KeyUsage.ENCAPSULATE, KemContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> new NtrulPrimeKemContext(this, k), () -> VoidSpec.INSTANCE); + capability(AlgorithmFamily.KEM, KeyUsage.DECAPSULATE, KemContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> new NtrulPrimeKemContext(this, k), () -> VoidSpec.INSTANCE); + + // AGREEMENT (initiator): Alice has Bob's public key → encapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← return your + // existing KemContext + PublicKey.class, // ← initiator uses recipient's public key + VoidSpec.class, // ← must implement ContextSpec + (PublicKey recipient, VoidSpec spec) -> { + // create a context bound to recipient public key for encapsulation + return KemMessageAgreementAdapter.builder().upon(new NtrulPrimeKemContext(this, recipient)) + .asInitiator().build(); + }, () -> VoidSpec.INSTANCE // default + ); + + // AGREEMENT (responder): Bob has his private key → decapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← same KemContext + // type + PrivateKey.class, // ← responder uses their private key + VoidSpec.class, (PrivateKey myPriv, VoidSpec spec) -> { + return KemMessageAgreementAdapter.builder().upon(new NtrulPrimeKemContext(this, myPriv)) + .asResponder().build(); + }, () -> VoidSpec.INSTANCE); + + registerAsymmetricKeyBuilder(NtrulPrimeKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(NtrulPrimeKeyGenSpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("NTRULPRime", providerName()); + NTRULPRimeParameterSpec params = switch (spec.variant()) { + case NTRULPR653 -> NTRULPRimeParameterSpec.ntrulpr653; + case NTRULPR761 -> NTRULPRimeParameterSpec.ntrulpr761; + case NTRULPR857 -> NTRULPRimeParameterSpec.ntrulpr857; + case NTRULPR953 -> NTRULPRimeParameterSpec.ntrulpr953; + case NTRULPR1013 -> NTRULPRimeParameterSpec.ntrulpr1013; + case NTRULPR1277 -> NTRULPRimeParameterSpec.ntrulpr1277; + }; + kpg.initialize(params, new SecureRandom()); + return kpg.generateKeyPair(); + } + + @Override + public PublicKey importPublic(NtrulPrimeKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(NtrulPrimeKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + }, NtrulPrimeKeyGenSpec::ntrulpr1277); + + registerAsymmetricKeyBuilder(NtrulPrimePublicKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(NtrulPrimePublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(NtrulPrimePublicKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("NTRULPRime", providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); + } + + @Override + public PrivateKey importPrivate(NtrulPrimePublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + }, null); + + registerAsymmetricKeyBuilder(NtrulPrimePrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(NtrulPrimePrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(NtrulPrimePrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(NtrulPrimePrivateKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("NTRULPRime", providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + } + }, null); + } + + /** + * Ensures that the Bouncy Castle PQC provider is present in the current + * {@code Security} providers list. + * + * @throws NoSuchProviderException if + * {@code BouncyCastlePQCProvider.PROVIDER_NAME} + * is not registered + */ + private static void ensureProvider() throws NoSuchProviderException { + Provider p = Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME); + if (p == null) { + throw new NoSuchProviderException("BCPQC provider not registered"); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKemContext.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKemContext.java new file mode 100644 index 0000000..5d3052b --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKemContext.java @@ -0,0 +1,219 @@ +/******************************************************************************* + * 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.core.alg.ntruprime; + +import java.io.IOException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Objects; + +import javax.security.auth.DestroyFailedException; + +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.pqc.crypto.ntruprime.NTRULPRimeKEMExtractor; +import org.bouncycastle.pqc.crypto.ntruprime.NTRULPRimeKEMGenerator; +import org.bouncycastle.pqc.crypto.ntruprime.NTRULPRimePrivateKeyParameters; +import org.bouncycastle.pqc.crypto.ntruprime.NTRULPRimePublicKeyParameters; +import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory; +import org.bouncycastle.pqc.crypto.util.PublicKeyFactory; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.KemContext; + +/** + * KEM context for NTRU LPRime that performs encapsulation and decapsulation + * using the underlying key material. + * + *

Overview

Instances of this context are initialized either for + * encapsulation with a recipient {@code PublicKey} or for decapsulation with a + * holder {@code PrivateKey}. The role is fixed at construction time. On + * encapsulation, the context generates a fresh shared secret and its + * corresponding ciphertext. On decapsulation, it derives the shared secret from + * a provided ciphertext. + * + *

Usage

{@code
+ * // Encapsulation side (initiator)
+ * KemContext enc = new NtrulPrimeKemContext(algorithm, recipientPublicKey);
+ * KemResult kem = enc.encapsulate();
+ * byte[] ct = kem.ciphertext();
+ * byte[] secret = kem.secret();
+ *
+ * // Decapsulation side (responder)
+ * KemContext dec = new NtrulPrimeKemContext(algorithm, myPrivateKey);
+ * byte[] secret2 = dec.decapsulate(ct);
+ * }
+ */ +public final class NtrulPrimeKemContext implements KemContext { + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean encapsulate; + + /** + * Creates an encapsulation context bound to a recipient public key. + * + * @param algorithm the owning algorithm that defines provider and naming + * conventions; must not be null + * @param k the recipient public key used to encapsulate to; must not be + * null + * @throws NullPointerException if {@code algorithm} or {@code k} is null + */ + public NtrulPrimeKemContext(CryptoAlgorithm algorithm, PublicKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = true; + } + + /** + * Creates a decapsulation context bound to a holder private key. + * + * @param algorithm the owning algorithm that defines provider and naming + * conventions; must not be null + * @param k the private key used to decapsulate ciphertexts; must not be + * null + * @throws NullPointerException if {@code algorithm} or {@code k} is null + */ + public NtrulPrimeKemContext(CryptoAlgorithm algorithm, PrivateKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = false; + } + + /** + * Returns the algorithm that owns this context. + * + * @return the associated algorithm + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + *

+ * For encapsulation this is a {@code PublicKey}. For decapsulation this is a + * {@code PrivateKey}. + *

+ * + * @return the bound key + */ + @Override + public Key key() { + return key; + } + + /** + * Closes this context and releases any resources. + * + *

+ * This implementation has no external resources to release and is a no-op. + *

+ */ + @Override + public void close() { + // empty + } + + /** + * Generates a fresh shared secret and its ciphertext using the recipient public + * key. + * + *

+ * This method may only be called on contexts constructed for encapsulation. If + * the context was constructed with a private key, an + * {@code IllegalStateException} is thrown. + *

+ * + * @return a KEM result containing the ciphertext and the derived shared secret + * @throws IllegalStateException if this context is not initialized for + * encapsulation + * @throws IOException if the underlying provider fails to perform + * encapsulation + */ + @Override + public KemResult encapsulate() throws IOException { + if (!encapsulate) { + throw new IllegalStateException("Not initialized for ENCAPSULATE"); + } + try { + final NTRULPRimePublicKeyParameters keyParam = (NTRULPRimePublicKeyParameters) PublicKeyFactory + .createKey(key.getEncoded()); + NTRULPRimeKEMGenerator gen = new NTRULPRimeKEMGenerator(new SecureRandom()); + SecretWithEncapsulation res = gen.generateEncapsulated(keyParam); + byte[] secret = res.getSecret(); + byte[] ct = res.getEncapsulation(); + res.destroy(); + return new KemResult(ct, secret); + } catch (DestroyFailedException e) { + throw new IOException("NTRULPRime encapsulate failed", e); + } + } + + /** + * Derives the shared secret from the provided ciphertext using the holder + * private key. + * + *

+ * This method may only be called on contexts constructed for decapsulation. If + * the context was constructed with a public key, an + * {@code IllegalStateException} is thrown. + *

+ * + * @param ciphertext the KEM ciphertext produced by encapsulation + * @return the derived shared secret bytes + * @throws NullPointerException if {@code ciphertext} is null + * @throws IllegalStateException if this context is not initialized for + * decapsulation + * @throws IOException if the underlying provider fails to perform + * decapsulation + */ + @Override + public byte[] decapsulate(byte[] ciphertext) throws IOException { + if (encapsulate) { + throw new IllegalStateException("Not initialized for DECAPSULATE"); + } + try { + final NTRULPRimePrivateKeyParameters keyParam = (NTRULPRimePrivateKeyParameters) PrivateKeyFactory + .createKey(key.getEncoded()); + NTRULPRimeKEMExtractor ex = new NTRULPRimeKEMExtractor(keyParam); + return ex.extractSecret(ciphertext); + } catch (Exception e) { // NOPMD + throw new IOException("NTRULPRime decapsulate failed", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java new file mode 100644 index 0000000..11fe4fe --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimeKeyGenSpec.java @@ -0,0 +1,228 @@ +/******************************************************************************* + * 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.core.alg.ntruprime; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Algorithm-specific key generation parameters for NTRU LPRime. + * + *

+ * {@code NtrulPrimeKeyGenSpec} selects one of the standardized parameter + * variants of the NTRU LPRime key encapsulation mechanism. Each variant + * determines the security level, key sizes, and performance characteristics of + * the generated key pair. + *

+ * + *

Variants

+ *
    + *
  • {@link Variant#NTRULPR653}: ~128-bit classical security.
  • + *
  • {@link Variant#NTRULPR761}: ~192-bit security.
  • + *
  • {@link Variant#NTRULPR857}: ~256-bit security.
  • + *
  • {@link Variant#NTRULPR953}, {@link Variant#NTRULPR1013}, + * {@link Variant#NTRULPR1277}: higher strength variants with progressively + * larger keys and ciphertexts.
  • + *
+ * + *

+ * Instances are immutable and typically passed to + * {@link zeroecho.core.CryptoAlgorithm#generateKeyPair} or retrieved from a + * {@code CryptoAlgorithm} builder. For convenience, static factory methods are + * provided for each variant. + *

+ * + *

Example

{@code
+ * CryptoAlgorithm alg = CryptoAlgorithms.require("NTRULPRime");
+ * KeyPair kp = alg.generateKeyPair(NtrulPrimeKeyGenSpec.ntrulpr761());
+ * }
+ * + * @since 1.0 + */ +public final class NtrulPrimeKeyGenSpec implements AlgorithmKeySpec, Describable { + /** + * Enumeration of supported NTRU LPRime parameter sets. + */ + public enum Variant { + /** Variant with ~128-bit classical security. */ + NTRULPR653, + /** Variant with ~192-bit classical security. */ + NTRULPR761, + /** Variant with ~256-bit classical security. */ + NTRULPR857, + /** Variant beyond 256-bit, with larger parameters. */ + NTRULPR953, + /** Variant beyond 256-bit, with larger parameters. */ + NTRULPR1013, + /** Highest-strength standardized variant. */ + NTRULPR1277 + } + + private final Variant variant; + + private NtrulPrimeKeyGenSpec(Variant v) { + this.variant = v; + } + + /** + * Creates a specification from the given variant. + * + * @param v selected NTRU LPRime parameter set + * @return new specification bound to {@code v} + * @throws NullPointerException if {@code v} is {@code null} + */ + public static NtrulPrimeKeyGenSpec of(Variant v) { + return new NtrulPrimeKeyGenSpec(v); + } + + /** + * Creates a key generation specification for the {@link Variant#NTRULPR653} + * parameter set. + * + *

+ * This variant offers around 128-bit classical security with relatively small + * key and ciphertext sizes, making it suitable for constrained environments. + *

+ * + * @return a new specification targeting {@code NTRULPR653} + */ + public static NtrulPrimeKeyGenSpec ntrulpr653() { + return new NtrulPrimeKeyGenSpec(Variant.NTRULPR653); + } + + /** + * Creates a key generation specification for the {@link Variant#NTRULPR761} + * parameter set. + * + *

+ * This variant balances efficiency and strength, offering roughly 192-bit + * security. It is a common choice for post-quantum readiness in practical + * deployments. + *

+ * + * @return a new specification targeting {@code NTRULPR761} + */ + public static NtrulPrimeKeyGenSpec ntrulpr761() { + return new NtrulPrimeKeyGenSpec(Variant.NTRULPR761); + } + + /** + * Creates a key generation specification for the {@link Variant#NTRULPR857} + * parameter set. + * + *

+ * This variant aims for ~256-bit classical security with larger keys and + * ciphertexts compared to the 653 and 761 sets, offering stronger margins. + *

+ * + * @return a new specification targeting {@code NTRULPR857} + */ + public static NtrulPrimeKeyGenSpec ntrulpr857() { + return new NtrulPrimeKeyGenSpec(Variant.NTRULPR857); + } + + /** + * Creates a key generation specification for the {@link Variant#NTRULPR953} + * parameter set. + * + *

+ * Provides very high security beyond 256-bit classical strength, with larger + * key material and ciphertexts. Suitable for highly conservative use cases. + *

+ * + * @return a new specification targeting {@code NTRULPR953} + */ + public static NtrulPrimeKeyGenSpec ntrulpr953() { + return new NtrulPrimeKeyGenSpec(Variant.NTRULPR953); + } + + /** + * Creates a key generation specification for the {@link Variant#NTRULPR1013} + * parameter set. + * + *

+ * A higher-strength variant designed for long-term security horizons, offering + * protection well above 256-bit security levels at the cost of larger keys. + *

+ * + * @return a new specification targeting {@code NTRULPR1013} + */ + public static NtrulPrimeKeyGenSpec ntrulpr1013() { + { + return new NtrulPrimeKeyGenSpec(Variant.NTRULPR1013); + } + } + + /** + * Creates a key generation specification for the {@link Variant#NTRULPR1277} + * parameter set. + * + *

+ * The largest standardized parameter set, offering the highest security margin + * and designed for long-term quantum resistance in highly sensitive + * applications. + *

+ * + * @return a new specification targeting {@code NTRULPR1277} + */ + public static NtrulPrimeKeyGenSpec ntrulpr1277() { + { + return new NtrulPrimeKeyGenSpec(Variant.NTRULPR1277); + } + } + + /** + * Returns the selected variant for this specification. + * + * @return variant of NTRU LPRime key parameters + */ + public Variant variant() { + return variant; + } + + /** + * Returns a textual description of this specification. + * + *

+ * The description is the variant name (e.g., {@code "NTRULPR761"}). + *

+ * + * @return variant name string + */ + @Override + public String description() { + return variant.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java new file mode 100644 index 0000000..77cbe0c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePrivateKeySpec.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * 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.core.alg.ntruprime; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Specification wrapper for an encoded NTRU LPRime private key in PKCS#8 + * format. + * + *

+ * {@code NtrulPrimePrivateKeySpec} provides a type-safe holder for + * PKCS#8-encoded private key material belonging to the NTRU LPRime KEM. It is + * immutable and defensive copies are made on construction and retrieval. + *

+ * + *

Usage

Instances are typically created after parsing or receiving + * encoded key data, and passed to an algorithm’s key builder to import a usable + * {@link java.security.PrivateKey}. + * + *
{@code
+ * // Wrap existing encoded key
+ * byte[] pkcs8 = Files.readAllBytes(Paths.get("ntru-private.pk8"));
+ * NtrulPrimePrivateKeySpec spec = new NtrulPrimePrivateKeySpec(pkcs8);
+ *
+ * // Import via CryptoAlgorithms
+ * PrivateKey priv = CryptoAlgorithms.privateKey("NTRULPRime", spec);
+ * }
+ * + *

Serialization

A lightweight marshaling format is supported via + * {@link PairSeq}: + *
    + *
  • {@link #marshal(NtrulPrimePrivateKeySpec)} produces a base64 string under + * the key {@code "pkcs8.b64"}.
  • + *
  • {@link #unmarshal(PairSeq)} reconstructs a new spec instance from such a + * sequence.
  • + *
+ * + * @since 1.0 + */ +public final class NtrulPrimePrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Constructs a new private key specification from a PKCS#8-encoded byte array. + * + *

+ * A defensive copy is made; the input array may be reused or modified without + * affecting this instance. + *

+ * + * @param pkcs8Der PKCS#8 encoded private key bytes (DER form) + * @throws NullPointerException if {@code pkcs8Der} is null + */ + public NtrulPrimePrivateKeySpec(byte[] pkcs8Der) { + this.pkcs8 = Objects.requireNonNull(pkcs8Der).clone(); + } + + /** + * Returns a clone of the PKCS#8-encoded private key bytes. + * + * @return copy of the encoded private key + */ + public byte[] pkcs8() { + return pkcs8.clone(); + } + + /** + * Serializes this specification into a {@link PairSeq}. + * + *

+ * The result contains: + *

    + *
  • {@code type = "NtrulPrimePrivateKeySpec"}
  • + *
  • {@code pkcs8.b64 = ...} base64-encoded key
  • + *
+ * + * @param spec specification to marshal + * @return serialized representation containing the base64-encoded key + */ + public static PairSeq marshal(NtrulPrimePrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "NtrulPrimePrivateKeySpec", PKCS8_B64, b64); + } + + /** + * Reconstructs a private key specification from a {@link PairSeq}. + * + *

+ * The sequence must contain a {@code pkcs8.b64} entry; otherwise an + * {@link IllegalArgumentException} is thrown. + *

+ * + * @param p serialized pair sequence produced by + * {@link #marshal(NtrulPrimePrivateKeySpec)} + * @return a new specification with the decoded PKCS#8 bytes + * @throws IllegalArgumentException if the required field is missing + */ + public static NtrulPrimePrivateKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (PKCS8_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("NtrulPrimePrivateKeySpec: missing pkcs8.b64"); + } + return new NtrulPrimePrivateKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string identifying this specification. + * + *

+ * The string includes the encoded key length but never the key material. + *

+ * + * @return a string such as {@code "NtrulPrimePrivateKeySpec[len=1234]"} + */ + @Override + public String toString() { + return "NtrulPrimePrivateKeySpec[len=" + pkcs8.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePublicKeySpec.java new file mode 100644 index 0000000..a1e9dfd --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/NtrulPrimePublicKeySpec.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * 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.core.alg.ntruprime; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Specification wrapper for an encoded NTRU LPRime public key in X.509 format. + * + *

+ * {@code NtrulPrimePublicKeySpec} provides a type-safe holder for X.509 + * SubjectPublicKeyInfo bytes belonging to the NTRU LPRime KEM. It is immutable + * and defensive copies are made on construction and retrieval. + *

+ * + *

Usage

Instances are typically created after parsing or receiving + * encoded key data, and passed to an algorithm’s key builder to import a usable + * {@link java.security.PublicKey}. + * + *
{@code
+ * // Wrap existing encoded public key
+ * byte[] der = Files.readAllBytes(Paths.get("ntru-public.spki"));
+ * NtrulPrimePublicKeySpec spec = new NtrulPrimePublicKeySpec(der);
+ *
+ * // Import via CryptoAlgorithms
+ * PublicKey pub = CryptoAlgorithms.publicKey("NTRULPRime", spec);
+ * }
+ * + *

Serialization

A lightweight marshaling format is supported via + * {@link PairSeq}: + *
    + *
  • {@link #marshal(NtrulPrimePublicKeySpec)} produces a base64 string under + * the key {@code "x509.b64"}.
  • + *
  • {@link #unmarshal(PairSeq)} reconstructs a new spec instance from such a + * sequence.
  • + *
+ * + * @since 1.0 + */ +public final class NtrulPrimePublicKeySpec implements AlgorithmKeySpec { + + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Constructs a new public key specification from an X.509-encoded byte array. + * + *

+ * A defensive copy is made; the input array may be reused or modified without + * affecting this instance. + *

+ * + * @param x509Der X.509 SubjectPublicKeyInfo bytes + * @throws NullPointerException if {@code x509Der} is null + */ + public NtrulPrimePublicKeySpec(byte[] x509Der) { + this.x509 = Objects.requireNonNull(x509Der).clone(); + } + + /** + * Returns a clone of the X.509-encoded public key bytes. + * + * @return copy of the encoded public key + */ + public byte[] x509() { + return x509.clone(); + } + + /** + * Serializes this specification into a {@link PairSeq}. + * + *

+ * The result contains: + *

    + *
  • {@code type = "NtrulPrimePublicKeySpec"}
  • + *
  • {@code x509.b64 = ...} base64-encoded key
  • + *
+ * + * @param spec specification to marshal + * @return serialized representation containing the base64-encoded key + */ + public static PairSeq marshal(NtrulPrimePublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "NtrulPrimePublicKeySpec", X509_B64, b64); + } + + /** + * Reconstructs a public key specification from a {@link PairSeq}. + * + *

+ * The sequence must contain an {@code x509.b64} entry; otherwise an + * {@link IllegalArgumentException} is thrown. + *

+ * + * @param p serialized pair sequence produced by + * {@link #marshal(NtrulPrimePublicKeySpec)} + * @return a new specification with the decoded X.509 bytes + * @throws IllegalArgumentException if the required field is missing + */ + public static NtrulPrimePublicKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (X509_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("NtrulPrimePublicKeySpec: missing x509.b64"); + } + return new NtrulPrimePublicKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string identifying this specification. + * + *

+ * The string includes the encoded key length but never the key material. + *

+ * + * @return a string such as {@code "NtrulPrimePublicKeySpec[len=1234]"} + */ + @Override + public String toString() { + return "NtrulPrimePublicKeySpec[len=" + x509.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeAlgorithm.java new file mode 100644 index 0000000..72c7a73 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeAlgorithm.java @@ -0,0 +1,245 @@ +/******************************************************************************* + * 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.core.alg.ntruprime; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.bouncycastle.pqc.jcajce.spec.SNTRUPrimeParameterSpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * Configures and exposes the SNTRU Prime post-quantum KEM and a + * message-agreement adapter backed by the Bouncy Castle PQC provider. + * + *

Overview

This algorithm wrapper integrates Bouncy Castle's + * {@code "SNTRUPrime"} implementation into the ZeroEcho capability model. It + * registers two KEM capabilities (encapsulation and decapsulation) and maps + * them to an agreement-style API via a lightweight adapter so that callers can + * use a single {@code MessageAgreementContext} for both initiator and responder + * roles. + * + *

Capabilities

+ *
    + *
  • {@code AlgorithmFamily.KEM / KeyUsage.ENCAPSULATE} - creates a + * {@code KemContext} bound to a recipient public key.
  • + *
  • {@code AlgorithmFamily.KEM / KeyUsage.DECAPSULATE} - creates a + * {@code KemContext} bound to a holder's private key.
  • + *
  • {@code AlgorithmFamily.AGREEMENT / KeyUsage.AGREEMENT} - provides a + * {@code MessageAgreementContext} that delegates to a {@code KemContext} for + * initiator and responder roles.
  • + *
+ * + *

Key material

A default asymmetric key builder is registered for + * {@code SntruPrimeKeyGenSpec} with {@code sntrup1277} as the default variant. + * Additional import builders are provided for X.509-encoded public keys and + * PKCS#8-encoded private keys. + * + *

Provider requirements

All key operations rely on + * {@code BouncyCastlePQCProvider}. The provider must be installed in the + * current JVM before use, otherwise key generation and import will fail with a + * {@code NoSuchProviderException}. See {@link #ensureProvider()}. + * + *

Example

{@code
+ * // Initialize the algorithm and generate a key pair
+ * SntruPrimeAlgorithm alg = new SntruPrimeAlgorithm();
+ * KeyPair kp = alg.keys(SntruPrimeKeyGenSpec.sntrup761())
+ *                 .generateKeyPair(SntruPrimeKeyGenSpec.sntrup761());
+ *
+ * // Initiator (Alice) encapsulates to Bob's public key
+ * MessageAgreementContext alice = alg
+ *     .context(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class,
+ *              kp.getPublic(), VoidSpec.INSTANCE);
+ *
+ * // Responder (Bob) decapsulates with his private key
+ * MessageAgreementContext bob = alg
+ *     .context(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class,
+ *              kp.getPrivate(), VoidSpec.INSTANCE);
+ * }
+ * + * @see KemContext + * @see MessageAgreementContext + * @see SntruPrimeKeyGenSpec + * @see VoidSpec + */ +public final class SntruPrimeAlgorithm extends AbstractCryptoAlgorithm { + /** + * Creates a new algorithm wrapper for SNTRU Prime and registers its KEM and + * agreement capabilities, together with key builders for generation and import. + * + *

+ * The constructor: + *

    + *
  • Declares the algorithm names used by the provider.
  • + *
  • Registers KEM encapsulation and decapsulation capabilities.
  • + *
  • Registers agreement capabilities that delegate to a KEM-based + * adapter.
  • + *
  • Registers an asymmetric key builder for {@code SntruPrimeKeyGenSpec} + * including the default {@code sntrup1277} variant.
  • + *
  • Registers import builders for X.509 public keys and PKCS#8 private + * keys.
  • + *
+ */ + public SntruPrimeAlgorithm() { + super("SNTRUPrime", "SNTRU Prime", BouncyCastlePQCProvider.PROVIDER_NAME); + + capability(AlgorithmFamily.KEM, KeyUsage.ENCAPSULATE, KemContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> new SntruPrimeKemContext(this, k), () -> VoidSpec.INSTANCE); + capability(AlgorithmFamily.KEM, KeyUsage.DECAPSULATE, KemContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> new SntruPrimeKemContext(this, k), () -> VoidSpec.INSTANCE); + + // AGREEMENT (initiator): Alice has Bob's public key → encapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← return your + // existing KemContext + PublicKey.class, // ← initiator uses recipient's public key + VoidSpec.class, // ← must implement ContextSpec + (PublicKey recipient, VoidSpec spec) -> { + // create a context bound to recipient public key for encapsulation + return KemMessageAgreementAdapter.builder().upon(new SntruPrimeKemContext(this, recipient)) + .asInitiator().build(); + }, () -> VoidSpec.INSTANCE // default + ); + + // AGREEMENT (responder): Bob has his private key → decapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← same KemContext + // type + PrivateKey.class, // ← responder uses their private key + VoidSpec.class, (PrivateKey myPriv, VoidSpec spec) -> { + return KemMessageAgreementAdapter.builder().upon(new SntruPrimeKemContext(this, myPriv)) + .asResponder().build(); + }, () -> VoidSpec.INSTANCE); + + registerAsymmetricKeyBuilder(SntruPrimeKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(SntruPrimeKeyGenSpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("SNTRUPrime", providerName()); + + SNTRUPrimeParameterSpec params = switch (spec.variant()) { + case SNTRUP653 -> SNTRUPrimeParameterSpec.sntrup653; + case SNTRUP761 -> SNTRUPrimeParameterSpec.sntrup761; + case SNTRUP857 -> SNTRUPrimeParameterSpec.sntrup857; + case SNTRUP953 -> SNTRUPrimeParameterSpec.sntrup953; + case SNTRUP1013 -> SNTRUPrimeParameterSpec.sntrup1013; + case SNTRUP1277 -> SNTRUPrimeParameterSpec.sntrup1277; + }; + kpg.initialize(params, new SecureRandom()); + return kpg.generateKeyPair(); + } + + @Override + public PublicKey importPublic(SntruPrimeKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(SntruPrimeKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + }, SntruPrimeKeyGenSpec::sntrup1277); + + registerAsymmetricKeyBuilder(SntruPrimePublicKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(SntruPrimePublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(SntruPrimePublicKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("SNTRUPrime", providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); + } + + @Override + public PrivateKey importPrivate(SntruPrimePublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + }, null); + + registerAsymmetricKeyBuilder(SntruPrimePrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(SntruPrimePrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(SntruPrimePrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(SntruPrimePrivateKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("SNTRUPrime", providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + } + }, null); + } + + /** + * Ensures that the Bouncy Castle PQC provider is present in the current + * {@code Security} providers list. + * + * @throws NoSuchProviderException if + * {@code BouncyCastlePQCProvider.PROVIDER_NAME} + * is not registered + */ + private static void ensureProvider() throws NoSuchProviderException { + Provider p = Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME); + if (p == null) { + throw new NoSuchProviderException("BCPQC provider not registered"); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKemContext.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKemContext.java new file mode 100644 index 0000000..b490062 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKemContext.java @@ -0,0 +1,225 @@ +/******************************************************************************* + * 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.core.alg.ntruprime; + +import java.io.IOException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Objects; + +import javax.security.auth.DestroyFailedException; + +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimeKEMExtractor; +import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimeKEMGenerator; +import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimePrivateKeyParameters; +import org.bouncycastle.pqc.crypto.ntruprime.SNTRUPrimePublicKeyParameters; +import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory; +import org.bouncycastle.pqc.crypto.util.PublicKeyFactory; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.KemContext; + +/** + * KEM context for SNTRU Prime that performs encapsulation and decapsulation + * using the underlying key material. + * + *

Overview

Instances of this context are initialized either for + * encapsulation with a recipient {@link java.security.PublicKey} or for + * decapsulation with a holder {@link java.security.PrivateKey}. The role is + * fixed at construction time. + *
    + *
  • Encapsulation: generates a fresh shared secret and its corresponding + * ciphertext.
  • + *
  • Decapsulation: derives the shared secret from a provided ciphertext.
  • + *
+ * + *

Usage

{@code
+ * // Encapsulation side (initiator)
+ * KemContext enc = new SntruPrimeKemContext(algorithm, recipientPublicKey);
+ * KemResult kem = enc.encapsulate();
+ * byte[] ct = kem.ciphertext();
+ * byte[] secret = kem.sharedSecret();
+ *
+ * // Decapsulation side (responder)
+ * KemContext dec = new SntruPrimeKemContext(algorithm, myPrivateKey);
+ * byte[] secret2 = dec.decapsulate(ct);
+ * }
+ * + * @see KemContext + * @see SntruPrimeAlgorithm + */ +public final class SntruPrimeKemContext implements KemContext { + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean encapsulate; + + /** + * Creates an encapsulation context bound to a recipient public key. + * + * @param algorithm the owning algorithm that defines provider and naming + * conventions; must not be null + * @param k the recipient public key used to encapsulate to; must not be + * null + * @throws NullPointerException if {@code algorithm} or {@code k} is null + */ + public SntruPrimeKemContext(CryptoAlgorithm algorithm, PublicKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = true; + } + + /** + * Creates a decapsulation context bound to a holder private key. + * + * @param algorithm the owning algorithm that defines provider and naming + * conventions; must not be null + * @param k the private key used to decapsulate ciphertexts; must not be + * null + * @throws NullPointerException if {@code algorithm} or {@code k} is null + */ + public SntruPrimeKemContext(CryptoAlgorithm algorithm, PrivateKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = false; + } + + /** + * Returns the algorithm that owns this context. + * + * @return the associated algorithm + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + *

+ * For encapsulation this is a {@link java.security.PublicKey}. For + * decapsulation this is a {@link java.security.PrivateKey}. + *

+ * + * @return the bound key + */ + @Override + public Key key() { + return key; + } + + /** + * Closes this context and releases any resources. + * + *

+ * This implementation has no external resources to release and is a no-op. + *

+ */ + @Override + public void close() { + // empty + } + + /** + * Generates a fresh shared secret and its ciphertext using the recipient public + * key. + * + *

+ * This method may only be called on contexts constructed for encapsulation. If + * the context was constructed with a private key, an + * {@code IllegalStateException} is thrown. + *

+ * + * @return a KEM result containing the ciphertext and the derived shared secret + * @throws IllegalStateException if this context is not initialized for + * encapsulation + * @throws IOException if the underlying provider fails to perform + * encapsulation + */ + @Override + public KemResult encapsulate() throws IOException { + if (!encapsulate) { + throw new IllegalStateException("Not initialized for ENCAPSULATE"); + } + try { + final SNTRUPrimePublicKeyParameters keyParam = (SNTRUPrimePublicKeyParameters) PublicKeyFactory + .createKey(key.getEncoded()); + SNTRUPrimeKEMGenerator gen = new SNTRUPrimeKEMGenerator(new SecureRandom()); + SecretWithEncapsulation res = gen.generateEncapsulated(keyParam); + byte[] secret = res.getSecret(); + byte[] ct = res.getEncapsulation(); + res.destroy(); + return new KemResult(ct, secret); + } catch (DestroyFailedException e) { + throw new IOException("SNTRUPrime encapsulate failed", e); + } + } + + /** + * Derives the shared secret from the provided ciphertext using the holder + * private key. + * + *

+ * This method may only be called on contexts constructed for decapsulation. If + * the context was constructed with a public key, an + * {@code IllegalStateException} is thrown. + *

+ * + * @param ciphertext the KEM ciphertext produced by encapsulation + * @return the derived shared secret bytes + * @throws NullPointerException if {@code ciphertext} is null + * @throws IllegalStateException if this context is not initialized for + * decapsulation + * @throws IOException if the underlying provider fails to perform + * decapsulation + */ + @Override + public byte[] decapsulate(byte[] ciphertext) throws IOException { + if (encapsulate) { + throw new IllegalStateException("Not initialized for DECAPSULATE"); + } + try { + final SNTRUPrimePrivateKeyParameters keyParam = (SNTRUPrimePrivateKeyParameters) PrivateKeyFactory + .createKey(key.getEncoded()); + SNTRUPrimeKEMExtractor ex = new SNTRUPrimeKEMExtractor(keyParam); + return ex.extractSecret(ciphertext); + } catch (Exception e) { // NOPMD + throw new IOException("SNTRUPrime decapsulate failed", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java new file mode 100644 index 0000000..ffe47f8 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimeKeyGenSpec.java @@ -0,0 +1,196 @@ +/******************************************************************************* + * 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.core.alg.ntruprime; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Algorithm-specific key generation parameters for SNTRU Prime. + * + *

+ * {@code SntruPrimeKeyGenSpec} selects one of the standardized parameter + * variants of the SNTRU Prime key encapsulation mechanism. Each variant + * determines the security level, key sizes, and performance characteristics of + * the generated key pair. + *

+ * + *

Variants

+ *
    + *
  • {@link Variant#SNTRUP653}: ~128-bit classical security.
  • + *
  • {@link Variant#SNTRUP761}: ~192-bit security.
  • + *
  • {@link Variant#SNTRUP857}: ~256-bit security.
  • + *
  • {@link Variant#SNTRUP953}, {@link Variant#SNTRUP1013}, + * {@link Variant#SNTRUP1277}: larger sets providing higher security margins + * with larger keys and ciphertexts.
  • + *
+ * + *

+ * Instances are immutable and typically passed to + * {@link zeroecho.core.CryptoAlgorithm#generateKeyPair} or retrieved from a + * {@code CryptoAlgorithm} builder. For convenience, static factory methods are + * provided for each variant. + *

+ * + *

Example

{@code
+ * CryptoAlgorithm alg = CryptoAlgorithms.require("SNTRUPrime");
+ * KeyPair kp = alg.generateKeyPair(SntruPrimeKeyGenSpec.sntrup761());
+ * }
+ * + * @since 1.0 + */ +public final class SntruPrimeKeyGenSpec implements AlgorithmKeySpec, Describable { + /** + * Enumeration of supported SNTRU Prime parameter sets. + */ + public enum Variant { + /** Variant with ~128-bit classical security. */ + SNTRUP653, + /** Variant with ~192-bit classical security. */ + SNTRUP761, + /** Variant with ~256-bit classical security. */ + SNTRUP857, + /** Variant beyond 256-bit with larger parameters. */ + SNTRUP953, + /** Variant beyond 256-bit with even larger parameters. */ + SNTRUP1013, + /** Largest standardized variant, with very high security margins. */ + SNTRUP1277 + } + + private final Variant variant; + + private SntruPrimeKeyGenSpec(Variant v) { + this.variant = v; + } + + /** + * Creates a specification from the given variant. + * + * @param v selected SNTRU Prime parameter set + * @return new specification bound to {@code v} + * @throws NullPointerException if {@code v} is {@code null} + */ + public static SntruPrimeKeyGenSpec of(Variant v) { + return new SntruPrimeKeyGenSpec(v); + } + + /** + * Creates a key generation specification for the {@link Variant#SNTRUP653} + * parameter set. + * + * @return a new specification targeting {@code SNTRUP653} + */ + public static SntruPrimeKeyGenSpec sntrup653() { + return new SntruPrimeKeyGenSpec(Variant.SNTRUP653); + } + + /** + * Creates a key generation specification for the {@link Variant#SNTRUP761} + * parameter set. + * + * @return a new specification targeting {@code SNTRUP761} + */ + public static SntruPrimeKeyGenSpec sntrup761() { + return new SntruPrimeKeyGenSpec(Variant.SNTRUP761); + } + + /** + * Creates a key generation specification for the {@link Variant#SNTRUP857} + * parameter set. + * + * @return a new specification targeting {@code SNTRUP857} + */ + public static SntruPrimeKeyGenSpec sntrup857() { + return new SntruPrimeKeyGenSpec(Variant.SNTRUP857); + } + + /** + * Creates a key generation specification for the {@link Variant#SNTRUP953} + * parameter set. + * + * @return a new specification targeting {@code SNTRUP953} + */ + public static SntruPrimeKeyGenSpec sntrup953() { + return new SntruPrimeKeyGenSpec(Variant.SNTRUP953); + } + + /** + * Creates a key generation specification for the {@link Variant#SNTRUP1013} + * parameter set. + * + * @return a new specification targeting {@code SNTRUP1013} + */ + public static SntruPrimeKeyGenSpec sntrup1013() { + { + return new SntruPrimeKeyGenSpec(Variant.SNTRUP1013); + } + } + + /** + * Creates a key generation specification for the {@link Variant#SNTRUP1277} + * parameter set. + * + * @return a new specification targeting {@code SNTRUP1277} + */ + public static SntruPrimeKeyGenSpec sntrup1277() { + { + return new SntruPrimeKeyGenSpec(Variant.SNTRUP1277); + } + } + + /** + * Returns the selected variant for this specification. + * + * @return variant of SNTRU Prime key parameters + */ + public Variant variant() { + return variant; + } + + /** + * Returns a textual description of this specification. + * + *

+ * The description is the variant name (e.g., {@code "SNTRUP761"}). + *

+ * + * @return variant name string + */ + @Override + public String description() { + return variant.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java new file mode 100644 index 0000000..6b10ba4 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePrivateKeySpec.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * 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.core.alg.ntruprime; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Specification wrapper for an encoded SNTRU Prime private key in PKCS#8 + * format. + * + *

+ * {@code SntruPrimePrivateKeySpec} provides a type-safe holder for + * PKCS#8-encoded private key material belonging to the SNTRU Prime KEM. It is + * immutable and defensive copies are made on construction and retrieval. + *

+ * + *

Usage

Instances are typically created after parsing or receiving + * encoded key data, and passed to an algorithm’s key builder to import a usable + * {@link java.security.PrivateKey}. + * + *
{@code
+ * // Wrap existing encoded key
+ * byte[] pkcs8 = Files.readAllBytes(Paths.get("sntru-private.pk8"));
+ * SntruPrimePrivateKeySpec spec = new SntruPrimePrivateKeySpec(pkcs8);
+ *
+ * // Import via CryptoAlgorithms
+ * PrivateKey priv = CryptoAlgorithms.privateKey("SNTRUPrime", spec);
+ * }
+ * + *

Serialization

A lightweight marshaling format is supported via + * {@link PairSeq}: + *
    + *
  • {@link #marshal(SntruPrimePrivateKeySpec)} produces a base64 string under + * the key {@code "pkcs8.b64"}.
  • + *
  • {@link #unmarshal(PairSeq)} reconstructs a new spec instance from such a + * sequence.
  • + *
+ * + * @since 1.0 + */ +public final class SntruPrimePrivateKeySpec implements AlgorithmKeySpec { + + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Constructs a new private key specification from a PKCS#8-encoded byte array. + * + *

+ * A defensive copy is made; the input array may be reused or modified without + * affecting this instance. + *

+ * + * @param pkcs8Der PKCS#8 encoded private key bytes (DER form) + * @throws NullPointerException if {@code pkcs8Der} is null + */ + public SntruPrimePrivateKeySpec(byte[] pkcs8Der) { + this.pkcs8 = Objects.requireNonNull(pkcs8Der).clone(); + } + + /** + * Returns a clone of the PKCS#8-encoded private key bytes. + * + * @return copy of the encoded private key + */ + public byte[] pkcs8() { + return pkcs8.clone(); + } + + /** + * Serializes this specification into a {@link PairSeq}. + * + *

+ * The result contains: + *

    + *
  • {@code type = "SntruPrimePrivateKeySpec"}
  • + *
  • {@code pkcs8.b64 = ...} base64-encoded key
  • + *
+ * + * @param spec specification to marshal + * @return serialized representation containing the base64-encoded key + */ + public static PairSeq marshal(SntruPrimePrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "SntruPrimePrivateKeySpec", PKCS8_B64, b64); + } + + /** + * Reconstructs a private key specification from a {@link PairSeq}. + * + *

+ * The sequence must contain a {@code pkcs8.b64} entry; otherwise an + * {@link IllegalArgumentException} is thrown. + *

+ * + * @param p serialized pair sequence produced by + * {@link #marshal(SntruPrimePrivateKeySpec)} + * @return a new specification with the decoded PKCS#8 bytes + * @throws IllegalArgumentException if the required field is missing + */ + public static SntruPrimePrivateKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (PKCS8_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("SntruPrimePrivateKeySpec: missing pkcs8.b64"); + } + return new SntruPrimePrivateKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string identifying this specification. + * + *

+ * The string includes the encoded key length but never the key material. + *

+ * + * @return a string such as {@code "SntruPrimePrivateKeySpec[len=1234]"} + */ + @Override + public String toString() { + return "SntruPrimePrivateKeySpec[len=" + pkcs8.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePublicKeySpec.java new file mode 100644 index 0000000..e657d2f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/SntruPrimePublicKeySpec.java @@ -0,0 +1,162 @@ +/******************************************************************************* + * 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.core.alg.ntruprime; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Specification wrapper for an encoded SNTRU Prime public key in X.509 format. + * + *

+ * {@code SntruPrimePublicKeySpec} provides a type-safe holder for X.509 + * SubjectPublicKeyInfo bytes belonging to the SNTRU Prime KEM. It is immutable + * and defensive copies are made on construction and retrieval. + *

+ * + *

Usage

Instances are typically created after parsing or receiving + * encoded key data, and passed to an algorithm’s key builder to import a usable + * {@link java.security.PublicKey}. + * + *
{@code
+ * // Wrap existing encoded public key
+ * byte[] der = Files.readAllBytes(Paths.get("sntru-public.spki"));
+ * SntruPrimePublicKeySpec spec = new SntruPrimePublicKeySpec(der);
+ *
+ * // Import via CryptoAlgorithms
+ * PublicKey pub = CryptoAlgorithms.publicKey("SNTRUPrime", spec);
+ * }
+ * + *

Serialization

A lightweight marshaling format is supported via + * {@link PairSeq}: + *
    + *
  • {@link #marshal(SntruPrimePublicKeySpec)} produces a base64 string under + * the key {@code "x509.b64"}.
  • + *
  • {@link #unmarshal(PairSeq)} reconstructs a new spec instance from such a + * sequence.
  • + *
+ * + * @since 1.0 + */ +public final class SntruPrimePublicKeySpec implements AlgorithmKeySpec { + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Constructs a new public key specification from an X.509-encoded byte array. + * + *

+ * A defensive copy is made; the input array may be reused or modified without + * affecting this instance. + *

+ * + * @param x509Der X.509 SubjectPublicKeyInfo bytes + * @throws NullPointerException if {@code x509Der} is null + */ + public SntruPrimePublicKeySpec(byte[] x509Der) { + this.x509 = Objects.requireNonNull(x509Der).clone(); + } + + /** + * Returns a clone of the X.509-encoded public key bytes. + * + * @return copy of the encoded public key + */ + public byte[] x509() { + return x509.clone(); + } + + /** + * Serializes this specification into a {@link PairSeq}. + * + *

+ * The result contains: + *

    + *
  • {@code type = "SntruPrimePublicKeySpec"}
  • + *
  • {@code x509.b64 = ...} base64-encoded key
  • + *
+ * + * @param spec specification to marshal + * @return serialized representation containing the base64-encoded key + */ + public static PairSeq marshal(SntruPrimePublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "SntruPrimePublicKeySpec", X509_B64, b64); + } + + /** + * Reconstructs a public key specification from a {@link PairSeq}. + * + *

+ * The sequence must contain an {@code x509.b64} entry; otherwise an + * {@link IllegalArgumentException} is thrown. + *

+ * + * @param p serialized pair sequence produced by + * {@link #marshal(SntruPrimePublicKeySpec)} + * @return a new specification with the decoded X.509 bytes + * @throws IllegalArgumentException if the required field is missing + */ + public static SntruPrimePublicKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (X509_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("SntruPrimePublicKeySpec: missing x509.b64"); + } + return new SntruPrimePublicKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string identifying this specification. + * + *

+ * The string includes the encoded key length but never the key material. + *

+ * + * @return a string such as {@code "SntruPrimePublicKeySpec[len=1234]"} + */ + @Override + public String toString() { + return "SntruPrimePublicKeySpec[len=" + x509.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/ntruprime/package-info.java b/lib/src/main/java/zeroecho/core/alg/ntruprime/package-info.java new file mode 100644 index 0000000..163b2fb --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/ntruprime/package-info.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * NTRU Prime family key encapsulation mechanisms and related utilities. + * + *

+ * This package integrates the NTRU LPRime and SNTRU Prime post-quantum KEMs + * into the core layer. It provides algorithm descriptors, runtime KEM contexts, + * key generation specifications, and encoded key specifications for import and + * export. Provider-specific details are encapsulated behind small factories + * while roles and metadata remain explicit to higher layers. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register canonical algorithms for NTRU LPRime and SNTRU Prime and declare + * ENCAPSULATE and DECAPSULATE roles, plus an agreement adapter that maps KEM to + * a message-style initiator/responder API.
  • + *
  • Provide {@link zeroecho.core.context.KemContext} implementations bound to + * a public key (encapsulation) or a private key (decapsulation).
  • + *
  • Expose immutable specifications for key generation variants and + * encoded-key carriers with compact marshalling helpers.
  • + *
  • Validate provider availability and fail fast if the PQC provider is + * absent.
  • + *
+ * + *

Components

+ *
    + *
  • NtrulPrimeAlgorithm and SntruPrimeAlgorithm: algorithm + * descriptors that wire KEM and agreement roles and register asymmetric key + * builders for generation and encoded-key import.
  • + *
  • NtrulPrimeKemContext and SntruPrimeKemContext: runtime + * contexts implementing encapsulate and decapsulate operations.
  • + *
  • NtrulPrimeKeyGenSpec / SntruPrimeKeyGenSpec: immutable + * selection of standardized parameter variants with convenience factories.
  • + *
  • NtrulPrimePublicKeySpec, NtrulPrimePrivateKeySpec, + * SntruPrimePublicKeySpec, SntruPrimePrivateKeySpec: wrappers + * over X.509 and PKCS#8 encodings with defensive copying and simple marshalling + * utilities.
  • + *
+ * + *

Provider requirements

+ *

+ * All key operations rely on a PQC provider (Bouncy Castle PQC). The provider + * must be installed in the current JVM before use; otherwise key generation and + * import fail. + *

+ * + *

Thread-safety

+ *
    + *
  • Algorithm descriptors are immutable and safe to share across + * threads.
  • + *
  • KEM contexts are stateful and not thread-safe; create one per + * operation.
  • + *
  • Encoded key specs clone input and output arrays to avoid leaking internal + * state.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.ntruprime; diff --git a/lib/src/main/java/zeroecho/core/alg/package-info.java b/lib/src/main/java/zeroecho/core/alg/package-info.java new file mode 100644 index 0000000..7be974f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/package-info.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Concrete cryptographic algorithms and small helpers used by algorithms. + * + *

+ * This package hosts provider-specific and library-native implementations of + * algorithms along with lightweight utilities that assist those + * implementations. It focuses on binding algorithm roles to runtime contexts, + * publishing capabilities for discovery, and keeping construction logic concise + * and safe. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Provide concrete algorithm classes that can be discovered and registered + * by {@link zeroecho.core.CryptoAlgorithms}.
  • + *
  • Offer a convenience base class {@link AbstractCryptoAlgorithm} to + * standardize capability declaration and runtime factory binding.
  • + *
  • Publish structured metadata (capabilities) for higher layers to inspect + * and select suitable algorithms.
  • + *
+ * + *

Key concepts

+ *
    + *
  • Algorithm model: Implementations conform to + * {@link zeroecho.core.CryptoAlgorithm}, exposing canonical identifiers, + * provider names, priorities, and role bindings.
  • + *
  • Roles and contexts: Roles are enumerated by + * {@link zeroecho.core.KeyUsage} and map to concrete runtime contexts that + * implement {@link zeroecho.core.context.CryptoContext}.
  • + *
  • Capabilities: Each declared role produces a + * {@link zeroecho.core.Capability} descriptor describing family + * ({@link zeroecho.core.AlgorithmFamily}), role, context type, accepted key + * type, and optional specification type.
  • + *
+ * + *

Capability binding

+ *

+ * Implementations typically extend {@link AbstractCryptoAlgorithm} and use its + * {@code capability(...)} method to perform two actions atomically: + *

+ *
    + *
  • Runtime binding: register a factory that constructs the role + * specific context from a key and optional + * {@link zeroecho.core.spec.ContextSpec}.
  • + *
  • Metadata publication: add a {@link zeroecho.core.Capability} to + * the algorithm so that higher layers can discover supported features.
  • + *
+ * + *

Construction and safety

+ *
    + *
  • Algorithm instances are expected to be immutable and safe to share across + * threads once constructed.
  • + *
  • Factories must return a context assignable to the declared context type; + * a mismatch is a programming error and should result in a failure during + * creation.
  • + *
  • Null checks and basic type validation are performed eagerly to fail fast + * on misconfiguration.
  • + *
+ * + *

Extensibility

+ *
    + *
  • New algorithms should prefer extending {@link AbstractCryptoAlgorithm} to + * reduce boilerplate and keep capability descriptions consistent.
  • + *
  • Additional roles can be introduced by declaring further capability + * bindings that share the same canonical algorithm id.
  • + *
  • Provider specific implementations should keep provider details + * encapsulated behind context factories.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg; diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/BlockGeometry.java b/lib/src/main/java/zeroecho/core/alg/rsa/BlockGeometry.java new file mode 100644 index 0000000..63c4b63 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/rsa/BlockGeometry.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * 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.core.alg.rsa; + +import java.math.BigInteger; +import java.security.Key; +import java.security.interfaces.RSAKey; + +/** + * Describes block sizing rules for RSA encryption and decryption. + * + *

+ * A {@code BlockGeometry} defines how many plaintext or ciphertext bytes can be + * processed per block and how many bytes are emitted, given an RSA modulus, + * padding scheme, and direction (encryption or decryption). This information is + * required by {@link RsaCipherContext} to configure the + * {@code CipherTransformInputStreamBuilder} for streaming operation. + *

+ * + *

RSA rules

+ *
    + *
  • Encryption: + *
      + *
    • Input block size is the modulus length minus padding overhead.
    • + *
    • Output block size is exactly the modulus length in bytes.
    • + *
    + *
  • + *
  • Decryption: + *
      + *
    • Input block size is exactly the modulus length in bytes.
    • + *
    • Output block size is bounded by the encryption input maximum, but + * {@code modulus length} is used as a safe upper bound.
    • + *
    + *
  • + *
+ * + *

Example

{@code
+ * Key pub = ...; // RSA public key
+ * RsaEncSpec spec = RsaEncSpec.ofPkcs1v15();
+ * BlockGeometry g = BlockGeometry.forRsa(spec, pub, true);
+ * System.out.printf("Plaintext block: %d, Ciphertext block: %d%n",
+ *                   g.inChunkSize, g.outChunkSize);
+ * }
+ * + * @since 1.0 + */ +public final class BlockGeometry { + /** Number of input bytes per block. */ + public final int inChunkSize; + /** Number of output bytes per block. */ + public final int outChunkSize; + /** Extra chunks produced during finalization (always 0 for RSA). */ + public final int finalizationOutputChunks; + + /** + * Constructs a new block geometry descriptor. + * + * @param inChunkSize maximum input bytes per block + * @param outChunkSize output bytes per block + * @param finalizationOutputChunks number of additional chunks during + * finalization + */ + public BlockGeometry(int inChunkSize, int outChunkSize, int finalizationOutputChunks) { + this.inChunkSize = inChunkSize; + this.outChunkSize = outChunkSize; + this.finalizationOutputChunks = finalizationOutputChunks; + } + + /** + * Computes RSA block geometry for the given specification and key. + * + * @param spec RSA encryption specification with padding and hash parameters + * @param key RSA key; must implement + * {@link java.security.interfaces.RSAKey} + * @param encrypt {@code true} for encryption, {@code false} for decryption + * @return block geometry with input and output block sizes + * @throws IllegalArgumentException if {@code key} is not RSA or modulus is too + * small + */ + public static BlockGeometry forRsa(RsaEncSpec spec, Key key, boolean encrypt) { + if (!(key instanceof RSAKey rsa)) { + throw new IllegalArgumentException("RSA key required"); + } + int k = byteLen(rsa.getModulus()); // modulus size in bytes + + int inBlock = 0; + int outBlock = k; + if (encrypt) { + switch (spec.padding()) { + case PKCS1V15 -> { + inBlock = k - 11; + } + case OAEP -> { + int hLen = oaepHashLen(spec.hash()); + inBlock = k - 2 * hLen - 2; + } + } + if (inBlock <= 0) { + throw new IllegalArgumentException("RSA modulus too small for chosen padding/hash"); + } + } else { + // Decryption: input blocks are exactly k bytes; output ≤ encrypt max. Use k as + // safe bound. + inBlock = k; + } + + return new BlockGeometry(inBlock, outBlock, 0); + } + + private static int byteLen(BigInteger n) { + int bits = n.bitLength(); + return (bits + 7) >>> 3; + } + + private static int oaepHashLen(RsaEncSpec.Hash h) { + return switch (h) { + case SHA1 -> 20; + case SHA256 -> 32; + case SHA384 -> 48; + case SHA512 -> 64; + }; + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaAlgorithm.java new file mode 100644 index 0000000..cc581a1 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaAlgorithm.java @@ -0,0 +1,219 @@ +/******************************************************************************* + * 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.core.alg.rsa; + +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAKeyGenParameterSpec; +import java.security.spec.X509EncodedKeySpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * RSA algorithm binding for encryption/decryption and signature/verification. + * + *

+ * This class wires RSA to the ZeroEcho algorithm registry by declaring its + * capabilities, supported roles, and key builders. It supports both encryption + * (typically RSA-OAEP) and digital signatures (typically RSA-PSS). + *

+ * + *

Capabilities

+ *
    + *
  • Encryption/Decryption: Uses {@link RsaEncSpec}, with OAEP + * (SHA-256) as the default padding scheme.
  • + *
  • Signing/Verification: Uses {@link RsaSigSpec}, with PSS (SHA-256, + * 32-byte salt) as the default signature scheme.
  • + *
+ * + *

Key builders

+ *
    + *
  • {@link RsaKeyGenSpec}: Generates RSA key pairs with configurable key size + * and public exponent (default: 2048 bits, exponent 65537).
  • + *
  • {@link RsaPublicKeySpec}: Imports an X.509-encoded public key.
  • + *
  • {@link RsaPrivateKeySpec}: Imports a PKCS#8-encoded private key.
  • + *
+ * + *

Example

{@code
+ * RsaAlgorithm rsa = new RsaAlgorithm();
+ *
+ * // Generate a new RSA-2048 key pair
+ * KeyPair kp = rsa.asymmetricKeyBuilder(RsaKeyGenSpec.class)
+ *                 .generateKeyPair(RsaKeyGenSpec.rsa2048());
+ *
+ * // Encrypt with OAEP (SHA-256)
+ * EncryptionContext enc = rsa.create(KeyUsage.ENCRYPT, kp.getPublic(), null);
+ *
+ * // Sign with PSS (SHA-256)
+ * SignatureContext sig = rsa.create(KeyUsage.SIGN, kp.getPrivate(), null);
+ * }
+ * + *

+ * Security note: This implementation defaults to OAEP with SHA-256 for + * encryption and PSS with SHA-256 for signatures, which are considered secure + * and modern choices. Legacy padding modes such as PKCS#1 v1.5 are deliberately + * not exposed to avoid misuse. + *

+ * + * @since 1.0 + */ +public final class RsaAlgorithm extends AbstractCryptoAlgorithm { + /** + * Constructs a new RSA algorithm definition and registers its roles and key + * builders. + * + *

+ * The constructor wires the following: + *

+ *
    + *
  • {@link KeyUsage#ENCRYPT} / {@link KeyUsage#DECRYPT} using + * {@link RsaEncSpec}, with OAEP (SHA-256) as the default scheme.
  • + *
  • {@link KeyUsage#SIGN} / {@link KeyUsage#VERIFY} using {@link RsaSigSpec}, + * with PSS (SHA-256, 32-byte salt) as the default scheme.
  • + *
  • {@link RsaKeyGenSpec}: RSA key pair generation (default: 2048-bit, public + * exponent 65537).
  • + *
  • {@link RsaPublicKeySpec}: Import of X.509-encoded public keys.
  • + *
  • {@link RsaPrivateKeySpec}: Import of PKCS#8-encoded private keys.
  • + *
+ * + *

+ * All roles are registered under the {@link AlgorithmFamily#ASYMMETRIC} family. + * Default specs are chosen to be modern and secure, excluding legacy padding + * modes such as PKCS#1 v1.5. + *

+ * + * @throws IllegalArgumentException if an RSA context cannot be initialized due + * to provider-level constraints (e.g., + * unsupported parameters). + */ + public RsaAlgorithm() { + super("RSA", "RSA"); + + // Encryption + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.ENCRYPT, EncryptionContext.class, PublicKey.class, + RsaEncSpec.class, (PublicKey k, RsaEncSpec s) -> new RsaCipherContext(this, k, true, s), + () -> RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256)); + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.DECRYPT, EncryptionContext.class, PrivateKey.class, + RsaEncSpec.class, (PrivateKey k, RsaEncSpec s) -> new RsaCipherContext(this, k, false, s), + () -> RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256)); + + // Signatures + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.SIGN, SignatureContext.class, PrivateKey.class, + RsaSigSpec.class, (PrivateKey k, RsaSigSpec s) -> { + try { + return new RsaSignatureContext(this, k, s); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("RSA signer init", e); + } + }, () -> RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32)); + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.VERIFY, SignatureContext.class, PublicKey.class, + RsaSigSpec.class, (PublicKey k, RsaSigSpec s) -> { + try { + return new RsaSignatureContext(this, k, s); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("RSA verifier init", e); + } + }, () -> RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32)); + + // Key builders + registerAsymmetricKeyBuilder(RsaKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(RsaKeyGenSpec spec) throws GeneralSecurityException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + RSAKeyGenParameterSpec params = new RSAKeyGenParameterSpec(spec.keySize(), + BigInteger.valueOf(spec.publicExponent())); + kpg.initialize(params); + return kpg.generateKeyPair(); + } + + @Override + public PublicKey importPublic(RsaKeyGenSpec spec) { + throw new UnsupportedOperationException("Use RsaPublicKeySpec to import a public key."); + } + + @Override + public PrivateKey importPrivate(RsaKeyGenSpec spec) { + throw new UnsupportedOperationException("Use RsaPrivateKeySpec to import a private key."); + } + }, RsaKeyGenSpec::rsa2048); + + registerAsymmetricKeyBuilder(RsaPublicKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(RsaPublicKeySpec spec) { + throw new UnsupportedOperationException("Generation not supported for encoded spec."); + } + + @Override + public PublicKey importPublic(RsaPublicKeySpec spec) throws GeneralSecurityException { + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); + } + + @Override + public PrivateKey importPrivate(RsaPublicKeySpec spec) { + throw new UnsupportedOperationException("Use RsaPrivateKeySpec for private keys."); + } + }, null); + + registerAsymmetricKeyBuilder(RsaPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(RsaPrivateKeySpec spec) { + throw new UnsupportedOperationException("Generation not supported for encoded spec."); + } + + @Override + public PublicKey importPublic(RsaPrivateKeySpec spec) { + throw new UnsupportedOperationException("Use RsaPublicKeySpec for public keys."); + } + + @Override + public PrivateKey importPrivate(RsaPrivateKeySpec spec) throws GeneralSecurityException { + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + } + }, null); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java new file mode 100644 index 0000000..a21891d --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaCipherContext.java @@ -0,0 +1,233 @@ +/******************************************************************************* + * 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.core.alg.rsa; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.spec.MGF1ParameterSpec; +import java.util.Objects; + +import javax.crypto.Cipher; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.io.CipherTransformInputStreamBuilder; + +/** + * Streaming RSA cipher context that transforms data block-by-block using OAEP + * or PKCS#1 v1.5 padding. + * + *

+ * An instance is created for a specific role (encrypt or decrypt) and a + * concrete {@link RsaEncSpec}. The context exposes a pull-style pipeline via + * {@link #attach(java.io.InputStream)} that converts bytes on-the-fly, making + * it suitable for large streams without buffering the whole message in memory. + *

+ * + *

Behavior

+ *
    + *
  • Encryption reads plaintext in RSA-size blocks according to the selected + * padding, producing fixed-size ciphertext blocks equal to the modulus + * length.
  • + *
  • Decryption requires ciphertext in full modulus-length blocks and yields + * variable-size plaintext blocks determined by padding.
  • + *
  • Finalization occurs on end of stream; decryption rejects trailing partial + * ciphertext blocks.
  • + *
+ * + *

Example

{@code
+ * RsaEncSpec spec = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256);
+ * try (RsaCipherContext ctx = new RsaCipherContext(algo, pubKey, true, spec);
+ *      InputStream in  = Files.newInputStream(plaintextPath);
+ *      InputStream enc = ctx.attach(in)) {
+ *     Files.copy(enc, ciphertextPath);
+ * }
+ * }
+ * + *

+ * Security note: OAEP with SHA-256 is preferred over PKCS#1 v1.5. + * Decryption enforces full ciphertext blocks to avoid truncation hazards. The + * context does not manage or destroy keys. + *

+ */ +public final class RsaCipherContext implements EncryptionContext { + + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean encrypt; + private final RsaEncSpec spec; + + /** + * Creates a new RSA cipher context for encryption or decryption with the given + * parameters. + * + *

+ * The {@code encrypt} flag selects the role of this context: {@code true} for + * encryption (public key is typical), {@code false} for decryption (private key + * is required). The {@code spec} defines padding and hash parameters; for OAEP + * the hash is used both for the digest and MGF1 unless specified otherwise by + * the spec. + *

+ * + * @param algorithm the owning algorithm descriptor used for metadata and + * auditing + * @param key the RSA key for this role; public for encryption/verify, + * private for decrypt/sign + * @param encrypt {@code true} to encrypt, {@code false} to decrypt + * @param spec RSA encryption specification including padding mode and hash + * @throws NullPointerException if any argument is {@code null} + */ + public RsaCipherContext(CryptoAlgorithm algorithm, Key key, boolean encrypt, RsaEncSpec spec) { + this.algorithm = Objects.requireNonNull(algorithm, "algorithm must not be null"); + this.key = Objects.requireNonNull(key, "key must not be null"); + this.encrypt = encrypt; + this.spec = Objects.requireNonNull(spec, "spec must not be null"); + } + + /** + * Returns the algorithm descriptor associated with this context. + * + * @return the algorithm that created this context + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + * @return the RSA key used by this context + */ + @Override + public Key key() { + return key; + } + + /** + * Closes the context and releases resources. + * + *

+ * This implementation is a no-op because the attached stream performs + * finalize-on-close. + *

+ */ + @Override + public void close() { + // no-op; stream handles finalize-on-close + } + + /** + * Attaches this cipher to an upstream stream and returns a transforming stream. + * + *

+ * The returned stream pulls from {@code upstream} and emits encrypted or + * decrypted bytes according to the constructor-selected role and + * {@link RsaEncSpec}. On EOF, encryption will process a final short plaintext + * block if present, while decryption requires that the final buffered + * ciphertext forms a complete modulus-sized block. + *

+ * + *

+ * The caller is responsible for closing the returned stream to ensure + * finalization and resource cleanup. + *

+ * + * @param upstream the source stream to transform + * @return a new input stream that produces transformed bytes + * @throws NullPointerException if {@code upstream} is {@code null} + * @throws IOException if the cipher cannot be initialized or a block + * transform fails + */ + @Override + public InputStream attach(InputStream upstream) throws IOException { + Objects.requireNonNull(upstream, "upstream must not be null"); + try { + Cipher cipher = Cipher.getInstance(transformation(spec)); + init(cipher); + // int modBytes = modulusBytes(key); + // int inBlock = encrypt ? plainBlockSize(modBytes, spec) : modBytes; + // return new Stream(upstream, cipher, inBlock, modBytes, encrypt); + + BlockGeometry rsaGeometry = BlockGeometry.forRsa(spec, key, encrypt); + + return CipherTransformInputStreamBuilder.builder().withUpstream(upstream).withCipher(cipher) + .withInputBlockSize(rsaGeometry.inChunkSize).withOutputBlockSize(rsaGeometry.outChunkSize) + .withBufferedBlocks(100).withFinalizationOutputChunks(rsaGeometry.finalizationOutputChunks) + .withUpdateStreaming(false).build(); + } catch (GeneralSecurityException e) { + throw new IOException(spec.description() + " RSA attach/init failed: " + e.getMessage(), e); + } + } + + private void init(Cipher cipher) throws GeneralSecurityException { + if (spec.padding() == RsaEncSpec.Padding.OAEP) { + OAEPParameterSpec oaep = new OAEPParameterSpec(jcaHash(spec.hash()), "MGF1", mgf1(spec.hash()), + spec.label() == null ? PSource.PSpecified.DEFAULT : new PSource.PSpecified(spec.label())); + cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, oaep); + } else { + cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key); + } + } + + private static String transformation(RsaEncSpec s) { + if (s.padding() == RsaEncSpec.Padding.PKCS1V15) { + return "RSA/ECB/PKCS1Padding"; + } + return "RSA/ECB/OAEPWith" + jcaHash(s.hash()) + "AndMGF1Padding"; + } + + private static String jcaHash(RsaEncSpec.Hash h) { + return switch (h) { + case SHA1 -> "SHA-1"; + case SHA256 -> "SHA-256"; + case SHA384 -> "SHA-384"; + case SHA512 -> "SHA-512"; + }; + } + + private static MGF1ParameterSpec mgf1(RsaEncSpec.Hash h) { + return switch (h) { + case SHA1 -> MGF1ParameterSpec.SHA1; + case SHA256 -> MGF1ParameterSpec.SHA256; + case SHA384 -> MGF1ParameterSpec.SHA384; + case SHA512 -> MGF1ParameterSpec.SHA512; + }; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaEncSpec.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaEncSpec.java new file mode 100644 index 0000000..4aa0118 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaEncSpec.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * 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.core.alg.rsa; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.annotation.DisplayName; +import zeroecho.core.spec.ContextSpec; + +/** + * Specification of RSA encryption parameters including padding mode, hash + * function, and optional OAEP label. + * + *

+ * This spec is used with {@link RsaCipherContext} to configure how RSA + * encryption and decryption should be performed. It supports two padding + * schemes: + *

+ *
    + *
  • {@link Padding#OAEP} with a configurable hash function and optional label + * (as defined in PKCS#1 v2.2).
  • + *
  • {@link Padding#PKCS1V15} (PKCS#1 v1.5 padding), without hash or + * label.
  • + *
+ * + *

Construction

Use the static factories to create a spec:
{@code
+ * // OAEP with SHA-256
+ * RsaEncSpec oaep = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256);
+ *
+ * // PKCS#1 v1.5
+ * RsaEncSpec pkcs1 = RsaEncSpec.pkcs1v15();
+ *
+ * // OAEP with SHA-384 and custom UTF-8 label
+ * RsaEncSpec labeled = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA384)
+ *                                .withLabelUtf8("context");
+ * }
+ * + *

+ * Instances are immutable. Methods {@link #withLabel(byte[])} and + * {@link #withLabelUtf8(String)} return new copies with the given label. + *

+ * + *

Security notes

+ *
    + *
  • OAEP with SHA-256 or stronger is recommended; PKCS#1 v1.5 is retained + * only for interoperability and should be avoided for new designs.
  • + *
  • Labels in OAEP can provide domain separation but are optional; most + * applications leave them unset.
  • + *
+ * + * @since 1.0 + */ +@DisplayName("RSA encryption parameters") +public final class RsaEncSpec implements ContextSpec, Describable { + /** + * Supported RSA padding schemes. + */ + public enum Padding { + /** Optimal Asymmetric Encryption Padding (PKCS#1 OAEP). */ + OAEP, + /** Legacy PKCS#1 v1.5 padding. */ + PKCS1V15 + } + + /** + * Hash algorithms usable within OAEP padding. + */ + public enum Hash { + /** SHA-1 (deprecated, retained for compatibility). */ + SHA1, + /** SHA-256. */ + SHA256, + /** SHA-384. */ + SHA384, + /** SHA-512. */ + SHA512 + } // used for OAEP only + + private final Padding padding; + private final Hash hash; // OAEP only + private final byte[] label; // OAEP optional label + + /** + * Creates a new RSA encryption spec with the given parameters. + * + * @param padding padding scheme; must not be null + * @param hash hash function used for OAEP, or null if not applicable + * @param label optional OAEP label (copied defensively), or null if none + * @throws NullPointerException if {@code padding} is null + * @throws IllegalArgumentException if {@code padding} is OAEP and {@code hash} + * is null, or if {@code padding} is PKCS1V15 + * but {@code hash} or {@code label} are set + */ + private RsaEncSpec(Padding padding, Hash hash, byte[] label) { + this.padding = Objects.requireNonNull(padding, "padding must not be null"); + this.hash = hash; + if (padding == Padding.OAEP && hash == null) { + throw new IllegalArgumentException("OAEP requires a hash"); + } + if (padding == Padding.PKCS1V15 && (hash != null || label != null)) { + throw new IllegalArgumentException("PKCS1 v1.5 does not use hash/label"); + } + this.label = (label == null ? null : Arrays.copyOf(label, label.length)); + } + + /** + * Creates an OAEP spec with the given hash function and no label. + * + * @param h hash function for OAEP + * @return new {@code RsaEncSpec} for OAEP + */ + public static RsaEncSpec oaep(Hash h) { + return new RsaEncSpec(Padding.OAEP, h, null); + } + + /** + * Creates a PKCS#1 v1.5 spec. + * + * @return new {@code RsaEncSpec} for PKCS#1 v1.5 + */ + public static RsaEncSpec pkcs1v15() { + return new RsaEncSpec(Padding.PKCS1V15, null, null); + } + + /** + * Returns a copy of this spec with the given OAEP label. + * + * @param l label bytes (cloned); may be null to clear + * @return new {@code RsaEncSpec} with the same padding/hash and new label + */ + public RsaEncSpec withLabel(byte[] l) { + return new RsaEncSpec(padding, hash, l); + } + + /** + * Returns a copy of this spec with the given UTF-8 OAEP label. + * + * @param s label string encoded in UTF-8; may be null to clear + * @return new {@code RsaEncSpec} with the same padding/hash and new label + */ + public RsaEncSpec withLabelUtf8(String s) { + return new RsaEncSpec(padding, hash, s == null ? null : s.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Returns the padding scheme. + * + * @return padding scheme + */ + public Padding padding() { + return padding; + } + + /** + * Returns the hash function used in OAEP. + * + * @return OAEP hash function, or null if padding is PKCS#1 v1.5 + */ + public Hash hash() { + return hash; + } + + /** + * Returns the OAEP label as a defensive copy. + * + * @return cloned OAEP label, or null if none + */ + public byte[] label() { + return label == null ? null : Arrays.copyOf(label, label.length); + } + + /** + * Returns a short human-readable description of this spec. + * + *

+ * Examples: + *

    + *
  • {@code "PKCS1v1.5"}
  • + *
  • {@code "OAEP(SHA256,label=8B)"}
  • + *
+ * + * @return description string + */ + @Override + public String description() { + if (padding == Padding.PKCS1V15) { + return "PKCS1v1.5"; + } + return "OAEP(" + hash + (label != null ? ",label=" + label.length + "B" : "") + ")"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java new file mode 100644 index 0000000..9e339f2 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaKeyGenSpec.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.rsa; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Key generation specification for RSA key pairs. + * + *

+ * This spec defines the modulus length and public exponent used when generating + * an RSA key pair. It is passed to the algorithm’s key builder (see + * {@code RsaAlgorithm}) to produce a {@link java.security.KeyPair}. + *

+ * + *

Parameters

+ *
    + *
  • keySize: the RSA modulus size in bits. Must be at least 1024; + * typical secure values are 2048 or 3072, with 4096 used for high-security + * applications.
  • + *
  • publicExponent: the RSA public exponent. The common and + * recommended value is 65537 (F4), balancing security and performance.
  • + *
+ * + *

Usage example

{@code
+ * // Create a spec for a 2048-bit key with exponent 65537
+ * RsaKeyGenSpec spec = RsaKeyGenSpec.rsa2048();
+ *
+ * // Generate the key pair
+ * KeyPair kp = rsaAlgorithm.asymmetricKeyBuilder(RsaKeyGenSpec.class)
+ *                          .generateKeyPair(spec);
+ * }
+ * + * @since 1.0 + */ +public final class RsaKeyGenSpec implements AlgorithmKeySpec, Describable { + private final int keySize; + private final int publicExponent; // typical 65537 + + /** + * Constructs an RSA key generation spec with the given parameters. + * + * @param keySize modulus size in bits; must be at least 1024 + * @param publicExponent RSA public exponent, usually 65537 + * @throws IllegalArgumentException if {@code keySize} is smaller than 1024 + */ + public RsaKeyGenSpec(int keySize, int publicExponent) { + if (keySize < 1024) { // NOPMD + throw new IllegalArgumentException("keySize too small"); + } + this.keySize = keySize; + this.publicExponent = publicExponent; + } + + /** + * Returns the modulus length in bits. + * + * @return RSA modulus size + */ + public int keySize() { + return keySize; + } + + /** + * Returns the public exponent. + * + * @return RSA public exponent (typically 65537) + */ + public int publicExponent() { + return publicExponent; + } + + /** + * Convenience factory for a 2048-bit RSA spec with exponent 65537. + * + * @return new {@code RsaKeyGenSpec} instance for 2048/65537 + */ + public static RsaKeyGenSpec rsa2048() { + return new RsaKeyGenSpec(2_048, 65_537); + } + + /** + * Convenience factory for a 3072-bit RSA spec with exponent 65537. + * + * @return new {@code RsaKeyGenSpec} instance for 3072/65537 + */ + public static RsaKeyGenSpec rsa3072() { + return new RsaKeyGenSpec(3_072, 65_537); + } + + /** + * Convenience factory for a 4096-bit RSA spec with exponent 65537. + * + * @return new {@code RsaKeyGenSpec} instance for 4096/65537 + */ + public static RsaKeyGenSpec rsa4096() { + return new RsaKeyGenSpec(4_096, 65_537); + } + + /** + * Returns a short description string. + * + *

+ * Format is {@code ":"}, for example + * {@code "2048:65537"}. + *

+ * + * @return human-readable description of this spec + */ + @Override + public String description() { + return keySize + ":" + publicExponent; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java new file mode 100644 index 0000000..3cd6385 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaPrivateKeySpec.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * 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.core.alg.rsa; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Encoded RSA private key specification in PKCS#8 format. + * + *

+ * This spec wraps the DER-encoded PKCS#8 representation of an RSA private key + * and is used for importing keys into {@link java.security.KeyFactory} or + * algorithm builders (see {@code RsaAlgorithm}). It provides safe cloning of + * the encoded form and supports simple marshal/unmarshal via {@link PairSeq}. + *

+ * + *

Usage

{@code
+ * // Import a PKCS#8 encoded key
+ * byte[] pkcs8 = Files.readAllBytes(Path.of("rsa_private.der"));
+ * RsaPrivateKeySpec spec = new RsaPrivateKeySpec(pkcs8);
+ *
+ * // Retrieve encoded form
+ * byte[] encoded = spec.encoded();
+ *
+ * // Marshal for storage or transmission
+ * PairSeq seq = RsaPrivateKeySpec.marshal(spec);
+ *
+ * // Rebuild from serialized representation
+ * RsaPrivateKeySpec parsed = RsaPrivateKeySpec.unmarshal(seq);
+ * }
+ * + *

+ * Instances are immutable and defensively copy their input. The encoded key + * material remains sensitive and should be handled with care. + *

+ * + * @since 1.0 + */ +public final class RsaPrivateKeySpec implements AlgorithmKeySpec { + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Constructs a new RSA private key spec from a PKCS#8-encoded key. + * + * @param pkcs8 DER-encoded PKCS#8 private key; cloned internally + * @throws NullPointerException if {@code pkcs8} is {@code null} + */ + public RsaPrivateKeySpec(byte[] pkcs8) { + this.pkcs8 = pkcs8.clone(); + } + + /** + * Returns a clone of the PKCS#8-encoded private key. + * + * @return defensive copy of the encoded key + */ + public byte[] encoded() { + return pkcs8.clone(); + } + + /** + * Marshals this spec to a {@link PairSeq}. + * + *

+ * The output sequence contains: + *

    + *
  • {@code type = "RSA-PRIV"}
  • + *
  • {@code pkcs8.b64 = }
  • + *
+ * + * @param spec RSA private key spec + * @return pair sequence with type and base64-encoded key material + */ + public static PairSeq marshal(RsaPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "RSA-PRIV", PKCS8_B64, b64); + } + + /** + * Reconstructs an RSA private key spec from a marshaled {@link PairSeq}. + * + *

+ * Expects an entry {@code pkcs8.b64} containing the Base64-encoded PKCS#8 + * private key. The {@code type} field is ignored for forward compatibility. + *

+ * + * @param p serialized pair sequence + * @return RSA private key spec with decoded key material + * @throws IllegalArgumentException if the sequence does not contain a + * {@code pkcs8.b64} entry + */ + public static RsaPrivateKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (PKCS8_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for RSA private key"); + } + return new RsaPrivateKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaPublicKeySpec.java new file mode 100644 index 0000000..e2965e4 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaPublicKeySpec.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.rsa; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Encoded RSA public key specification in X.509 SubjectPublicKeyInfo format. + * + *

+ * This spec wraps the DER-encoded X.509 representation of an RSA public key and + * is used for importing keys into {@link java.security.KeyFactory} or algorithm + * builders (see {@code RsaAlgorithm}). It provides safe cloning of the encoded + * form and supports simple marshal/unmarshal via {@link PairSeq}. + *

+ * + *

Usage

{@code
+ * // Import an X.509 encoded public key
+ * byte[] x509 = Files.readAllBytes(Path.of("rsa_public.der"));
+ * RsaPublicKeySpec spec = new RsaPublicKeySpec(x509);
+ *
+ * // Retrieve encoded form
+ * byte[] encoded = spec.encoded();
+ *
+ * // Marshal for storage or transmission
+ * PairSeq seq = RsaPublicKeySpec.marshal(spec);
+ *
+ * // Rebuild from serialized representation
+ * RsaPublicKeySpec parsed = RsaPublicKeySpec.unmarshal(seq);
+ * }
+ * + *

+ * Instances are immutable and defensively copy their input. Although public + * keys are not secret, encoded values should still be treated as security + * relevant data and preserved accurately. + *

+ * + * @since 1.0 + */ +public final class RsaPublicKeySpec implements AlgorithmKeySpec { + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Constructs an RSA public key spec from an X.509-encoded key. + * + * @param x509 DER-encoded X.509 public key; cloned internally + * @throws NullPointerException if {@code x509} is {@code null} + */ + public RsaPublicKeySpec(byte[] x509) { + this.x509 = x509.clone(); + } + + /** + * Returns a clone of the X.509-encoded public key. + * + * @return defensive copy of the encoded key + */ + public byte[] encoded() { + return x509.clone(); + } + + /** + * Marshals this spec to a {@link PairSeq}. + * + *

+ * The output sequence contains: + *

    + *
  • {@code type = "RSA-PUB"}
  • + *
  • {@code x509.b64 = }
  • + *
+ * + * @param spec RSA public key spec + * @return pair sequence with type and base64-encoded key material + */ + public static PairSeq marshal(RsaPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "RSA-PUB", X509_B64, b64); + } + + /** + * Reconstructs an RSA public key spec from a marshaled {@link PairSeq}. + * + *

+ * Expects an entry {@code x509.b64} containing the Base64-encoded X.509 public + * key. The {@code type} field is ignored for forward compatibility. + *

+ * + * @param p serialized pair sequence + * @return RSA public key spec with decoded key material + * @throws IllegalArgumentException if the sequence does not contain a + * {@code x509.b64} entry + */ + public static RsaPublicKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (X509_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("x509.b64 missing for RSA public key"); + } + return new RsaPublicKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaSigSpec.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaSigSpec.java new file mode 100644 index 0000000..babe11f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaSigSpec.java @@ -0,0 +1,212 @@ +/******************************************************************************* + * 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.core.alg.rsa; + +import java.util.Objects; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.annotation.DisplayName; +import zeroecho.core.spec.ContextSpec; + +/** + * Specification of RSA signature parameters including padding mode, hash + * function, and (for PSS) salt length. + * + *

+ * This spec is used with {@link RsaSignatureContext} to configure how RSA + * signatures are created and verified. It supports two modes: + *

+ *
    + *
  • {@link Mode#PKCS1V15} — legacy PKCS#1 v1.5 signatures using a selected + * hash.
  • + *
  • {@link Mode#PSS} — probabilistic signatures with PSS padding using a + * selected hash and configurable salt length.
  • + *
+ * + *

Construction

Use the static factories to create a spec:
{@code
+ * // PKCS#1 v1.5 with SHA-256
+ * RsaSigSpec pkcs1 = RsaSigSpec.pkcs1v15(RsaSigSpec.Hash.SHA256);
+ *
+ * // PSS with SHA-384 and salt length equal to digest length
+ * RsaSigSpec pss = RsaSigSpec.pss(RsaSigSpec.Hash.SHA384, -1);
+ *
+ * // PSS with SHA-512 and explicit 32-byte salt
+ * RsaSigSpec pss32 = RsaSigSpec.pss(RsaSigSpec.Hash.SHA512, 32);
+ * }
+ * + *

Security notes

+ *
    + *
  • PSS is recommended over PKCS#1 v1.5 for new applications, as it provides + * stronger provable security.
  • + *
  • The default salt length of {@code -1} means “use the digest length”, as + * recommended by PKCS#1.
  • + *
  • PKCS#1 v1.5 signatures remain widely used for compatibility but are less + * robust against certain attack classes.
  • + *
+ * + * @since 1.0 + */ +@DisplayName("RSA signature parameters") +public final class RsaSigSpec implements ContextSpec, Describable { + /** + * Signature padding modes supported by RSA. + */ + public enum Mode { + /** Legacy PKCS#1 v1.5 signature scheme. */ + PKCS1V15, + /** Probabilistic Signature Scheme (PSS), recommended for new systems. */ + PSS + } + + /** + * Supported hash algorithms for RSA signatures. + * + *

+ * These values are common secure choices; SHA-1 is deliberately excluded. + *

+ */ + public enum Hash { + /** SHA-256 digest. */ + SHA256, + /** SHA-384 digest. */ + SHA384, + /** SHA-512 digest. */ + SHA512 + } // common secure choices + + private final Mode mode; + private final Hash hash; + private final int pssSaltLen; // for PSS: -1 means "use digest length" + + /** + * Constructs an RSA signature specification. + * + * @param mode signature mode (PKCS#1 v1.5 or PSS) + * @param hash hash function used in signature computation + * @param saltLen PSS salt length; -1 means digest length, ignored for PKCS#1 + * v1.5 + * @throws NullPointerException if {@code mode} or {@code hash} is + * {@code null} + * @throws IllegalArgumentException if PSS is selected and {@code saltLen < -1}, + * or if PKCS#1 v1.5 is selected but + * {@code saltLen} is not -1 + */ + private RsaSigSpec(Mode mode, Hash hash, int saltLen) { + this.mode = Objects.requireNonNull(mode, "mode must not be null"); + this.hash = Objects.requireNonNull(hash, "hash must not be null"); + if (mode == Mode.PSS && saltLen < -1) { + throw new IllegalArgumentException("PSS salt length must be -1 (digest length) or >= 0"); + } + if (mode == Mode.PKCS1V15 && saltLen != -1) { + throw new IllegalArgumentException("PKCS1v1.5 does not use salt length"); + } + this.pssSaltLen = saltLen; + } + + /** + * Creates a PKCS#1 v1.5 spec with the given hash. + * + * @param h hash algorithm + * @return new {@code RsaSigSpec} instance for PKCS#1 v1.5 + */ + public static RsaSigSpec pkcs1v15(Hash h) { + return new RsaSigSpec(Mode.PKCS1V15, h, -1); + } + + /** + * Creates a PSS spec with the given hash and salt length. + * + *

+ * A salt length of {@code -1} means “use digest length”, as recommended by + * PKCS#1. + *

+ * + * @param h hash algorithm + * @param saltLen PSS salt length, or -1 for digest length + * @return new {@code RsaSigSpec} instance for PSS + */ + public static RsaSigSpec pss(Hash h, int saltLen) { + return new RsaSigSpec(Mode.PSS, h, saltLen); + } + + /** + * Returns the signature mode. + * + * @return mode (PKCS#1 v1.5 or PSS) + */ + public Mode mode() { + return mode; + } + + /** + * Returns the hash algorithm. + * + * @return hash algorithm used for signing/verification + */ + public Hash hash() { + return hash; + } + + /** + * Returns the salt length for PSS. + * + * @return salt length in bytes, or -1 to indicate digest length + */ + public int pssSaltLen() { + return pssSaltLen; + } + + /** + * Returns a short human-readable description of this spec. + * + *

+ * Examples: + *

    + *
  • {@code "PKCS1v1.5(SHA256)"}
  • + *
  • {@code "PSS(SHA512,salt=32)"}
  • + *
  • {@code "PSS(SHA384,salt=digestLen)"}
  • + *
+ * + * @return description string + */ + @Override + public String description() { + if (mode == Mode.PKCS1V15) { + return "PKCS1v1.5(" + hash + ")"; + } + String salt = pssSaltLen == -1 ? "digestLen" : String.valueOf(pssSaltLen); + return "PSS(" + hash + ",salt=" + salt + ")"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/RsaSignatureContext.java b/lib/src/main/java/zeroecho/core/alg/rsa/RsaSignatureContext.java new file mode 100644 index 0000000..b28a288 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/rsa/RsaSignatureContext.java @@ -0,0 +1,530 @@ +/******************************************************************************* + * 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.core.alg.rsa; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; +import java.util.Arrays; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.err.VerificationException; +import zeroecho.core.io.AbstractPassthroughInputStream; +import zeroecho.core.tag.ByteVerificationStrategy; +import zeroecho.core.tag.SignatureVerificationStrategy; +import zeroecho.core.tag.ThrowingBiPredicate; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; + +/** + * Streaming RSA signature context for signing or verifying data in a pull + * pipeline. + * + *

+ * This context binds an RSA key and a signature specification, exposes a + * passthrough view via {@link #wrap(java.io.InputStream)}, and finalizes at + * end-of-stream. In produce (SIGN) mode the signature is emitted as a trailer. + * In verify (VERIFY) mode the computed signature is compared against an + * expected tag using a configured verification approach. + *

+ * + *

Modes

+ *
    + *
  • SIGN - consumes body bytes and appends the computed signature as a + * trailer after the original data has been read.
  • + *
  • VERIFY - consumes body bytes and compares the computed signature + * at EOF to the value provided via {@link #setExpectedTag(byte[])}. The outcome + * is handled by the verification approach set with + * {@link #setVerificationApproach(ThrowingBiPredicate.VerificationBiPredicate)}.
  • + *
+ * + *

Usage

+ *

Sign with RSASSA-PSS (SHA-256, salt 32)

+ * {@code
+ * RsaSigSpec spec = RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32);
+ * try (RsaSignatureContext ctx = new RsaSignatureContext(algo, privateKey, spec);
+ *      InputStream in  = Files.newInputStream(msgPath);
+ *      InputStream out = ctx.wrap(in)) {
+ *     // out yields message bytes, then signature trailer of length ctx.tagLength()
+ *     Files.copy(out, signedPayloadPath);
+ * }
+ * }
+ * 
+ * + *

Verify a detached RSA signature

+ * {@code
+ * byte[] sig = Files.readAllBytes(sigPath);
+ * RsaSigSpec spec = RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32);
+ * try (RsaSignatureContext ctx = new RsaSignatureContext(algo, publicKey, spec);
+ *      InputStream in  = Files.newInputStream(bodyWithoutTrailer);
+ *      InputStream out = ctx.wrap(in)) {
+ *     ctx.setExpectedTag(sig);
+ *     // Throw on mismatch at EOF:
+ *     ctx.setVerificationApproach(ctx.getVerificationCore().getThrowOnMismatch());
+ *     Files.copy(out, recoveredMsgPath);
+ * }
+ * }
+ * 
+ * + *

Notes

+ *
    + *
  • Supported modes: PKCS#1 v1.5 and RSASSA-PSS. If {@code pssSaltLen} in the + * spec is -1, the salt length defaults to the digest size.
  • + *
  • {@link #tagLength()} equals the RSA modulus length in bytes.
  • + *
  • Instances are single-use and not thread-safe. Calling + * {@link #wrap(InputStream)} more than once throws + * {@link IllegalStateException}.
  • + *
+ */ +public final class RsaSignatureContext implements SignatureContext { + private static final Logger LOG = Logger.getLogger(RsaSignatureContext.class.getName()); + + private final CryptoAlgorithm algorithm; + private final Key key; // java.security.Key (PrivateKey or PublicKey) + private final boolean signMode; // true = SIGN, false = VERIFY + private final Signature engine; + private final int tagLen; // signature length in bytes (modulus size) + + // verification configuration (VERIFY mode) + private byte[] expectedTag; + private VerificationBiPredicate verificationStrategy; + + // lifecycle + private boolean wrapped; // = false; + private Stream activeStream; + private boolean autoCloseActiveStream; + + /** + * Constructs a signing context using the given private key and signature + * specification. + * + *

+ * The context operates in SIGN mode. At end-of-stream, a signature of length + * {@link #tagLength()} is produced and emitted as a trailer by the wrapped + * stream. + *

+ * + * @param alg algorithm descriptor owning this context; must not be + * {@code null} + * @param k private key used for signing; must not be {@code null} + * @param spec RSA signature specification (PKCS#1 v1.5 or PSS); must not be + * {@code null} + * @throws GeneralSecurityException if the JCA signature engine cannot be + * initialized + * @throws NullPointerException if any argument is {@code null} + */ + public RsaSignatureContext(final CryptoAlgorithm alg, final java.security.PrivateKey k, final RsaSigSpec spec) + throws GeneralSecurityException { + this.algorithm = Objects.requireNonNull(alg, "algorithm"); + this.key = Objects.requireNonNull(k, "private key"); + this.signMode = true; + this.engine = initEngine(spec, true, k); + this.tagLen = modulusBytes(k); + } + + /** + * Constructs a verification context using the given public key and signature + * specification. + * + *

+ * The context operates in VERIFY mode. Provide the expected signature via + * {@link #setExpectedTag(byte[])} before the wrapped stream reaches EOF. Final + * verification occurs at EOF using the configured verification approach. + *

+ * + * @param alg algorithm descriptor owning this context; must not be + * {@code null} + * @param k public key used for verification; must not be {@code null} + * @param spec RSA signature specification (PKCS#1 v1.5 or PSS); must not be + * {@code null} + * @throws GeneralSecurityException if the JCA signature engine cannot be + * initialized + * @throws NullPointerException if any argument is {@code null} + */ + public RsaSignatureContext(final CryptoAlgorithm alg, final java.security.PublicKey k, final RsaSigSpec spec) + throws GeneralSecurityException { + this.algorithm = Objects.requireNonNull(alg, "algorithm"); + this.key = Objects.requireNonNull(k, "public key"); + this.signMode = false; + this.engine = initEngine(spec, false, k); + this.tagLen = modulusBytes(k); + } + + private static Signature initEngine(final RsaSigSpec spec, final boolean sign, final Key k) + throws GeneralSecurityException { + Signature s = newEngine(spec); + if (sign) { + s.initSign((java.security.PrivateKey) k); + } else { + s.initVerify((java.security.PublicKey) k); + } + configurePssIfNeeded(s, spec); + return s; + } + + private static Signature newEngine(final RsaSigSpec spec) throws NoSuchAlgorithmException { + if (spec.mode() == RsaSigSpec.Mode.PKCS1V15) { + return Signature.getInstance(jcaSigName(spec.hash())); + } + return Signature.getInstance("RSASSA-PSS"); + } + + private static String jcaSigName(final RsaSigSpec.Hash h) { + return switch (h) { + case SHA256 -> "SHA256withRSA"; + case SHA384 -> "SHA384withRSA"; + case SHA512 -> "SHA512withRSA"; + }; + } + + private static String jcaHash(final RsaSigSpec.Hash h) { + return switch (h) { + case SHA256 -> "SHA-256"; + case SHA384 -> "SHA-384"; + case SHA512 -> "SHA-512"; + }; + } + + private static MGF1ParameterSpec mgf1(final RsaSigSpec.Hash h) { + return switch (h) { + case SHA256 -> MGF1ParameterSpec.SHA256; + case SHA384 -> MGF1ParameterSpec.SHA384; + case SHA512 -> MGF1ParameterSpec.SHA512; + }; + } + + private static void configurePssIfNeeded(final Signature s, final RsaSigSpec spec) + throws InvalidAlgorithmParameterException { + if (spec.mode() == RsaSigSpec.Mode.PSS) { + final int salt = (spec.pssSaltLen() == -1) ? digestLen(spec.hash()) : spec.pssSaltLen(); + final PSSParameterSpec pss = new PSSParameterSpec(jcaHash(spec.hash()), "MGF1", mgf1(spec.hash()), salt, 1); + s.setParameter(pss); + } + } + + private static int digestLen(final RsaSigSpec.Hash h) { + return switch (h) { + case SHA256 -> 32; + case SHA384 -> 48; + case SHA512 -> 64; + }; + } + + private static int modulusBytes(final Key k) { + return switch (k) { + case RSAPrivateKey rk -> (rk.getModulus().bitLength() + 7) / 8; + case RSAPublicKey rk -> (rk.getModulus().bitLength() + 7) / 8; + default -> throw new IllegalArgumentException("Not an RSA key: " + k.getClass().getName()); + }; + } + + /** + * Returns the algorithm descriptor associated with this context. + * + * @return the owning algorithm + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + *

+ * This is a private key in SIGN mode and a public key in VERIFY mode. + *

+ * + * @return the RSA key used by this context + */ + @Override + public Key key() { + return key; + } + + /** + * Closes the context and its active stream if present. + * + *

+ * If a wrapped stream is active, this method attempts to close it, which + * triggers finalization of signing or verification. + *

+ */ + @Override + public void close() { + LOG.log(Level.INFO, "close"); + + if (autoCloseActiveStream) { + try { + if (activeStream != null) { + activeStream.close(); + } + } catch (IOException ignore) { + LOG.log(Level.INFO, "exception ignored on close", ignore); + } + } + } + + /** + * Wraps an upstream stream and returns a passthrough stream that performs + * signing or verification. + * + *

+ * The returned stream relays bytes from {@code upstream} while updating the + * signature engine. On end-of-stream: + *

+ *
    + *
  • SIGN: computes the signature and emits it as a trailer.
  • + *
  • VERIFY: verifies against the {@linkplain #setExpectedTag(byte[]) + * expected tag} and signals the outcome according to + * {@link #setVerificationApproach(VerificationBiPredicate)}.
  • + *
+ * + *

+ * A context instance is single-use; calling this method twice throws + * {@link IllegalStateException}. + *

+ * + * @param upstream source stream to wrap + * @return a new input stream that performs signing or verification as data is + * read + * @throws NullPointerException if {@code upstream} is {@code null} + * @throws IllegalStateException if this context was already used for a previous + * stream + * @throws IOException if the signature engine update or finalization + * fails + */ + @Override + public InputStream wrap(final InputStream upstream) throws IOException { + Objects.requireNonNull(upstream, "upstream"); + if (wrapped) { + throw new IllegalStateException( + "This RsaSignatureContext instance was already used; create a new one per stream."); + } + wrapped = true; + Stream s = new Stream(upstream, verifier()); + this.activeStream = s; + return s; + } + + /** + * Returns the signature length in bytes. + * + *

+ * This equals the RSA modulus size in bytes and is the length of the trailer + * emitted in SIGN mode or the expected tag length in VERIFY mode. + *

+ * + * @return signature length in bytes + */ + @Override + public int tagLength() { + return tagLen; + } + + /** + * Sets the expected signature for verification mode. + * + *

+ * Only meaningful in VERIFY mode. The array is copied defensively. If set to + * {@code null}, verification will fail unless the chosen verification approach + * records and ignores failures. + *

+ * + * @param expected expected signature bytes or {@code null} to clear + */ + @Override + public void setExpectedTag(final byte[] expected) { + if (!signMode) { + this.expectedTag = (expected == null) ? null : Arrays.copyOf(expected, expected.length); + } + } + + /** + * Passthrough stream that feeds data into the RSA {@link Signature} engine and + * finalizes signing or verification at EOF (or on draining {@link #close()} in + * the base). + * + *

+ * Modes + *

+ *
    + *
  • SIGN: relays data unchanged and, after EOF, emits the computed + * signature as a single trailer.
  • + *
  • VERIFY: relays data unchanged and, after EOF, verifies against the + * expected tag according to policy; no trailer is emitted.
  • + *
+ * + *

Lifecycle mapping

+ *
    + *
  • {@link #update(byte[], int, int)} - feeds chunks into the + * {@code Signature} engine.
  • + *
  • {@link #produceTrailer(byte[])} - SIGN only: computes and emits the + * signature once; VERIFY: returns 0.
  • + *
  • {@link #onCompleted()} - runs exactly once immediately after the trailer + * attempt; VERIFY-only verification.
  • + *
+ */ + private final class Stream extends AbstractPassthroughInputStream { + /** Cached signature for trailer emission in SIGN mode. */ + private byte[] signature; + + /* default */ final VerificationBiPredicate verifier; + + private Stream(final InputStream upstream, final VerificationBiPredicate verifier) { + super(upstream, 8192); + this.verifier = verifier; + } + + /** + * Feeds the provided buffer segment into the signature engine. + * + * @param buf input buffer containing data to sign/verify + * @param off offset of first byte + * @param len number of bytes + * @throws IOException if the signature engine rejects the update + */ + @Override + protected void update(final byte[] buf, final int off, final int len) throws IOException { + try { + engine.update(buf, off, len); + } catch (SignatureException e) { + throw new IOException("RSA update failed", e); + } + } + + /** + * Single-shot trailer production. + * + *

+ * SIGN mode: compute signature and emit it. VERIFY mode: do not emit a trailer. + *

+ * + * @param buf destination buffer to receive the trailer + * @return number of bytes written; 0 to emit no trailer + * @throws IOException if signature generation fails or the trailer does not fit + */ + @Override + protected int produceTrailer(byte[] buf) throws IOException { + if (!signMode) { + return 0; // VERIFY mode never emits a trailer + } + if (signature == null) { + try { + signature = engine.sign(); + } catch (GeneralSecurityException e) { + throw new IOException("RSA finalize failed", e); + } + } + if (signature.length == 0) { + return 0; + } + if (signature.length > buf.length) { + throw new IOException("Trailer does not fit into buffer"); + } + System.arraycopy(signature, 0, buf, 0, signature.length); + return signature.length; + } + + /** + * Verification routine; invoked exactly once after trailer production attempt. + * Returns immediately in SIGN mode. + */ + @Override + protected void onCompleted() throws IOException { + if (signMode) { + return; // nothing to do for signing + } + try { + verifier.verify(engine, expectedTag); + } catch (VerificationException e) { + throw new IOException(e); + } + } + } + + /** + * Sets the verification approach used to handle comparison outcomes in VERIFY + * mode. + * + *

+ * Common strategies include constant-time byte comparison via + * {@link ByteVerificationStrategy} and JCA-signature-based checks via + * {@link SignatureVerificationStrategy}. Decorators such as + * {@link ThrowingBiPredicate.VerificationBiPredicate#getThrowOnMismatch()} and + * {@link ThrowingBiPredicate.VerificationBiPredicate#getFlagOkInCtx(conflux.CtxInterface, conflux.Key)} + * can be used to throw on mismatch or to record a boolean in a context. + *

+ * + * @param strategy verification predicate; may be {@code null} to keep the + * default + */ + @Override + public void setVerificationApproach(VerificationBiPredicate strategy) { + verificationStrategy = strategy; + } + + /** + * Returns the core verification predicate for RSA signatures. + * + *

+ * The default behavior is to throw on mismatch, implemented by decorating the + * returned predicate with {@code getThrowOnMismatch()} when the context is used + * and no custom strategy has been supplied. + *

+ * + * @return a predicate that delegates to {@link Signature#verify(byte[])} + */ + @Override + public VerificationBiPredicate getVerificationCore() { + return new SignatureVerificationStrategy(); + } + + private VerificationBiPredicate verifier() { + return verificationStrategy == null ? getVerificationCore().getThrowOnMismatch() : verificationStrategy; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/rsa/package-info.java b/lib/src/main/java/zeroecho/core/alg/rsa/package-info.java new file mode 100644 index 0000000..83e9c0c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/rsa/package-info.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * RSA encryption and signature integration. + * + *

+ * This package wires RSA into the core layer, covering asymmetric encryption + * (OAEP or PKCS#1 v1.5) and digital signatures (PKCS#1 v1.5 or RSASSA-PSS). It + * provides the algorithm descriptor, streaming contexts for cipher and + * signature operations, small utilities for block sizing, and key + * specifications for generation and encoded-key import. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register RSA with roles for ENCRYPT/DECRYPT and SIGN/VERIFY and expose + * defaults that favor OAEP (SHA-256) and PSS (SHA-256).
  • + *
  • Provide streaming contexts that attach to input streams and transform + * data on-the-fly without buffering entire messages.
  • + *
  • Expose immutable specifications for cipher padding, signature mode and + * hashing, and key generation parameters.
  • + *
  • Offer encoded key specs for X.509 (public) and PKCS#8 (private) with + * defensive copying and compact marshalling helpers.
  • + *
+ * + *

Components

+ *
    + *
  • RsaAlgorithm: algorithm descriptor that declares capabilities, + * selects secure defaults, and registers key builders.
  • + *
  • RsaCipherContext: streaming encryption/decryption context that + * initializes an RSA cipher according to {@code RsaEncSpec} and enforces block + * geometry.
  • + *
  • RsaSignatureContext: streaming sign/verify context driven by + * {@code RsaSigSpec}, emitting a trailer in SIGN mode or verifying at + * end-of-stream in VERIFY mode.
  • + *
  • BlockGeometry: utility describing per-block input/output sizes + * derived from modulus length, padding, and direction.
  • + *
  • RsaEncSpec: cipher specification (OAEP with selectable hash and + * optional label, or PKCS#1 v1.5).
  • + *
  • RsaSigSpec: signature specification (PKCS#1 v1.5 or PSS with + * selectable hash and salt length).
  • + *
  • RsaKeyGenSpec: key generation parameters (modulus size and public + * exponent).
  • + *
  • RsaPublicKeySpec and RsaPrivateKeySpec: encoded-key + * carriers for import/export.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and thread-safe.
  • + *
  • Streaming contexts are stateful and not thread-safe; create a new + * instance per pipeline.
  • + *
  • Defaults prioritize modern schemes (OAEP, PSS). Legacy PKCS#1 v1.5 is + * available primarily for interoperability and should be used with care.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.rsa; diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java new file mode 100644 index 0000000..e909b61 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberAlgorithm.java @@ -0,0 +1,243 @@ +/******************************************************************************* + * 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.core.alg.saber; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.KemMessageAgreementAdapter; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * Implements the SABER post-quantum key encapsulation mechanism for the crypto + * catalog and wires it to the Bouncy Castle PQC provider. + * + *

Capabilities

This algorithm exposes two capability families: + *
    + *
  • KEM: encapsulation with a recipient {@code PublicKey} and decapsulation + * with a {@code PrivateKey} using {@link KemContext}.
  • + *
  • AGREEMENT: message-based agreement built on top of KEM via a + * {@code KemMessageAgreementAdapter}, returning a + * {@link MessageAgreementContext} for both initiator and responder flows.
  • + *
+ * The agreement capability simply adapts the underlying KEM operation into a + * message-oriented interface suitable for one-pass key agreement. + * + *

Key material import and generation

The algorithm registers + * {@code AsymmetricKeyBuilder} implementations for: + *
    + *
  • {@code SaberKeyGenSpec}: generates SABER key pairs for the requested + * parameter set using {@code KeyPairGenerator} from the provider.
  • + *
  • {@code zeroecho.core.alg.saber.SaberPublicKeySpec}: imports an + * X.509-encoded SABER public key.
  • + *
  • {@code zeroecho.core.alg.saber.SaberPrivateKeySpec}: imports a + * PKCS#8-encoded SABER private key.
  • + *
+ * + *

+ * Instances of this class are lightweight and stateless after construction. All + * provider lookups are performed on demand. + * + *

+ * Example

{@code
+ * CryptoAlgorithm saber = new SaberAlgorithm();
+ *
+ * // Generate a key pair for a specific SABER variant
+ * KeyPair kp = saber.build(SaberKeyGenSpec.saberkem256r3()).generateKeyPair();
+ *
+ * // Encapsulation (initiator)
+ * KemContext enc = saber.create(KemContext.class, kp.getPublic(), VoidSpec.INSTANCE);
+ * KemResult out = enc.encapsulate();
+ *
+ * // Decapsulation (responder)
+ * KemContext dec = saber.create(KemContext.class, kp.getPrivate(), VoidSpec.INSTANCE);
+ * SecretKey k = dec.decapsulate(out.ciphertext());
+ * }
+ */ +public final class SaberAlgorithm extends AbstractCryptoAlgorithm { + /** + * Creates a SABER algorithm instance backed by the Bouncy Castle PQC provider + * and registers KEM and AGREEMENT capabilities, along with key generation and + * key import builders for SABER public and private keys. + * + *

+ * The constructor: + *

    + *
  • Registers KEM encapsulate and decapsulate capabilities bound to + * {@link KemContext} and {@code SaberKemContext}.
  • + *
  • Registers AGREEMENT capabilities that adapt KEM to + * {@link MessageAgreementContext} for initiator and responder roles.
  • + *
  • Registers asymmetric key builders for generating SABER key pairs via + * {@code SaberKeyGenSpec} and importing X.509 and PKCS#8 encodings.
  • + *
+ * + *

+ * Example

{@code
+     * SaberAlgorithm alg = new SaberAlgorithm();
+     * }
+ */ + public SaberAlgorithm() { + super("SABER", "SABER", BouncyCastlePQCProvider.PROVIDER_NAME); + + capability(AlgorithmFamily.KEM, KeyUsage.ENCAPSULATE, KemContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> new SaberKemContext(this, k), () -> VoidSpec.INSTANCE); + capability(AlgorithmFamily.KEM, KeyUsage.DECAPSULATE, KemContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> new SaberKemContext(this, k), () -> VoidSpec.INSTANCE); + + // AGREEMENT (initiator): Alice has Bob's public key → encapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← return your + // existing KemContext + PublicKey.class, // ← initiator uses recipient's public key + VoidSpec.class, // ← must implement ContextSpec + (PublicKey recipient, VoidSpec spec) -> { + // create a context bound to recipient public key for encapsulation + return KemMessageAgreementAdapter.builder().upon(new SaberKemContext(this, recipient)).asInitiator() + .build(); + }, () -> VoidSpec.INSTANCE // default + ); + + // AGREEMENT (responder): Bob has his private key → decapsulate + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, MessageAgreementContext.class, // ← same KemContext + // type + PrivateKey.class, // ← responder uses their private key + VoidSpec.class, (PrivateKey myPriv, VoidSpec spec) -> { + return KemMessageAgreementAdapter.builder().upon(new SaberKemContext(this, myPriv)).asResponder() + .build(); + }, () -> VoidSpec.INSTANCE); + + registerAsymmetricKeyBuilder(SaberKeyGenSpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(SaberKeyGenSpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("SABER", providerName()); + org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec params = switch (spec.variant()) { + case LIGHTSABERKEM128R3 -> org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec.lightsaberkem128r3; + case SABERKEM128R3 -> org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec.saberkem128r3; + case FIRESABERKEM128R3 -> org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec.firesaberkem128r3; + + case LIGHTSABERKEM192R3 -> org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec.lightsaberkem192r3; + case SABERKEM192R3 -> org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec.saberkem192r3; + case FIRESABERKEM192R3 -> org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec.firesaberkem192r3; + + case LIGHTSABERKEM256R3 -> org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec.lightsaberkem256r3; + case SABERKEM256R3 -> org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec.saberkem256r3; + case FIRESABERKEM256R3 -> org.bouncycastle.pqc.jcajce.spec.SABERParameterSpec.firesaberkem256r3; + }; + + kpg.initialize(params, new SecureRandom()); + return kpg.generateKeyPair(); + } + + @Override + public PublicKey importPublic(SaberKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(SaberKeyGenSpec spec) { + throw new UnsupportedOperationException(); + } + }, SaberKeyGenSpec::saberkem256r3); + + registerAsymmetricKeyBuilder(SaberPublicKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(SaberPublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(SaberPublicKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("SABER", providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.x509())); + } + + @Override + public PrivateKey importPrivate(SaberPublicKeySpec spec) { + throw new UnsupportedOperationException(); + } + }, null); + + registerAsymmetricKeyBuilder(SaberPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + @Override + public KeyPair generateKeyPair(SaberPrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PublicKey importPublic(SaberPrivateKeySpec spec) { + throw new UnsupportedOperationException(); + } + + @Override + public PrivateKey importPrivate(SaberPrivateKeySpec spec) throws GeneralSecurityException { + ensureProvider(); + KeyFactory kf = KeyFactory.getInstance("SABER", providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.pkcs8())); + } + }, null); + } + + /** + * Ensures that the Bouncy Castle PQC provider is present in the current + * {@link Security} providers registry. + * + * @throws NoSuchProviderException if the provider is not registered + */ + private static void ensureProvider() throws NoSuchProviderException { + Provider p = Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME); + if (p == null) { + throw new NoSuchProviderException("BCPQC provider not registered"); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberKemContext.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberKemContext.java new file mode 100644 index 0000000..38c8b3e --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberKemContext.java @@ -0,0 +1,224 @@ +/******************************************************************************* + * 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.core.alg.saber; + +import java.io.IOException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Objects; + +import javax.security.auth.DestroyFailedException; + +import org.bouncycastle.crypto.SecretWithEncapsulation; +import org.bouncycastle.pqc.crypto.saber.SABERKEMExtractor; +import org.bouncycastle.pqc.crypto.saber.SABERKEMGenerator; +import org.bouncycastle.pqc.crypto.saber.SABERPrivateKeyParameters; +import org.bouncycastle.pqc.crypto.saber.SABERPublicKeyParameters; +import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory; +import org.bouncycastle.pqc.crypto.util.PublicKeyFactory; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.KemContext; + +/** + * KEM context for SABER supporting encapsulation with a public key and + * decapsulation with a private key. + * + *

+ * A {@code SaberKemContext} is created in one of two roles: + *

+ *
    + *
  • Encapsulator: constructed with a {@code PublicKey}, it produces an + * encapsulation ciphertext and a fresh shared secret.
  • + *
  • Decapsulator: constructed with a {@code PrivateKey}, it derives + * the shared secret from a received encapsulation ciphertext.
  • + *
+ * + *

Lifecycle and thread-safety

+ *

+ * Instances are single-use per operation style and not thread-safe. Callers + * should not share a context across threads without external synchronization. + * {@link #close()} is a no-op because this context maintains no stream + * resources. + *

+ * + *

Usage example

{@code
+ * // Encapsulator side
+ * KemContext enc = new SaberKemContext(algo, recipientPublicKey);
+ * KemContext.KemResult r = enc.encapsulate();
+ * byte[] ciphertext = r.ciphertext();
+ * byte[] shared = r.sharedSecret();
+ *
+ * // Decapsulator side
+ * KemContext dec = new SaberKemContext(algo, myPrivateKey);
+ * byte[] shared2 = dec.decapsulate(ciphertext);
+ * }
+ */ +public final class SaberKemContext implements KemContext { + private final CryptoAlgorithm algorithm; + private final Key key; + private final boolean encapsulate; + + /** + * Creates an encapsulation context bound to the recipient's {@link PublicKey}. + * + * @param algorithm the owning algorithm used for metadata and provider identity + * @param k recipient public key for SABER encapsulation + * @throws NullPointerException if {@code algorithm} or {@code k} is + * {@code null} + */ + public SaberKemContext(CryptoAlgorithm algorithm, PublicKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = true; + } + + /** + * Creates a decapsulation context bound to our {@link PrivateKey}. + * + * @param algorithm the owning algorithm used for metadata and provider identity + * @param k private key for SABER decapsulation + * @throws NullPointerException if {@code algorithm} or {@code k} is + * {@code null} + */ + public SaberKemContext(CryptoAlgorithm algorithm, PrivateKey k) { + this.algorithm = Objects.requireNonNull(algorithm); + this.key = Objects.requireNonNull(k); + this.encapsulate = false; + } + + /** + * Returns the algorithm that created this context. + * + * @return the associated {@link CryptoAlgorithm} + */ + @Override + public CryptoAlgorithm algorithm() { + return algorithm; + } + + /** + * Returns the key bound to this context. + * + *

+ * For encapsulation this is a {@link PublicKey}. For decapsulation this is a + * {@link PrivateKey}. + *

+ * + * @return the bound {@link Key} + */ + @Override + public Key key() { + return key; + } + + /** + * Closes the context and releases resources. + * + *

+ * This implementation holds no stream resources, so the method is a no-op. + *

+ */ + @Override + public void close() { + // empty + } + + /** + * Performs SABER encapsulation and returns the ciphertext and shared secret. + * + *

+ * This method is valid only when the context was constructed with a + * {@link PublicKey}. The returned {@link KemResult} contains the encapsulation + * to transmit and the derived shared secret. + *

+ * + * @return a {@link KemResult} holding the encapsulation and shared secret + * @throws IllegalStateException if the context was initialized for + * decapsulation + * @throws IOException if the underlying SABER operation fails + */ + @Override + public KemResult encapsulate() throws IOException { + if (!encapsulate) { + throw new IllegalStateException("Not initialized for ENCAPSULATE"); + } + try { + final SABERPublicKeyParameters keyParam = (SABERPublicKeyParameters) PublicKeyFactory + .createKey(key.getEncoded()); + SABERKEMGenerator gen = new SABERKEMGenerator(new SecureRandom()); + SecretWithEncapsulation res = gen.generateEncapsulated(keyParam); + byte[] secret = res.getSecret(); + byte[] ct = res.getEncapsulation(); + res.destroy(); + return new KemResult(ct, secret); + } catch (DestroyFailedException e) { + throw new IOException("SABER encapsulate failed", e); + } + } + + /** + * Performs SABER decapsulation and returns the shared secret derived from + * {@code ciphertext}. + * + *

+ * This method is valid only when the context was constructed with a + * {@link PrivateKey}. + *

+ * + * @param ciphertext the peer's encapsulation ciphertext + * @return the derived shared secret bytes + * @throws IllegalStateException if the context was initialized for + * encapsulation + * @throws IOException if the underlying SABER operation fails or + * input is invalid + */ + @Override + public byte[] decapsulate(byte[] ciphertext) throws IOException { + if (encapsulate) { + throw new IllegalStateException("Not initialized for DECAPSULATE"); + } + try { + final SABERPrivateKeyParameters keyParam = (SABERPrivateKeyParameters) PrivateKeyFactory + .createKey(key.getEncoded()); + SABERKEMExtractor ex = new SABERKEMExtractor(keyParam); + return ex.extractSecret(ciphertext); + } catch (Exception e) { // NOPMD + throw new IOException("SABER decapsulate failed", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberKeyGenSpec.java new file mode 100644 index 0000000..4d77a25 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberKeyGenSpec.java @@ -0,0 +1,284 @@ +/******************************************************************************* + * 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.core.alg.saber; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Key generation specification for the SABER post-quantum KEM algorithm. + * + *

+ * A {@code SaberKeyGenSpec} selects one of the SABER parameter variants + * standardized in round-3 submissions. Each variant balances performance, + * bandwidth, and security level. This spec is passed to a registered + * {@link zeroecho.core.spi.AsymmetricKeyBuilder} to generate a SABER key pair. + *

+ * + *

Variants

The {@link Variant} enumeration identifies supported SABER + * parameter sets: + *
    + *
  • {@link Variant#LIGHTSABERKEM128R3}: lightweight security, smallest key + * sizes.
  • + *
  • {@link Variant#SABERKEM128R3}: standard security level, moderate key + * sizes.
  • + *
  • {@link Variant#FIRESABERKEM128R3}: strongest within 128-bit category, + * largest key sizes.
  • + *
  • {@link Variant#LIGHTSABERKEM192R3}, {@link Variant#SABERKEM192R3}, + * {@link Variant#FIRESABERKEM192R3}: parameter sets targeting ~192-bit + * security.
  • + *
  • {@link Variant#LIGHTSABERKEM256R3}, {@link Variant#SABERKEM256R3}, + * {@link Variant#FIRESABERKEM256R3}: parameter sets targeting ~256-bit + * security.
  • + *
+ * + *

Usage example

{@code
+ * SaberKeyGenSpec spec = SaberKeyGenSpec.saberkem256r3();
+ * KeyPair kp = CryptoAlgorithms.keyPair("SABER", spec);
+ * }
+ * + * @since 1.0 + */ +public final class SaberKeyGenSpec implements AlgorithmKeySpec, Describable { + /** + * Enumeration of SABER parameter set variants. + * + *

+ * Each constant maps directly to a standardized SABER submission parameter set. + * They differ in module rank, polynomial dimension, and security level. + *

+ */ + public enum Variant { + /** + * LightSaber KEM, round-3, targeting ~128-bit security with smallest footprint. + */ + LIGHTSABERKEM128R3, + /** Saber KEM, round-3, baseline 128-bit security parameter set. */ + SABERKEM128R3, + /** FireSaber KEM, round-3, strong 128-bit security with larger parameters. */ + FIRESABERKEM128R3, + /** LightSaber KEM, round-3, targeting ~192-bit security. */ + LIGHTSABERKEM192R3, + /** Saber KEM, round-3, baseline 192-bit security parameter set. */ + SABERKEM192R3, + /** FireSaber KEM, round-3, strong 192-bit security with larger parameters. */ + FIRESABERKEM192R3, + /** LightSaber KEM, round-3, targeting ~256-bit security. */ + LIGHTSABERKEM256R3, + /** Saber KEM, round-3, baseline 256-bit security parameter set. */ + SABERKEM256R3, + /** FireSaber KEM, round-3, strong 256-bit security with larger parameters. */ + FIRESABERKEM256R3 + } + + private final Variant variant; + + private SaberKeyGenSpec(Variant v) { + this.variant = v; + } + + /** + * Creates a specification for a given variant. + * + * @param v the SABER variant + * @return new key generation spec for the variant + */ + public static SaberKeyGenSpec of(Variant v) { + return new SaberKeyGenSpec(v); + } + + /** + * Creates a specification for the LightSaberKEM128R3 variant. + * + *

+ * This is the smallest SABER parameter set at the 128-bit security level, + * optimized for constrained environments. It trades some performance margin for + * reduced bandwidth and memory footprint. + *

+ * + * @return a new {@code SaberKeyGenSpec} representing the LightSaberKEM128R3 + * variant + */ + public static SaberKeyGenSpec lightsaberkem128r3() { + return new SaberKeyGenSpec(Variant.LIGHTSABERKEM128R3); + } + + /** + * Creates a specification for the SaberKEM128R3 variant. + * + *

+ * This is the baseline SABER parameter set at the 128-bit security level, + * balancing efficiency and security margin. It is the recommended choice for + * standard 128-bit use cases. + *

+ * + * @return a new {@code SaberKeyGenSpec} representing the SaberKEM128R3 variant + */ + public static SaberKeyGenSpec saberkem128r3() { + return new SaberKeyGenSpec(Variant.SABERKEM128R3); + } + + /** + * Creates a specification for the FireSaberKEM128R3 variant. + * + *

+ * This is the strongest SABER parameter set at the 128-bit security level, + * using larger dimensions to maximize security margin. It comes with higher + * computational and bandwidth cost than LightSaber or Saber. + *

+ * + * @return a new {@code SaberKeyGenSpec} representing the FireSaberKEM128R3 + * variant + */ + public static SaberKeyGenSpec firesaberkem128r3() { + return new SaberKeyGenSpec(Variant.FIRESABERKEM128R3); + } + + /** + * Creates a specification for the LightSaberKEM192R3 variant. + * + *

+ * This variant targets approximately 192-bit security with minimized resource + * usage. It is designed for higher security requirements while still + * considering efficiency. + *

+ * + * @return a new {@code SaberKeyGenSpec} representing the LightSaberKEM192R3 + * variant + */ + public static SaberKeyGenSpec lightsaberkem192r3() { + return new SaberKeyGenSpec(Variant.LIGHTSABERKEM192R3); + } + + /** + * Creates a specification for the SaberKEM192R3 variant. + * + *

+ * This is the balanced SABER parameter set at the 192-bit security level, + * offering a midpoint between performance and conservative security margin. + *

+ * + * @return a new {@code SaberKeyGenSpec} representing the SaberKEM192R3 variant + */ + public static SaberKeyGenSpec saberkem192r3() { + return new SaberKeyGenSpec(Variant.SABERKEM192R3); + } + + /** + * Creates a specification for the FireSaberKEM192R3 variant. + * + *

+ * This is the strongest SABER configuration at the 192-bit security level, + * providing the highest margin at the cost of larger keys and slower + * operations. + *

+ * + * @return a new {@code SaberKeyGenSpec} representing the FireSaberKEM192R3 + * variant + */ + public static SaberKeyGenSpec firesaberkem192r3() { + return new SaberKeyGenSpec(Variant.FIRESABERKEM192R3); + } + + /** + * Creates a specification for the LightSaberKEM256R3 variant. + * + *

+ * This variant targets approximately 256-bit security with reduced parameter + * sizes compared to the full Saber/FireSaber sets. It favors efficiency in very + * high-security applications. + *

+ * + * @return a new {@code SaberKeyGenSpec} representing the LightSaberKEM256R3 + * variant + */ + public static SaberKeyGenSpec lightsaberkem256r3() { + return new SaberKeyGenSpec(Variant.LIGHTSABERKEM256R3); + } + + /** + * Creates a specification for the SaberKEM256R3 variant. + * + *

+ * This is the balanced SABER parameter set at the 256-bit security level, + * recommended for long-term, high-assurance use cases where efficiency is still + * important. + *

+ * + * @return a new {@code SaberKeyGenSpec} representing the SaberKEM256R3 variant + */ + public static SaberKeyGenSpec saberkem256r3() { + return new SaberKeyGenSpec(Variant.SABERKEM256R3); + } + + /** + * Creates a specification for the FireSaberKEM256R3 variant. + * + *

+ * This is the strongest SABER configuration at the 256-bit security level, + * prioritizing maximum security margin over efficiency. It uses the largest + * parameter sizes and yields the heaviest resource usage. + *

+ * + * @return a new {@code SaberKeyGenSpec} representing the FireSaberKEM256R3 + * variant + */ + public static SaberKeyGenSpec firesaberkem256r3() { + return new SaberKeyGenSpec(Variant.FIRESABERKEM256R3); + } + + /** + * Returns the variant this spec represents. + * + * @return selected SABER variant + */ + public Variant variant() { + return variant; + } + + /** + * Returns a textual description of this spec. + * + *

+ * Currently this is the {@link Variant} name, e.g. {@code SABERKEM256R3}. + *

+ * + * @return string description of the spec + */ + @Override + public String description() { + return variant.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java new file mode 100644 index 0000000..9781360 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberPrivateKeySpec.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * 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.core.alg.saber; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Encoded private key specification for the SABER post-quantum KEM algorithm. + * + *

+ * A {@code SaberPrivateKeySpec} wraps a SABER private key in standard PKCS#8 + * format. It provides safe accessors for cloning, along with serialization + * helpers to marshal and unmarshal from a compact {@link PairSeq} + * representation. + *

+ * + *

Immutability

+ *

+ * The internal PKCS#8 encoding is defensively copied on construction and on + * retrieval via {@link #pkcs8()}. Instances are therefore immutable and + * thread-safe. + *

+ * + *

Usage example

{@code
+ * // Import SABER private key from encoded form
+ * byte[] pkcs8Bytes = ...;
+ * SaberPrivateKeySpec spec = new SaberPrivateKeySpec(pkcs8Bytes);
+ * PrivateKey key = CryptoAlgorithms.privateKey("SABER", spec);
+ * }
+ */ +public final class SaberPrivateKeySpec implements AlgorithmKeySpec { + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Constructs a private key specification from a PKCS#8-encoded SABER private + * key. + * + * @param pkcs8Der the PKCS#8-encoded private key bytes (DER format) + * @throws NullPointerException if {@code pkcs8Der} is {@code null} + */ + public SaberPrivateKeySpec(byte[] pkcs8Der) { + this.pkcs8 = Objects.requireNonNull(pkcs8Der, "pkcs8Der").clone(); + } + + /** + * Returns a defensive copy of the PKCS#8-encoded private key. + * + * @return cloned byte array containing the PKCS#8 encoding + */ + public byte[] pkcs8() { + return pkcs8.clone(); + } + + /** + * Serializes this specification into a compact {@link PairSeq} form. + * + *

+ * The encoding includes: + *

    + *
  • {@code type} = {@code SaberPrivateKeySpec}
  • + *
  • {@code pkcs8.b64} = Base64 encoding of the PKCS#8 bytes (no padding)
  • + *
+ * + * @param spec the private key specification to serialize + * @return a {@link PairSeq} containing the encoded data + * @throws NullPointerException if {@code spec} is {@code null} + */ + public static PairSeq marshal(SaberPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "SaberPrivateKeySpec", PKCS8_B64, b64); + } + + /** + * Deserializes a {@link SaberPrivateKeySpec} from its {@link PairSeq} form. + * + *

+ * The sequence must contain a field {@code pkcs8.b64} with the Base64-encoded + * PKCS#8 private key bytes. If the field is missing, an exception is thrown. + *

+ * + * @param p the pair sequence to parse + * @return a reconstructed {@link SaberPrivateKeySpec} + * @throws IllegalArgumentException if the {@code pkcs8.b64} field is missing + */ + public static SaberPrivateKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (PKCS8_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("SaberPrivateKeySpec: missing pkcs8.b64"); + } + return new SaberPrivateKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string showing the length of the encoded key. + * + *

+ * This string does not expose the key material itself and is safe for logs. + *

+ * + * @return string representation of this spec, including encoded length + */ + @Override + public String toString() { + return "SaberPrivateKeySpec[len=" + pkcs8.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java new file mode 100644 index 0000000..dc13624 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/saber/SaberPublicKeySpec.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.saber; + +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.marshal.PairSeq.Cursor; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Encoded public key specification for the SABER post-quantum KEM algorithm. + * + *

+ * A {@code SaberPublicKeySpec} wraps a SABER public key in standard X.509 + * SubjectPublicKeyInfo format. It provides safe accessors for cloning, along + * with serialization helpers to marshal and unmarshal from a compact + * {@link PairSeq} representation. + *

+ * + *

Immutability

+ *

+ * The internal X.509 encoding is defensively copied on construction and when + * returned by {@link #x509()}. Instances are therefore immutable and safe to + * share across threads. + *

+ * + *

Usage example

{@code
+ * // Import SABER public key from encoded form
+ * byte[] x509Bytes = ...;
+ * SaberPublicKeySpec spec = new SaberPublicKeySpec(x509Bytes);
+ * PublicKey key = CryptoAlgorithms.publicKey("SABER", spec);
+ * }
+ */ +public final class SaberPublicKeySpec implements AlgorithmKeySpec { + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Constructs a public key specification from an X.509-encoded SABER public key. + * + * @param x509Der the X.509 SubjectPublicKeyInfo bytes + * @throws NullPointerException if {@code x509Der} is {@code null} + */ + public SaberPublicKeySpec(byte[] x509Der) { + this.x509 = Objects.requireNonNull(x509Der, "x509Der").clone(); + } + + /** + * Returns a defensive copy of the X.509-encoded public key. + * + * @return cloned byte array containing the X.509 encoding + */ + public byte[] x509() { + return x509.clone(); + } + + /** + * Serializes this specification into a compact {@link PairSeq} form. + * + *

+ * The encoding includes: + *

    + *
  • {@code type} = {@code SaberPublicKeySpec}
  • + *
  • {@code x509.b64} = Base64 encoding of the X.509 bytes (no padding)
  • + *
+ * + * @param spec the public key specification to serialize + * @return a {@link PairSeq} containing the encoded data + * @throws NullPointerException if {@code spec} is {@code null} + */ + public static PairSeq marshal(SaberPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "SaberPublicKeySpec", X509_B64, b64); + } + + /** + * Deserializes a {@link SaberPublicKeySpec} from its {@link PairSeq} form. + * + *

+ * The sequence must contain a field {@code x509.b64} with the Base64-encoded + * X.509 public key bytes. If the field is missing, an exception is thrown. + *

+ * + * @param p the pair sequence to parse + * @return a reconstructed {@link SaberPublicKeySpec} + * @throws IllegalArgumentException if the {@code x509.b64} field is missing + */ + public static SaberPublicKeySpec unmarshal(PairSeq p) { + String b64 = null; + for (Cursor cur = p.cursor(); cur.next();) { + if (X509_B64.equals(cur.key())) { + b64 = cur.value(); + } + } + if (b64 == null) { + throw new IllegalArgumentException("SaberPublicKeySpec: missing x509.b64"); + } + return new SaberPublicKeySpec(Base64.getDecoder().decode(b64)); + } + + /** + * Returns a diagnostic string showing the length of the encoded key. + * + *

+ * This string does not expose the key material itself and is safe for logs. + *

+ * + * @return string representation of this spec, including encoded length + */ + @Override + public String toString() { + return "SaberPublicKeySpec[len=" + x509.length + "]"; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/saber/package-info.java b/lib/src/main/java/zeroecho/core/alg/saber/package-info.java new file mode 100644 index 0000000..d7fc0c0 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/saber/package-info.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * SABER post-quantum key encapsulation integration. + * + *

+ * This package wires the SABER KEM into the core layer. It provides the + * algorithm descriptor, a runtime KEM context, a key-generation specification + * selecting parameter variants, and encoded key specifications for import and + * export. Provider details are encapsulated behind small factories while roles + * and metadata remain explicit to higher layers. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register SABER under a canonical identifier and declare ENCAPSULATE and + * DECAPSULATE roles, with an optional message-style agreement adapter built on + * KEM.
  • + *
  • Provide a {@link zeroecho.core.context.KemContext} bound to a public key + * (encapsulation) or a private key (decapsulation).
  • + *
  • Expose immutable specifications for key generation and encoded-key + * carriers with compact marshalling helpers.
  • + *
  • Validate presence of a suitable PQC provider before performing + * operations.
  • + *
+ * + *

Components

+ *
    + *
  • SaberAlgorithm: algorithm descriptor that wires KEM and, when + * needed, a message-agreement adapter, and registers asymmetric key + * builders.
  • + *
  • SaberKemContext: runtime context implementing encapsulate and + * decapsulate using provider primitives.
  • + *
  • SaberKeyGenSpec: immutable selection of SABER parameter variants + * with simple factory methods.
  • + *
  • SaberPublicKeySpec and SaberPrivateKeySpec: wrappers over + * X.509 and PKCS#8 encodings with defensive copying and marshalling + * helpers.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and safe to share across + * threads.
  • + *
  • KEM contexts are stateful and not thread-safe; create one per + * operation.
  • + *
  • Key specifications preserve the provided encodings without internal + * parsing and clone byte arrays on input and output.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.saber; diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusAlgorithm.java new file mode 100644 index 0000000..30c9fa1 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusAlgorithm.java @@ -0,0 +1,160 @@ +/******************************************************************************* + * 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.core.alg.sphincsplus; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.spec.VoidSpec; + +/** + * SPHINCS+ signature algorithm binding for the ZeroEcho framework. + * + *

+ * {@code SphincsPlusAlgorithm} integrates the SPHINCS+ stateless hash-based + * signature scheme as defined in the NIST PQC standardization process. It + * registers signing and verification roles, along with asymmetric key builders + * for generation and import of SPHINCS+ key material. + *

+ * + *

Capabilities

+ *
    + *
  • {@link KeyUsage#SIGN}: produces SPHINCS+ signatures using a + * {@link java.security.PrivateKey}.
  • + *
  • {@link KeyUsage#VERIFY}: verifies SPHINCS+ signatures using a + * {@link java.security.PublicKey}.
  • + *
+ * + *

+ * Each role is bound to a {@link zeroecho.core.context.SignatureContext} + * created via {@link SphincsPlusSignatureContext}. Both roles are configured + * with a {@link zeroecho.core.spec.VoidSpec}, as SPHINCS+ requires no runtime + * context parameters. + *

+ * + *

Key builders

+ *
    + *
  • {@link SphincsPlusKeyGenSpec}: generate new key pairs with parameter set + * selection (e.g., SPHINCS+-SHAKE-256s, fast/robust variants).
  • + *
  • {@link SphincsPlusPublicKeySpec}: import X.509-encoded public keys.
  • + *
  • {@link SphincsPlusPrivateKeySpec}: import PKCS#8-encoded private + * keys.
  • + *
+ * + *

Example

{@code
+ * CryptoAlgorithm alg = new SphincsPlusAlgorithm();
+ * KeyPair kp = alg.asymmetricKeyBuilder(SphincsPlusKeyGenSpec.class)
+ *                 .generateKeyPair(SphincsPlusKeyGenSpec.sphincsPlusSha256_128s());
+ *
+ * try (SignatureContext signer =
+ *          alg.create(KeyUsage.SIGN, kp.getPrivate(), null)) {
+ *     signer.update(message);
+ *     byte[] sig = signer.sign();
+ * }
+ *
+ * try (SignatureContext verifier =
+ *          alg.create(KeyUsage.VERIFY, kp.getPublic(), null)) {
+ *     verifier.update(message);
+ *     boolean ok = verifier.verify(sig);
+ * }
+ * }
+ * + *

Thread-safety

Instances of this class are immutable and can be + * shared across threads. The created {@link SignatureContext} objects are not + * thread-safe. + * + * @since 1.0 + */ +public final class SphincsPlusAlgorithm extends AbstractCryptoAlgorithm { + /** + * Creates a new SPHINCS+ algorithm instance and registers its capabilities. + * + *

+ * The constructor binds the algorithm identifier {@code "SPHINCS+"} (with + * display name {@code "SPHINCSPLUS"}) to the following: + *

+ *
    + *
  • Capability for {@link KeyUsage#SIGN}, producing a + * {@link zeroecho.core.context.SignatureContext} from a + * {@link java.security.PrivateKey}.
  • + *
  • Capability for {@link KeyUsage#VERIFY}, producing a + * {@link zeroecho.core.context.SignatureContext} from a + * {@link java.security.PublicKey}.
  • + *
  • Asymmetric key builders for {@link SphincsPlusKeyGenSpec}, + * {@link SphincsPlusPublicKeySpec}, and {@link SphincsPlusPrivateKeySpec} to + * support key pair generation and key import from standard encodings.
  • + *
+ * + *

+ * Both signing and verifying contexts are configured with + * {@link zeroecho.core.spec.VoidSpec}, since SPHINCS+ requires no runtime + * parameters beyond the key material. + *

+ * + * @throws IllegalArgumentException if a signature context cannot be initialized + * due to provider errors + */ + public SphincsPlusAlgorithm() { + super("SPHINCS+", "SPHINCSPLUS"); + + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.SIGN, SignatureContext.class, PrivateKey.class, VoidSpec.class, + (PrivateKey k, VoidSpec s) -> { + try { + return new SphincsPlusSignatureContext(this, k); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Cannot init SPHINCS+ signer", e); + } + }, () -> VoidSpec.INSTANCE); + + capability(AlgorithmFamily.ASYMMETRIC, KeyUsage.VERIFY, SignatureContext.class, PublicKey.class, VoidSpec.class, + (PublicKey k, VoidSpec s) -> { + try { + return new SphincsPlusSignatureContext(this, k); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Cannot init SPHINCS+ verifier", e); + } + }, () -> VoidSpec.INSTANCE); + + registerAsymmetricKeyBuilder(SphincsPlusKeyGenSpec.class, new SphincsPlusKeyGenBuilder(), + SphincsPlusKeyGenSpec::defaultSpec); + registerAsymmetricKeyBuilder(SphincsPlusPublicKeySpec.class, new SphincsPlusPublicKeyBuilder(), null); + registerAsymmetricKeyBuilder(SphincsPlusPrivateKeySpec.class, new SphincsPlusPrivateKeyBuilder(), null); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java new file mode 100644 index 0000000..352cdb0 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenBuilder.java @@ -0,0 +1,216 @@ +/******************************************************************************* + * 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.core.alg.sphincsplus; + +import java.lang.reflect.Field; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Locale; +import java.util.Objects; + +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * Key pair builder for the SPHINCS+ post-quantum signature scheme. + * + *

+ * {@code SphincsPlusKeyGenBuilder} integrates with the Bouncy Castle PQC + * provider to generate key pairs for SPHINCS+. It maps high-level + * {@link SphincsPlusKeyGenSpec} parameters (hash family, security level, + * variant, and mode) to the appropriate + * {@code org.bouncycastle.pqc.jcajce.spec.SPHINCSPlusParameterSpec} constant. + * Reflection is used to avoid a hard dependency on all parameter variants. + *

+ * + *

Supported flows

+ *
    + *
  • {@link #generateKeyPair(SphincsPlusKeyGenSpec)}: creates a fresh key pair + * using the specified parameter set and optional provider name.
  • + *
  • {@link #importPublic(SphincsPlusKeyGenSpec)} and + * {@link #importPrivate(SphincsPlusKeyGenSpec)}: not supported; use + * {@link SphincsPlusPublicKeySpec} or {@link SphincsPlusPrivateKeySpec} + * instead.
  • + *
+ * + *

Example

{@code
+ * SphincsPlusKeyGenSpec spec =
+ *     SphincsPlusKeyGenSpec.sphincsPlusSha2_128s_robust();
+ *
+ * KeyPair kp = new SphincsPlusKeyGenBuilder().generateKeyPair(spec);
+ * }
+ * + * @since 1.0 + */ +public final class SphincsPlusKeyGenBuilder implements AsymmetricKeyBuilder { + + private static final String ALG = "SPHINCSPlus"; + + /** + * Generates a SPHINCS+ key pair using the given specification. + * + *

+ * The method resolves the corresponding Bouncy Castle + * {@code SPHINCSPlusParameterSpec} constant via reflection and initializes a + * {@link KeyPairGenerator}. If {@link SphincsPlusKeyGenSpec#providerName()} is + * non-null, the generator is obtained from that provider. + *

+ * + * @param spec the key generation specification, including hash family, security + * level, variant, and mode + * @return a freshly generated SPHINCS+ {@link KeyPair} + * @throws NullPointerException if {@code spec} is {@code null} + * @throws GeneralSecurityException if the parameter spec cannot be resolved or + * if the provider fails to generate a key pair + */ + @Override + public KeyPair generateKeyPair(SphincsPlusKeyGenSpec spec) throws GeneralSecurityException { + Objects.requireNonNull(spec, "spec"); + // Resolve BC SPHINCSPlusParameterSpec via reflection by constant name(s) + Object bcParamSpec = resolveBcParameterSpec(spec); + + KeyPairGenerator kpg = (spec.providerName() == null) ? KeyPairGenerator.getInstance(ALG) + : KeyPairGenerator.getInstance(ALG, spec.providerName()); + if (bcParamSpec != null) { + kpg.initialize((java.security.spec.AlgorithmParameterSpec) bcParamSpec); + } + return kpg.generateKeyPair(); + } + + /** + * Not supported for this builder. + * + *

+ * Public key import is delegated to {@link SphincsPlusPublicKeySpec} and its + * associated builder. + *

+ * + * @param spec ignored + * @return never returns normally + * @throws UnsupportedOperationException always + */ + @Override + public java.security.PublicKey importPublic(SphincsPlusKeyGenSpec spec) { + throw new UnsupportedOperationException("Use SphincsPlusPublicKeySpec to import a public key."); + } + + /** + * Not supported for this builder. + * + *

+ * Private key import is delegated to {@link SphincsPlusPrivateKeySpec} and its + * associated builder. + *

+ * + * @param spec ignored + * @return never returns normally + * @throws UnsupportedOperationException always + */ + @Override + public java.security.PrivateKey importPrivate(SphincsPlusKeyGenSpec spec) { + throw new UnsupportedOperationException("Use SphincsPlusPrivateKeySpec to import a private key."); + } + + /** + * Resolves the Bouncy Castle parameter spec constant that corresponds to the + * given high-level {@link SphincsPlusKeyGenSpec}. + * + *

+ * Preferred constant names follow the convention: + * {@code {sha2|shake|haraka}_{128|192|256}{f|s}_{robust|simple}}. Legacy names + * without a suffix (e.g., {@code sha2_128f}) are also tried for robust mode. + *

+ * + * @param spec the key generation specification + * @return a matching {@code SPHINCSPlusParameterSpec} instance + * @throws GeneralSecurityException if no matching constant is found + */ + private static Object resolveBcParameterSpec(SphincsPlusKeyGenSpec spec) throws GeneralSecurityException { + // explicit override by constant name + if (spec.explicitParamConstant() != null) { + Object c = fetchStaticField("org.bouncycastle.pqc.jcajce.spec.SPHINCSPlusParameterSpec", + spec.explicitParamConstant()); + if (c != null) { + return c; + } + throw new GeneralSecurityException( + "Unknown SPHINCSPlusParameterSpec constant: " + spec.explicitParamConstant()); + } + + String fam = switch (spec.hash()) { + case SHA2 -> "sha2"; + case SHAKE -> "shake"; + case HARAKA -> "haraka"; + }; + String bits = Integer.toString(spec.security().bits); + String v = (spec.variant() == SphincsPlusKeyGenSpec.Variant.FAST) ? "f" : "s"; + + // candidates in order + final String[] names; + if (spec.mode() == SphincsPlusKeyGenSpec.Mode.ROBUST) { + names = new String[] { fam + "_" + bits + v + "_robust", // new (robust-explicit) + fam + "_" + bits + v // legacy (robust default) + }; + } else { + names = new String[] { fam + "_" + bits + v + "_simple" }; + } + + for (String n : names) { + Object c = fetchStaticField("org.bouncycastle.pqc.jcajce.spec.SPHINCSPlusParameterSpec", n); + if (c != null) { + return c; + } + } + throw new GeneralSecurityException("Cannot resolve SPHINCSPlusParameterSpec for " + fam + "_" + bits + v + " (" + + spec.mode().name().toLowerCase(Locale.ROOT) + ")"); + } + + /** + * Attempts to fetch a static field by name from the given class. + * + * @param className fully qualified class name + * @param field the name of the static field to retrieve + * @return the value of the field, or {@code null} if not found + */ + private static Object fetchStaticField(String className, String field) { + try { + Class cls = Class.forName(className); + Field f = cls.getField(field); + return f.get(null); + } catch (ReflectiveOperationException e) { + return null; + } + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenSpec.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenSpec.java new file mode 100644 index 0000000..32a5f0e --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusKeyGenSpec.java @@ -0,0 +1,269 @@ +/******************************************************************************* + * 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.core.alg.sphincsplus; + +import java.util.Objects; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Specification for generating SPHINCS+ key pairs. + * + *

+ * {@code SphincsPlusKeyGenSpec} selects the hash family, security strength, + * signature variant, and mode for the SPHINCS+ post-quantum signature scheme. + * It also carries an optional provider name and explicit Bouncy Castle + * parameter constant for direct mapping. + *

+ * + *

Parameters

+ *
    + *
  • {@link Hash}: underlying hash/XOF function ({@code SHA2}, {@code SHAKE}, + * or {@code HARAKA}).
  • + *
  • {@link Security}: NIST security level, expressed as {@code 128}, + * {@code 192}, or {@code 256} bits.
  • + *
  • {@link Variant}: tradeoff between {@code FAST} (smaller signatures, + * higher signing speed) and {@code SMALL} (shorter signatures, slower signing). + * Security is unaffected.
  • + *
  • {@link Mode}: {@code ROBUST} (conservative construction) or + * {@code SIMPLE} (more efficient, narrower assumptions).
  • + *
  • {@code providerName}: JCA provider name (e.g., {@code "BCPQC"} for Bouncy + * Castle PQC).
  • + *
  • {@code explicitParamConstant}: optional override; the exact field name of + * a {@code SPHINCSPlusParameterSpec} constant in the Bouncy Castle API. If + * non-null, it bypasses the automatic mapping.
  • + *
+ * + *

Defaults

The {@link #defaultSpec()} uses: + *
    + *
  • Provider: {@code "BCPQC"}
  • + *
  • Hash: {@link Hash#SHAKE}
  • + *
  • Security: {@link Security#L5_256}
  • + *
  • Variant: {@link Variant#SMALL}
  • + *
  • Mode: {@link Mode#ROBUST}
  • + *
+ * + *

Example

{@code
+ * // Conservative, NIST Level 3 security, SHA-2 based
+ * SphincsPlusKeyGenSpec spec = SphincsPlusKeyGenSpec.of(
+ *     "BCPQC", 
+ *     SphincsPlusKeyGenSpec.Hash.SHA2,
+ *     SphincsPlusKeyGenSpec.Security.L3_192,
+ *     SphincsPlusKeyGenSpec.Variant.FAST,
+ *     SphincsPlusKeyGenSpec.Mode.ROBUST);
+ *
+ * KeyPair kp = new SphincsPlusKeyGenBuilder().generateKeyPair(spec);
+ * }
+ * + * @since 1.0 + */ +public final class SphincsPlusKeyGenSpec implements AlgorithmKeySpec { + /** + * Hash function families supported by SPHINCS+. + */ + public enum Hash { + /** SHA-2 based instantiation. */ + SHA2, + /** SHAKE (SHA-3 XOF) based instantiation. */ + SHAKE, + /** Haraka (AES-based permutation) instantiation. */ + HARAKA + } + + /** + * Security levels as defined by NIST PQC (L1, L3, L5). The {@code bits} field + * gives the claimed classical security level. + */ + public enum Security { + /** NIST Level 1 (~128-bit security). */ + L1_128(128), + /** NIST Level 3 (~192-bit security). */ + L3_192(192), + /** NIST Level 5 (~256-bit security). */ + L5_256(256); + + /** Claimed security level in bits. */ + public final int bits; + + Security(int b) { + bits = b; + } + } + + /** + * Signature variants trading performance against signature size. + * + *

+ * {@code FAST} variants are optimized for speed (larger signatures). + * {@code SMALL} variants reduce signature size at some performance cost. + * Security is equivalent. + *

+ */ + public enum Variant { + /** Larger, faster signatures. */ + FAST, + /** Smaller, slower signatures. */ + SMALL + } // f vs s (speed vs signature size), same security + + /** + * Construction mode: conservative {@code ROBUST} vs. more efficient + * {@code SIMPLE}. + * + *

+ * {@code ROBUST} is recommended when in doubt; {@code SIMPLE} offers smaller + * signatures and faster signing under additional assumptions. + *

+ */ + public enum Mode { + /** Conservative construction (recommended default). */ + ROBUST, + /** Simpler and more efficient construction. */ + SIMPLE + } // ROBUST = conservative + + private final String providerName; + private final Hash hash; + private final Security security; + private final Variant variant; + private final Mode mode; + private final String explicitParamConstant; // nullable + + private static final SphincsPlusKeyGenSpec DEFAULT = new SphincsPlusKeyGenSpec("BCPQC", Hash.SHAKE, Security.L5_256, + Variant.SMALL, Mode.ROBUST, null); + + private SphincsPlusKeyGenSpec(String providerName, Hash hash, Security sec, Variant var, Mode mode, + String explicit) { + this.providerName = Objects.requireNonNull(providerName, "providerName"); + this.hash = Objects.requireNonNull(hash, "hash"); + this.security = Objects.requireNonNull(sec, "security"); + this.variant = Objects.requireNonNull(var, "variant"); + this.mode = Objects.requireNonNull(mode, "mode"); + this.explicitParamConstant = explicit; + } + + /** + * Returns the default SPHINCS+ key generation spec. + * + * @return a singleton default specification (SHAKE, L5, SMALL, ROBUST) + */ + public static SphincsPlusKeyGenSpec defaultSpec() { + return DEFAULT; + } + + /** + * Creates a new specification with explicit algorithm parameters. + * + * @param providerName JCA provider name (e.g., "BCPQC") + * @param hash hash function family + * @param sec security level + * @param var signature variant (FAST vs SMALL) + * @param mode construction mode (ROBUST vs SIMPLE) + * @return a new {@code SphincsPlusKeyGenSpec} + */ + public static SphincsPlusKeyGenSpec of(String providerName, Hash hash, Security sec, Variant var, Mode mode) { + return new SphincsPlusKeyGenSpec(providerName, hash, sec, var, mode, null); + } + + /** + * Returns a copy of this specification with an explicit Bouncy Castle parameter + * constant override. + * + *

+ * This bypasses automatic mapping of hash family, security, variant, and mode. + * It is useful to test new or provider-specific constants. + *

+ * + * @param name the name of the static field in {@code SPHINCSPlusParameterSpec} + * @return a new {@code SphincsPlusKeyGenSpec} with the override + */ + public SphincsPlusKeyGenSpec withExplicitParamConstant(String name) { + return new SphincsPlusKeyGenSpec(providerName, hash, security, variant, mode, name); + } + + /** + * Returns the provider name used for key generation. + * + * @return provider name (never {@code null}) + */ + public String providerName() { + return providerName; + } + + /** + * Returns the hash family of this specification. + * + * @return hash family + */ + public Hash hash() { + return hash; + } + + /** + * Returns the security level of this specification. + * + * @return security level + */ + public Security security() { + return security; + } + + /** + * Returns the variant of this specification. + * + * @return signature variant + */ + public Variant variant() { + return variant; + } + + /** + * Returns the construction mode of this specification. + * + * @return construction mode + */ + public Mode mode() { + return mode; + } + + /** + * Returns the explicit parameter constant override, if set. + * + * @return constant name, or {@code null} if automatic mapping is used + */ + public String explicitParamConstant() { + return explicitParamConstant; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java new file mode 100644 index 0000000..3583033 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeyBuilder.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * 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.core.alg.sphincsplus; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; + +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * Builder for importing SPHINCS+ private keys from encoded specifications. + * + *

+ * {@code SphincsPlusPrivateKeyBuilder} integrates with the JCA + * {@link KeyFactory} to parse PKCS#8-encoded SPHINCS+ private keys. Unlike + * {@link SphincsPlusKeyGenBuilder}, this builder does not generate new key + * pairs but focuses solely on importing private key material. + *

+ * + *

Supported flows

+ *
    + *
  • {@link #importPrivate(SphincsPlusPrivateKeySpec)}: imports a SPHINCS+ + * private key from its encoded PKCS#8 representation.
  • + *
  • {@link #generateKeyPair(SphincsPlusPrivateKeySpec)}: not supported.
  • + *
  • {@link #importPublic(SphincsPlusPrivateKeySpec)}: not supported; use + * {@link SphincsPlusPublicKeySpec} and its builder instead.
  • + *
+ * + *

Example

{@code
+ * // Assuming bytes contain a PKCS#8-encoded SPHINCS+ private key:
+ * SphincsPlusPrivateKeySpec spec =
+ *     new SphincsPlusPrivateKeySpec("BCPQC", encodedBytes);
+ *
+ * PrivateKey priv =
+ *     new SphincsPlusPrivateKeyBuilder().importPrivate(spec);
+ * }
+ * + * @since 1.0 + */ +public final class SphincsPlusPrivateKeyBuilder implements AsymmetricKeyBuilder { + /** + * Not supported for this builder. + * + *

+ * Key pair generation requires a parameter set and is handled by + * {@link SphincsPlusKeyGenBuilder}. This method always throws. + *

+ * + * @param spec ignored + * @return never returns normally + * @throws UnsupportedOperationException always + */ + @Override + public KeyPair generateKeyPair(SphincsPlusPrivateKeySpec spec) { + throw new UnsupportedOperationException("Generation not supported by this spec."); + } + + /** + * Not supported for this builder. + * + *

+ * Public key import should be performed via {@link SphincsPlusPublicKeySpec} + * and its associated builder. + *

+ * + * @param spec ignored + * @return never returns normally + * @throws UnsupportedOperationException always + */ + @Override + public PublicKey importPublic(SphincsPlusPrivateKeySpec spec) { + throw new UnsupportedOperationException("Use SphincsPlusPublicKeySpec for public keys."); + } + + /** + * Imports a SPHINCS+ private key from PKCS#8 encoding. + * + *

+ * The method uses {@link KeyFactory} with algorithm {@code "SPHINCSPlus"} and + * the provider specified in the {@code spec}. If no provider name is given, the + * default provider resolution is used. + *

+ * + * @param spec private key specification containing PKCS#8-encoded key material + * and optional provider name + * @return the imported SPHINCS+ {@link PrivateKey} + * @throws NullPointerException if {@code spec} or its encoded bytes are + * null + * @throws GeneralSecurityException if the key factory cannot be initialized or + * the encoded key is invalid + */ + @Override + public PrivateKey importPrivate(SphincsPlusPrivateKeySpec spec) throws GeneralSecurityException { + KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance("SPHINCSPlus") + : KeyFactory.getInstance("SPHINCSPlus", spec.providerName()); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java new file mode 100644 index 0000000..6009078 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPrivateKeySpec.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * 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.core.alg.sphincsplus; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Encoded representation of a SPHINCS+ private key. + * + *

+ * {@code SphincsPlusPrivateKeySpec} wraps a PKCS#8-encoded SPHINCS+ private key + * along with the provider name that should be used for import. It is a simple + * immutable holder designed for use with {@link SphincsPlusPrivateKeyBuilder}. + *

+ * + *

Encoding

+ *
    + *
  • The key is stored as a defensive copy of the provided PKCS#8 byte + * array.
  • + *
  • Marshalling and unmarshalling methods convert between this object and a + * {@link PairSeq} representation using Base64 encoding.
  • + *
  • The {@code providerName} defaults to {@code "BCPQC"} (Bouncy Castle PQC + * provider) if not explicitly supplied.
  • + *
+ * + *

Thread-safety

Instances are immutable and can be shared safely + * across threads. Defensive copies are returned for all sensitive material. + * + *

Example

{@code
+ * // Wrap a PKCS#8-encoded private key
+ * byte[] pkcs8 = ...;
+ * SphincsPlusPrivateKeySpec spec = new SphincsPlusPrivateKeySpec(pkcs8);
+ *
+ * // Import the key via the builder
+ * PrivateKey priv = new SphincsPlusPrivateKeyBuilder().importPrivate(spec);
+ *
+ * // Serialize to PairSeq for transport or persistence
+ * PairSeq seq = SphincsPlusPrivateKeySpec.marshal(spec);
+ *
+ * // Reconstruct from PairSeq
+ * SphincsPlusPrivateKeySpec restored = SphincsPlusPrivateKeySpec.unmarshal(seq);
+ * }
+ * + * @since 1.0 + */ +public final class SphincsPlusPrivateKeySpec implements AlgorithmKeySpec { + private final byte[] encodedPkcs8; + private final String providerName; // e.g. "BCPQC" + + /** + * Constructs a new specification with the default provider {@code "BCPQC"}. + * + * @param encodedPkcs8 PKCS#8-encoded SPHINCS+ private key bytes + * @throws IllegalArgumentException if {@code encodedPkcs8} is {@code null} + */ + public SphincsPlusPrivateKeySpec(byte[] encodedPkcs8) { + this(encodedPkcs8, "BCPQC"); + } + + /** + * Constructs a new specification with the given provider. + * + * @param encodedPkcs8 PKCS#8-encoded SPHINCS+ private key bytes + * @param providerName JCA provider to use for import; if {@code null}, defaults + * to {@code "BCPQC"} + * @throws IllegalArgumentException if {@code encodedPkcs8} is {@code null} + */ + public SphincsPlusPrivateKeySpec(byte[] encodedPkcs8, String providerName) { + if (encodedPkcs8 == null) { + throw new IllegalArgumentException("encodedPkcs8 must not be null"); + } + this.encodedPkcs8 = encodedPkcs8.clone(); + this.providerName = (providerName == null ? "BCPQC" : providerName); + } + + /** + * Returns a defensive copy of the PKCS#8-encoded private key. + * + * @return clone of the encoded key bytes + */ + public byte[] encoded() { + return encodedPkcs8.clone(); + } + + /** + * Returns the provider name associated with this specification. + * + * @return provider name, never {@code null} + */ + public String providerName() { + return providerName; + } + + /** + * Serializes this specification into a {@link PairSeq}. + * + *

+ * The key bytes are Base64-encoded under the key {@code "pkcs8.b64"}. The + * provider name is stored under the key {@code "provider"}. + *

+ * + * @param spec private key spec to marshal + * @return serialized {@link PairSeq} representation + */ + public static PairSeq marshal(SphincsPlusPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedPkcs8); + return PairSeq.of("type", "SPHINCSPLUS-PRIV", "pkcs8.b64", b64, "provider", spec.providerName); + } + + /** + * Deserializes a {@code SphincsPlusPrivateKeySpec} from a {@link PairSeq}. + * + *

+ * Expects keys {@code "pkcs8.b64"} (required, Base64 string) and + * {@code "provider"} (optional, defaults to {@code "BCPQC"}). + *

+ * + * @param p serialized input + * @return a reconstructed private key spec + * @throws IllegalArgumentException if the encoded key is missing + */ + public static SphincsPlusPrivateKeySpec unmarshal(PairSeq p) { + byte[] out = null; + String prov = "BCPQC"; + PairSeq.Cursor c = p.cursor(); + while (c.next()) { + String k = c.key(); + String v = c.value(); + switch (k) { + case "pkcs8.b64" -> out = Base64.getDecoder().decode(v); + case "provider" -> prov = v; + default -> { + } + } + } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for SPHINCS+ private key"); + } + return new SphincsPlusPrivateKeySpec(out, prov); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java new file mode 100644 index 0000000..eb5a54c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeyBuilder.java @@ -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.core.alg.sphincsplus; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; + +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * Builder for importing SPHINCS+ public keys from encoded specifications. + * + *

+ * {@code SphincsPlusPublicKeyBuilder} integrates with the JCA + * {@link KeyFactory} to parse X.509-encoded SPHINCS+ public keys. Unlike + * {@link SphincsPlusKeyGenBuilder}, this builder does not generate new key + * pairs, but focuses solely on importing public key material. + *

+ * + *

Supported flows

+ *
    + *
  • {@link #importPublic(SphincsPlusPublicKeySpec)}: imports a SPHINCS+ + * public key from its encoded X.509 representation.
  • + *
  • {@link #generateKeyPair(SphincsPlusPublicKeySpec)}: not supported.
  • + *
  • {@link #importPrivate(SphincsPlusPublicKeySpec)}: not supported; use + * {@link SphincsPlusPrivateKeySpec} and its builder instead.
  • + *
+ * + *

Example

{@code
+ * // Assuming bytes contain an X.509-encoded SPHINCS+ public key:
+ * SphincsPlusPublicKeySpec spec =
+ *     new SphincsPlusPublicKeySpec("BCPQC", encodedBytes);
+ *
+ * PublicKey pub =
+ *     new SphincsPlusPublicKeyBuilder().importPublic(spec);
+ * }
+ * + * @since 1.0 + */ +public final class SphincsPlusPublicKeyBuilder implements AsymmetricKeyBuilder { + + /** + * Not supported for this builder. + * + *

+ * Key pair generation requires algorithm parameters and is handled by + * {@link SphincsPlusKeyGenBuilder}. This method always throws. + *

+ * + * @param spec ignored + * @return never returns normally + * @throws UnsupportedOperationException always + */ + @Override + public KeyPair generateKeyPair(SphincsPlusPublicKeySpec spec) { + throw new UnsupportedOperationException("Generation not supported by this spec."); + } + + /** + * Imports a SPHINCS+ public key from X.509 encoding. + * + *

+ * The method uses {@link KeyFactory} with algorithm {@code "SPHINCSPlus"} and + * the provider specified in the {@code spec}. If no provider name is given, the + * default provider resolution is used. + *

+ * + * @param spec public key specification containing X.509-encoded key material + * and optional provider name + * @return the imported SPHINCS+ {@link PublicKey} + * @throws NullPointerException if {@code spec} or its encoded bytes are + * null + * @throws GeneralSecurityException if the key factory cannot be initialized or + * the encoded key is invalid + */ + @Override + public PublicKey importPublic(SphincsPlusPublicKeySpec spec) throws GeneralSecurityException { + KeyFactory kf = (spec.providerName() == null) ? KeyFactory.getInstance("SPHINCSPlus") + : KeyFactory.getInstance("SPHINCSPlus", spec.providerName()); + return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); + } + + /** + * Not supported for this builder. + * + *

+ * Private key import should be performed via {@link SphincsPlusPrivateKeySpec} + * and its associated builder. + *

+ * + * @param spec ignored + * @return never returns normally + * @throws UnsupportedOperationException always + */ + @Override + public PrivateKey importPrivate(SphincsPlusPublicKeySpec spec) { + throw new UnsupportedOperationException("Use SphincsPlusPrivateKeySpec for private keys."); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeySpec.java new file mode 100644 index 0000000..134fb22 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusPublicKeySpec.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * 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.core.alg.sphincsplus; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Encoded representation of a SPHINCS+ public key. + * + *

+ * {@code SphincsPlusPublicKeySpec} wraps an X.509-encoded SPHINCS+ public key + * along with the provider name that should be used for import. It is a simple + * immutable holder designed for use with {@link SphincsPlusPublicKeyBuilder}. + *

+ * + *

Encoding

+ *
    + *
  • The key is stored as a defensive copy of the provided X.509 byte + * array.
  • + *
  • Marshalling and unmarshalling methods convert between this object and a + * {@link PairSeq} representation using Base64 encoding.
  • + *
  • The {@code providerName} defaults to {@code "BCPQC"} (Bouncy Castle PQC + * provider) if not explicitly supplied.
  • + *
+ * + *

Thread-safety

Instances are immutable and can be shared safely + * across threads. Defensive copies are returned for all sensitive material. + * + *

Example

{@code
+ * // Wrap an X.509-encoded public key
+ * byte[] x509 = ...;
+ * SphincsPlusPublicKeySpec spec = new SphincsPlusPublicKeySpec(x509);
+ *
+ * // Import the key via the builder
+ * PublicKey pub = new SphincsPlusPublicKeyBuilder().importPublic(spec);
+ *
+ * // Serialize to PairSeq for transport or persistence
+ * PairSeq seq = SphincsPlusPublicKeySpec.marshal(spec);
+ *
+ * // Reconstruct from PairSeq
+ * SphincsPlusPublicKeySpec restored = SphincsPlusPublicKeySpec.unmarshal(seq);
+ * }
+ * + * @since 1.0 + */ +public final class SphincsPlusPublicKeySpec implements AlgorithmKeySpec { + private final byte[] encodedX509; + private final String providerName; // e.g. "BCPQC" + + /** + * Constructs a new specification with the default provider {@code "BCPQC"}. + * + * @param encodedX509 X.509-encoded SPHINCS+ public key bytes + * @throws IllegalArgumentException if {@code encodedX509} is {@code null} + */ + public SphincsPlusPublicKeySpec(byte[] encodedX509) { + this(encodedX509, "BCPQC"); + } + + /** + * Constructs a new specification with the given provider. + * + * @param encodedX509 X.509-encoded SPHINCS+ public key bytes + * @param providerName JCA provider to use for import; if {@code null}, defaults + * to {@code "BCPQC"} + * @throws IllegalArgumentException if {@code encodedX509} is {@code null} + */ + public SphincsPlusPublicKeySpec(byte[] encodedX509, String providerName) { + if (encodedX509 == null) { + throw new IllegalArgumentException("encodedX509 must not be null"); + } + this.encodedX509 = encodedX509.clone(); + this.providerName = (providerName == null ? "BCPQC" : providerName); + } + + /** + * Returns a defensive copy of the X.509-encoded public key. + * + * @return clone of the encoded key bytes + */ + public byte[] encoded() { + return encodedX509.clone(); + } + + /** + * Returns the provider name associated with this specification. + * + * @return provider name, never {@code null} + */ + public String providerName() { + return providerName; + } + + /** + * Serializes this specification into a {@link PairSeq}. + * + *

+ * The key bytes are Base64-encoded under the key {@code "x509.b64"}. The + * provider name is stored under the key {@code "provider"}. + *

+ * + * @param spec public key spec to marshal + * @return serialized {@link PairSeq} representation + */ + public static PairSeq marshal(SphincsPlusPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.encodedX509); + return PairSeq.of("type", "SPHINCSPLUS-PUB", "x509.b64", b64, "provider", spec.providerName); + } + + /** + * Deserializes a {@code SphincsPlusPublicKeySpec} from a {@link PairSeq}. + * + *

+ * Expects keys {@code "x509.b64"} (required, Base64 string) and + * {@code "provider"} (optional, defaults to {@code "BCPQC"}). + *

+ * + * @param p serialized input + * @return a reconstructed public key spec + * @throws IllegalArgumentException if the encoded key is missing + */ + public static SphincsPlusPublicKeySpec unmarshal(PairSeq p) { + byte[] out = null; + String prov = "BCPQC"; + PairSeq.Cursor c = p.cursor(); + while (c.next()) { + String k = c.key(); + String v = c.value(); + switch (k) { + case "x509.b64" -> out = Base64.getDecoder().decode(v); + case "provider" -> prov = v; + default -> { + } + } + } + if (out == null) { + throw new IllegalArgumentException("x509.b64 missing for SPHINCS+ public key"); + } + return new SphincsPlusPublicKeySpec(out, prov); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusSignatureContext.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusSignatureContext.java new file mode 100644 index 0000000..26d9b00 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/SphincsPlusSignatureContext.java @@ -0,0 +1,321 @@ +/******************************************************************************* + * 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.core.alg.sphincsplus; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; + +import org.bouncycastle.pqc.jcajce.interfaces.SPHINCSPlusPublicKey; +import org.bouncycastle.pqc.jcajce.spec.SPHINCSPlusParameterSpec; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.alg.common.sig.GenericJcaSignatureContext; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.tag.TagEngine; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; + +/** + * SPHINCS+ signature context that adapts a JCA engine for streaming sign and + * verify. + * + *

+ * This context uses the Bouncy Castle PQC provider to obtain a JCA + * {@code Signature} instance for the "SPHINCSPlus" algorithm and exposes it via + * the {@link SignatureContext} and {@link TagEngine} contracts. In produce + * (SIGN) mode, the wrapped stream emits the original data followed by a + * detached signature trailer. In verify (VERIFY) mode, the wrapped stream emits + * only the body and compares the computed signature at EOF against a + * caller-supplied expected value. + *

+ * + *

Delegation

+ *

+ * The implementation delegates to {@link GenericJcaSignatureContext} for the + * streaming mechanics and JCA interaction, including trailer handling and + * verification strategy configuration. + *

+ * + *

Signature length

+ *

+ * SPHINCS+ signatures have fixed byte lengths that depend on the parameter set + * attached to the key. When constructed with a public key, this context + * resolves the expected signature length from + * {@link org.bouncycastle.pqc.jcajce.spec.SPHINCSPlusParameterSpec} on the key. + *

+ * + *

Canonical sizes (bytes)

+ *
    + *
  • 128s: 7856, 128f: 17088
  • + *
  • 192s: 16224, 192f: 35664
  • + *
  • 256s: 29792, 256f: 49856
  • + *
+ * + *

Examples

+ *

Signing pipeline

+ * {@code
+ * TagEngine eng =
+ *     new SphincsPlusSignatureContext(alg, privateKey);
+ *
+ * try (java.io.InputStream in = eng.wrap(upstream)) {
+ *     in.transferTo(out); // body, then SPHINCS+ signature trailer
+ * }
+ * }
+ * 
+ * + *

Verification pipeline (detached)

+ * {@code
+ * byte[] expectedSig = ...; // obtained via a trusted channel
+ *
+ * TagEngine eng =
+ *     new SphincsPlusSignatureContext(alg, publicKey);
+ *
+ * // Optional: throw on mismatch at EOF
+ * eng.setVerificationApproach(eng.getVerificationCore().getThrowOnMismatch());
+ * eng.setExpectedTag(expectedSig);
+ *
+ * try (java.io.InputStream in = eng.wrap(bodyWithoutTrailer)) {
+ *     in.transferTo(java.io.OutputStream.nullOutputStream());
+ * }
+ * }
+ * 
+ * + *

Thread-safety

+ *

+ * Instances are not thread-safe and are single-use within a pipeline. Use one + * instance per signing or verification stream. + *

+ * + * @since 1.0 + */ +public final class SphincsPlusSignatureContext implements SignatureContext { + private static final String ALG = "SPHINCSPlus"; + private static final String PROVIDER = "BCPQC"; + + private final GenericJcaSignatureContext delegate; + + /** + * Creates a SPHINCS+ signing context bound to a private key. + * + *

+ * The JCA engine is obtained from the "BCPQC" provider using the "SPHINCSPlus" + * algorithm name. The signature length resolver probes the JCA engine to + * determine the produced signature size. + *

+ * + * @param algorithm the parent cryptographic algorithm abstraction; must not be + * {@code null} + * @param privateKey a SPHINCS+ private key; must not be {@code null} + * @throws GeneralSecurityException if the JCA signature engine cannot be + * initialized + */ + public SphincsPlusSignatureContext(final CryptoAlgorithm algorithm, final PrivateKey privateKey) + throws GeneralSecurityException { + this.delegate = new GenericJcaSignatureContext(algorithm, privateKey, + GenericJcaSignatureContext.jcaFactory(ALG, PROVIDER), + GenericJcaSignatureContext.SignLengthResolver.probeWith(ALG, PROVIDER)); + } + + /** + * Creates a SPHINCS+ verification context bound to a public key. + * + *

+ * The expected signature length is derived from the public key's + * {@link org.bouncycastle.pqc.jcajce.spec.SPHINCSPlusParameterSpec}. + *

+ * + * @param algorithm the parent cryptographic algorithm abstraction; must not be + * {@code null} + * @param publicKey a SPHINCS+ public key; must not be {@code null} + * @throws GeneralSecurityException if the key is invalid or the signature + * length cannot be determined + */ + public SphincsPlusSignatureContext(final CryptoAlgorithm algorithm, final PublicKey publicKey) + throws GeneralSecurityException { + this.delegate = new GenericJcaSignatureContext(algorithm, publicKey, + GenericJcaSignatureContext.jcaFactory(ALG, PROVIDER), SphincsPlusSignatureContext::sigLenFromPublicKey); + } + + /** + * Resolves the canonical signature length from the SPHINCS+ parameter set on a + * public key. + * + *

+ * Expected key type is Bouncy Castle's + * {@link org.bouncycastle.pqc.jcajce.interfaces.SPHINCSPlusPublicKey}. + * Supported parameter names match the pattern + * {@code -(-robust|-simple)?}, for example + * {@code shake-256s-robust}, {@code sha2-192f-simple}, or {@code haraka-128s}. + *

+ * + * @param pk a SPHINCS+ public key carrying a parameter spec + * @return the canonical signature length in bytes + * @throws GeneralSecurityException if the key type or parameter spec is missing + * or unrecognized + */ + private static int sigLenFromPublicKey(PublicKey pk) throws GeneralSecurityException { + if (!(pk instanceof SPHINCSPlusPublicKey spk)) { + throw new GeneralSecurityException("Expected a BouncyCastle SPHINCS+ public key (BCPQC)"); + } + final SPHINCSPlusParameterSpec ps = spk.getParameterSpec(); + if (ps == null) { + throw new GeneralSecurityException("Missing SPHINCS+ parameter spec on public key"); + } + final String name = ps.getName(); // e.g. "shake-256s-robust", "sha2-192f-simple", "haraka-128s" + if (name == null || name.isEmpty()) { + throw new GeneralSecurityException("Unknown SPHINCS+ parameter (no name)"); + } + + // Pattern: -(-robust|-simple)? + // Examples: shake-256s-robust, sha2-192f-simple, haraka-128s + final java.util.regex.Matcher m = java.util.regex.Pattern + .compile("^[a-z0-9]+-(128|192|256)([sf])(?:-(robust|simple))?$").matcher(name); + if (!m.matches()) { + throw new GeneralSecurityException("Cannot parse SPHINCS+ security level/variant from: " + name); + } + final int level = Integer.parseInt(m.group(1)); + final char var = m.group(2).charAt(0); // 's' or 'f' + final boolean isSmall = var == 's'; + + // Canonical signature sizes (bytes) for SPHINCS+ per spec: + // 128s: 7856, 128f: 17088 + // 192s: 16224, 192f: 35664 + // 256s: 29792, 256f: 49856 + return switch (level) { + case 128 -> isSmall ? 7_856 : 17_088; + case 192 -> isSmall ? 16_224 : 35_664; + case 256 -> isSmall ? 29_792 : 49_856; + default -> throw new GeneralSecurityException("Unsupported SPHINCS+ level: " + level); + }; + } + + /** + * Returns the algorithm associated with this context. + * + * @return the parent {@link CryptoAlgorithm} + */ + @Override + public CryptoAlgorithm algorithm() { + return delegate.algorithm(); + } + + /** + * Returns the key bound to this context. + * + * @return the signing {@link PrivateKey} or verification {@link PublicKey} + */ + @Override + public java.security.Key key() { + return delegate.key(); + } + + /** + * Closes this context and releases underlying resources. + * + *

+ * Once closed, the context should not be reused. If auditing is enabled, + * closing may also trigger key destruction attempts. + *

+ */ + @Override + public void close() { + delegate.close(); + } + + /** + * Wraps an input stream to produce or verify a SPHINCS+ signature as data is + * consumed. + * + *

+ * For SIGN mode this feeds bytes into the signing engine and appends the + * signature at EOF. For VERIFY mode this feeds bytes into the verification + * engine and compares the computed signature at EOF against the expected value + * (supplied via {@link #setExpectedTag(byte[])}). + *

+ * + * @param upstream the input stream providing raw data; must not be {@code null} + * @return a wrapped input stream that updates the signature engine + * @throws IOException if wrapping fails + */ + @Override + public InputStream wrap(InputStream upstream) throws IOException { + return delegate.wrap(upstream); + } + + /** + * Returns the length of the signature (tag) produced or expected. + * + * @return signature length in bytes + */ + @Override + public int tagLength() { + return delegate.tagLength(); + } + + /** + * Supplies the expected signature for verification mode. + * + * @param expected the expected signature bytes; implementations may defensively + * copy the array + */ + @Override + public void setExpectedTag(byte[] expected) { + delegate.setExpectedTag(expected); + } + + /** + * Sets the verification approach to compare computed and expected signatures. + * + * @param strategy verification predicate; must not be {@code null} + */ + @Override + public void setVerificationApproach(VerificationBiPredicate strategy) { + delegate.setVerificationApproach(strategy); + } + + /** + * Returns the core verification predicate in use. + * + * @return the verification predicate + */ + @Override + public VerificationBiPredicate getVerificationCore() { + return delegate.getVerificationCore(); + } + +} diff --git a/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java b/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java new file mode 100644 index 0000000..20d5d2d --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/sphincsplus/package-info.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * SPHINCS+ post-quantum signature integration. + * + *

+ * This package wires the SPHINCS+ stateless hash-based signature scheme into + * the core. It provides the algorithm descriptor, a streaming signature context + * that adapts JCA engines, key-pair generation facilities, and encoded key + * specifications for import and export. Provider-specific details are + * encapsulated behind small factories while roles and metadata remain explicit + * to higher layers. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register a canonical SPHINCS+ algorithm and declare the SIGN and VERIFY + * roles.
  • + *
  • Offer a streaming signature context with a fixed tag length determined by + * the key's parameter set.
  • + *
  • Provide key builders for generating new key pairs and for importing + * encoded public and private keys.
  • + *
  • Expose immutable key specification types that defensively copy sensitive + * material and support compact marshalling.
  • + *
+ * + *

Components

+ *
    + *
  • SphincsPlusAlgorithm: algorithm descriptor that binds roles to the + * signature context and registers builders and specs.
  • + *
  • SphincsPlusSignatureContext: streaming sign/verify context; + * determines fixed signature size from the key's parameter set.
  • + *
  • SphincsPlusKeyGenBuilder and SphincsPlusKeyGenSpec: + * generator and specification for producing key pairs with selected + * variants.
  • + *
  • SphincsPlusPublicKeyBuilder / SphincsPlusPrivateKeyBuilder: + * importers backed by JCA key factories.
  • + *
  • SphincsPlusPublicKeySpec / SphincsPlusPrivateKeySpec: + * immutable wrappers over X.509 and PKCS#8 encodings with marshalling + * helpers.
  • + *
+ * + *

Design notes

+ *
    + *
  • Algorithm descriptors are immutable and safe to share; signature contexts + * are stateful and not thread-safe.
  • + *
  • Key specification classes never expose internal byte arrays; cloning is + * used on input and output.
  • + *
  • Marshalling helpers use a compact key-value form intended for + * configuration, transport, and tests.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.sphincsplus; diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java new file mode 100644 index 0000000..346f372 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhAlgorithm.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * 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.core.alg.xdh; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import zeroecho.core.AlgorithmFamily; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.AbstractCryptoAlgorithm; +import zeroecho.core.alg.common.agreement.GenericJcaAgreementContext; +import zeroecho.core.context.AgreementContext; +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * Algorithm definition for XDH (elliptic curve Diffie-Hellman) key agreement, + * backed by the JCA {@code KeyAgreement} API. + * + *

+ * This algorithm integrates the XDH family (currently X25519, optionally X448 + * in supporting providers) into the ZeroEcho registry. It exposes the + * {@link KeyUsage#AGREEMENT} role and produces {@link AgreementContext} + * instances using {@code GenericJcaAgreementContext}, which delegates to the + * standard JCA {@code KeyAgreement} primitive (e.g., {@code "XDH"}, + * {@code "X25519"}). + *

+ * + *

Behavior

+ *
    + *
  • Context type: {@link AgreementContext} constructed as a + * {@code GenericJcaAgreementContext}.
  • + *
  • Keys: Requires a local {@link java.security.PrivateKey}; the peer + * {@link java.security.PublicKey} must be supplied via + * {@link AgreementContext#setPeerPublic(java.security.PublicKey)} before + * calling {@link AgreementContext#deriveSecret()}.
  • + *
  • Secret material: {@code deriveSecret()} returns the raw XDH shared + * secret. Protocols must apply a KDF before deriving symmetric keys or other + * session material.
  • + *
  • Providers: The context uses the default JCA provider lookup unless + * a provider is explicitly configured in {@code GenericJcaAgreementContext} + * (this algorithm passes {@code null}).
  • + *
  • Default spec: {@link XdhSpec#X25519} is used when no explicit spec + * is provided.
  • + *
+ * + *

Key material and builders

+ *
    + *
  • Asymmetric key pairs are generated or imported via + * {@link XdhKeyGenBuilder}, parameterized by {@link XdhSpec}.
  • + *
  • The default builder spec is {@link XdhSpec#X25519}.
  • + *
  • Additional asymmetric builders are registered for + * {@link XdhPublicKeySpec} and {@link XdhPrivateKeySpec} to import encoded keys + * directly via the JCA {@link java.security.KeyFactory} for "XDH".
  • + *
+ * + *

Example

{@code
+ * // Discover the algorithm definition (via ServiceLoader or explicit construction)
+ * CryptoAlgorithm xdh = new XdhAlgorithm();
+ *
+ * // Generate key pairs
+ * KeyPair a = xdh.asymmetricKeyBuilder(XdhSpec.class).generateKeyPair(XdhSpec.X25519);
+ * KeyPair b = xdh.asymmetricKeyBuilder(XdhSpec.class).generateKeyPair(XdhSpec.X25519);
+ *
+ * // Perform agreement on side A
+ * AgreementContext ctxA = xdh.create(KeyUsage.AGREEMENT, a.getPrivate(), XdhSpec.X25519);
+ * ctxA.setPeerPublic(b.getPublic());
+ * byte[] secretA = ctxA.deriveSecret();
+ *
+ * // Perform agreement on side B
+ * AgreementContext ctxB = xdh.create(KeyUsage.AGREEMENT, b.getPrivate(), XdhSpec.X25519);
+ * ctxB.setPeerPublic(a.getPublic());
+ * byte[] secretB = ctxB.deriveSecret();
+ *
+ * // secretA and secretB are equal; apply a KDF before using them
+ * }
+ * + * @since 1.0 + */ +public final class XdhAlgorithm extends AbstractCryptoAlgorithm { + /** + * Creates a new XDH algorithm registration. + * + *

+ * This constructor performs the following registrations: + *

+ *
    + *
  • Binds the {@link KeyUsage#AGREEMENT} role under + * {@link AlgorithmFamily#AGREEMENT} to produce {@link AgreementContext} + * instances with {@link java.security.PrivateKey} keys and {@link XdhSpec} + * specs, backed by {@code GenericJcaAgreementContext} with the JCA name from + * {@link XdhSpec#keyAgreementName()}.
  • + *
  • Registers the {@link XdhKeyGenBuilder} for key pair generation with + * {@link XdhSpec}, defaulting to {@link XdhSpec#X25519}.
  • + *
  • Registers asymmetric key builders for {@link XdhPublicKeySpec} and + * {@link XdhPrivateKeySpec} to allow direct key import using + * {@link java.security.KeyFactory} ("XDH").
  • + *
+ * + *

+ * The bound contexts use the platform’s default JCA provider + * ({@code provider = null}). + *

+ */ + public XdhAlgorithm() { + super("Xdh", "Xdh"); + + capability(AlgorithmFamily.AGREEMENT, KeyUsage.AGREEMENT, AgreementContext.class, PrivateKey.class, + XdhSpec.class, + (PrivateKey k, XdhSpec s) -> new GenericJcaAgreementContext(this, k, s.keyAgreementName(), null), + () -> XdhSpec.X25519); + + registerAsymmetricKeyBuilder(XdhSpec.class, new XdhKeyGenBuilder(), () -> XdhSpec.X25519); + registerAsymmetricKeyBuilder(XdhPublicKeySpec.class, new AsymmetricKeyBuilder<>() { + + @Override + public KeyPair generateKeyPair(XdhPublicKeySpec spec) throws GeneralSecurityException { + throw new UnsupportedOperationException("Use XdhKeyGenBuilder for keypair generation."); + } + + @Override + public PublicKey importPublic(XdhPublicKeySpec spec) throws GeneralSecurityException { + KeyFactory kf = KeyFactory.getInstance("XDH"); + return kf.generatePublic(new X509EncodedKeySpec(spec.encoded())); + } + + @Override + public PrivateKey importPrivate(XdhPublicKeySpec spec) throws GeneralSecurityException { + throw new UnsupportedOperationException("Use XdhPrivateKeySpec for private key import."); + } + }, null); + registerAsymmetricKeyBuilder(XdhPrivateKeySpec.class, new AsymmetricKeyBuilder<>() { + + @Override + public KeyPair generateKeyPair(XdhPrivateKeySpec spec) throws GeneralSecurityException { + throw new UnsupportedOperationException("Use XdhKeyGenBuilder for keypair generation."); + } + + @Override + public PublicKey importPublic(XdhPrivateKeySpec spec) throws GeneralSecurityException { + throw new UnsupportedOperationException("Use XdhPrivateKeySpec for public key import."); + } + + @Override + public PrivateKey importPrivate(XdhPrivateKeySpec spec) throws GeneralSecurityException { + KeyFactory kf = KeyFactory.getInstance("XDH"); + return kf.generatePrivate(new PKCS8EncodedKeySpec(spec.encoded())); + } + }, null); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java new file mode 100644 index 0000000..5853096 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhKeyGenBuilder.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * 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.core.alg.xdh; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +import zeroecho.core.spi.AsymmetricKeyBuilder; + +/** + * KeyPair generator for XDH curves using the JCA KeyPairGenerator SPI. + * + *

+ * This builder produces XDH key pairs (e.g., X25519, X448 depending on the + * runtime provider) by instantiating a JCA {@code KeyPairGenerator} with the + * algorithm name supplied by {@link XdhSpec#kpgName()}. + *

+ * + *

Design and scope

+ *
    + *
  • Generation only: This builder supports key generation. + * Public/private import is intentionally unsupported and will throw + * {@link UnsupportedOperationException}.
  • + *
  • Provider resolution: The default JCA provider selection is used. + * If a specific provider is required, supply or register one that exposes the + * requested XDH algorithm name.
  • + *
  • Thread-safety: Instances are stateless and thread-safe.
  • + *
+ * + *

Example

{@code
+ * // Generate an X25519 key pair
+ * XdhKeyGenBuilder builder = new XdhKeyGenBuilder();
+ * KeyPair kp = builder.generateKeyPair(XdhSpec.X25519);
+ * PublicKey pub = kp.getPublic();
+ * PrivateKey prv = kp.getPrivate();
+ * }
+ * + * @since 1.0 + */ +public final class XdhKeyGenBuilder implements AsymmetricKeyBuilder { + /** + * Generates a new XDH key pair using the JCA + * {@link java.security.KeyPairGenerator}. + * + *

+ * The generator instance is obtained via + * {@link java.security.KeyPairGenerator#getInstance(String)} with + * {@link XdhSpec#kpgName()} (for example, {@code "XDH"} or {@code "X25519"}). + * No additional initialization parameters are supplied; providers are expected + * to choose safe defaults for the requested curve. + *

+ * + * @param spec the XDH key specification indicating which XDH variant to use + * (e.g., {@code X25519}) + * @return a freshly generated {@link java.security.KeyPair} for the requested + * XDH variant + * @throws GeneralSecurityException if the algorithm is not available, the + * provider rejects the request, or key + * generation fails + */ + @Override + public KeyPair generateKeyPair(XdhSpec spec) throws GeneralSecurityException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance(spec.kpgName()); + return kpg.generateKeyPair(); + } + + /** + * Not supported: importing XDH public keys is outside the scope of this + * builder. + * + *

+ * Use a dedicated import builder or provider-specific utilities if you need to + * wrap encoded public keys. + *

+ * + * @param spec the XDH key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown to indicate unsupported + * operation + */ + @Override + public java.security.PublicKey importPublic(XdhSpec spec) { + throw new UnsupportedOperationException(); + } + + /** + * Not supported: importing XDH private keys is outside the scope of this + * builder. + * + *

+ * Use a dedicated import builder or provider-specific utilities if you need to + * wrap encoded private keys. + *

+ * + * @param spec the XDH key specification + * @return never returns normally + * @throws UnsupportedOperationException always thrown to indicate unsupported + * operation + */ + @Override + public java.security.PrivateKey importPrivate(XdhSpec spec) { + throw new UnsupportedOperationException(); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java new file mode 100644 index 0000000..01c49b5 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhPrivateKeySpec.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * 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.core.alg.xdh; + +import java.util.Base64; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Key specification for an XDH (elliptic curve Diffie-Hellman) private key. + * + *

+ * This class encapsulates the PKCS#8-encoded representation of a private key + * used in modern Diffie-Hellman style agreements such as X25519 or X448. It + * provides safe construction, serialization, and deserialization utilities + * suitable for persistence, transmission, or integration with + * {@link zeroecho.core.CryptoAlgorithm} key builders. + *

+ * + *

Design goals

+ *
    + *
  • Encapsulation: the encoded key bytes are cloned defensively on input and + * output to prevent accidental modification.
  • + *
  • Interoperability: supports round-trip conversion to/from + * {@link zeroecho.core.marshal.PairSeq} with base64 encoding.
  • + *
  • Validation: throws explicit exceptions when mandatory fields are + * missing.
  • + *
+ * + *

Usage

{@code
+ * // Construct from a PKCS#8-encoded private key
+ * byte[] encoded = ...; // obtained from key generation or storage
+ * XdhPrivateKeySpec spec = new XdhPrivateKeySpec(encoded);
+ *
+ * // Marshal to a serializable structure
+ * PairSeq p = XdhPrivateKeySpec.marshal(spec);
+ *
+ * // Unmarshal back from serialized form
+ * XdhPrivateKeySpec spec2 = XdhPrivateKeySpec.unmarshal(p);
+ *
+ * assert Arrays.equals(spec.encoded(), spec2.encoded());
+ * }
+ * + * @since 1.0 + */ +public class XdhPrivateKeySpec implements AlgorithmKeySpec { + private static final String PKCS8_B64 = "pkcs8.b64"; + private final byte[] pkcs8; + + /** + * Constructs a new specification from the given PKCS#8-encoded private key. + * + * @param pkcs8 the raw encoded key bytes, must not be {@code null} + * @throws IllegalArgumentException if {@code pkcs8} is {@code null} + */ + public XdhPrivateKeySpec(byte[] pkcs8) { + if (pkcs8 == null) { + throw new IllegalArgumentException("pkcs8 must not be null"); + } + this.pkcs8 = pkcs8.clone(); + } + + /** + * Returns a defensive copy of the PKCS#8-encoded private key bytes. + * + * @return a clone of the internal key encoding + */ + public byte[] encoded() { + return pkcs8.clone(); + } + + /** + * Serializes the given key specification into a {@link PairSeq} with base64 + * encoding. + * + *

+ * The output contains two entries: + *

    + *
  • {@code type = "XDH-PRIV"}
  • + *
  • {@code pkcs8.b64 = }
  • + *
+ * + * @param spec the specification to marshal + * @return a pair sequence containing the encoded key + * @throws NullPointerException if {@code spec} is {@code null} + */ + public static PairSeq marshal(XdhPrivateKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.pkcs8); + return PairSeq.of("type", "XDH-PRIV", PKCS8_B64, b64); + } + + /** + * Deserializes a specification from the given {@link PairSeq}. + * + *

+ * The sequence must contain an entry {@code pkcs8.b64} with a base64-encoded + * key. Additional entries are ignored. + *

+ * + * @param p the pair sequence containing serialized fields + * @return a reconstructed {@code XdhPrivateKeySpec} + * @throws IllegalArgumentException if {@code pkcs8.b64} is missing or cannot be + * decoded + */ + public static XdhPrivateKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (PKCS8_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("pkcs8.b64 missing for DH private key"); + } + return new XdhPrivateKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhPublicKeySpec.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhPublicKeySpec.java new file mode 100644 index 0000000..e21c129 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhPublicKeySpec.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * 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.core.alg.xdh; + +import java.util.Arrays; +import java.util.Base64; +import java.util.Objects; + +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Key specification for an XDH (elliptic curve Diffie-Hellman) public key. + * + *

+ * This class encapsulates the X.509-encoded representation of a public key used + * in modern Diffie-Hellman style agreements such as X25519 or X448. It provides + * safe construction, serialization, and deserialization utilities suitable for + * persistence, transmission, or integration with + * {@link zeroecho.core.CryptoAlgorithm} key builders. + *

+ * + *

Design goals

+ *
    + *
  • Encapsulation: the encoded key bytes are cloned defensively on input and + * output to prevent accidental modification.
  • + *
  • Interoperability: supports round-trip conversion to/from + * {@link zeroecho.core.marshal.PairSeq} with base64 encoding.
  • + *
  • Validation: throws explicit exceptions when mandatory fields are + * missing.
  • + *
+ * + *

Usage

{@code
+ * // Construct from an X.509-encoded public key
+ * byte[] encoded = ...; // obtained from a certificate or peer
+ * XdhPublicKeySpec spec = new XdhPublicKeySpec(encoded);
+ *
+ * // Marshal to a serializable structure
+ * PairSeq p = XdhPublicKeySpec.marshal(spec);
+ *
+ * // Unmarshal back from serialized form
+ * XdhPublicKeySpec spec2 = XdhPublicKeySpec.unmarshal(p);
+ *
+ * assert Arrays.equals(spec.encoded(), spec2.encoded());
+ * }
+ * + * @since 1.0 + */ +public class XdhPublicKeySpec implements AlgorithmKeySpec { + private static final String X509_B64 = "x509.b64"; + private final byte[] x509; + + /** + * Constructs a new specification from the given X.509-encoded public key. + * + * @param key the raw encoded key bytes, must not be {@code null} + * @throws NullPointerException if {@code key} is {@code null} + */ + public XdhPublicKeySpec(byte[] key) { + Objects.requireNonNull(key, "key must not be null"); + this.x509 = Arrays.copyOf(key, key.length); + } + + /** + * Returns a defensive copy of the X.509-encoded public key bytes. + * + * @return a clone of the internal key encoding + */ + public byte[] encoded() { + return x509.clone(); + } + + /** + * Serializes the given key specification into a {@link PairSeq} with base64 + * encoding. + * + *

+ * The output contains two entries: + *

    + *
  • {@code type = "XDH-PUB"}
  • + *
  • {@code x509.b64 = }
  • + *
+ * + * @param spec the specification to marshal + * @return a pair sequence containing the encoded key + * @throws NullPointerException if {@code spec} is {@code null} + */ + public static PairSeq marshal(XdhPublicKeySpec spec) { + String b64 = Base64.getEncoder().withoutPadding().encodeToString(spec.x509); + return PairSeq.of("type", "XDH-PUB", X509_B64, b64); + } + + /** + * Deserializes a specification from the given {@link PairSeq}. + * + *

+ * The sequence must contain an entry {@code x509.b64} with a base64-encoded + * key. Additional entries are ignored. + *

+ * + * @param p the pair sequence containing serialized fields + * @return a reconstructed {@code XdhPublicKeySpec} + * @throws IllegalArgumentException if {@code x509.b64} is missing or cannot be + * decoded + */ + public static XdhPublicKeySpec unmarshal(PairSeq p) { + byte[] out = null; + PairSeq.Cursor cur = p.cursor(); + while (cur.next()) { + String k = cur.key(); + String v = cur.value(); + if (X509_B64.equals(k)) { + out = Base64.getDecoder().decode(v); + } + } + if (out == null) { + throw new IllegalArgumentException("x509.b64 missing for DH public key"); + } + return new XdhPublicKeySpec(out); + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java b/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java new file mode 100644 index 0000000..68b258f --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/xdh/XdhSpec.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * 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.core.alg.xdh; + +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spec.ContextSpec; + +/** + * Specification for X25519 and X448 Diffie-Hellman over Montgomery-form + * elliptic curves. + * + *

+ * {@code XdhSpec} enumerates the parameter sets supported by the JCA "XDH" + * interface: {@link #X25519} and {@link #X448}. Each constant binds a + * {@code KeyPairGenerator} name (for key generation and import) and a + * {@code KeyAgreement} name (for computing shared secrets). + *

+ * + *

Usage

{@code
+ * // Generate a key pair for X25519
+ * KeyPair kp = CryptoAlgorithms.keyPair("XDH", XdhSpec.X25519);
+ *
+ * // Perform key agreement
+ * AgreementContext ctx = CryptoAlgorithms.create("XDH", KeyUsage.AGREEMENT,
+ *                                                kp.getPrivate(), XdhSpec.X25519);
+ * ctx.setPeerPublic(peerPublicKey);
+ * byte[] sharedSecret = ctx.deriveSecret();
+ * }
+ * + *

Design notes

+ *
    + *
  • Both variants share the JCA key-agreement name {@code "XDH"}, but have + * distinct keypair generator names: {@code "X25519"} and {@code "X448"}.
  • + *
  • The spec acts as a {@link zeroecho.core.spec.ContextSpec} to configure + * context creation, and as an {@link zeroecho.core.spec.AlgorithmKeySpec} to + * drive key generation.
  • + *
  • Always apply a KDF to the raw shared secret before use as a symmetric + * key.
  • + *
+ * + * @since 1.0 + */ +public enum XdhSpec implements ContextSpec, AlgorithmKeySpec, Describable { + /** X25519 Diffie-Hellman over Curve25519. */ + X25519("X25519", "XDH"), // JCA: KeyAgreement "XDH" + /** X448 Diffie-Hellman over Curve448. */ + X448("X448", "XDH"); + + private final String kpgName; // KeyPairGenerator/KeyFactory name + private final String kaName; // KeyAgreement name + + XdhSpec(String kpgName, String kaName) { + this.kpgName = kpgName; + this.kaName = kaName; + } + + /** + * Returns the canonical JCA name for {@link java.security.KeyPairGenerator}. + * + * @return generator name, e.g. {@code "X25519"} or {@code "X448"} + */ + public String kpgName() { + return kpgName; + } + + /** + * Returns the canonical JCA name for {@link javax.crypto.KeyAgreement}. + * + * @return agreement algorithm name, typically {@code "XDH"} + */ + public String keyAgreementName() { + return kaName; + } + + /** + * Returns a short, human-readable description of this object. + * + *

+ * The description should be concise and stable enough for logging or display + * purposes, while avoiding exposure of any sensitive information. + *

+ * + * @return non-null descriptive string + */ + @Override + public String description() { + return kpgName; + } +} diff --git a/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java b/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java new file mode 100644 index 0000000..ff4de71 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/alg/xdh/package-info.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * XDH (X25519/X448) Diffie-Hellman key agreement integration. + * + *

+ * This package provides the XDH algorithm descriptor, an agreement context + * backed by the JCA {@code KeyAgreement} API, a key-pair generator, and encoded + * key specifications for import and export. It focuses on safe defaults and + * clear separation between algorithm metadata, runtime contexts, and key + * builders. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Register a canonical XDH algorithm and declare the + * {@link zeroecho.core.KeyUsage#AGREEMENT} role.
  • + *
  • Expose a runtime agreement context that derives raw shared secrets + * suitable for KDF input.
  • + *
  • Provide key builders for generating key pairs and importing encoded + * public/private keys.
  • + *
  • Offer immutable key specifications that defensively copy encoded material + * and support compact marshalling.
  • + *
+ * + *

Components

+ *
    + *
  • XdhAlgorithm: algorithm descriptor that binds the agreement role + * to a generic JCA-backed context and wires key builders.
  • + *
  • XdhKeyGenBuilder: key-pair generator using + * {@code KeyPairGenerator} with names supplied by the spec.
  • + *
  • XdhSpec: enumeration of supported variants (X25519, X448) acting + * as both context spec and key-gen spec.
  • + *
  • XdhPublicKeySpec / XdhPrivateKeySpec: wrappers over X.509 + * and PKCS#8 encodings with defensive copying and simple marshalling + * helpers.
  • + *
+ * + *

Behavior and notes

+ *
    + *
  • Agreement contexts require a local private key and a peer public key + * before deriving the shared secret; callers must apply a KDF.
  • + *
  • Algorithm descriptors are immutable and safe to share; contexts are + * stateful and not thread-safe.
  • + *
  • Provider selection follows standard JCA lookup unless a specific provider + * is configured upstream.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.alg.xdh; diff --git a/lib/src/main/java/zeroecho/core/annotation/Describable.java b/lib/src/main/java/zeroecho/core/annotation/Describable.java new file mode 100644 index 0000000..7624ba8 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/annotation/Describable.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.annotation; + +/** + * Defines a contract for objects that can provide a concise, human-readable + * description. + * + *

+ * Implementations of {@code Describable} are intended to supply metadata that + * is suitable for display in user interfaces, diagnostic logs, or serialization + * outputs. This avoids overloading {@link Object#toString()} with potentially + * verbose or unstable text, and ensures that a stable, context-appropriate + * description is available. + *

+ * + *

Design considerations

+ *
    + *
  • The description should never include sensitive data such as key + * material.
  • + *
  • Descriptions are meant for human consumption, not for protocol + * negotiation or cryptographic decisions.
  • + *
  • Catalogs and registries (for example, {@code CryptoCatalog}) can use this + * interface to extract meaningful labels for algorithms or specs.
  • + *
+ * + *

Example

{@code
+ * public final class AesSpec implements Describable {
+ *     @Override
+ *     public String description() {
+ *         return "AES-256 in GCM mode with 128-bit tag";
+ *     }
+ * }
+ * }
+ * + * @since 1.0 + */ +public interface Describable { // NOPMD + /** + * Returns a short, human-readable description of this object. + * + *

+ * The description should be concise and stable enough for logging or display + * purposes, while avoiding exposure of any sensitive information. + *

+ * + * @return non-null descriptive string + */ + String description(); +} diff --git a/lib/src/main/java/zeroecho/core/annotation/DisplayName.java b/lib/src/main/java/zeroecho/core/annotation/DisplayName.java new file mode 100644 index 0000000..50686f2 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/annotation/DisplayName.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Declares a human-friendly display name for a class, specification, or + * capability. + * + *

+ * The {@code DisplayName} annotation is intended for use in contexts where a + * concise, stable label is required for presentation or diagnostic purposes. + * Frameworks such as catalogs, serializers, or logging utilities may check for + * this annotation and use its value instead of relying on + * {@link Class#getSimpleName()}. + *

+ * + *

Design considerations

+ *
    + *
  • Display names are not identifiers and must not be used for protocol + * negotiation or security-critical decisions.
  • + *
  • The value should be concise, human-readable, and stable across versions + * where compatibility matters.
  • + *
  • When absent, consumers typically fall back to {@code Describable} or + * class names.
  • + *
+ * + *

Example

{@code
+ * @DisplayName("AES-GCM 256-bit")
+ * public final class AesSpec implements AlgorithmKeySpec {
+ *     ...
+ * }
+ * }
+ * + * @since 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface DisplayName { + /** + * Returns the human-friendly display name. + * + * @return a descriptive string, never {@code null} + */ + String value(); +} diff --git a/lib/src/main/java/zeroecho/core/annotation/package-info.java b/lib/src/main/java/zeroecho/core/annotation/package-info.java new file mode 100644 index 0000000..75c173b --- /dev/null +++ b/lib/src/main/java/zeroecho/core/annotation/package-info.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Provides metadata contracts and annotations for cryptographic algorithms and + * specifications. + * + *

+ * This package offers simple mechanisms for attaching human-readable labels to + * classes, specifications, or capabilities. The metadata is intended for + * display, diagnostics, and serialization, helping avoid exposure of internal + * class names or verbose {@code toString()} output. + *

+ * + *

Key elements

+ *
    + *
  • {@link Describable} - interface for supplying a concise, human-readable + * description string.
  • + *
  • {@link DisplayName} - annotation that associates a type with a stable, + * human-friendly label preferred by catalogs and UIs.
  • + *
+ * + *

Usage notes

+ *
    + *
  • Annotate user-facing types with the display-name annotation to keep + * labels stable even if class names change.
  • + *
  • Have specification objects implement the description interface to provide + * short, precise labels for configuration and documentation.
  • + *
  • Consumers should prefer the display-name label when present; otherwise + * use the provided description, then fall back to the class name.
  • + *
+ * + *

Design philosophy

+ *
    + *
  • Metadata must never include sensitive information such as keys or raw + * secrets.
  • + *
  • These constructs are for human-facing contexts (logs, configuration + * dumps, UIs), not for protocol negotiation or security decisions.
  • + *
  • Separating descriptive metadata from functional APIs keeps labels stable + * and predictable without impacting core security behavior.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.annotation; diff --git a/lib/src/main/java/zeroecho/core/audit/AuditListener.java b/lib/src/main/java/zeroecho/core/audit/AuditListener.java new file mode 100644 index 0000000..034ee71 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/audit/AuditListener.java @@ -0,0 +1,485 @@ +/******************************************************************************* + * 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.core.audit; + +import java.security.Key; +import java.security.KeyPair; +import java.util.Map; + +import zeroecho.core.KeyUsage; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spec.ContextSpec; + +/** + * Listener for structured audit events emitted by audited crypto contexts. + * + *

+ * The listener receives lifecycle, progress, metadata, and failure callbacks + * from proxies created by utilities such as {@code AuditedContexts}. All + * default methods are no-ops so that implementors can override only the events + * of interest. + *

+ * + *

Usage

{@code
+ * AuditListener listener = new AuditListener() {
+ *     @Override
+ *     public void onProgress(String ctxId, long bodyBytes, long trailerBytes) {
+ *         System.out.println("ctx=" + ctxId + " body=" + bodyBytes + " trailer=" + trailerBytes);
+ *     }
+ *     @Override
+ *     public void onFailure(String ctxId, String stage, String op, Throwable cause) {
+ *         logger.warn("ctx={} stage={} op={}", ctxId, stage, op, cause);
+ *     }
+ * };
+ * }
+ * + *

+ * Unless stated otherwise, all methods are best-effort and are not guaranteed + * to be called for every possible code path. Implementations should be + * defensive and tolerate repeated or missing events. + *

+ */ +public interface AuditListener { + /** + * Emitted right after a context has been created and wrapped. + * + *

+ * The callback conveys basic provenance such as provider label, the intended + * key usage role, and optional key and specification objects. Implementations + * must not attempt to extract secrets from the provided objects. + *

+ * + * @param the concrete context specification type + * @param the concrete key type + * @param id an algorithm or implementation identifier supplied by the + * creator; never null but may be a generic label such as + * "unknown" + * @param provider a provider or vendor label; may be "unknown" + * @param role the usage role associated with the context, for example + * ENCRYPTION or VERIFY + * @param key the key associated with the context if available, or null if + * not applicable + * @param spec the context specification if available, or null if not + * applicable + */ + default void onContextCreated(String id, String provider, KeyUsage role, + K key, S spec) { + // empty + } + + /** + * Emitted after key pair generation via SPI. + * + *

+ * This event signals successful generation and provides the resulting pair. + * Implementations must not persist private key material unless explicitly + * required by policy. + *

+ * + * @param id an algorithm or implementation identifier supplied by the + * generator + * @param provider a provider or vendor label + * @param spec the algorithm key specification used for generation; never + * null + * @param kp the generated key pair; never null + */ + default void onKeyGenerated(String id, String provider, AlgorithmKeySpec spec, KeyPair kp) { + // empty + } + + /** + * Emitted after a key is constructed or imported. + * + *

+ * This event can be used to track import flows separately from on-device + * generation. + *

+ * + * @param id an algorithm or implementation identifier supplied by the + * builder + * @param provider a provider or vendor label + * @param spec the algorithm key specification used for construction; never + * null + * @param key the constructed or imported key; never null + */ + default void onKeyBuilt(String id, String provider, AlgorithmKeySpec spec, Key key) { + // empty + } + + /** + * Emitted when a context is closed (generic form). + * + *

+ * This form mirrors legacy summary notifications and may be emitted in addition + * to the id-based closure callback. + *

+ * + * @param id an algorithm or implementation identifier supplied at + * creation time + * @param provider a provider or vendor label + * @param role the usage role associated with the context + * @param key the primary key associated with the context, or null if + * unavailable + */ + default void onContextClosed(String id, String provider, KeyUsage role, Key key) { + // empty + } + + /** + * Emitted when a key was destroyed. + * + *

+ * Destruction is best-effort and may not be observable for all key types. + * Receipt of this event does not guarantee secure erasure. + *

+ * + * @param id an algorithm or implementation identifier related to the key + * @param provider a provider or vendor label + * @param key the key being destroyed; may be null for non-extractable + * handles + */ + default void onKeyDestroyed(String id, String provider, Key key) { + // empty + } + + /** + * Creation event with correlation id and non-secret specification metadata. + * + *

+ * The metadata map is a best-effort summary extracted from the specification + * object and must not include secret values. Typical entries include mode, tag + * length, IV length, and header codec id. + *

+ * + * @param ctxId a generated correlation id unique to the wrapped + * context instance + * @param algoId an algorithm or implementation identifier; never null + * but may be "unknown" + * @param provider a provider or vendor label; never null but may be + * "unknown" + * @param role the usage role associated with the context + * @param keyFingerprint a short, non-reversible fingerprint of the key material + * or "n/a" + * @param specMeta a map of non-secret specification attributes, or null + * if unavailable + */ + default void onContextCreatedMeta(String ctxId, String algoId, String provider, KeyUsage role, // NOPMD + String keyFingerprint, Map specMeta) { + // empty + } + + /** + * Streaming progress for a context, split into body and trailer bytes. + * + *

+ * For TagEngine-style streams, the trailer typically corresponds to an + * authentication tag that is excluded from the body count. + *

+ * + * @param ctxId the correlation id for the context instance + * @param bodyBytes cumulative number of body bytes observed so far + * @param trailerBytes cumulative number of trailer bytes observed so far + */ + default void onProgress(String ctxId, long bodyBytes, long trailerBytes) { + // empty + } + + /** + * Notification that a tag or signature was produced at end of stream. + * + *

+ * This is typically emitted for SIGN, MAC, or DIGEST roles when the tag length + * is known and no verification was requested. + *

+ * + * @param ctxId the correlation id for the context instance + * @param tagLength the length, in bytes, of the produced tag + * @param policy a string label describing the verification or emission + * policy + */ + default void onTagProduced(String ctxId, int tagLength, String policy) { + // empty + } + + /** + * Verification outcome for contexts operating in verify mode. + * + *

+ * The expected tag source indicates how the verifier obtained the expected + * value, for example provided directly by the caller, read from the stream + * tail, or parsed from a header. + *

+ * + * @param ctxId the correlation id for the context instance + * @param ok true if verification succeeded, false otherwise + * @param policy a string label describing the verification policy in + * effect + * @param expectedSource expected tag provenance, for example "provided", + * "tail", or "header" + * @param tagLength the length, in bytes, of the expected tag + */ + default void onVerifyResult(String ctxId, boolean ok, String policy, String expectedSource, int tagLength) { + // empty + } + + /** + * Notification that a header was written to a stream. + * + *

+ * Only lengths are reported. No secret material is exposed. + *

+ * + * @param ctxId the correlation id for the context instance + * @param codecId an identifier for the header codec used + * @param headerLen total header length in bytes + * @param ivLen initialization vector length in bytes, or 0 if none + * @param saltLen salt length in bytes, or 0 if none + * @param aadLen associated data length in bytes, or 0 if none + */ + default void onHeaderWritten(String ctxId, String codecId, int headerLen, int ivLen, int saltLen, int aadLen) { + // empty + } + + /** + * Notification that a header was read from a stream. + * + *

+ * Only lengths are reported. No secret material is exposed. + *

+ * + * @param ctxId the correlation id for the context instance + * @param codecId an identifier for the header codec used + * @param headerLen total header length in bytes + * @param ivLen initialization vector length in bytes, or 0 if none + * @param saltLen salt length in bytes, or 0 if none + * @param aadLen associated data length in bytes, or 0 if none + */ + default void onHeaderRead(String ctxId, String codecId, int headerLen, int ivLen, int saltLen, int aadLen) { + // empty + } + + /** + * Notification about random material generation. + * + *

+ * Examples include IVs, nonces, salts, or keys created via a secure RNG. + *

+ * + * @param ctxId the correlation id for the context instance + * @param kind a label describing the material kind, for example "iv", "salt", + * or "keygen" + * @param rng a label or provider name for the random source + * @param length the number of bytes generated + */ + default void onRandomMaterial(String ctxId, String kind, String rng, int length) { + // empty + } + + /** + * Summary of key derivation function usage. + * + *

+ * Implementations can use this for parameter auditing and policy checks. + *

+ * + * @param ctxId the correlation id for the context instance + * @param kdf the KDF identifier, for example "HKDF" or "PBKDF2" + * @param saltLen salt length in bytes, or 0 if none + * @param infoLen info/context length in bytes, or 0 if none + * @param outLen length in bytes of the derived output + * @param iterations iteration count when applicable, or null if not applicable + */ + default void onKdfUsed(String ctxId, String kdf, int saltLen, int infoLen, int outLen, Integer iterations) { + // empty + } + + /** + * Failure during streaming or reflective invocation. + * + *

+ * The stage describes the processing phase, while the op gives a short + * operation label such as "read" or "invoke:deriveSecret". + *

+ * + * @param ctxId the correlation id for the context instance + * @param stage a coarse-grained stage label, for example "read" or "invoke" + * @param op a fine-grained operation label, for example "wrap", "attach", or + * "invoke:setPeerPublic" + * @param cause the exception that caused the failure; never null + */ + default void onFailure(String ctxId, String stage, String op, Throwable cause) { + // empty + } + + /** + * Closure notification with totals and elapsed time. + * + *

+ * This event reports cumulative body and trailer byte counts and the duration + * in milliseconds between creation and closure. + *

+ * + * @param ctxId the correlation id for the context instance + * @param bodyBytes total body bytes processed + * @param trailerBytes total trailer bytes processed + * @param durationMillis processing duration in milliseconds, minimum of 1 + */ + default void onContextClosed(String ctxId, long bodyBytes, long trailerBytes, long durationMillis) { + // empty + } + + /** + * Notification that a peer public key was provided to an agreement context. + * + *

+ * The fingerprint is a short, non-reversible identifier suitable for logs. + *

+ * + * @param ctxId the correlation id for the context instance + * @param peerFingerprint a short, non-reversible fingerprint of the peer public + * key + */ + default void onAgreementPeerSet(String ctxId, String peerFingerprint) { + // empty + } + + /** + * Notification that an agreement secret was derived. + * + *

+ * Only the length is reported. The secret value itself is never exposed. + *

+ * + * @param ctxId the correlation id for the context instance + * @param secretLength the derived secret length in bytes, or -1 if unknown + */ + default void onAgreementDerived(String ctxId, int secretLength) { + // empty + } + + /** + * Notification that a peer message was provided to a message agreement context. + * + * @param ctxId the correlation id for the context instance + * @param msgLen the provided message length in bytes + */ + default void onAgreementPeerMessageSet(String ctxId, int msgLen) { + // empty + } + + /** + * Notification that a peer message was retrieved from a message agreement + * context. + * + * @param ctxId the correlation id for the context instance + * @param msgLen the retrieved message length in bytes, or -1 if unavailable + */ + default void onAgreementPeerMessageGet(String ctxId, int msgLen) { + // empty + } + + /** + * Legacy single-argument creation callback. + * + *

+ * Emitted when a context is wrapped. Prefer + * {@link #onContextCreatedMeta(String, String, String, KeyUsage, String, Map)} + * for structured metadata and correlation. + *

+ * + * @param ctx the wrapped crypto context; never null + */ + default void onContextCreated(CryptoContext ctx) { + // empty + } + + /** + * Legacy cumulative byte counter for any role. + * + *

+ * Prefer {@link #onProgress(String, long, long)} for trailer-aware accounting. + *

+ * + * @param role the usage role associated with the counted bytes + * @param count the cumulative number of bytes observed so far + */ + default void onBytes(KeyUsage role, long count) { + // empty + } + + /** + * Legacy KEM encapsulation event. + * + *

+ * Prefer id-scoped events such as + * {@link #onContextCreatedMeta(String, String, String, KeyUsage, String, Map)} + * combined with higher level KEM notifications when available. + *

+ * + * @param ciphertextLength the length, in bytes, of the produced ciphertext, + * or -1 if unknown + * @param sharedSecretLength the length, in bytes, of the produced shared + * secret, or -1 if unknown + */ + default void onKemEncapsulated(int ciphertextLength, int sharedSecretLength) { + // empty + } + + /** + * Legacy KEM decapsulation event. + * + * @param sharedSecretLength the length, in bytes, of the decapsulated shared + * secret, or -1 if unknown + */ + default void onKemDecapsulated(int sharedSecretLength) { + // empty + } + + /** + * Returns a ready-to-use listener that performs no actions. + * + *

+ * This is convenient for tests and for configuring systems where auditing is + * optional. + *

+ * + * @return a no-op listener instance + */ + static AuditListener noop() { + return new AuditListener() { + }; + } +} diff --git a/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java b/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java new file mode 100644 index 0000000..22be474 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/audit/AuditedContexts.java @@ -0,0 +1,704 @@ +/******************************************************************************* + * 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.core.audit; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.AgreementContext; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.KemContext.KemResult; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.ContextSpec; + +/** + * Lightweight dynamic proxy that audits crypto contexts without changing their + * behavior. + * + *

+ * This utility wraps an existing context instance and transparently intercepts + * selected operations in order to emit audit events. The wrapper delegates all + * calls to the underlying context and does not alter the functional results. + *

+ * + *

What is audited

+ *
    + *
  • attach(InputStream): classic byte counting over the + * processed body.
  • + *
  • wrap(InputStream) on TagEngine-style contexts + * (SIGN/VERIFY, MAC, DIGEST): trailer-aware accounting that distinguishes body + * bytes from the authentication tag trailer; verification success or failure is + * inferred at EOF when possible.
  • + *
  • KEM: {@code encapsulate()} and + * {@code decapsulate(byte[])} are observed to report ciphertext and + * shared-secret lengths.
  • + *
  • Agreement: peer material assignment and secret + * derivation are captured with non-reversible fingerprints and byte sizes.
  • + *
+ * + *

Events and correlation

+ *

+ * Creation, progress, failure, and closure events are emitted. Each wrapped + * instance is associated with a generated correlation id that is included with + * progress and failure notifications. When available, algorithm id, provider + * label, key fingerprint, and a best-effort summary of the context + * specification are recorded at creation time. + *

+ * + *

Behavioral guarantees

+ *
    + *
  • The proxy preserves the original semantics and exceptions of the wrapped + * context. Any failure reported to the listener mirrors the underlying + * failure.
  • + *
  • Counting is performed by decorating the returned {@code InputStream}s; no + * buffering beyond normal {@code FilterInputStream} forwarding is + * introduced.
  • + *
  • Idempotent wrapping: contexts that are already JDK proxies will be + * returned unchanged.
  • + *
+ * + *

Usage example

{@code
+ * AuditListener listener = ...;
+ * KeyUsage role = KeyUsage.ENCRYPTION;
+ * CryptoContext original = ...; // e.g., an EncryptionContext
+ *
+ * CryptoContext audited = AuditedContexts.wrap(original, listener, role);
+ * try (InputStream in = audited.attach(sourceStream)) {
+ *     in.transferTo(sink);
+ * }
+ * }
+ */ +public final class AuditedContexts { + private static final String UNKNOWN = "unknown"; + + /** + * Prevents instantiation. + */ + private AuditedContexts() { + } + + /** + * Wraps a crypto context with an auditing proxy that emits lifecycle, progress, + * and error events while preserving the original behavior. + * + *

+ * If {@code ctx} is {@code null}, this method returns {@code null}. If + * {@code ctx} is already a JDK dynamic proxy, the instance is returned as-is to + * keep wrapping idempotent. + *

+ * + *

+ * The returned proxy implements the same set of interfaces as the original + * object. It observes: + *

    + *
  • {@code attach(InputStream)} for classic cumulative byte counts,
  • + *
  • {@code wrap(InputStream)} for TagEngine-style contexts to account for + * body vs. authentication tag trailer and to report tag production or verify + * results at EOF when the tag length is known,
  • + *
  • {@code encapsulate()} and {@code decapsulate(byte[])} on KEM contexts to + * report ciphertext and secret lengths,
  • + *
  • agreement operations (peer material assignment and secret derivation) + * with non-reversible key fingerprints and derived sizes.
  • + *
+ * Creation metadata includes a generated correlation id, algorithm id, provider + * label, key fingerprint (if extractable), and a best-effort, non-secret spec + * summary when available. + * + *

+ * This call does not invoke any context operations other than the minimal, + * best-effort introspection used to produce creation metadata. Functional + * results and exceptions from the wrapped object are forwarded unchanged. + *

+ * + *

+ * Thread-safety: the proxy does not introduce additional + * synchronization. It is safe only to the extent the wrapped {@code ctx} is + * safe. + *

+ * + * @param ctx the context to wrap; may be {@code null} + * @param audit the listener that will receive audit events; must not be + * {@code null} + * @param role the usage role associated with the context (for example, + * ENCRYPTION, DECRYPTION, SIGNING); must not be {@code null} + * @return the auditing proxy for {@code ctx}, the original {@code ctx} if it is + * already a proxy, or {@code null} if {@code ctx} is {@code null} + */ + public static CryptoContext wrap(final CryptoContext ctx, final AuditListener audit, final KeyUsage role) { + if (ctx == null) { + return null; + } + if (Proxy.isProxyClass(ctx.getClass())) { + return ctx; // idempotent + } + // IMPORTANT: do not call any other wrap(...) here — avoid recursion. + return replaceWithProxy(ctx, audit, role); + } + + @SuppressWarnings("unchecked") + private static T replaceWithProxy(final T ctx, final AuditListener audit, final KeyUsage role) { // NOPMD + Objects.requireNonNull(ctx, "ctx must not be null"); + Objects.requireNonNull(audit, "audit must not be null"); + Objects.requireNonNull(role, "role must not be null"); + + // Emit creation metadata if we can resolve it + String ctxId = UUID.randomUUID().toString(); + String algoId = null; + String provider = null; + String keyFp; + Map specMeta = null; + Key keyObj = null; + ContextSpec specObj = null; + + if (ctx instanceof CryptoContext cctx) { // NOPMD + try { + CryptoAlgorithm alg = cctx.algorithm(); + if (alg != null) { + try { + algoId = safeString(invokeNoArg(alg, "id")); + } catch (Throwable ignore) { // NOPMD + algoId = alg.getClass().getSimpleName(); + } + try { + provider = safeString(invokeNoArg(alg, "providerLabel")); + } catch (Throwable ignore) { // NOPMD + try { + provider = safeString(invokeNoArg(alg, "provider")); + } catch (Throwable ignoredToo) { // NOPMD + provider = alg.getClass().getPackageName(); + } + } + } + } catch (Throwable ignore) { // NOPMD + // best-effort + } + try { + keyObj = cctx.key(); + keyFp = fingerprint(keyObj); + } catch (Throwable ignore) { // NOPMD + keyFp = "n/a"; + } + try { + // optional: contexts may expose a spec() accessor + Object s = invokeNoArg(cctx, "spec"); + if (s instanceof ContextSpec) { + specObj = (ContextSpec) s; + specMeta = SpecIntrospector.summarize(specObj); + } + } catch (Throwable ignore) { // NOPMD + specObj = null; + specMeta = null; + } + + // New-style context-created event with metadata + audit.onContextCreatedMeta(ctxId, algoId == null ? UNKNOWN : algoId, provider == null ? UNKNOWN : provider, + role, keyFp, specMeta); + + // Back-compat event forms + try { + // Generic with id/provider/role/key/spec + audit.onContextCreated(algoId == null ? UNKNOWN : algoId, provider == null ? UNKNOWN : provider, role, + keyObj, specObj); + } catch (Throwable ignore) { // NOPMD + } + try { + // Legacy single-arg callback + audit.onContextCreated(cctx); + } catch (Throwable ignore) { // NOPMD + } + } + + ClassLoader cl = ctx.getClass().getClassLoader(); // NOPMD + Class[] ifaces = allInterfaces(ctx.getClass()); + InvocationHandler handler = new AuditingHandler(ctx, audit, role, ctxId, algoId == null ? UNKNOWN : algoId, + provider == null ? UNKNOWN : provider, keyObj); + + return (T) Proxy.newProxyInstance(cl, ifaces, handler); + } + + private static Class[] allInterfaces(Class type) { + Set> set = new LinkedHashSet<>(); + for (Class c = type; c != null; c = c.getSuperclass()) { + Class[] local = c.getInterfaces(); + for (Class i : local) { + set.add(i); + } + } + return set.toArray(Class[]::new); + } + + private static final class AuditingHandler implements InvocationHandler { // NOPMD + private final Object target; + private final AuditListener audit; + private final KeyUsage role; + + private final String ctxId; + private final String algoId; + private final String provider; + private final Key keyForClose; // may be null + + private long bodyBytes; + private long trailerBytes; + private final long startNanos; + + private Integer tagLen; // for TagEngine contexts + private boolean verifyMode; // true if setExpectedTag(...) was called + private String policyLabel = "UNSET"; + private String expectedSource = "provided"; // default for setExpectedTag(byte[]) + + private AuditingHandler(Object target, AuditListener audit, KeyUsage role, String ctxId, String algoId, + String provider, Key keyForClose) { + this.target = target; + this.audit = audit; + this.role = role; + this.ctxId = ctxId; + this.algoId = algoId; + this.provider = provider; + this.keyForClose = keyForClose; + this.startNanos = System.nanoTime(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // NOPMD + final String name = method.getName(); + + // Object methods + if ("toString".equals(name) && (args == null || args.length == 0)) { + return "AuditedProxy(" + target + ")"; + } + if ("hashCode".equals(name) && (args == null || args.length == 0)) { + return System.identityHashCode(proxy); + } + if ("equals".equals(name) && args != null && args.length == 1) { + return proxy == args[0]; // NOPMD + } + + try { + // --- TagEngine plumbing --- + + // Track tagLength() if requested explicitly + if ("tagLength".equals(name) && (args == null || args.length == 0)) { + Object res = method.invoke(target, args); + if (res instanceof Integer) { + this.tagLen = (Integer) res; + } + return res; + } + + // Track verification policy (label only; do not depend on specific enum type) + if ("setVerificationPolicy".equals(name) && args != null && args.length == 1) { + Object pol = args[0]; + this.policyLabel = (pol == null ? "null" : pol.toString()); + return method.invoke(target, args); + } + + // setExpectedTag(byte[]) marks verify mode + if ("setExpectedTag".equals(name) && args != null && args.length == 1 && args[0] instanceof byte[]) { + this.verifyMode = true; + this.expectedSource = "provided"; + return method.invoke(target, args); + } + + // Intercept wrap(InputStream) for TagEngine contexts + if ("wrap".equals(name) && args != null && args.length == 1 && args[0] instanceof InputStream) { + // Update tagLen lazily if not known + if (this.tagLen == null) { + try { + Object tl = target.getClass().getMethod("tagLength").invoke(target); + if (tl instanceof Integer i) { // NOPMD + this.tagLen = i; + } + } catch (Throwable ignore) { // NOPMD + this.tagLen = null; + } + } + InputStream transformed = (InputStream) method.invoke(target, args); + return countingTagAware(transformed, audit, ctxId, role, this); + } + + // --- Classic attach(InputStream) counting --- + if ("attach".equals(name) && args != null && args.length == 1 && args[0] instanceof InputStream) { + InputStream transformed = (InputStream) method.invoke(target, args); + return counting(transformed, audit, ctxId, role, this); + } + + // --- AgreementContext (DH/ECDH/X25519, etc.) --- + if (target instanceof AgreementContext) { + // setPeerPublic(PublicKey) + if ("setPeerPublic".equals(name) && args != null && args.length == 1 + && args[0] instanceof PublicKey) { + try { + Object res = method.invoke(target, args); + try { + String peerFp = fingerprint((Key) args[0]); // short, non-reversible + audit.onAgreementPeerSet(ctxId, peerFp); + } catch (Throwable ignore) { // NOPMD + } + return res; + } catch (Throwable t) { // NOPMD + Throwable cause = unwrapInvocationTarget(t); + try { + audit.onFailure(ctxId, "invoke:setPeerPublic", role.name(), cause); + } catch (Throwable ignore) { // NOPMD + } + throw cause; + } + } + + // deriveSecret() + if ("deriveSecret".equals(name) && (args == null || args.length == 0)) { + try { + byte[] secret = (byte[]) method.invoke(target); + try { + audit.onAgreementDerived(ctxId, secret == null ? -1 : secret.length); + } catch (Throwable ignore) { // NOPMD + } + return secret; + } catch (Throwable t) { // NOPMD + Throwable cause = unwrapInvocationTarget(t); + try { + audit.onFailure(ctxId, "invoke:deriveSecret", role.name(), cause); + } catch (Throwable ignore) { // NOPMD + } + throw cause; + } + } + } + + // =========== MessageAgreementContext (KEM-style) =========== + if (target instanceof MessageAgreementContext) { + // setPeerMessage(byte[]) + if ("setPeerMessage".equals(name) && args != null && args.length == 1 + && args[0] instanceof byte[]) { + try { + Object r = method.invoke(target, args); + try { + int len = ((byte[]) args[0]).length; + audit.onAgreementPeerMessageSet(ctxId, len); + } catch (Throwable ignore) { // NOPMD + } + return r; + } catch (Throwable t) { // NOPMD + Throwable cause = unwrapInvocationTarget(t); + try { + audit.onFailure(ctxId, "invoke:setPeerMessage", role.name(), cause); + } catch (Throwable ignore) { // NOPMD + } + throw cause; + } + } + + // getPeerMessage() + if ("getPeerMessage".equals(name) && (args == null || args.length == 0)) { + try { + byte[] msg = (byte[]) method.invoke(target); + try { + audit.onAgreementPeerMessageGet(ctxId, msg == null ? -1 : msg.length); + } catch (Throwable ignore) { // NOPMD + } + return msg; + } catch (Throwable t) { // NOPMD + Throwable cause = unwrapInvocationTarget(t); + try { + audit.onFailure(ctxId, "invoke:getPeerMessage", role.name(), cause); + } catch (Throwable ignore) { // NOPMD + } + throw cause; + } + } + } + + // --- KEM --- + if (target instanceof KemContext) { + if ("encapsulate".equals(name) && (args == null || args.length == 0)) { + KemResult r = (KemResult) method.invoke(target); + try { + audit.onKemEncapsulated(r == null || r.ciphertext() == null ? -1 : r.ciphertext().length, + r == null || r.sharedSecret() == null ? -1 : r.sharedSecret().length); + } catch (Throwable ignore) { // NOPMD + } + return r; + } + if ("decapsulate".equals(name) && args != null && args.length == 1 && args[0] instanceof byte[]) { + byte[] secret = (byte[]) method.invoke(target, args); + try { + audit.onKemDecapsulated(secret == null ? -1 : secret.length); + } catch (Throwable ignore) { // NOPMD + } + return secret; + } + } + + // --- close(): emit closure metadata --- + if ("close".equals(name) && (args == null || args.length == 0)) { + try { + return method.invoke(target); + } finally { + long durationMs = Math.max(1L, (System.nanoTime() - startNanos) / 1_000_000L); + try { + audit.onContextClosed(ctxId, bodyBytes, trailerBytes, durationMs); + } catch (Throwable ignore) { // NOPMD + } + try { + audit.onContextClosed(algoId, provider, role, keyForClose); + } catch (Throwable ignore) { // NOPMD + } + } + } + + // Default delegation + return method.invoke(target, args); + } catch (Throwable t) { // NOPMD + Throwable cause = unwrapInvocationTarget(t); + try { + audit.onFailure(ctxId, "invoke:" + name, role.name(), cause); + } catch (Throwable ignore) { // NOPMD + } + throw cause; + } + } + } + + private static InputStream counting(final InputStream in, final AuditListener audit, final String ctxId, + final KeyUsage role, final AuditingHandler h) { + return new FilterInputStream(in) { + private long count; + + @Override + public int read() throws IOException { + try { + int r = super.read(); + if (r >= 0) { + count++; + h.bodyBytes++; + try { + audit.onBytes(role, count); // legacy + audit.onProgress(ctxId, h.bodyBytes, h.trailerBytes); // new + } catch (Throwable ignore) { // NOPMD + } + } + return r; + } catch (IOException ioe) { + try { + audit.onFailure(ctxId, "read", role.name(), ioe); + } catch (Throwable ignore) { // NOPMD + } + throw ioe; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + try { + int n = super.read(b, off, len); + if (n > 0) { + count += n; + h.bodyBytes += n; + try { + audit.onBytes(role, count); // legacy + audit.onProgress(ctxId, h.bodyBytes, h.trailerBytes); // new + } catch (Throwable ignore) { // NOPMD + } + } + return n; + } catch (IOException ioe) { + try { + audit.onFailure(ctxId, "read", role.name(), ioe); + } catch (Throwable ignore) { // NOPMD + } + throw ioe; + } + } + }; + } + + private static InputStream countingTagAware(final InputStream in, final AuditListener audit, final String ctxId, + final KeyUsage role, final AuditingHandler h) { + return new FilterInputStream(in) { + private long total; + private boolean eof; + + @Override + public int read() throws IOException { + try { + int r = super.read(); + if (r >= 0) { + total++; + h.bodyBytes++; // provisional (reclassified at EOF if tagLen known) + emitProgress(); + } else { + onEof(); + } + return r; + } catch (IOException ioe) { + onFailureMaybeVerify(ioe); + throw ioe; + } + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + try { + int n = super.read(b, off, len); + if (n > 0) { + total += n; + h.bodyBytes += n; // provisional + emitProgress(); + } else if (n == -1) { + onEof(); + } + return n; + } catch (IOException ioe) { + onFailureMaybeVerify(ioe); + throw ioe; + } + } + + @Override + public void close() throws IOException { + try { // NOPMD + transferTo(OutputStream.nullOutputStream()); + onEof(); + } finally { + super.close(); + } + } + + private void emitProgress() { + try { + audit.onBytes(role, total); // legacy cumulative + audit.onProgress(ctxId, h.bodyBytes, h.trailerBytes); + } catch (Throwable ignore) { // NOPMD + } + } + + private void onEof() { + if (eof) { + return; + } + eof = true; + + // Reclassify trailer if tagLen known + if (h.tagLen != null && h.tagLen > 0 && h.bodyBytes >= h.tagLen) { + h.bodyBytes -= h.tagLen; + h.trailerBytes += h.tagLen; + try { + audit.onProgress(ctxId, h.bodyBytes, h.trailerBytes); + } catch (Throwable ignore) { // NOPMD + } + + // Produce tag or verification result at EOF (no exception thrown) + try { + if (h.verifyMode) { + audit.onVerifyResult(ctxId, true, h.policyLabel, h.expectedSource, h.tagLen); + } else { + audit.onTagProduced(ctxId, h.tagLen, h.policyLabel); + } + } catch (Throwable ignore) { // NOPMD + } + } + } + + private void onFailureMaybeVerify(IOException ioe) { + try { + audit.onFailure(ctxId, "read", role.name(), ioe); + if (h.verifyMode && h.tagLen != null && h.tagLen > 0) { + // Heuristic: verification failures commonly bubble as IOException from the + // engine. + audit.onVerifyResult(ctxId, false, h.policyLabel, h.expectedSource, h.tagLen); + } + } catch (Throwable ignore) { // NOPMD + } + } + }; + } + + private static Object invokeNoArg(Object target, String method) throws Throwable { + Method m = target.getClass().getMethod(method); + m.setAccessible(true); // NOPMD + return m.invoke(target); + } + + private static Throwable unwrapInvocationTarget(Throwable t) { + if (t instanceof java.lang.reflect.InvocationTargetException ite && ite.getTargetException() != null) { + return ite.getTargetException(); + } + return t; + } + + private static String safeString(Object o) { + return o == null ? "null" : o.toString(); + } + + private static String fingerprint(Key key) { + if (key == null) { + return "n/a"; + } + try { + byte[] enc = key.getEncoded(); + if (enc == null) { + // Non-extractable key: fall back to type information + return key.getAlgorithm() + ":" + key.getClass().getSimpleName(); + } + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] d = md.digest(enc); + // hex-short: first 8 bytes + StringBuilder sb = new StringBuilder(2 * 8); + for (int i = 0; i < Math.min(8, d.length); i++) { + sb.append(String.format("%02x", d[i])); + } + return key.getAlgorithm() + ":" + sb.toString(); + } catch (NoSuchAlgorithmException e) { + return key.getAlgorithm() + ":fp-error"; + } + } +} diff --git a/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java b/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java new file mode 100644 index 0000000..2a19711 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/audit/JulAuditListenerStd.java @@ -0,0 +1,669 @@ +/******************************************************************************* + * 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.core.audit; + +import java.security.Key; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.core.KeyUsage; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spec.ContextSpec; + +/** + * AuditListener implementation that emits structured Java Util Logging records. + * + *

+ * The listener produces parameterized JUL messages in a stable key=value format + * suitable for ingestion by log processors. It never logs secret material: keys + * are represented only by short, non-reversible fingerprints and specification + * objects are summarized by simple type names. + *

+ * + *

Configuration

+ *

+ * Instances are created via {@link #builder()}. The builder allows selecting + * the target {@code Logger}, individual levels for informational, warning, and + * progress messages, and whether stack traces are included for failures. + *

+ * + *

Logging format

+ *

+ * Each callback logs a single structured line. Examples: + *

+ *
{@code
+ * // Context created:
+ * CTX_CREATED algo=AES/GCM provider=ZeroEcho role=ENCRYPTION keyFp=AES:1a2b3c4d spec=EncryptSpec
+ *
+ * // Progress tick:
+ * PROGRESS ctxId=abc-123 body=4096 trailer=16
+ *
+ * // Failure with stack trace when enabled:
+ * FAILURE ctxId=abc-123 stage=read op=wrap error=IOException message=stream closed
+ * }
+ * + *

+ * This implementation checks {@code Logger#isLoggable(Level)} before formatting + * and uses parameterized {@code Logger#log(Level, String, Object[])} to defer + * formatting cost when messages are disabled at the chosen level. + *

+ */ +public final class JulAuditListenerStd implements AuditListener { + /** + * Underlying JUL logger used to emit audit records. + */ + private final Logger log; + /** + * Log level used for informational events such as creation, tag produced, and + * closure metadata. + */ + private final Level infoLevel; + + /** + * Log level used for warnings and verification failures. + */ + private final Level warnLevel; + /** + * Log level used for frequent progress-style events such as header I/O, KDF + * summaries, and byte counters. + */ + private final Level progressLevel; + /** + * Whether failure callbacks include stack traces in addition to a structured + * summary line. + */ + private final boolean includeStackTraces; + + /** + * Creates a listener configured by the supplied builder. + * + * @param b the configuration builder; must not be null + */ + private JulAuditListenerStd(Builder b) { + this.log = (b.logger != null ? b.logger : Logger.getLogger("zeroecho.audit")); + this.infoLevel = b.infoLevel; + this.warnLevel = b.warnLevel; + this.progressLevel = b.progressLevel; + this.includeStackTraces = b.includeStackTraces; + } + + /** + * Fluent factory for {@code JulAuditListenerStd}. + * + *

+ * The builder defaults are: + *

    + *
  • {@code logger}: {@code Logger.getLogger("zeroecho.audit")}
  • + *
  • {@code infoLevel}: {@code Level.INFO}
  • + *
  • {@code warnLevel}: {@code Level.WARNING}
  • + *
  • {@code progressLevel}: {@code Level.FINE}
  • + *
  • {@code includeStackTraces}: {@code true}
  • + *
+ * + *

Example

{@code
+     * JulAuditListenerStd listener = JulAuditListenerStd.builder()
+     *     .logger(Logger.getLogger("audit"))
+     *     .infoLevel(Level.INFO)
+     *     .warnLevel(Level.WARNING)
+     *     .progressLevel(Level.FINER)
+     *     .includeStackTraces(false)
+     *     .build();
+     * }
+ */ + public static final class Builder { + private Logger logger; + private Level infoLevel = Level.INFO; + private Level warnLevel = Level.WARNING; + private Level progressLevel = Level.FINE; + private boolean includeStackTraces = true; + + /** + * Sets the JUL logger that will receive audit messages. + * + * @param logger the target logger; must not be null + * @return this builder for chaining + */ + public Builder logger(Logger logger) { + this.logger = Objects.requireNonNull(logger); + return this; + } + + /** + * Sets the level for informational events. + * + * @param level the level to use for info events; must not be null + * @return this builder for chaining + */ + public Builder infoLevel(Level level) { + this.infoLevel = Objects.requireNonNull(level); + return this; + } + + /** + * Sets the level for warnings and verification failures. + * + * @param level the level to use for warnings; must not be null + * @return this builder for chaining + */ + public Builder warnLevel(Level level) { + this.warnLevel = Objects.requireNonNull(level); + return this; + } + + /** + * Sets the level for progress-style, high-frequency events. + * + * @param level the level to use for progress events; must not be null + * @return this builder for chaining + */ + public Builder progressLevel(Level level) { + this.progressLevel = Objects.requireNonNull(level); + return this; + } + + /** + * Controls whether {@link #onFailure(String, String, String, Throwable)} + * appends a stack trace in addition to the structured summary. + * + * @param include true to include stack traces, false to omit them + * @return this builder for chaining + */ + public Builder includeStackTraces(boolean include) { + this.includeStackTraces = include; + return this; + } + + /** + * Builds a new {@code JulAuditListenerStd} instance with the current settings. + * + * @return a configured listener + */ + public JulAuditListenerStd build() { + return new JulAuditListenerStd(this); + } + } + + /** + * Creates a new builder with sensible defaults. + * + * @return a fresh builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Logs a structured context creation event. + * + * @param context specification type + * @param key type + * @param id algorithm or implementation identifier + * @param provider provider or vendor label + * @param role key usage role for the context + * @param key associated key, if any + * @param spec context specification, if any + */ + @Override + public void onContextCreated(String id, String provider, KeyUsage role, + K key, S spec) { + if (!log.isLoggable(infoLevel)) { + return; + } + log.log(infoLevel, "CTX_CREATED algo={0} provider={1} role={2} keyFp={3} spec={4}", + new Object[] { id, provider, role, fingerprint(key), specName(spec) }); + } + + /** + * Logs a structured context closure event in generic form. + * + * @param id algorithm or implementation identifier + * @param provider provider or vendor label + * @param role key usage role for the context + * @param key associated key, if any + */ + @Override + public void onContextClosed(String id, String provider, KeyUsage role, Key key) { + if (!log.isLoggable(infoLevel)) { + return; + } + log.log(infoLevel, "CTX_CLOSED algo={0} provider={1} role={2} keyFp={3}", + new Object[] { id, provider, role, fingerprint(key) }); + } + + /** + * Logs key pair generation with non-secret metadata. + * + * @param id algorithm or implementation identifier + * @param provider provider or vendor label + * @param spec algorithm key specification used for generation + * @param kp generated key pair + */ + @Override + public void onKeyGenerated(String id, String provider, AlgorithmKeySpec spec, KeyPair kp) { + if (!log.isLoggable(infoLevel)) { + return; + } + log.log(infoLevel, "KEY_GENERATED algo={0} provider={1} spec={2} publicFp={3} privateType={4}", + new Object[] { id, provider, specType(spec), fingerprint(kp == null ? null : kp.getPublic()), + keyType(kp == null ? null : kp.getPrivate()) }); + } + + /** + * Logs key construction or import. + * + * @param id algorithm or implementation identifier + * @param provider provider or vendor label + * @param spec algorithm key specification used to build the key + * @param key constructed or imported key + */ + @Override + public void onKeyBuilt(String id, String provider, AlgorithmKeySpec spec, Key key) { + if (!log.isLoggable(infoLevel)) { + return; + } + log.log(infoLevel, "KEY_BUILT algo={0} provider={1} spec={2} keyFp={3}", + new Object[] { id, provider, specType(spec), fingerprint(key) }); + } + + /** + * Logs best-effort key destruction. + * + * @param id algorithm or implementation identifier + * @param provider provider or vendor label + * @param key key being destroyed + */ + @Override + public void onKeyDestroyed(String id, String provider, Key key) { + if (!log.isLoggable(infoLevel)) { + return; + } + log.log(infoLevel, "KEY_DESTROYED algo={0} provider={1} keyFp={2}", + new Object[] { id, provider, fingerprint(key) }); + } + + /** + * Logs creation metadata with correlation id and non-secret spec attributes. + * + * @param ctxId correlation id for the wrapped context + * @param algoId algorithm or implementation identifier + * @param provider provider or vendor label + * @param role key usage role for the context + * @param keyFingerprint short, non-reversible key fingerprint or "n/a" + * @param specMeta best-effort non-secret attributes map + */ + @Override + public void onContextCreatedMeta(String ctxId, String algoId, String provider, KeyUsage role, String keyFingerprint, // NOPMD + Map specMeta) { + if (!log.isLoggable(infoLevel)) { + return; + } + log.log(infoLevel, "CTX_CREATED_META ctxId={0} algo={1} provider={2} role={3} keyFp={4} specMeta={5}", + new Object[] { ctxId, algoId, provider, role, keyFingerprint, specMeta }); + } + + /** + * Logs streaming progress for the given context id. + * + * @param ctxId correlation id + * @param bodyBytes cumulative body bytes + * @param trailerBytes cumulative trailer bytes + */ + @Override + public void onProgress(String ctxId, long bodyBytes, long trailerBytes) { + if (log.isLoggable(progressLevel)) { + log.log(progressLevel, "PROGRESS ctxId={0} body={1} trailer={2}", + new Object[] { ctxId, bodyBytes, trailerBytes }); + } + } + + /** + * Logs that a tag or signature was produced. + * + * @param ctxId correlation id + * @param tagLength produced tag length in bytes + * @param policy verification or emission policy label + */ + @Override + public void onTagProduced(String ctxId, int tagLength, String policy) { + if (log.isLoggable(infoLevel)) { + log.log(infoLevel, "TAG_PRODUCED ctxId={0} tagLen={1} policy={2}", + new Object[] { ctxId, tagLength, policy }); + } + } + + /** + * Logs verification outcome for verify mode. + * + * @param ctxId correlation id + * @param ok true if verification succeeded + * @param policy verification policy label + * @param expectedSource expected tag provenance label + * @param tagLength expected tag length in bytes + */ + @Override + public void onVerifyResult(String ctxId, boolean ok, String policy, String expectedSource, int tagLength) { + final Level lvl = ok ? infoLevel : warnLevel; + if (log.isLoggable(lvl)) { + log.log(lvl, "VERIFY_RESULT ctxId={0} ok={1} policy={2} expected={3} tagLen={4}", + new Object[] { ctxId, ok, policy, expectedSource, tagLength }); + } + } + + /** + * Logs that a header was written to a stream. + * + * @param ctxId correlation id + * @param codecId header codec identifier + * @param headerLen total header length in bytes + * @param ivLen IV length in bytes + * @param saltLen salt length in bytes + * @param aadLen AAD length in bytes + */ + @Override + public void onHeaderWritten(String ctxId, String codecId, int headerLen, int ivLen, int saltLen, int aadLen) { + if (log.isLoggable(progressLevel)) { + log.log(progressLevel, "HEADER_WRITTEN ctxId={0} codec={1} len={2} ivLen={3} saltLen={4} aadLen={5}", + new Object[] { ctxId, codecId, headerLen, ivLen, saltLen, aadLen }); + } + } + + /** + * Logs that a header was read from a stream. + * + * @param ctxId correlation id + * @param codecId header codec identifier + * @param headerLen total header length in bytes + * @param ivLen IV length in bytes + * @param saltLen salt length in bytes + * @param aadLen AAD length in bytes + */ + @Override + public void onHeaderRead(String ctxId, String codecId, int headerLen, int ivLen, int saltLen, int aadLen) { + if (log.isLoggable(progressLevel)) { + log.log(progressLevel, "HEADER_READ ctxId={0} codec={1} len={2} ivLen={3} saltLen={4} aadLen={5}", + new Object[] { ctxId, codecId, headerLen, ivLen, saltLen, aadLen }); + } + } + + /** + * Logs random material generation such as IVs, salts, nonces, or key bytes. + * + * @param ctxId correlation id + * @param kind material kind label + * @param rng random source label + * @param length number of bytes generated + */ + @Override + public void onRandomMaterial(String ctxId, String kind, String rng, int length) { + if (log.isLoggable(progressLevel)) { + log.log(progressLevel, "RANDOM ctxId={0} kind={1} rng={2} len={3}", + new Object[] { ctxId, kind, rng, length }); + } + } + + /** + * Logs a KDF usage summary. + * + * @param ctxId correlation id + * @param kdf KDF identifier + * @param saltLen salt length in bytes + * @param infoLen info length in bytes + * @param outLen derived output length in bytes + * @param iterations iteration count, or null if not applicable + */ + @Override + public void onKdfUsed(String ctxId, String kdf, int saltLen, int infoLen, int outLen, Integer iterations) { + if (log.isLoggable(progressLevel)) { + log.log(progressLevel, "KDF ctxId={0} kdf={1} saltLen={2} infoLen={3} outLen={4} iters={5}", + new Object[] { ctxId, kdf, saltLen, infoLen, outLen, (iterations == null ? "n/a" : iterations) }); + } + } + + /** + * Logs a failure with a structured summary and optional stack trace. + * + * @param ctxId correlation id + * @param stage coarse-grained stage label such as "read" or "invoke" + * @param op fine-grained operation label + * @param cause the exception that caused the failure; may be null + */ + @Override + public void onFailure(String ctxId, String stage, String op, Throwable cause) { + if (!log.isLoggable(warnLevel)) { + return; + } + String msg = "FAILURE ctxId={0} stage={1} op={2} error={3} message={4}"; + Object[] params = { ctxId, stage, op, (cause == null ? "unknown" : cause.getClass().getSimpleName()), + (cause == null ? "" : safeMessage(cause.getMessage())) }; + if (includeStackTraces && cause != null) { + log.log(warnLevel, msg, params); + log.log(warnLevel, "STACKTRACE", cause); + } else { + log.log(warnLevel, msg, params); + } + } + + /** + * Logs closure totals and elapsed time for the correlation id. + * + * @param ctxId correlation id + * @param bodyBytes total body bytes processed + * @param trailerBytes total trailer bytes processed + * @param durationMillis processing time in milliseconds + */ + @Override + public void onContextClosed(String ctxId, long bodyBytes, long trailerBytes, long durationMillis) { + if (log.isLoggable(infoLevel)) { + log.log(infoLevel, "CTX_CLOSED_META ctxId={0} body={1} trailer={2} durationMs={3}", + new Object[] { ctxId, bodyBytes, trailerBytes, durationMillis }); + } + } + + /** + * Logs that a peer message was provided to a message agreement context. + * + * @param ctxId correlation id + * @param msgLen provided message length in bytes + */ + @Override + public void onAgreementPeerMessageSet(String ctxId, int msgLen) { + if (log.isLoggable(infoLevel)) { + log.log(infoLevel, "AGREE_MSG_SET ctxId={0} msgLen={1}", new Object[] { ctxId, msgLen }); + } + } + + /** + * Logs that a peer message was retrieved from a message agreement context. + * + * @param ctxId correlation id + * @param msgLen retrieved message length in bytes + */ + @Override + public void onAgreementPeerMessageGet(String ctxId, int msgLen) { + if (log.isLoggable(infoLevel)) { + log.log(infoLevel, "AGREE_MSG_GET ctxId={0} msgLen={1}", new Object[] { ctxId, msgLen }); + } + } + + /** + * Logs the legacy single-argument creation callback with the context type. + * + * @param ctx the wrapped context + */ + @Override + public void onContextCreated(CryptoContext ctx) { + if (log.isLoggable(progressLevel)) { + log.log(progressLevel, "CTX_CREATED_LEGACY type={0}", + new Object[] { (ctx == null ? "null" : ctx.getClass().getSimpleName()) }); + } + } + + /** + * Logs the legacy cumulative byte counter. + * + * @param role key usage role + * @param count cumulative bytes observed + */ + @Override + public void onBytes(KeyUsage role, long count) { + if (log.isLoggable(progressLevel)) { + log.log(progressLevel, "BYTES role={0} count={1}", new Object[] { role, count }); + } + } + + /** + * Logs legacy KEM encapsulation lengths. + * + * @param cipherLength ciphertext length in bytes, or -1 if unknown + * @param sharedSecretLength shared secret length in bytes, or -1 if unknown + */ + @Override + public void onKemEncapsulated(int cipherLength, int sharedSecretLength) { + if (log.isLoggable(infoLevel)) { + log.log(infoLevel, "KEM_ENCAPSULATED ctLen={0} ssLen={1}", + new Object[] { cipherLength, sharedSecretLength }); + } + } + + /** + * Logs legacy KEM decapsulation length. + * + * @param sharedSecretLength shared secret length in bytes, or -1 if unknown + */ + @Override + public void onKemDecapsulated(int sharedSecretLength) { + log.log(infoLevel, "KEM_DECAPSULATED ssLen={0}", sharedSecretLength); + } + + /** + * Logs that a peer public key was provided to an agreement context. + * + * @param ctxId correlation id + * @param peerFingerprint short, non-reversible peer key fingerprint + */ + @Override + public void onAgreementPeerSet(String ctxId, String peerFingerprint) { + if (log.isLoggable(infoLevel)) { + log.log(infoLevel, "AGREE_PEER_SET ctxId={0} peerFp={1}", new Object[] { ctxId, peerFingerprint }); + } + } + + /** + * Logs that an agreement secret was derived with the reported length only. + * + * @param ctxId correlation id + * @param secretLength derived secret length in bytes, or -1 if unknown + */ + @Override + public void onAgreementDerived(String ctxId, int secretLength) { + if (log.isLoggable(infoLevel)) { + log.log(infoLevel, "AGREE_DERIVED ctxId={0} secretLen={1}", new Object[] { ctxId, secretLength }); + } + } + + /** + * Returns the simple name of the provided specification or "null". + * + * @param spec a context specification, possibly null + * @return simple class name or "null" + */ + private static String specName(ContextSpec spec) { + return spec == null ? "null" : spec.getClass().getSimpleName(); + } + + /** + * Returns the simple name of the provided key specification or "null". + * + * @param spec an algorithm key specification, possibly null + * @return simple class name or "null" + */ + private static String specType(AlgorithmKeySpec spec) { + return spec == null ? "null" : spec.getClass().getSimpleName(); + } + + /** + * Returns an algorithm/type descriptor for the given key or "null". + * + * @param k a key, possibly null + * @return algorithm and type descriptor or "null" + */ + private static String keyType(Key k) { + return k == null ? "null" : k.getAlgorithm() + "/" + k.getClass().getSimpleName(); + } + + /** + * Computes a short, non-reversible fingerprint for the key without logging raw + * key bytes. Non-extractable keys fall back to algorithm and type. + * + * @param key the key to summarize; may be null + * @return a fingerprint string or "n/a" if the key is null + */ + private static String fingerprint(Key key) { + if (key == null) { + return "n/a"; + } + try { + byte[] enc = key.getEncoded(); + if (enc == null) { + return key.getAlgorithm() + ":" + key.getClass().getSimpleName(); + } + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] d = md.digest(enc); + StringBuilder sb = new StringBuilder(key.getAlgorithm()).append(':'); + for (int i = 0; i < Math.min(8, d.length); i++) { + sb.append(String.format("%02x", d[i])); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + return key.getAlgorithm() + ":fp-error"; + } + } + + /** + * Returns a non-null message string for logging. + * + * @param s a message, possibly null + * @return the message or an empty string if null + */ + private static String safeMessage(String s) { + return s == null ? "" : s; + } +} diff --git a/lib/src/main/java/zeroecho/core/audit/SpecIntrospector.java b/lib/src/main/java/zeroecho/core/audit/SpecIntrospector.java new file mode 100644 index 0000000..f4086e0 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/audit/SpecIntrospector.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * 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.core.audit; + +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; + +import zeroecho.core.spec.ContextSpec; + +/** + * Utility for reflective inspection of {@link zeroecho.core.spec.ContextSpec} + * implementations. + * + *

+ * {@code SpecIntrospector} attempts to call common accessor methods on a + * {@code ContextSpec} (such as {@code mode()}, {@code tagLength()}, + * {@code hash()}, etc.) and collect their values into a map. The output is + * suitable for diagnostics, debugging, or summary views where a lightweight, + * uniform description of diverse spec types is required. + *

+ * + *

Design notes

+ *
    + *
  • Inspection is best-effort. If a method does not exist or invocation + * fails, the field is silently skipped.
  • + *
  • The returned map is unmodifiable. At minimum it always contains a + * {@code "type"} entry naming the spec’s simple class name.
  • + *
  • Values are stringified using {@link String#valueOf(Object)} to provide + * stable rendering across implementations.
  • + *
+ * + *

+ * This class is not intended for production cryptographic decisions. Its output + * is informational only and should not be used to enforce security policies. + *

+ * + * @since 1.0 + */ +final class SpecIntrospector { + /** + * Summarizes a {@link ContextSpec} into a diagnostic map. + * + *

+ * Known optional fields include: + *

    + *
  • {@code mode} – operation mode (e.g., ENCRYPT, DECRYPT)
  • + *
  • {@code tagLen} – authentication tag length
  • + *
  • {@code ivLen} – initialization vector length
  • + *
  • {@code aad} – whether additional authenticated data is present
  • + *
  • {@code hash} – hash algorithm (e.g., SHA256)
  • + *
  • {@code pssSaltLen} – salt length for PSS signatures
  • + *
  • {@code kdf} – key derivation function name
  • + *
  • {@code saltLen} – KDF salt length
  • + *
  • {@code infoLen} – KDF info/context length
  • + *
  • {@code header} – identifier of any associated header codec
  • + *
+ *

+ * + *

+ * Any field not available on the given spec type is omitted. If reflection + * fails entirely, the result will only contain {@code "type"}. + *

+ * + * @param spec the context specification to summarize + * @return unmodifiable map of field names to string values + */ + /* package */ static Map summarize(ContextSpec spec) { + try { + String klass = spec.getClass().getSimpleName(); + Object mode = tryCall(spec, "mode"); + Object tagLen = tryCall(spec, "tagLength"); + Object ivLen = tryCall(spec, "ivLength"); + Object aad = tryCall(spec, "aadPresent"); + Object pssHash = tryCall(spec, "hash"); + Object pssSalt = tryCall(spec, "pssSaltLen"); + Object kdf = tryCall(spec, "kdfName"); + Object hkdfSaltLen = tryCall(spec, "saltLength"); + Object hkdfInfoLen = tryCall(spec, "infoLength"); + Object headerCodec = tryCall(spec, "headerCodecId"); + + Map m = new LinkedHashMap<>(); + m.put("type", klass); + putIfNonNull(m, "mode", mode); + putIfNonNull(m, "tagLen", tagLen); + putIfNonNull(m, "ivLen", ivLen); + putIfNonNull(m, "aad", aad); + putIfNonNull(m, "hash", pssHash); + putIfNonNull(m, "pssSaltLen", pssSalt); + putIfNonNull(m, "kdf", kdf); + putIfNonNull(m, "saltLen", hkdfSaltLen); + putIfNonNull(m, "infoLen", hkdfInfoLen); + putIfNonNull(m, "header", headerCodec); + + return Map.copyOf(m); // unmodifiable view + } catch (Throwable ignore) { // NOPMD + return Map.of("type", spec.getClass().getSimpleName()); + } + } + + /** + * Hidden constructor; this class cannot be instantiated. + */ + private SpecIntrospector() { + // empty + } + + /** + * Puts a stringified value into a map if it is non-null. + * + * @param m map to update + * @param k field name + * @param v value to stringify and insert; ignored if {@code null} + */ + private static void putIfNonNull(Map m, String k, Object v) { + if (v != null) { + m.put(k, String.valueOf(v)); + } + } + + /** + * Attempts to call a no-arg method by reflection. + * + *

+ * If the method does not exist or invocation fails, {@code null} is returned. + *

+ * + * @param obj target object + * @param method method name to call + * @return return value of the method, or {@code null} if unavailable + */ + private static Object tryCall(Object obj, String method) { + try { + Method m = obj.getClass().getMethod(method); + m.setAccessible(true); // NOPMD + return m.invoke(obj); + } catch (Throwable ignore) { // NOPMD + return null; + } + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/audit/package-info.java b/lib/src/main/java/zeroecho/core/audit/package-info.java new file mode 100644 index 0000000..1ac9770 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/audit/package-info.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Auditing utilities for cryptographic contexts, including event contracts and + * a JUL-backed listener. + * + *

+ * This package provides a lightweight dynamic-proxy wrapper that observes + * selected operations of crypto contexts and emits structured audit events + * without changing behavior. It also defines the listener API and a + * production-ready {@link java.util.logging java.util.logging}-based + * implementation. + *

+ * + *

Scope and responsibilities

+ *
    + *
  • Wrap existing contexts and transparently intercept lifecycle, progress, + * verification, and failure signals while preserving functional results and + * exceptions.
  • + *
  • Offer a stable, structured callback surface suitable for logs, metrics, + * and policy hooks.
  • + *
  • Summarize non-secret specification attributes for diagnostics (mode, tag + * length, IV length, and similar).
  • + *
+ * + *

Key elements

+ *
    + *
  • {@link AuditedContexts} - creates a dynamic proxy around a context and + * emits creation metadata, streaming progress, verification outcomes, + * KEM/Agreement signals, closure totals, and failure notifications. The wrapper + * delegates all calls to the target and does not alter return values or + * exceptions.
  • + *
  • {@link AuditListener} - event sink interface; default methods are no-ops + * so implementors override only what they need. Callbacks cover creation, + * progress, headers, random material, KDF usage, verify results, failures, and + * closure.
  • + *
  • {@link JulAuditListenerStd} - concrete listener that emits parameterized + * {@code java.util.logging} records in a stable {@code key=value} format; a + * fluent builder configures logger, levels, and stack-trace inclusion.
  • + *
  • {@link SpecIntrospector} - internal helper that summarizes non-secret + * {@code ContextSpec} attributes for diagnostics.
  • + *
+ * + *

Typical usage

{@code
+ * // Configure a JUL-backed listener.
+ * AuditListener audit = JulAuditListenerStd.builder()
+ *     .progressLevel(java.util.logging.Level.FINE)
+ *     .includeStackTraces(false)
+ *     .build();
+ *
+ * // Create a context and wrap it for auditing.
+ * zeroecho.core.context.CryptoContext raw = ...; // created by an algorithm descriptor
+ * zeroecho.core.KeyUsage role = zeroecho.core.KeyUsage.ENCRYPT;
+ * zeroecho.core.context.CryptoContext ctx = AuditedContexts.wrap(raw, audit, role);
+ *
+ * // Use as usual; the proxy emits structured events.
+ * try (java.io.InputStream in =
+ *         ((zeroecho.core.context.EncryptionContext) ctx).attach(upstream)) {
+ *     in.transferTo(out);
+ * }
+ * }
+ * + *

Privacy and safety

+ *
    + *
  • No secret material is logged; keys are summarized by short, + * non-reversible fingerprints.
  • + *
  • Counting uses passthrough streams; the wrapper introduces no buffering + * beyond normal streaming.
  • + *
  • Thread-safety follows the wrapped context; no additional synchronization + * is added.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.audit; diff --git a/lib/src/main/java/zeroecho/core/context/AgreementContext.java b/lib/src/main/java/zeroecho/core/context/AgreementContext.java new file mode 100644 index 0000000..b2418ef --- /dev/null +++ b/lib/src/main/java/zeroecho/core/context/AgreementContext.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * 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.core.context; + +import java.security.PublicKey; + +/** + * Context for performing key agreement protocols using a private key and a + * peer's public key. + * + *

+ * An {@code AgreementContext} encapsulates the state needed to derive a shared + * secret between two parties. It is created by a + * {@link zeroecho.core.CryptoAlgorithm} for the + * {@link zeroecho.core.KeyUsage#AGREEMENT} role. Typical implementations + * include elliptic-curve Diffie–Hellman (ECDH) and X25519/X448. + *

+ * + *

Lifecycle

+ *
    + *
  • Create a context via {@code CryptoAlgorithm#create(...)} or + * {@code CryptoAlgorithms.create(...)} for the {@code AGREEMENT} role.
  • + *
  • Call {@link #setPeerPublic(PublicKey)} with the peer’s public key.
  • + *
  • Invoke {@link #deriveSecret()} once to compute the raw shared + * secret.
  • + *
  • Dispose of the context with {@link #close()} to release any internal + * resources.
  • + *
+ * + *

Security considerations

+ *
    + *
  • Shared secrets are returned as raw byte arrays; callers should + * immediately feed them into a key-derivation function (KDF) and clear the + * array when done.
  • + *
  • Most agreements require validated public keys. Implementations should + * enforce curve/mathematical constraints and reject invalid points.
  • + *
  • Contexts are not generally thread-safe; do not share a single instance + * across concurrent threads.
  • + *
+ * + * @since 1.0 + */ +public non-sealed interface AgreementContext extends CryptoContext { + + /** + * Sets the peer's public key to be used in the key agreement. + * + *

+ * This method must be called before {@link #deriveSecret()}. Implementations + * may validate that the provided key is suitable for the underlying algorithm + * (e.g., curve membership checks). + *

+ * + * @param peer the peer's public key + * @throws NullPointerException if {@code peer} is {@code null} + * @throws IllegalArgumentException if {@code peer} is not valid for this + * algorithm + */ + void setPeerPublic(PublicKey peer); + + /** + * Performs the key agreement and returns the derived raw shared secret. + * + *

+ * The returned byte array contains secret material and should be used + * immediately in a KDF. Callers are responsible for clearing or zeroizing the + * array after use to reduce the risk of leakage. + *

+ * + * @return a new byte array containing the raw shared secret + * @throws IllegalStateException if the peer public key has not been set before + * invocation + */ + byte[] deriveSecret(); +} diff --git a/lib/src/main/java/zeroecho/core/context/CryptoContext.java b/lib/src/main/java/zeroecho/core/context/CryptoContext.java new file mode 100644 index 0000000..7ee2c34 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/context/CryptoContext.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.context; + +import java.io.Closeable; +import java.security.Key; + +import zeroecho.core.CryptoAlgorithm; + +/** + * Common interface for all cryptographic operation contexts. + * + *

+ * A {@code CryptoContext} represents the active state of an algorithm bound to + * a specific key and optional parameters. Contexts encapsulate resources such + * as cipher instances, digest engines, or signature state machines. They are + * created by a {@link zeroecho.core.CryptoAlgorithm} for a particular + * {@link zeroecho.core.KeyUsage} role and remain valid until {@link #close()} + * is called. + *

+ * + *

Lifecycle

+ *
    + *
  • Contexts are created via {@code CryptoAlgorithm#create(...)} or the + * convenience methods in {@code CryptoAlgorithms}.
  • + *
  • They may wrap native or provider-managed resources that must be + * released.
  • + *
  • Once closed, a context must not be reused; callers should request a new + * one for subsequent operations.
  • + *
+ * + *

Security considerations

+ *
    + *
  • Closing a context may attempt to destroy the underlying {@link Key} if it + * implements {@code javax.security.auth.Destroyable} and auditing is + * enabled.
  • + *
  • Applications should always call {@link #close()} promptly to avoid + * leaking key material or other sensitive state.
  • + *
  • Contexts are not guaranteed to be thread-safe; concurrent use should be + * avoided unless explicitly documented by the implementation.
  • + *
+ * + *

Subtypes

Concrete context kinds are represented by specialized + * subinterfaces: + *
    + *
  • {@link DigestContext} - unkeyed hash and XOF pipelines
  • + *
  • {@link EncryptionContext} - symmetric/asymmetric encryption and + * decryption streams
  • + *
  • {@link MacContext} - message authentication codes
  • + *
  • {@link SignatureContext} - digital signatures (sign/verify)
  • + *
  • {@link AgreementContext} - key agreement protocols
  • + *
  • {@link KemContext} - key encapsulation mechanisms (KEM)
  • + *
+ * + * @since 1.0 + */ +public sealed interface CryptoContext extends Closeable + permits DigestContext, EncryptionContext, KemContext, MacContext, SignatureContext, AgreementContext { + /** + * Returns the algorithm that created this context. + * + * @return the algorithm definition that owns this context + */ + CryptoAlgorithm algorithm(); + + /** + * Returns the key bound to this context. + * + *

+ * For unkeyed contexts such as digests, this may be + * {@link zeroecho.core.NullKey#INSTANCE}. + *

+ * + * @return the cryptographic key or sentinel bound to this context + */ + Key key(); + + /** + * Closes this context and releases all associated resources. + * + *

+ * Implementations should free provider state and native handles. If the bound + * key supports destruction, the library may attempt to invoke {@code destroy()} + * on it when auditing is enabled. + *

+ * + * @throws java.io.IOException if the underlying provider encounters an I/O + * error during cleanup + */ + @Override + void close() throws java.io.IOException; +} diff --git a/lib/src/main/java/zeroecho/core/context/DigestContext.java b/lib/src/main/java/zeroecho/core/context/DigestContext.java new file mode 100644 index 0000000..9ffada1 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/context/DigestContext.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * 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.core.context; + +import zeroecho.core.tag.TagEngine; + +/** + * Context for computing unkeyed message digests and extendable-output functions + * (XOFs) in streaming pipelines. + * + *

+ * A {@code DigestContext} combines digest/XOF state with the {@link TagEngine} + * contract: the wrapped input stream passes bytes through while updating the + * computation; at end-of-file, a fixed-length tag is appended (produce mode) or + * compared with an expected tag (verify mode). + *

+ * + *

Operation

+ *
    + *
  • Produce mode: {@link TagEngine#wrap(java.io.InputStream)} emits + * the original data, then appends the digest (or configured-length XOF output) + * as a trailer.
  • + *
  • Verify mode: Provide the expected tag via + * {@link TagEngine#setExpectedTag(byte[])}; the engine compares the computed + * tag at EOF using the configured verification approach.
  • + *
+ * + *

Usage

+ *

Digest trailer

+ * {@code
+ * TagEngine eng = TagEngineBuilder.digest(DigestSpec.sha256()).get();
+ * try (java.io.InputStream in = eng.wrap(upstream)) {
+ *     in.transferTo(out);
+ * }
+ * }
+ * 
+ * + *

Detached verification

+ * {@code
+ * TagEngine eng = TagEngineBuilder.digest(DigestSpec.sha256()).get();
+ * eng.setExpectedTag(expectedDigest);
+ * try (java.io.InputStream in = eng.wrap(bodyWithoutTrailer)) {
+ *     in.transferTo(java.io.OutputStream.nullOutputStream());
+ * }
+ * }
+ * 
+ * + *

Security considerations

+ *
    + *
  • Unkeyed digests do not provide authenticity; use {@link MacContext} or + * {@link SignatureContext} for that.
  • + *
  • For XOFs, choose an output length appropriate for your security + * level.
  • + *
  • Implementations are stateful and not guaranteed to be thread-safe.
  • + *
+ * + * @since 1.0 + */ +public non-sealed interface DigestContext extends TagEngine, CryptoContext { +} diff --git a/lib/src/main/java/zeroecho/core/context/EncryptionContext.java b/lib/src/main/java/zeroecho/core/context/EncryptionContext.java new file mode 100644 index 0000000..dfc6f9d --- /dev/null +++ b/lib/src/main/java/zeroecho/core/context/EncryptionContext.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * 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.core.context; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Context for performing encryption or decryption in streaming pipelines. + * + *

+ * An {@code EncryptionContext} transforms data on-the-fly as it is read from an + * upstream source. It provides a passthrough {@link InputStream} that + * transparently applies encryption or decryption according to the configured + * algorithm, key, and parameters. + *

+ * + *

Lifecycle

+ *
    + *
  • Create a context via {@code CryptoAlgorithm#create(...)} for the + * {@link zeroecho.core.KeyUsage#ENCRYPT} or + * {@link zeroecho.core.KeyUsage#DECRYPT} role.
  • + *
  • Call {@link #attach(InputStream)} to obtain a wrapped stream.
  • + *
  • Read from the returned stream; data is encrypted or decrypted on demand + * until end-of-file.
  • + *
  • Call {@link #close()} to release underlying resources and finalize any + * state (e.g., authentication tags in AEAD modes).
  • + *
+ * + *

Security considerations

+ *
    + *
  • When using AEAD modes (e.g., AES-GCM), callers must preserve the + * associated authentication tag and any nonces or IVs required for + * decryption.
  • + *
  • Reading the stream to completion is usually required for integrity checks + * to succeed; truncation can result in verification failures.
  • + *
  • Contexts are not guaranteed to be thread-safe; use one context per + * pipeline.
  • + *
+ * + * @since 1.0 + */ +public non-sealed interface EncryptionContext extends CryptoContext { + /** + * Attaches this context to an upstream input stream. + * + *

+ * The returned stream applies encryption or decryption as bytes are read. + * Implementations may buffer internally, and callers should always close the + * stream (or the context) to ensure all data is flushed and state is finalized. + *

+ * + * @param upstream the source stream supplying plaintext (for encryption) or + * ciphertext (for decryption) + * @return a new input stream that transforms data according to the context + * @throws IOException if the stream cannot be wrapped or the underlying cipher + * initialization fails + */ + InputStream attach(InputStream upstream) throws IOException; +} diff --git a/lib/src/main/java/zeroecho/core/context/KemContext.java b/lib/src/main/java/zeroecho/core/context/KemContext.java new file mode 100644 index 0000000..0abd3b7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/context/KemContext.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.context; + +import java.io.IOException; + +/** + * Context for performing key encapsulation and decapsulation (KEM) operations. + * + *

+ * A {@code KemContext} encapsulates the state for algorithms that exchange + * shared secrets using asymmetric primitives. KEMs are widely used in + * post-quantum cryptography and hybrid key exchange protocols. + *

+ * + *

Lifecycle

+ *
    + *
  • Create a context via {@code CryptoAlgorithm#create(...)} for the + * {@link zeroecho.core.KeyUsage#ENCAPSULATE} or + * {@link zeroecho.core.KeyUsage#DECAPSULATE} role.
  • + *
  • Call {@link #encapsulate()} when acting as an initiator. The result + * includes both the ciphertext to send and the derived shared secret.
  • + *
  • Call {@link #decapsulate(byte[])} when acting as a responder, supplying + * the received ciphertext to recover the same shared secret.
  • + *
  • Dispose of the context with {@link #close()} to release resources.
  • + *
+ * + *

Security considerations

+ *
    + *
  • The shared secret must be immediately fed into a key-derivation function + * (KDF). Applications should zeroize the byte array after use.
  • + *
  • Implementations must validate ciphertexts carefully; malformed inputs + * should fail decapsulation without leaking side-channel information.
  • + *
  • Contexts are not guaranteed to be thread-safe; use one instance per + * flow.
  • + *
+ * + * @since 1.0 + */ +public non-sealed interface KemContext extends CryptoContext { + /** + * Performs encapsulation, generating both the encapsulated ciphertext and the + * derived shared secret. + * + * @return a {@link KemResult} containing the ciphertext to transmit and the raw + * shared secret + * @throws IOException if encapsulation fails or the provider encounters an + * error + */ + KemResult encapsulate() throws IOException; + + /** + * Performs decapsulation of the given ciphertext to recover the shared secret. + * + * @param ciphertext the peer’s encapsulation ciphertext + * @return a new byte array containing the raw shared secret + * @throws IOException if decapsulation fails or the input is invalid + */ + byte[] decapsulate(byte[] ciphertext) throws IOException; + + /** + * Encapsulation result holding both ciphertext and shared secret. + * + *

+ * Instances of this class are immutable. The caller is responsible for handling + * and clearing the secret bytes after use. + *

+ */ + final class KemResult { + private final byte[] ciphertext; + private final byte[] sharedSecret; + + /** + * Creates a new result object with the provided values. + * + * @param ciphertext the encapsulation ciphertext to be transmitted + * @param sharedSecret the derived shared secret (sensitive material) + */ + public KemResult(byte[] ciphertext, byte[] sharedSecret) { + this.ciphertext = ciphertext; + this.sharedSecret = sharedSecret; + } + + /** + * Returns the encapsulation ciphertext. + * + * @return ciphertext to send to the peer + */ + public byte[] ciphertext() { + return ciphertext; // NOPMD + } + + /** + * Returns the derived shared secret. + * + *

+ * This material is sensitive and should be passed into a KDF and then cleared + * as soon as possible. + *

+ * + * @return the raw shared secret + */ + public byte[] sharedSecret() { + return sharedSecret; // NOPMD + } + } +} diff --git a/lib/src/main/java/zeroecho/core/context/MacContext.java b/lib/src/main/java/zeroecho/core/context/MacContext.java new file mode 100644 index 0000000..98575d4 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/context/MacContext.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.context; + +import zeroecho.core.tag.ByteVerificationStrategy; +import zeroecho.core.tag.TagEngine; +import zeroecho.core.tag.ThrowingBiPredicate; + +/** + * Context for computing message authentication codes (MACs) in streaming + * pipelines. + * + *

+ * A {@code MacContext} encapsulates the state of a keyed integrity primitive + * such as HMAC, KMAC, or CMAC and exposes the {@link TagEngine} contract: the + * wrapped input stream passes bytes through while the MAC state is updated; at + * end-of-file a fixed-length tag is either appended (produce mode) or compared + * with an expected tag (verify mode). + *

+ * + *

Operation

+ *
    + *
  • Produce mode: {@link TagEngine#wrap(java.io.InputStream)} emits + * the original data and then appends the computed MAC as a trailer.
  • + *
  • Verify mode: Supply the expected tag via + * {@link TagEngine#setExpectedTag(byte[])} and ensure the upstream body does + * not include a trailer. At EOF the computed tag is compared using the + * configured verification approach set with + * {@link TagEngine#setVerificationApproach(ThrowingBiPredicate.VerificationBiPredicate)}.
  • + *
+ * + *

Usage

+ *

Produce an HMAC trailer

+ * {@code
+ * javax.crypto.SecretKey key = ...;
+ * HmacSpec spec = HmacSpec.sha256();
+ *
+ * TagEngine eng = TagEngineBuilder.hmac(key, spec).get();
+ * try (java.io.InputStream in = eng.wrap(upstream)) {
+ *     in.transferTo(out); // body bytes, then HMAC trailer are written to 'out'
+ * }
+ * }
+ * 
+ * + *

Verify a detached MAC

+ * {@code
+ * byte[] expectedMac = ...; // obtained via a trusted channel
+ *
+ * TagEngine eng = TagEngineBuilder.hmac(key, HmacSpec.sha256()).get();
+ * // Optional: throw on mismatch instead of silent flagging
+ * eng.setVerificationApproach(eng.getVerificationCore().getThrowOnMismatch());
+ * eng.setExpectedTag(expectedMac);
+ *
+ * try (java.io.InputStream in = eng.wrap(bodyWithoutTrailer)) {
+ *     in.transferTo(java.io.OutputStream.nullOutputStream()); // comparison at EOF
+ * }
+ * }
+ * 
+ * + *

Security considerations

+ *
    + *
  • MACs are keyed and provide authenticity and integrity, unlike + * {@link DigestContext}.
  • + *
  • Comparisons should be constant time. Use a strategy such as + * {@link ByteVerificationStrategy} via + * {@link TagEngine#setVerificationApproach(ThrowingBiPredicate.VerificationBiPredicate)}.
  • + *
  • Instances are stateful and not thread-safe. Use one context per + * pipeline.
  • + *
+ * + * @since 1.0 + */ +public non-sealed interface MacContext extends TagEngine, CryptoContext { +} diff --git a/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java b/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java new file mode 100644 index 0000000..43d2517 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/context/MessageAgreementContext.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.context; + +/** + * Extension of {@link AgreementContext} for message-based key agreement + * protocols. + * + *

+ * A {@code MessageAgreementContext} models algorithms where the two parties + * must exchange explicit protocol messages in addition to providing a public + * key. Examples include many KEM-based protocols where initiators send an + * encapsulation and responders decapsulate it to derive the shared secret. + *

+ * + *

Design

+ *
    + *
  • For Diffie–Hellman–style agreements (e.g., X25519, ECDH), no explicit + * peer message is required. The peer’s public key is sufficient, and the + * message-related methods are unused.
  • + *
  • For KEM-based agreements (e.g., FrodoKEM, Kyber), the initiator generates + * an encapsulation ciphertext to send, and the responder must receive and + * decapsulate it. These flows rely on the {@link #setPeerMessage(byte[])} and + * {@link #getPeerMessage()} methods.
  • + *
+ * + *

Lifecycle

+ *
    + *
  • Create a context via {@code CryptoAlgorithm#create(...)} for the + * {@link zeroecho.core.KeyUsage#AGREEMENT} role.
  • + *
  • Initiators call {@link #getPeerMessage()} to obtain the message to send + * to the responder, then invoke {@link AgreementContext#deriveSecret()}.
  • + *
  • Responders call {@link #setPeerMessage(byte[])} to supply the received + * encapsulation, then invoke {@link AgreementContext#deriveSecret()}.
  • + *
  • Finally, call {@link #close()} to release any internal resources.
  • + *
+ * + *

Security considerations

+ *
    + *
  • Shared secrets must be passed through a key derivation function (KDF) + * immediately after computation; raw bytes should be cleared from memory once + * used.
  • + *
  • Malformed peer messages must be rejected without leaking side-channel + * information (e.g., via constant-time checks).
  • + *
  • As with other {@link CryptoContext} types, instances are not guaranteed + * to be thread-safe.
  • + *
+ * + * @since 1.0 + */ +public interface MessageAgreementContext extends AgreementContext { + /** + * Supplies the peer’s message to this context. + * + *

+ * Typically used by a responder in KEM-based protocols to provide the + * initiator’s encapsulation ciphertext. Diffie–Hellman contexts do not require + * peer messages and may throw if this method is called. + *

+ * + * @param message the peer’s message (e.g., encapsulated ciphertext), may be + * {@code null} to reset + * @throws IllegalStateException if the current role does not accept peer + * messages + */ + void setPeerMessage(byte[] message); + + /** + * Retrieves the peer message produced by this context. + * + *

+ * Typically used by an initiator in KEM-based protocols to obtain the + * encapsulation ciphertext that must be sent to the responder. Returns a clone + * to prevent accidental modification of internal state. + *

+ * + * @return a clone of the message to send (never {@code null}) + * @throws IllegalStateException if the current role does not produce messages + */ + byte[] getPeerMessage(); +} diff --git a/lib/src/main/java/zeroecho/core/context/SignatureContext.java b/lib/src/main/java/zeroecho/core/context/SignatureContext.java new file mode 100644 index 0000000..2504dfa --- /dev/null +++ b/lib/src/main/java/zeroecho/core/context/SignatureContext.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * 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.core.context; + +import java.security.Signature; + +import zeroecho.core.tag.TagEngine; +import zeroecho.core.tag.ThrowingBiPredicate; + +/** + * Context for streaming digital signature operations in pull-based pipelines. + * + *

+ * A {@code SignatureContext} encapsulates the state of a signature algorithm + * such as Ed25519, ECDSA, or RSA-PSS and exposes it through the + * {@link TagEngine} contract. In produce (SIGN) mode it consumes the message + * bytes and appends the computed signature as a trailer; in verify (VERIFY) + * mode it consumes the message bytes and checks an expected signature supplied + * by the caller. + *

+ * + *

Operation

+ *
    + *
  • Produce mode: {@link TagEngine#wrap(java.io.InputStream)} returns + * a passthrough stream that emits the original data and, at end-of-stream, + * appends the signature trailer.
  • + *
  • Verify mode: Provide the expected signature with + * {@link TagEngine#setExpectedTag(byte[])} and ensure the upstream body does + * not include a trailer. At end-of-stream the computed signature is compared + * against the expected one using the configured verification approach set via + * {@link TagEngine#setVerificationApproach(ThrowingBiPredicate.VerificationBiPredicate)}.
  • + *
+ * + *

Usage

+ *

Sign with Ed25519

+ * {@code
+ * TagEngine eng =
+ *     TagEngineBuilder.ed25519Sign(privateKey).get();
+ *
+ * try (java.io.InputStream in = eng.wrap(upstream)) {
+ *     in.transferTo(out); // body, then detached signature trailer
+ * }
+ * }
+ * 
+ * + *

Verify a detached signature and throw on mismatch

+ * {@code
+ * byte[] expectedSig = ...; // obtained via a trusted channel
+ * TagEngine eng =
+ *     TagEngineBuilder.ed25519Verify(publicKey).get();
+ *
+ * // Throw if verification fails at EOF:
+ * eng.setVerificationApproach(eng.getVerificationCore().getThrowOnMismatch());
+ * eng.setExpectedTag(expectedSig);
+ *
+ * try (java.io.InputStream in = eng.wrap(bodyWithoutTrailer)) {
+ *     in.transferTo(java.io.OutputStream.nullOutputStream()); // compare at EOF
+ * }
+ * }
+ * 
+ * + *

Notes

+ *
    + *
  • Instances bind to a specific key and are single-use; they are not + * thread-safe.
  • + *
  • The signature length reported by {@link TagEngine#tagLength()} is + * algorithm specific and can be used by higher layers to handle trailers.
  • + *
  • Verification failures are handled by the configured approach, for example + * {@link ThrowingBiPredicate.VerificationBiPredicate#getThrowOnMismatch()} or + * {@link ThrowingBiPredicate.VerificationBiPredicate#getFlagOkInCtx(conflux.CtxInterface, conflux.Key)}.
  • + *
+ * + * @since 1.0 + */ +public non-sealed interface SignatureContext extends TagEngine, CryptoContext { +} diff --git a/lib/src/main/java/zeroecho/core/context/package-info.java b/lib/src/main/java/zeroecho/core/context/package-info.java new file mode 100644 index 0000000..2633b41 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/context/package-info.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Abstractions for cryptographic operation contexts. + * + *

+ * This package defines the context layer that models active cryptographic + * operations. A context represents a live, single-use operation bound to a key + * and an optional specification. Contexts cover both stream-oriented transforms + * (for example, encryption, MAC, signatures, digests) and message-oriented + * exchanges (for example, key agreement and key encapsulation). + *

+ * + *

Responsibilities

+ *
    + *
  • Provide a stable set of operation-specific interfaces that algorithms + * implement when producing runnable contexts.
  • + *
  • Unify lifecycle concepts across operations: creation by an algorithm for + * a concrete {@link zeroecho.core.KeyUsage}, execution of the operation, and + * explicit closure via {@link CryptoContext#close()}.
  • + *
  • Support both streaming and non-streaming styles without leaking provider + * details into application code.
  • + *
+ * + *

Context kinds

+ *
    + *
  • {@link DigestContext} - unkeyed digests and extendable-output + * functions.
  • + *
  • {@link MacContext} - message authentication codes producing or verifying + * fixed-length tags.
  • + *
  • {@link EncryptionContext} - symmetric or asymmetric encryption and + * decryption over byte streams.
  • + *
  • {@link SignatureContext} - streaming digital signatures for sign and + * verify roles.
  • + *
  • {@link AgreementContext} - key agreement deriving a shared secret from a + * local private key and a peer public key.
  • + *
  • {@link MessageAgreementContext} - agreement modeled as an exchange of + * explicit peer messages.
  • + *
  • {@link KemContext} - key encapsulation mechanisms for + * encapsulate/decapsulate workflows.
  • + *
+ * + *

Lifecycle and safety

+ *
    + *
  • Contexts are typically single-use and not thread-safe; create one per + * pipeline or exchange.
  • + *
  • Implementations should avoid retaining sensitive byte arrays; callers are + * expected to apply a KDF to shared secrets and clear temporary material + * promptly.
  • + *
  • Unkeyed operations may use {@link zeroecho.core.NullKey#INSTANCE} where + * an API requires a key reference.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.context; diff --git a/lib/src/main/java/zeroecho/core/err/ProviderFailureException.java b/lib/src/main/java/zeroecho/core/err/ProviderFailureException.java new file mode 100644 index 0000000..c29ecb2 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/err/ProviderFailureException.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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.core.err; + +/** + * Exception signaling a failure in the underlying cryptographic provider during + * algorithm initialization or operation. + * + *

+ * This runtime exception is used to wrap provider or JCA/JCE errors that occur + * while creating or initializing cipher contexts (for example, inside AES-GCM + * or ChaCha20-Poly1305 {@code attach(...)} flows). Typical root causes include + * unavailable transformations, invalid provider state, or rejected parameters + * that surface as {@code GeneralSecurityException} and are rethrown as + * {@code ProviderFailureException}. + *

+ * + *

Usage and semantics

+ *
    + *
  • Thrown from context setup or transformation wiring when + * {@code Cipher.getInstance(...)} or {@code init(...)} fails in the + * provider.
  • + *
  • Represents provider-layer failure, distinct from unsupported role/spec + * conditions (see {@link UnsupportedRoleException} and + * {@link UnsupportedSpecException}).
  • + *
  • May be translated to {@code IOException} at API boundaries that declare + * I/O only, while preserving the original cause via {@link #getCause()}.
  • + *
+ * + *

Example

{@code
+ * try {
+ *     Cipher c = Cipher.getInstance(jceName());   // provider resolution
+ *     initCipher(c, nonce);                       // provider init
+ *     return new Stream(upstream, c, jceName());  // streaming transform
+ * } catch (GeneralSecurityException e) {
+ *     throw new ProviderFailureException(jceName() + " attach/init failed", e);
+ * }
+ * }
+ * + * @since 1.0 + */ +public class ProviderFailureException extends RuntimeException { + private static final long serialVersionUID = 2610774892439787602L; + + /** + * Creates a new exception wrapping a provider failure. + * + * @param m human-readable detail message describing the failed provider + * operation + * @param t original cause from the provider layer (usually a + * {@code GeneralSecurityException}) + */ + public ProviderFailureException(String m, Throwable t) { + super(m, t); + } +} diff --git a/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java b/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java new file mode 100644 index 0000000..9b255e1 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/err/UnsupportedRoleException.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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.core.err; + +/** + * Exception indicating that an algorithm does not support the requested role. + * + *

+ * This is thrown when a {@link zeroecho.core.CryptoAlgorithm} (or helpers such + * as {@link zeroecho.core.CryptoAlgorithms}) is asked to create a context for a + * {@link zeroecho.core.KeyUsage} that the algorithm never advertises. In other + * words, the operation is unavailable regardless of the provided key or spec. + *

+ * + *

When it is thrown

+ *
    + *
  • During + * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)} + * after policy validation, if the resolved algorithm exposes no bindings for + * the given role.
  • + *
  • Directly from + * {@link zeroecho.core.CryptoAlgorithm#create(zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)} + * when no binding exists for the role.
  • + *
+ * + *

+ * Contrast with {@link UnsupportedSpecException}, which means the role exists + * but no binding accepts the supplied key/spec types. + *

+ * + *

Example

{@code
+ * // Suppose "SHA-256" supports DIGEST only.
+ * var algo = zeroecho.core.CryptoAlgorithms.require("SHA-256");
+ * try {
+ *     // Asking for ENCRYPT on a digest algorithm will fail with UnsupportedRoleException.
+ *     zeroecho.core.CryptoAlgorithms.create("SHA-256", zeroecho.core.KeyUsage.ENCRYPT,
+ *         zeroecho.core.NullKey.INSTANCE, null);
+ * } catch (UnsupportedRoleException e) {
+ *     // Handle: algorithm does not implement the ENCRYPT role.
+ * }
+ * }
+ * + * @since 1.0 + */ +public class UnsupportedRoleException extends RuntimeException { + private static final long serialVersionUID = 1653286859112182562L; + + /** + * Creates a new exception with a detail message. + * + * @param m detail message describing the missing role for the algorithm + */ + public UnsupportedRoleException(String m) { + super(m); + } +} diff --git a/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java b/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java new file mode 100644 index 0000000..b699a28 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/err/UnsupportedSpecException.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.err; + +/** + * Exception indicating that a provided key/spec combination is not supported by + * the algorithm for the requested role. + * + *

+ * This is thrown by {@link zeroecho.core.CryptoAlgorithm#create} when: + *

+ *
    + *
  • The algorithm supports the requested {@link zeroecho.core.KeyUsage} role, + * but none of its bindings accept the given {@link java.security.Key} and + * {@code ContextSpec} type.
  • + *
  • The provided {@code spec} is incompatible with the key type or context + * expected by the binding.
  • + *
+ * + *

+ * Unlike {@link UnsupportedRoleException}, which signals that a role is + * completely unavailable for an algorithm, this exception means that the role + * exists but cannot be realized with the supplied parameters. + *

+ * + *

Example

{@code
+ * CryptoAlgorithm aes = CryptoAlgorithms.require("AES/GCM");
+ * SecretKey wrongKey = ... // an RSA key by mistake
+ * try {
+ *     aes.create(KeyUsage.ENCRYPT, wrongKey, null);
+ * } catch (UnsupportedSpecException e) {
+ *     // no binding accepted the RSA key for ENCRYPT role
+ * }
+ * }
+ * + * @since 1.0 + */ +public class UnsupportedSpecException extends RuntimeException { + private static final long serialVersionUID = -4569004728562695244L; + + /** + * Creates a new exception with the given message. + * + * @param m detail message describing the rejected key/spec combination + */ + public UnsupportedSpecException(String m) { + super(m); + } +} diff --git a/lib/src/main/java/zeroecho/core/err/VerificationException.java b/lib/src/main/java/zeroecho/core/err/VerificationException.java new file mode 100644 index 0000000..de9bca6 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/err/VerificationException.java @@ -0,0 +1,138 @@ +/******************************************************************************* + * 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.core.err; + +import java.security.SignatureException; + +import zeroecho.core.util.Strings; + +/** + * Exception thrown when a cryptographic signature or digest verification fails. + * + *

+ * This exception may carry the calculated value, and optionally the expected + * value, to aid in debugging and error reporting. In case of failures + * originating from lower-level signature APIs, the original + * {@link java.security.SignatureException} may be wrapped as the cause. + *

+ */ +public class VerificationException extends Exception { + private static final long serialVersionUID = 7226636637976744245L; + /** + * The calculated value that failed verification, typically a signature or + * digest. + */ + private final byte[] a; + /** + * The expected value to be compared against, or {@code null} if not provided. + */ + private final byte[] expected; + + /** + * Creates a new exception with only the calculated value. + * + *

+ * This form is typically used when there is no known expected value, but the + * calculated value is still useful for diagnostics. + *

+ * + * @param a the calculated value, may be {@code null}. A defensive copy is made. + */ + public VerificationException(byte[] a) { + super(); + + this.a = (a == null) ? null : a.clone(); + this.expected = null; + } + + /** + * Creates a new exception with both expected and calculated values. + * + *

+ * This form is useful when the verification mismatch can be expressed in terms + * of what was expected versus what was actually obtained. + *

+ * + * @param expected the expected value, may be {@code null}. A defensive copy is + * made. + * @param a the calculated value, may be {@code null}. A defensive copy + * is made. + */ + public VerificationException(byte[] expected, byte[] a) { + super(); + + this.a = (a == null) ? null : a.clone(); + this.expected = (expected == null) ? null : expected.clone(); + } + + /** + * Creates a new exception that wraps a lower-level + * {@link java.security.SignatureException}. + * + *

+ * This form allows the original exception to be preserved while attaching the + * calculated value that caused the failure. + *

+ * + * @param e the original signature exception to wrap, must not be {@code null} + * @param a the calculated value, may be {@code null}. A defensive copy is made. + */ + public VerificationException(SignatureException e, byte[] a) { + super(e); + + this.a = (a == null) ? null : a.clone(); + this.expected = null; + } + + /** + * Returns a string representation of this exception. + * + *

+ * The returned string includes both expected and calculated values if + * available, or the message of the wrapped cause if present. + *

+ * + * @return a descriptive string representation of the exception + */ + @Override + public String toString() { + return expected != null + ? "VerificationException: invalid signature, expected=" + Strings.toShortString(expected) + + " but calculated " + Strings.toShortString(a) + : getCause() == null ? "VerificationException: invalid signature " + Strings.toShortString(a) + : "VerificationException: " + getCause().getMessage() + " with " + Strings.toShortString(a); + } + +} diff --git a/lib/src/main/java/zeroecho/core/err/package-info.java b/lib/src/main/java/zeroecho/core/err/package-info.java new file mode 100644 index 0000000..7bc6d8e --- /dev/null +++ b/lib/src/main/java/zeroecho/core/err/package-info.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Exception types used by the core to signal configuration errors and + * unexpected provider failures. + * + *

+ * This package defines a small, clear taxonomy of exceptions thrown when + * constructing or running cryptographic contexts. Use these types to + * distinguish programming/configuration mistakes from lower-level provider + * failures. + *

+ * + *

Exception categories

+ *
    + *
  • {@link UnsupportedRoleException} - the requested + * {@link zeroecho.core.KeyUsage} is not supported by the selected algorithm, or + * the role is incompatible with the supplied key/spec.
  • + *
  • {@link UnsupportedSpecException} - the provided + * {@link zeroecho.core.spec.ContextSpec} is invalid or incompatible with the + * requested role or key.
  • + *
  • {@link ProviderFailureException} - an unexpected failure surfaced by an + * underlying JCA/JCE/PQC provider; the operation cannot proceed safely.
  • + *
+ * + *

Usage notes

+ *
    + *
  • Treat {@link UnsupportedRoleException} and + * {@link UnsupportedSpecException} as programming or configuration errors that + * should be fixed by adjusting inputs.
  • + *
  • Treat {@link ProviderFailureException} as an operational fault (provider + * bug, environment issue); log with context and surface a safe error to + * callers.
  • + *
+ * + *

Typical usage

{@code
+ * try {
+ *     zeroecho.core.context.EncryptionContext ctx =
+ *         algo.create(zeroecho.core.KeyUsage.ENCRYPT, key, spec);
+ *     // use ctx...
+ * } catch (zeroecho.core.err.UnsupportedRoleException
+ *        | zeroecho.core.err.UnsupportedSpecException e) {
+ *     // programming error: wrong role or incompatible key/spec
+ *     throw new IllegalArgumentException("Bad configuration", e);
+ * } catch (java.io.IOException e) {
+ *     // I/O failure when initializing context
+ *     ...
+ * } catch (zeroecho.core.err.ProviderFailureException e) {
+ *     // unexpected provider failure: wrap, log, or abort
+ *     ...
+ * }
+ * }
+ * + * @since 1.0 + */ +package zeroecho.core.err; diff --git a/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java b/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java new file mode 100644 index 0000000..e93f393 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/AbstractChunkTransformInputStream.java @@ -0,0 +1,355 @@ +/******************************************************************************* + * 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.core.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * InputStream that transforms data in fixed-size chunks with optional + * finalization. + * + *

+ * This abstract class provides a framework for streaming transformations where + * the input is processed in aligned blocks of {@code inChunkSize} bytes, + * producing corresponding output blocks of {@code outChunkSize} bytes. + * Subclasses implement the transformation logic in + * {@link #transform(byte[], int, int, byte[])} and handle any trailing input in + * {@link #doFinal(byte[], int, int, byte[], int)}. + *

+ * + *

Design and invariants

+ *
    + *
  • Input bytes are read from the wrapped {@link InputStream} into + * {@code inBuf}, preferably in multiples of {@code inChunkSize}.
  • + *
  • {@link #transform(byte[], int, int, byte[])} is invoked for all complete + * chunks. Its contract is to consume {@code inChunks * inChunkSize} bytes from + * {@code in} and produce contiguous output bytes into {@code out}.
  • + *
  • If the final read yields a partial chunk smaller than + * {@code inChunkSize}, {@link #doFinal(byte[], int, int, byte[], int)} is + * invoked to process the remainder and emit any trailer (e.g., padding block or + * authentication tag).
  • + *
  • Output bytes are staged in {@code outBuf} and consumed by {@link #read()} + * and {@link #read(byte[], int, int)}.
  • + *
+ * + *

Buffer sizing and finalization headroom

+ *

+ * The primary constructor sizes both input and output buffers to + * {@code chunks * inChunkSize} and {@code chunks * outChunkSize}, respectively. + * An overloaded constructor adds {@code finalizationOutputChunks} to the + * output-side capacity only, resulting in + * {@code outChunkSize * (chunks + finalizationOutputChunks)}. This headroom + * guarantees that the last {@code doFinal(...)} call can emit trailer bytes + * (for example, a padding block or AEAD tag) even when the steady-state output + * window is full. + *

+ *

+ * Typical values when {@code outChunkSize == 16} (AES-sized): + *

+ *
    + *
  • No padding modes (e.g., AES/CBC/NOPADDING): + * {@code finalizationOutputChunks = 0}.
  • + *
  • Padding modes (e.g., AES/CBC/PKCS5PADDING): {@code 1} to cover a full + * padding block.
  • + *
  • AEAD modes (e.g., AES/GCM/NOPADDING with 16-byte tag): {@code 2} to cover + * tail bytes (up to 15) plus the tag (commonly 16).
  • + *
+ * + *

Thread-safety

Instances are not thread-safe. If a single instance is + * shared across threads, external synchronization is required. + * + *

Example

{@code
+ * // Example subclass wiring (pseudocode)
+ * final class MyCipherStream extends AbstractChunkTransformInputStream {
+ *     private final Cipher cipher;
+ *     MyCipherStream(InputStream in, Cipher c) {
+ *         super(in, 16, 16, 256, 2); // +2 output chunks headroom (e.g., AES-GCM)
+ *         this.cipher = c;
+ *     }
+ *     @Override protected int transform(byte[] src, int off, int chunks, byte[] dst) throws IOException {
+ *         try {
+ *             return cipher.update(src, off, chunks * 16, dst, 0);
+ *         } catch (ShortBufferException e) {
+ *             throw new IOException(e);
+ *         }
+ *     }
+ *     @Override protected int doFinal(byte[] src, int off, int len, byte[] dst, int dstOff) throws IOException {
+ *         try {
+ *             int out = 0;
+ *             if (len > 0) { out = cipher.update(src, off, len, dst, dstOff); dstOff += out; }
+ *             out += cipher.doFinal(dst, dstOff);
+ *             return out;
+ *         } catch (GeneralSecurityException | ShortBufferException e) {
+ *             throw new IOException(e);
+ *         }
+ *     }
+ * }
+ * }
+ * + * @author Leo Galambos + * @since 1.0 + */ +public abstract class AbstractChunkTransformInputStream extends FilterInputStream { + /** Input buffer storing data read from the upstream stream. */ + protected final byte[] inBuf; + /** Output buffer storing transformed bytes awaiting consumption. */ + protected byte[] outBuf; + /** Number of valid bytes currently in {@code inBuf}. */ + protected int inLen; + /** Current read position within {@code outBuf}. */ + protected int outPtr; + /** Number of valid bytes currently in {@code outBuf}. */ + protected int outLen; + /** Flag indicating that end-of-stream has been reached. */ + protected boolean eofSeen; // = false; + /** Size of one logical input chunk. */ + protected final int inChunkSize; + /** Size of one logical output chunk. */ + protected final int outChunkSize; + + /** + * Constructs a new chunk-transforming input stream with symmetric input and + * output capacities. + * + *

+ * The input buffer is sized to {@code inChunkSize * chunks} and the output + * buffer to {@code outChunkSize * chunks}. No extra output headroom is reserved + * for finalization. + *

+ * + * @param upstream the underlying input stream + * @param inChunkSize size of input chunks expected by the transform (must be > + * 1) + * @param outChunkSize size of output chunks produced by the transform (must be + * > 0) + * @param chunks number of chunks buffered at once (must be > 0) + * @throws AssertionError if {@code chunks <= 0}, {@code inChunkSize <= 1}, or + * {@code outChunkSize <= 0} + */ + protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks) { + super(upstream); + assert chunks > 0 && inChunkSize > 1 && outChunkSize > 0; + + this.inBuf = new byte[inChunkSize * chunks]; + this.outBuf = new byte[outChunkSize * chunks]; + + this.inChunkSize = inChunkSize; + this.outChunkSize = outChunkSize; + } + + /** + * Constructs a new chunk-transforming input stream with extra output headroom + * for finalization. + * + *

+ * The input buffer is sized to {@code inChunkSize * chunks}. The output buffer + * is sized to {@code outChunkSize * (chunks + finalizationOutputChunks)} so + * that the last call to {@link #doFinal(byte[], int, int, byte[], int)} can + * emit trailer bytes even when the steady-state output region is full. + *

+ * + * @param upstream the underlying input stream + * @param inChunkSize size of input chunks expected by the + * transform (must be > 1) + * @param outChunkSize size of output chunks produced by the + * transform (must be > 0) + * @param chunks number of chunks buffered at once for + * steady-state (must be > 0) + * @param finalizationOutputChunks number of extra output chunks reserved for + * finalization (must be >= 0) + * @throws AssertionError if {@code chunks <= 0}, {@code inChunkSize <= 1}, or + * {@code outChunkSize <= 0} + */ + protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks, + int finalizationOutputChunks) { + super(upstream); + assert chunks > 0 && inChunkSize > 1 && outChunkSize > 0; + + this.inBuf = new byte[inChunkSize * chunks]; + this.outBuf = new byte[outChunkSize * (chunks + finalizationOutputChunks)]; + + this.inChunkSize = inChunkSize; + this.outChunkSize = outChunkSize; + } + + /** + * Transforms one or more full input chunks into output bytes. + * + * @param in buffer containing input data + * @param inOff offset in {@code in} where chunks begin + * @param inChunks number of complete input chunks available + * @param out destination buffer for transformed bytes + * @return number of bytes written to {@code out} + * @throws IOException if transformation fails + */ + protected abstract int transform(byte[] in, int inOff, int inChunks, byte[] out) throws IOException; + + /** + * Finalizes the transformation with an optional trailing incomplete chunk. + * + *

+ * Invoked at EOF if the last read produced fewer than {@code inChunkSize} + * bytes. Implementations may perform padding, compute an authentication tag, or + * flush any provider-internal state. The {@code outOff} argument specifies + * where to start writing the final bytes in {@code out}. + *

+ * + * @param in buffer containing the final partial chunk (may be empty) + * @param inOff offset of the partial data + * @param len number of available bytes (< {@code inChunkSize}) + * @param out destination buffer for transformed bytes + * @param outOff offset in {@code out} where bytes should be written + * @return number of bytes written to {@code out} + * @throws IOException if finalization fails or the provider reports an error + */ + protected abstract int doFinal(byte[] in, int inOff, int len, byte[] out, int outOff) throws IOException; + + /** + * Fills the output buffer by reading from the upstream and applying the + * transformation. + * + *

+ * On EOF, {@link #doFinal(byte[], int, int, byte[], int)} is executed exactly + * once (even if {@code len == 0}) and any produced bytes are exposed to the + * caller. + *

+ * + * @return {@code true} if new output is available, {@code false} if EOF reached + * and no output produced + * @throws IOException if the upstream read or the transformation fails + */ + private boolean fillBuffers() throws IOException { + assert outPtr == outLen; + + inLen = in.readNBytes(inBuf, 0, inBuf.length); + if (inLen == 0) { + // EOF: run finalization exactly once, even if there's no remainder, + // and surface any produced bytes (e.g., padding block, GCM tag). + if (!eofSeen) { + int finalOut = doFinal(inBuf, 0, 0, outBuf, 0); + outPtr = 0; + outLen = finalOut; + eofSeen = true; + return finalOut > 0; // allow the caller to drain final bytes + } + return false; + } + + // all chunks are aligned to the specified boundary (inChunkSize) -> transform + // can be simply invoked + outLen = transform(inBuf, 0, inLen / inChunkSize, outBuf); + outPtr = 0; + + int left = inLen % inChunkSize; + + if (left > 0) { + int finalOutChunkSize = doFinal(inBuf, inLen - left, left, outBuf, outLen); + outLen = outLen + finalOutChunkSize; + // we ask for whole inBufSize chunks, if readNBytes returns a partial chunk, it + // must be eof + eofSeen = true; + } + + return true; + } + + /** + * Reads the next transformed byte. + * + * @return the next byte as an unsigned int in the range {@code 0-255}, or + * {@code -1} if end-of-stream has been reached + * @throws IOException if an I/O error occurs + */ + @Override + public int read() throws IOException { + if (outPtr < outLen) { + return outBuf[outPtr++] & 0xff; + } + + return fillBuffers() ? outBuf[outPtr++] & 0xff : -1 /* eof */; + } + + /** + * Reads transformed bytes into the given array. + * + *

+ * This implementation delegates to {@link #read(byte[], int, int)}. + *

+ * + * @param b target buffer + * @return number of bytes read, or {@code -1} at EOF + * @throws IOException if an I/O error occurs + */ + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + /** + * Reads up to {@code len} transformed bytes into the target buffer. + * + *

+ * Blocks until at least one byte is available or EOF is reached. + *

+ * + * @param b target buffer + * @param off offset in {@code b} where data should be stored + * @param len maximum number of bytes to read + * @return number of bytes read, or {@code -1} if EOF and no data returned + * @throws IOException if an I/O error occurs + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + int total = 0; + while (len > 0) { + if (outPtr >= outLen) { + // we have not more, let us try to prepare new buffers + if (!fillBuffers()) { // NOPMD + return (total == 0) ? -1 : total; + } + } + + int available = Math.min(outLen - outPtr, len); + System.arraycopy(outBuf, outPtr, b, off, available); + off += available; // NOPMD + outPtr += available; + total += available; + len -= available; // NOPMD + } + return total; + } +} diff --git a/lib/src/main/java/zeroecho/core/io/AbstractPassthroughInputStream.java b/lib/src/main/java/zeroecho/core/io/AbstractPassthroughInputStream.java new file mode 100644 index 0000000..675d700 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/AbstractPassthroughInputStream.java @@ -0,0 +1,304 @@ +/******************************************************************************* + * 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.core.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Objects; + +/** + * Passthrough {@link InputStream} that forwards body bytes and optionally emits + * a trailer at EOF. + * + *

+ * This abstract helper implements a two-phase read pipeline suitable for + * tag-producing or verifying stages. During the BODY phase, bytes are pulled + * from the wrapped upstream stream and immediately exposed to the caller while + * {@link #update(byte[], int, int)} is invoked so subclasses can maintain + * running state (for example, hash, MAC, or signature update). After upstream + * EOF, the TRAILER phase runs once: {@link #produceTrailer(byte[])} may write a + * finite trailer into an internal buffer, and {@link #onCompleted()} is called + * exactly once. + *

+ * + *

Phases

+ *
    + *
  • BODY: bytes are read from {@link #in}, passed through to the + * caller, and fed to {@link #update(byte[], int, int)}.
  • + *
  • TRAILER: after EOF from upstream, {@link #produceTrailer(byte[])} + * is invoked once to optionally emit a trailer; then {@link #onCompleted()} is + * called and the stream becomes exhausted.
  • + *
+ * + *

Subclassing contract

+ *
    + *
  • {@link #update(byte[], int, int)} must be side-effect free with respect + * to the bytes already returned to the caller; it is for state maintenance + * only.
  • + *
  • {@link #produceTrailer(byte[])} must write at most {@code buf.length} + * bytes and return the count, or return {@code 0} to produce no trailer. If + * more space is required, it must throw {@link IOException}.
  • + *
  • {@link #onCompleted()} is invoked exactly once after + * {@code produceTrailer} returns, regardless of whether a trailer was produced; + * it should release resources and may perform final checks. It must not attempt + * to emit more bytes.
  • + *
+ * + *

Thread-safety

+ *

+ * Instances are not thread-safe. Use a separate instance per stream pipeline. + *

+ * + *

Example

+ * {@code
+ * final class DigestingStream extends AbstractPassthroughInputStream {
+ *     private final MessageDigest md;
+ *     DigestingStream(InputStream upstream, MessageDigest md) {
+ *         super(upstream, 8192);
+ *         this.md = md;
+ *     }
+ *     @Override protected void update(byte[] buf, int off, int len) { md.update(buf, off, len); }
+ *     @Override protected int produceTrailer(byte[] buf) {
+ *         byte[] tag = md.digest();
+ *         System.arraycopy(tag, 0, buf, 0, tag.length);
+ *         return tag.length; // append digest at EOF
+ *     }
+ *     @Override protected void onCompleted() { /* publish metrics, close resources, etc. *\/ }
+ * }
+ * }
+ * 
+ */ +public abstract class AbstractPassthroughInputStream extends FilterInputStream { + /** + * Scratch buffer used to read from {@link #in}. Also used once to hold the + * trailer, if any. + */ + protected final byte[] inBuf; + /** + * Current read position within the buffered output window {@link #outBuf()} + * (modeled by {@link #inBuf}). + */ + private int outPos; + /** + * Number of valid bytes currently buffered for output within {@link #inBuf}. + */ + private int outLen; + + /** + * True once EOF was observed on the upstream stream. + */ + private boolean eofSeen; + /** + * True once the trailer producer has completed (whether or not a trailer was + * emitted). + */ + private boolean trailerDone; + + /** + * Constructs a new passthrough stream with an internal buffer sized for body + * reads. + * + *

+ * The buffer is allocated as the maximum of {@code 1024} and the requested + * {@code bodyBufSize}, ensuring a reasonable minimum size even if the caller + * specifies a very small value. The buffer is reused both for normal body reads + * and, if applicable, for the single trailer emission. + *

+ * + * @param upstream source input stream to wrap + * @param bodyBufSize requested size of the body buffer, in bytes + * @throws NullPointerException if {@code upstream} is {@code null} + */ + protected AbstractPassthroughInputStream(InputStream upstream, int bodyBufSize) { + super(upstream); + this.inBuf = new byte[Math.max(1024, bodyBufSize)]; + this.outPos = 0; + this.outLen = 0; + } + + /** + * Called for each body chunk just read from the upstream. + * + *

+ * Implementations may update running state (e.g., hash or MAC) using the bytes + * in {@code buf[off..off+len)}. Implementations must not modify the visible + * output of the current chunk. + *

+ * + * @param buf buffer containing the newly read bytes + * @param off start offset + * @param len number of bytes read + * @throws IOException if processing the chunk fails + */ + protected abstract void update(byte[] buf, int off, int len) throws IOException; + + /** + * Produces an optional trailer after upstream EOF. + * + *

+ * This method is called at most once, after the BODY phase has ended. + * Implementations should write the trailer bytes (if any) into the provided + * buffer and return the number of bytes written. A return value of 0 means no + * trailer will be emitted. + *

+ * + *

+ * Contract: This method must be side-effect free with respect to + * subsequent reads; it is followed immediately by a call to + * {@link #onCompleted()}. If a trailer cannot fit into {@code buf}, + * implementations should throw {@link IOException} to avoid truncation. + *

+ * + * @param buf destination buffer to receive the trailer + * @return number of bytes written to {@code buf}; 0 to emit no trailer + * @throws IOException if trailer production fails or the trailer does not fit + * into {@code buf} + */ + protected abstract int produceTrailer(byte[] buf) throws IOException; + + /** + * Notifies that the stream has fully completed. + * + *

+ * This method is invoked exactly once, immediately after + * {@link #produceTrailer(byte[])} returns, regardless of whether a trailer was + * produced. Implementations typically perform final verification, publish + * flags, and/or release resources here. This method should be idempotent and + * must not attempt to write further bytes. + *

+ */ + protected abstract void onCompleted() throws IOException; + + private boolean ensureData() throws IOException { + while (outPos >= outLen) { + if (!eofSeen) { // NOPMD + // BODY phase: read from upstream + int n = in.readNBytes(inBuf, 0, inBuf.length); + if (n <= 0) { + eofSeen = true; + outPos = 0; + outLen = 0; // fall through to trailer production below + } else { + update(inBuf, 0, n); + outPos = 0; + outLen = n; + if (outLen > 0) { + return true; + } + // zero-length read should not happen for positive n, but keep the loop safe + } + } else { + // TRAILER phase: keep asking until it returns 0 + if (trailerDone) { + return false; + } + int trailerLen = produceTrailer(inBuf); + trailerDone = true; // produceTrailer can be asked just once + onCompleted(); + if (trailerLen <= 0) { + return false; + } + outPos = 0; + outLen = trailerLen; + return true; + } + } + return true; + } + + /** + * Reads the next byte from this stream. + * + * @return next byte as an unsigned int (0..255), or -1 if no more data is + * available + * @throws IOException if an I/O error occurs + */ + @Override + public int read() throws IOException { + if (!ensureData()) { + return -1; + } + return inBuf[outPos++] & 0xFF; + } + + /** + * Reads some bytes from this stream into the specified array. + * + * @param b target buffer; must not be null + * @param off start offset within {@code b} + * @param len maximum number of bytes to read + * @return number of bytes read, or -1 if end-of-stream is reached + * @throws IOException if an I/O error occurs + * @throws NullPointerException if {@code b} is null + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are invalid + * for {@code b} + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + Objects.checkFromIndexSize(off, len, b.length); + + if (len == 0) { + return 0; + } + if (!ensureData()) { + return -1; + } + int n = Math.min(len, outLen - outPos); + System.arraycopy(inBuf, outPos, b, off, n); + outPos += n; + return n; + } + + /** + * Drains remaining data (including any trailer) and then closes the upstream + * stream. + * + *

+ * This method transfers all remaining bytes to + * {@link OutputStream#nullOutputStream()}, which guarantees that the trailer + * (if any) is produced and {@link #onCompleted()} is invoked, and finally calls + * {@code super.close()}. + *

+ * + * @throws IOException if an I/O error occurs while draining or closing + */ + @Override + public void close() throws IOException { + transferTo(OutputStream.nullOutputStream()); + super.close(); + } +} diff --git a/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java b/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java new file mode 100644 index 0000000..9768515 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/CipherTransformInputStreamBuilder.java @@ -0,0 +1,343 @@ +/******************************************************************************* + * 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.core.io; + +import java.io.InputStream; +import java.util.Objects; + +import javax.crypto.Cipher; + +/** + * Builder for chunk-transforming {@link InputStream}s backed by a + * {@link Cipher}. + * + *

+ * This builder configures and creates an {@code InputStream} that reads from an + * upstream source and applies a block-based transformation using a provided + * {@link Cipher}. Three stream variants are available: + *

+ *
    + *
  • SmartBlockStream - invokes + * {@link Cipher#doFinal(byte[], int, int, byte[], int)} for each full input + * block; a final partial block (if any) is processed by a single + * {@code doFinal}.
  • + *
  • SmartPaddedBlockStream - like {@code SmartBlockStream}, but + * left-pads each transformed output block with zeros up to + * {@code outChunkSize}. Final blocks must be complete; otherwise an + * {@link IllegalStateException} is thrown.
  • + *
  • SmartContinuousBlockStream - streaming variant that uses + * {@code Cipher.update(...)} for bulk bytes and a single {@code doFinal()} at + * end of stream. This is suitable for CTR/CFB/OFB/GCM and padding modes.
  • + *
+ * + *

Block sizing

+ *

+ * The builder exposes {@code inChunkSize} and {@code outChunkSize}. For AES in + * ECB/CBC/CTR-like modes these are typically 16 bytes, but the stream can use + * larger logical chunks for I/O efficiency. The implementation buffers up to + * {@code bufferedBlocks} chunks per fill to balance throughput and memory. + *

+ * + *

Finalization headroom

+ *

+ * Some ciphers or modes produce extra bytes at end-of-stream (for example, a + * padding block for CBC/PKCS5 or an authentication tag for GCM). To ensure the + * final {@code doFinal(...)} fits even when the steady-state output area is + * full, you can reserve additional output capacity via + * {@link #withFinalizationOutputChunks(int)}. This increases only the + * output-side buffer, leaving the input buffer size unchanged. + *

+ * + *

Thread-safety

Instances produced by this builder are not + * thread-safe. Do not share a single stream across threads without external + * synchronization. + * + *

Examples

{@code
+ * // AES/ECB/NOPADDING: aligned input, no extra finalization output
+ * Cipher c1 = Cipher.getInstance("AES/ECB/NOPADDING");
+ * c1.init(Cipher.ENCRYPT_MODE, key);
+ * InputStream s1 = CipherTransformInputStreamBuilder.builder()
+ *     .withUpstream(in)
+ *     .withCipher(c1)
+ *     .withUpdateStreaming(true)     // allowed; final len must be multiple of 16
+ *     .withInputBlockSize(16)
+ *     .withOutputBlockSize(16)
+ *     .withBufferedBlocks(200)
+ *     .withFinalizationOutputChunks(0)
+ *     .build();
+ *
+ * // AES/CBC/PKCS5PADDING: one final padding block
+ * Cipher c2 = Cipher.getInstance("AES/CBC/PKCS5PADDING");
+ * c2.init(Cipher.ENCRYPT_MODE, key, ivSpec);
+ * InputStream s2 = CipherTransformInputStreamBuilder.builder()
+ *     .withUpstream(in)
+ *     .withCipher(c2)
+ *     .withUpdateStreaming(true)
+ *     .withInputBlockSize(16)
+ *     .withOutputBlockSize(16)
+ *     .withBufferedBlocks(200)
+ *     .withFinalizationOutputChunks(1)
+ *     .build();
+ *
+ * // AES/GCM/NOPADDING (16-byte tag): tail + tag -> reserve two chunks
+ * Cipher c3 = Cipher.getInstance("AES/GCM/NOPADDING");
+ * c3.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
+ * InputStream s3 = CipherTransformInputStreamBuilder.builder()
+ *     .withUpstream(in)
+ *     .withCipher(c3)
+ *     .withUpdateStreaming(true)
+ *     .withInputBlockSize(16).withOutputBlockSize(16)
+ *     .withBufferedBlocks(200)
+ *     .withFinalizationOutputChunks(2)
+ *     .build();
+ *
+ * // ElGamal (asymmetric; non-streaming semantics): no extra finalization output
+ * Cipher c4 = Cipher.getInstance("ElGamal/None/NOPADDING"); // provider-specific
+ * c4.init(Cipher.ENCRYPT_MODE, publicKey, params);
+ * InputStream s4 = CipherTransformInputStreamBuilder.builder()
+ *     .withUpstream(in)
+ *     .withCipher(c4)
+ *     .withUpdateStreaming(true)     // provider typically accepts update+doFinal
+ *     .withInputBlockSize(elgIn)
+ *     .withOutputBlockSize(elgOut)
+ *     .withBufferedBlocks(200)
+ *     .withFinalizationOutputChunks(0)
+ *     .build();
+ * }
+ * + * @since 1.0 + */ +public final class CipherTransformInputStreamBuilder { + private InputStream upstream; + private Cipher cipher; + private int inChunkSize = 256; + private int outChunkSize = 256; + private int bufferedBlocks = 100; + /** + * When > 0, enlarges the output buffer by this many extra chunks for the + * finalization burst. + */ + private int finalizationOutputChunks; + private boolean padding; + private boolean updateStreaming; // when true, use SmartContinuousBlockStream + + private CipherTransformInputStreamBuilder() { + } + + /** + * Returns a new builder instance with default settings. + * + *

+ * Defaults: {@code inChunkSize=256}, {@code outChunkSize=256}, + * {@code bufferedBlocks=100}, padding disabled. + *

+ * + * @return a new builder + */ + public static CipherTransformInputStreamBuilder builder() { + return new CipherTransformInputStreamBuilder(); + } + + /** + * Sets the upstream source from which bytes will be read and transformed. + * + * @param upstream the input stream to wrap; must not be null + * @return this builder for chaining + */ + public CipherTransformInputStreamBuilder withUpstream(InputStream upstream) { + this.upstream = upstream; + return this; + } + + /** + * Sets the {@link Cipher} used to transform blocks. + * + *

+ * The cipher must be fully initialized for the intended mode (encrypt or + * decrypt) before calling {@link #build()}. + *

+ * + * @param cipher initialized cipher instance; must not be null + * @return this builder for chaining + */ + public CipherTransformInputStreamBuilder withCipher(Cipher cipher) { + this.cipher = cipher; + return this; + } + + /** + * Configures whether left zero padding is applied to each output block. + * + *

+ * If set to {@code true}, the builder will produce a padded stream variant + * where each block is left-filled with zeros up to {@code outChunkSize}. If + * {@code false}, the plain non-padded variant is used. + *

+ * + * @param padding {@code true} to enable left zero padding, {@code false} to + * disable it + * @return this builder for chaining + */ + public CipherTransformInputStreamBuilder withLeftZeroPadding(boolean padding) { + this.padding = padding; + return this; + } + + /** + * Sets the size of a single input block fed to the cipher. + * + * @param inSize input chunk size in bytes; must be > 1 + * @return this builder for chaining + */ + public CipherTransformInputStreamBuilder withInputBlockSize(int inSize) { + this.inChunkSize = inSize; + return this; + } + + /** + * Sets the size of a single output block expected from the cipher. + * + * @param outSize output chunk size in bytes; must be > 0 + * @return this builder for chaining + */ + public CipherTransformInputStreamBuilder withOutputBlockSize(int outSize) { + this.outChunkSize = outSize; + return this; + } + + /** + * Sets the number of blocks buffered per internal fill cycle. + * + *

+ * The internal buffers will be sized to {@code inChunkSize * bufferedBlocks} + * and {@code outChunkSize * bufferedBlocks}. + *

+ * + * @param bufferedBlocks number of blocks to buffer; must be > 0 + * @return this builder for chaining + */ + public CipherTransformInputStreamBuilder withBufferedBlocks(int bufferedBlocks) { + this.bufferedBlocks = bufferedBlocks; + return this; + } + + /** + * Sets the number of extra output chunks reserved for the finalization step. + * + *

+ * Some modes may emit additional bytes at the very end (padding block for + * CBC/PKCS5, authentication tag for GCM/CCM/EAX, provider flush). The output + * buffer is allocated with {@code bufferedBlocks + finalizationOutputChunks} + * chunks to guarantee that the final {@code doFinal(...)} fits even when the + * steady-state area is full. + *

+ * + *

Recommended values

+ *
    + *
  • AES/CBC/PKCS5PADDING: {@code 1}
  • + *
  • AES/GCM (16-byte tag): {@code 2}
  • + *
  • AES/CTR, CFB, OFB: {@code 1} (covers a tail up to 15 bytes)
  • + *
  • AES/CBC/NOPADDING: {@code 0} (total length must be a multiple of + * block size)
  • + *
  • ElGamal/RSA: {@code 0} (non-streaming; no extra finalization + * block)
  • + *
+ * + *

Notes

+ *
    + *
  • For GCM with a different tag size, use + * {@code ceil((15 + tagLen)/outChunkSize)}. With 12–16 byte tags and + * {@code outChunkSize=16}, {@code 2} is still safe.
  • + *
  • This setting enlarges only the output buffer capacity; the input + * buffer remains {@code inChunkSize * bufferedBlocks}.
  • + *
+ * + * @param chunks extra output chunks reserved for finalization; must be + * {@code >= 0} + * @return this builder for chaining + */ + public CipherTransformInputStreamBuilder withFinalizationOutputChunks(int chunks) { + this.finalizationOutputChunks = chunks; + return this; + } + + /** + * Enables update-driven streaming (Cipher.update during transform, single + * doFinal at EOF). Suitable for CTR/CFB/OFB/GCM and padding modes. + */ + public CipherTransformInputStreamBuilder withUpdateStreaming() { + this.updateStreaming = true; + return this; + } + + /** + * Controls update-driven streaming explicitly. + * + * @param enable true to use update-driven streaming, false for per-block + * doFinal variant + * @return this builder + */ + public CipherTransformInputStreamBuilder withUpdateStreaming(boolean enable) { + this.updateStreaming = enable; + return this; + } + + /** + * Builds a chunk-transforming {@link InputStream} using the configured options. + * + *

+ * The returned stream pulls from {@code upstream} and applies the configured + * {@code cipher}. The output buffer is allocated to + * {@code outChunkSize * (bufferedBlocks + finalizationOutputChunks)} bytes so + * that the final {@code doFinal(...)} can emit padding or tags even when the + * steady-state output window is full. + *

+ * + * @return a new InputStream that transforms bytes on the fly + * @throws NullPointerException if {@code upstream} or {@code cipher} is null + * @throws AssertionError if buffer sizing assertions fail + */ + public InputStream build() { + Objects.requireNonNull(upstream, "upstream must not be null"); + Objects.requireNonNull(cipher, "cipher must not be null"); + + if (updateStreaming) { + return new SmartContinuousBlockStream(upstream, cipher, inChunkSize, outChunkSize, bufferedBlocks, + finalizationOutputChunks); + } + return padding ? new SmartPaddedBlockStream(upstream, cipher, inChunkSize, outChunkSize, bufferedBlocks) + : new SmartBlockStream(upstream, cipher, inChunkSize, outChunkSize, bufferedBlocks); + } +} diff --git a/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java b/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java new file mode 100644 index 0000000..5c232dd --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/SmartBlockStream.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.io; + +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; + +/** + * Chunk-transforming stream that applies a cipher to each full input block and + * forwards the produced bytes without extra padding. + * + *

+ * Full blocks are processed via + * {@link Cipher#doFinal(byte[], int, int, byte[], int)} in a loop. If the final + * read yields a partial block, it is finalized through a single {@code doFinal} + * and end-of-stream is marked. + *

+ */ +final class SmartBlockStream extends AbstractChunkTransformInputStream { + private static final Logger LOG = Logger.getLogger(SmartBlockStream.class.getName()); + + private final Cipher cipher; + private boolean doFinalCalled; + + /* package */ SmartBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize, + int bufferedBlocks) { + super(upstream, inChunkSize, outChunkSize, bufferedBlocks); + this.cipher = cipher; + } + + /** + * Applies the cipher to {@code inChunks} complete input blocks and writes + * results contiguously to {@code out}. + * + * @param in the input buffer + * @param inOff offset of the first complete block + * @param inChunks number of full blocks to process + * @param out destination buffer for output bytes + * @return number of bytes written to {@code out} + * @throws IOException if the cipher reports a processing error + */ + @Override + protected int transform(byte[] in, int inOff, int inChunks, byte[] out) throws IOException { + try { + int output = 0; + doFinalCalled = inChunks > 0; + for (int i = 0; i < inChunks; i++) { + int outOne = cipher.doFinal(in, inOff, inChunkSize, out, output); + output = output + outOne; + inOff += inChunkSize; // NOPMD + } + return output; + } catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { + LOG.logp(Level.WARNING, "SmartBlockStream", "transform", "Exception", e); + throw new IOException(e); + } + } + + /** + * Finalizes processing of a trailing partial block, if present. + * + * @param in input buffer containing the remainder + * @param inOff offset of the remainder + * @param len number of bytes in the remainder + * @param out destination buffer for output bytes + * @param outOff offset in {@code out} where bytes are written + * @return number of bytes written to {@code out} + * @throws IOException if the cipher reports a processing error + */ + @Override + protected int doFinal(byte[] in, int inOff, int len, byte[] out, int outOff) throws IOException { + try { + if (doFinalCalled && len == 0) { + return 0; + } + + doFinalCalled = true; + return cipher.doFinal(in, inOff, len, out, outOff); + } catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { + LOG.logp(Level.WARNING, "SmartBlockStream", "transform", "Exception", e); + throw new IOException(e); + } + } + +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java b/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java new file mode 100644 index 0000000..9a3d32c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/SmartContinuousBlockStream.java @@ -0,0 +1,178 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.io; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; + +/** + * Passthrough stream that transforms input with a {@link javax.crypto.Cipher} + * in continuous streaming mode. + * + *

+ * This class extends {@link AbstractChunkTransformInputStream} to adapt block + * sizes into calls to {@link Cipher#update(byte[], int, int, byte[], int)} and + * {@link Cipher#doFinal(byte[], int, int, byte[], int)}. It is suitable for + * symmetric ciphers in modes that support continuous streaming (e.g., CBC, GCM, + * CTR). + *

+ * + *

Operation

+ *
    + *
  • Transform phase: chunks of {@code inChunkSize} bytes are grouped + * and passed to the cipher in a single bulk {@code update} call. The provider + * may accept arbitrary multiples of the block size.
  • + *
  • Finalization phase: when EOF is reached, any trailing bytes are + * processed (if present) and {@code doFinal} is invoked exactly once to flush + * buffered state and produce final output (padding, tag, etc.).
  • + *
+ * + *

Buffer expansion

+ *

+ * Before calling {@code doFinal}, the required output length is queried via + * {@link Cipher#getOutputSize(int)}. If the current buffer is too small, it is + * expanded using {@link Arrays#copyOf(byte[], int)}. A warning is logged via + * {@link Logger} when expansion occurs. + *

+ * + *

Thread-safety

+ *

+ * Instances are not thread-safe and must be confined to a single consuming + * thread. + *

+ * + * @since 1.0 + */ +final class SmartContinuousBlockStream extends AbstractChunkTransformInputStream { + private static final Logger LOG = Logger.getLogger(SmartContinuousBlockStream.class.getName()); + /** Underlying cipher used for streaming updates and finalization. */ + private final Cipher cipher; + + /** + * Creates a new continuous block stream wrapper around a {@link Cipher}. + * + * @param upstream upstream source stream to transform + * @param cipher initialized cipher instance + * @param inChunkSize number of input bytes per chunk + * @param outChunkSize maximum output bytes per chunk + * @param bufferedBlocks number of input blocks buffered internally + * @param finalizationOutputChunks number of additional output chunks expected + * during finalization + * @throws NullPointerException if {@code upstream} or {@code cipher} is + * {@code null} + */ + /* package */ SmartContinuousBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize, + int bufferedBlocks, int finalizationOutputChunks) { + super(upstream, inChunkSize, outChunkSize, bufferedBlocks, finalizationOutputChunks); + this.cipher = cipher; + } + + /** + * Performs the streaming transform using + * {@link Cipher#update(byte[], int, int, byte[], int)}. + * + *

+ * Processes {@code inChunks * inChunkSize} bytes from {@code in} starting at + * {@code inOff}, and writes results into {@code out} starting at offset 0. + *

+ * + * @param in source buffer with plaintext or ciphertext + * @param inOff offset in {@code in} to start reading + * @param inChunks number of input chunks to process + * @param out destination buffer to receive transformed bytes + * @return number of output bytes written + * @throws IOException if the cipher signals a short buffer or provider error + */ + @Override + protected int transform(byte[] in, int inOff, int inChunks, byte[] out) throws IOException { + try { + int toProcess = inChunks * inChunkSize; + // Single bulk update; providers handle arbitrary sizes in streaming modes. + return cipher.update(in, inOff, toProcess, out, 0); + } catch (ShortBufferException e) { + LOG.logp(Level.WARNING, "SmartContinuousBlockStream", "transform", "Short buffer", e); + throw new IOException(e); + } + } + + /** + * Finalizes the cipher by processing any trailing input and calling + * {@link Cipher#doFinal(byte[], int, int, byte[], int)}. + * + *

+ * If the provided output buffer is too small for the final block (as reported + * by {@link Cipher#getOutputSize(int)}), the internal buffer is expanded and a + * warning is logged. This ensures complete final output is not truncated. + *

+ * + * @param in buffer containing final input bytes + * @param inOff offset in {@code in} where valid data starts + * @param len number of final input bytes + * @param out destination buffer for output + * @param outOff offset in {@code out} to begin writing + * @return number of bytes written to {@code out} + * @throws IOException if finalization fails (short buffer, bad padding, illegal + * block size) + */ + @Override + protected int doFinal(byte[] in, int inOff, int len, byte[] out, int outOff) throws IOException { + try { + int finBlockSize = cipher.getOutputSize(len); + if (out.length < outOff + finBlockSize) { + if (LOG.isLoggable(Level.WARNING)) { + LOG.log(Level.WARNING, "Expanding buffer of {0} from {1} bytes to {2} bytes", + new Object[] { cipher.getAlgorithm(), outBuf.length, outOff + finBlockSize }); + } + out = outBuf = Arrays.copyOf(outBuf, outOff + finBlockSize); // NOPMD + } + + int written = cipher.doFinal(in, inOff, len, out, outOff); // NOPMD + return written; + + // return cipher.doFinal(in, inOff, len, out, outOff); + } catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { + LOG.logp(Level.WARNING, "SmartContinuousBlockStream", "doFinal", "Exception", e); + throw new IOException(e); + } + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java b/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java new file mode 100644 index 0000000..3c8b3a6 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/SmartPaddedBlockStream.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.io; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; + +/** + * Chunk-transforming stream that applies a cipher to each full input block and + * left-pads each produced block with zeros to {@code outChunkSize}. + * + *

+ * For each complete input block, the method shifts the produced bytes to the + * right and fills the left region with zeros when the cipher output is shorter + * than {@code outChunkSize}. A final block must be complete; partial remainders + * are rejected with {@link IllegalStateException}. + *

+ */ +final class SmartPaddedBlockStream extends AbstractChunkTransformInputStream { + private static final Logger LOG = Logger.getLogger(SmartPaddedBlockStream.class.getName()); + + private final Cipher cipher; + private boolean doFinalCalled; + + /* package */ SmartPaddedBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize, + int bufferedBlocks) { + super(upstream, inChunkSize, outChunkSize, bufferedBlocks); + this.cipher = cipher; + } + + /** + * Applies the cipher to {@code inChunks} complete input blocks and pads each + * result on the left with zeros to exactly {@code outChunkSize} bytes. + * + * @param in the input buffer + * @param inOff offset of the first complete block + * @param inChunks number of full blocks to process + * @param out destination buffer for padded output blocks + * @return number of bytes written to {@code out} + * @throws IOException if the cipher reports a processing error + */ + @Override + protected int transform(byte[] in, int inOff, int inChunks, byte[] out) throws IOException { + try { + // return cipher.doFinal(in, inOff, inChunks * g.inputBlockSize(), out); + int output = 0; + doFinalCalled = inChunks > 0; + for (int i = 0; i < inChunks; i++) { + int outOne = cipher.doFinal(in, inOff, inChunkSize, out, output); + int diff = outChunkSize - outOne; + if (diff > 0) { + System.arraycopy(out, output, out, output + diff, outOne); + Arrays.fill(out, output, output + diff, (byte) 0); + outOne = outChunkSize; + } + output = output + outOne; + inOff += inChunkSize; // NOPMD + } + return output; + } catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { + LOG.logp(Level.WARNING, "SmartBlockStream", "transform", "Exception", e); + throw new IOException(e); + } + } + + /** + * Finalization requires a complete final block; partial tails are not + * supported. + * + * @param in input buffer + * @param inOff offset of the final block + * @param len length of the final block; must equal {@code inChunkSize} + * @param out destination buffer for output bytes + * @param outOff offset in {@code out} where bytes are written + * @return number of bytes written to {@code out} + * @throws IllegalStateException if {@code len != inChunkSize} + * @throws IOException if the cipher reports a processing error + */ + @Override + protected int doFinal(byte[] in, int inOff, int len, byte[] out, int outOff) throws IOException { + if (doFinalCalled && len == 0) { + return 0; + } + + if (len != inChunkSize) { + throw new IllegalStateException("Cannot process incomplete blocks: " + len + " instead of " + inChunkSize); + } + try { + doFinalCalled = true; + return cipher.doFinal(in, inOff, len, out, outOff); + } catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { + LOG.logp(Level.WARNING, "SmartBlockStream", "transform", "Exception", e); + throw new IOException(e); + } + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/io/TailStrippingInputStream.java b/lib/src/main/java/zeroecho/core/io/TailStrippingInputStream.java new file mode 100644 index 0000000..b649bc2 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/TailStrippingInputStream.java @@ -0,0 +1,384 @@ +/******************************************************************************* + * 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.core.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.core.util.Strings; + +/** + * InputStream that withholds the last N bytes from the upstream stream and + * emits everything before that tail, delivering the withheld tail to a callback + * at EOF. + * + *

Overview

This stream is useful when the final bytes carry + * out-of-band metadata such as checksums, MAC tags, or trailers that must not + * appear in the payload. During normal reads it buffers enough data to ensure + * the last {@code tailLen} bytes are never exposed. When the upstream reaches + * EOF, the accumulated tail is passed to {@link #processTail(byte[])} exactly + * once. + * + *

How it works

+ *
    + *
  • A sliding window of size {@code tailLen + ioBuffer} is maintained.
  • + *
  • While reading, any overflow beyond {@code tailLen} is immediately emitted + * to the caller; the final {@code tailLen} bytes are withheld in the + * window.
  • + *
  • On EOF, the remaining bytes in the window (length {@code <= tailLen}) are + * delivered to {@code processTail(byte[])} and never returned from + * {@code read()}.
  • + *
+ * + *

Behavioral notes

+ *
    + *
  • If the entire stream is shorter than {@code tailLen}, no payload bytes + * are ever emitted; the complete content is provided to + * {@code processTail(byte[])}.
  • + *
  • {@link #close()} drains the upstream to ensure tail processing happens + * even if the caller stops reading early.
  • + *
  • Mark/reset is not supported.
  • + *
+ * + *

Typical use

{@code
+ * InputStream raw = ...; // e.g., file or network
+ * int tagBytes = 16;     // withhold a 16-byte authentication tag at the end
+ * TailStrippingInputStream in = new TailStrippingInputStream(raw, tagBytes, 4096) {
+ *     @Override
+ *     protected void processTail(byte[] tail) throws IOException {
+ *         // verify MAC, parse footer, etc.
+ *     }
+ * };
+ *
+ * // Read payload only (tag withheld and never exposed here)
+ * byte[] buf = in.readAllBytes();
+ * in.close(); // ensures processTail(...) is invoked if not already
+ * }
+ * + *

Thread-safety

Instances are not thread-safe. Use a separate instance + * per stream. + * + * @since 1.0 + */ +public abstract class TailStrippingInputStream extends InputStream { + private static final Logger LOG = Logger.getLogger(TailStrippingInputStream.class.getName()); + + private final InputStream upstream; + private final int tailLen; + private final byte[] window; // capacity = tailLen + ioBuf + private final byte[] oneByte = new byte[1]; + + // State + private int accLen; // = 0; // valid bytes in window (includes emit+tail) + private int emitPos; // = 0; // current emission index + private int emitLen; // = 0; // prepared bytes to emit (the overflow beyond tail) + private boolean needCompactAfterEmit; // = false; + private boolean upstreamEof; // = false; + private boolean tailProcessed; // = false; + private boolean closed; // = false; + + /** + * Creates a new tail-stripping stream around an upstream source. + * + *

+ * The internal sliding window is allocated with capacity + * {@code tailLen + max(1024, ioBuffer)}. The parameter {@code ioBuffer} + * controls incremental emission granularity; {@code tailLen} controls how many + * bytes are withheld until EOF. + *

+ * + * @param upstream non-null source stream + * @param tailLen number of bytes to strip and deliver to + * {@link #processTail(byte[])}; must be >= 1 + * @param ioBuffer additional working buffer size; must be >= 1 (actual + * allocation is {@code max(1024, ioBuffer)}) + * @throws NullPointerException if {@code upstream} is null + * @throws IllegalArgumentException if {@code tailLen < 1} or + * {@code ioBuffer < 1} + */ + public TailStrippingInputStream(InputStream upstream, int tailLen, int ioBuffer) { + super(); + + this.upstream = Objects.requireNonNull(upstream, "upstream"); + if (tailLen < 1) { // NOPMD + throw new IllegalArgumentException("tailLen must be >= 1"); + } + if (ioBuffer < 1) { // NOPMD + throw new IllegalArgumentException("ioBuffer must be >= 1"); + } + this.tailLen = tailLen; + this.window = new byte[tailLen + Math.max(1024, ioBuffer)]; + } + + /** + * Hook invoked exactly once at end of stream with the withheld tail bytes. + * + *

+ * The provided array contains the last {@code min(totalBytesRead, tailLen)} + * bytes of the upstream stream. For short streams, this may be smaller than + * {@code tailLen} and can even contain the entire stream contents. + * Implementations typically verify or parse the trailer here. + *

+ * + *

+ * This method is called from the reading thread either upon the first read that + * observes upstream EOF or during {@link #close()} if the stream was not fully + * consumed. It is never called more than once. + *

+ * + * @param tail the withheld tail bytes (never null; length in [0, tailLen]) + * @throws IOException if processing the tail fails + */ + protected abstract void processTail(byte[] tail) throws IOException; + + /** + * Reads and returns a single byte of payload data. + * + *

+ * This method never returns bytes belonging to the tail. On end of stream, + * {@code -1} is returned and {@link #processTail(byte[])} is invoked if not + * already processed. + *

+ * + * @return the next payload byte (0..255) or {@code -1} if no more payload is + * available + * @throws IOException if an I/O error occurs, if the stream is closed, or if + * tail processing fails + */ + @Override + public int read() throws IOException { + int n = read(oneByte, 0, 1); + return (n == -1) ? -1 : (oneByte[0] & 0xFF); + } + + /** + * Reads up to {@code len} bytes of payload into the provided buffer. + * + *

+ * The bytes returned exclude the last {@code tailLen} bytes of the upstream + * stream. When EOF is reached, this method returns {@code -1} and ensures + * {@link #processTail(byte[])} is invoked exactly once with the withheld tail. + * If the stream is shorter than {@code tailLen}, this method returns {@code -1} + * immediately once EOF is known and the entire content is reported via + * {@code processTail}. + *

+ * + * @param b destination buffer; must not be null + * @param off start offset in {@code b} + * @param len maximum number of bytes to read + * @return the number of payload bytes read, or {@code -1} if the payload is + * finished + * @throws NullPointerException if {@code b} is null + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are invalid + * for {@code b} + * @throws IOException if an I/O error occurs or if the stream is + * closed + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { // NOPMD + if (closed) { + throw new IOException("stream closed"); + } + Objects.requireNonNull(b, "b"); + Objects.checkFromIndexSize(off, len, b.length); + if (len == 0) { + return 0; + } + + // If we already have prepared output, serve it first. + if (emitPos < emitLen) { + int chunk = Math.min(len, emitLen - emitPos); + System.arraycopy(window, emitPos, b, off, chunk); + emitPos += chunk; + if (emitPos == emitLen) { + // Emission finished; now it's safe to compact tail to start. + if (needCompactAfterEmit) { + int remaining = accLen - emitLen; // this is the withheld tail (<= tailLen) + if (remaining > 0) { // NOPMD + System.arraycopy(window, emitLen, window, 0, remaining); + } + accLen = remaining; + emitPos = emitLen = 0; + needCompactAfterEmit = false; + } else { + emitPos = emitLen = 0; + } + } + return chunk; + } + + // Prepare more output if possible. + while (emitLen == 0) { + if (upstreamEof) { + if (!tailProcessed) { + // Whatever remains in window are the final tail bytes (<= tailLen). + byte[] tail = Arrays.copyOf(window, accLen); + + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "tail found {0}", Strings.toShortString(tail)); + } + tailProcessed = true; + processTail(tail); + } + return -1; + } + + int free = window.length - accLen; + int n = upstream.read(window, accLen, free); + if (n == -1) { + upstreamEof = true; + // loop again to process tail + continue; + } + accLen += n; + + // If we hold more than tailLen, the overflow is safe to emit. + if (accLen > tailLen) { + emitPos = 0; + emitLen = accLen - tailLen; // bytes to emit now from window[0..emitLen) + needCompactAfterEmit = true; // compact tail after emission completes + // Do NOT compact now—would overwrite the bytes we’re about to emit. + break; + } + // else: still not enough to emit (all could be tail); read more. + } + + // Emit what we prepared (if any). + if (emitLen > 0) { + int chunk = Math.min(len, emitLen - emitPos); + System.arraycopy(window, emitPos, b, off, chunk); + emitPos += chunk; + if (emitPos == emitLen) { + if (needCompactAfterEmit) { + int remaining = accLen - emitLen; + if (remaining > 0) { + System.arraycopy(window, emitLen, window, 0, remaining); + } + accLen = remaining; + emitPos = emitLen = 0; + needCompactAfterEmit = false; + } else { + emitPos = emitLen = 0; + } + } + return chunk; + } + + // Could not prepare any output (short stream < tailLen); read() must + // block/continue. + // Since we read at least once above, try again recursively to respect 'len' + // contract. + return read(b, off, len); + } + + /** + * Closes this stream and the underlying upstream stream. + * + *

+ * If the upstream has not yet reached EOF, this method drains it to force tail + * detection and ensures {@link #processTail(byte[])} is invoked exactly once. + * Subsequent calls are ignored. + *

+ * + * @throws IOException if an I/O error occurs while draining or closing + */ + @Override + public void close() throws IOException { + if (closed) { + return; + } + try { // NOPMD + if (!upstreamEof) { + transferTo(OutputStream.nullOutputStream()); + } + } finally { + closed = true; + upstream.close(); + } + } + + /** + * Returns an estimate of the number of bytes that can be read without blocking. + * + *

+ * This includes any previously prepared payload bytes and the upstream's + * reported availability. Note that upstream availability may over-report + * relative to payload because the last {@code tailLen} bytes are withheld. + *

+ * + * @return the number of bytes that can be read without blocking + * @throws IOException if an I/O error occurs in the upstream + */ + @Override + public int available() throws IOException { + int ready = emitLen - emitPos; + int up = upstream.available(); + return ready + up; + } + + /** + * Indicates that mark/reset is not supported. + * + * @return always {@code false} + */ + @Override + public boolean markSupported() { + return false; + } + + /** + * Unsupported operation; always throws. + * + * @throws IOException always thrown because mark/reset is not supported + */ + @Override + public void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } + + /** + * Returns the number of bytes stripped from the end of the stream. + * + * @return the configured tail length in bytes + */ + public int tailLength() { + return tailLen; + } +} diff --git a/lib/src/main/java/zeroecho/core/io/Util.java b/lib/src/main/java/zeroecho/core/io/Util.java new file mode 100644 index 0000000..4fc817e --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/Util.java @@ -0,0 +1,385 @@ +/******************************************************************************* + * 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.core.io; + +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** + * Utility class for performing efficient I/O operations with support for packed + * integers. This class provides static methods to write and read data with a + * compact variable-length encoding ("Pack7") and custom handling of UUIDs and + * UTF-8 strings. + *

+ * Design rationale: Using static methods allows developers to perform + * encoding and decoding operations directly on existing {@link InputStream} and + * {@link OutputStream} instances without the need for additional stream + * wrappers such as {@link FilterInputStream} or {@link FilterOutputStream}. + * This approach eliminates extra object creation and method call overhead, + * potentially improving performance, especially in high-throughput scenarios. + *

+ *

+ * Usage: These methods can be used independently with any I/O stream to + * achieve custom serialization formats, such as length-prefixed UTF-8 strings, + * packed integers, or custom binary structures. + *

+ */ +public final class Util { // NOPMD + /** + * The default buffer size (in bytes) used when reading from an + * {@link InputStream}. + *

+ * This value is chosen to balance throughput and memory efficiency during I/O + * operations. It is large enough to provide good performance for most + * workloads, while avoiding excessive memory allocation for small streams. + *

+ * + *

+ * The value is set to 32768 bytes (32 KB). + *

+ */ + private static final int DEFAULT_BUFFER_SIZE = 32 * 1024; + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Util() { + // this is a utility class + } + + /** + * Writes a byte array to the output stream with its length encoded as a packed + * 7-bit integer. + * + * @param out the output stream + * @param buf the byte array to write + * @throws IOException if an I/O error occurs + */ + public static void write(final OutputStream out, final byte[] buf) throws IOException { + writePack7I(out, buf.length); + out.write(buf); + } + + /** + * Writes a UUID to the output stream as two big-endian long values. + * + * @param out the output stream + * @param uuid the UUID to write + * @throws IOException if an I/O error occurs + */ + public static void write(final OutputStream out, final UUID uuid) throws IOException { + writeLong(out, uuid.getMostSignificantBits()); + writeLong(out, uuid.getLeastSignificantBits()); + } + + /** + * Writes a UTF-8 encoded string to the output stream with its length as a + * packed 7-bit integer. + * + * @param out the output stream + * @param str the string to write + * @throws IOException if an I/O error occurs + */ + public static void writeUTF8(final OutputStream out, final String str) throws IOException { + final byte[] buf = str.getBytes(StandardCharsets.UTF_8); + writePack7I(out, buf.length); + out.write(buf); + } + + /** + * Writes a long value to the output stream in big-endian order (8 bytes). + * + * @param out the output stream + * @param val the long value to write + * @throws IOException if an I/O error occurs + */ + public static void writeLong(final OutputStream out, long val) throws IOException { + final byte[] buf = new byte[8]; + for (int i = buf.length; --i >= 0;) { // NOPMD + buf[i] = (byte) (val & 0xff); + val = val >>> 8; // NOPMD + } + out.write(buf); + } + + /** + * Reads a UUID from the input stream, composed of two big-endian long values. + * + * @param in the input stream + * @return the UUID read from the stream + * @throws IOException if an I/O error occurs or if the stream ends prematurely + */ + public static UUID readUUID(final InputStream in) throws IOException { + final long msb = readLong(in); + final long lsb = readLong(in); + + return new UUID(msb, lsb); + } + + /** + * Reads a UTF-8 encoded string from the input stream. The string is prefixed + * with a packed 7-bit integer indicating its length. To prevent excessive + * allocation, the maximum allowable length must be specified. + * + * @param in the input stream + * @param maxLength the maximum allowable length of the string in bytes + * @return the string read from the stream + * @throws IOException if an I/O error occurs, the length exceeds maxLength, or + * if the stream ends prematurely + */ + public static String readUTF8(final InputStream in, final int maxLength) throws IOException { + final int len = readPack7I(in); + if (len > maxLength) { + throw new IOException("readUTF8 length " + len + " exceeds maximum allowed length " + maxLength); + } + final byte[] buf = new byte[len]; + if (in.readNBytes(buf, 0, buf.length) != buf.length) { + throw new EOFException("readUTF8 EOF"); + } + return new String(buf, StandardCharsets.UTF_8); + } + + /** + * Reads a long value from the input stream in big-endian order (8 bytes). + * + * @param in the input stream + * @return the long value read + * @throws IOException if an I/O error occurs or if the stream ends prematurely + */ + public static long readLong(final InputStream in) throws IOException { + final byte[] buff = new byte[8]; + if (in.readNBytes(buff, 0, buff.length) != buff.length) { + throw new EOFException("readLong EOF"); + } + + long result = 0; + for (final byte b : buff) { + result = (result << 8) | (b & 0xffL); + } + return result; + } + + /** + * Writes an integer to the output stream using packed 7-bit encoding (variable + * length). + * + * @param out the output stream + * @param val the integer value to write + * @throws IOException if an I/O error occurs + */ + public static void writePack7I(final OutputStream out, int val) throws IOException { + final byte[] buff = new byte[5]; + int idx = buff.length; + while ((val & ~0x7f) != 0) { + buff[--idx] = (byte) (val & 0x7f); + val = val >>> 7; // NOPMD + } + buff[--idx] = (byte) val; + buff[buff.length - 1] |= 0x80; + out.write(buff, idx, buff.length - idx); + } + + /** + * Writes a long value to the output stream using packed 7-bit encoding + * (variable length). + * + * @param out the output stream + * @param val the long value to write + * @throws IOException if an I/O error occurs + */ + public static void writePack7L(final OutputStream out, long val) throws IOException { + final byte[] buff = new byte[10]; + int idx = buff.length; + while ((val & ~0x7fL) != 0) { + buff[--idx] = (byte) (val & 0x7f); + val = val >>> 7; // NOPMD + } + buff[--idx] = (byte) val; + buff[buff.length - 1] |= 0x80; + out.write(buff, idx, buff.length - idx); + } + + /** + * Reads a byte array from the input stream. The length of the array is + * specified as a packed 7-bit integer prefix. To prevent excessive allocation, + * the maximum allowable length must be specified. + * + * @param in the input stream + * @param maxLength the maximum allowable length of the byte array + * @return the byte array read + * @throws IOException if an I/O error occurs, the length exceeds maxLength, or + * if the stream ends prematurely + */ + public static byte[] read(final InputStream in, final int maxLength) throws IOException { + final int len = readPack7I(in); + if (len > maxLength || len < 0) { + throw new IOException("read length " + len + " exceeds maximum allowed length " + maxLength); + } + final byte[] result = new byte[len]; + if (in.readNBytes(result, 0, result.length) != result.length) { + throw new EOFException("read EOF"); + } + return result; + } + + /** + * Reads an integer from the input stream using packed 7-bit encoding (variable + * length). + * + * @param in the input stream + * @return the integer value read + * @throws IOException if an I/O error occurs or if the stream ends prematurely + */ + public static int readPack7I(final InputStream in) throws IOException { + int result = in.read(); + if (result > 0x7f) { // NOPMD + return result & 0x7f; + } + int i; + for (i = in.read(); i < 0x80; i = in.read()) { + result = (result << 7) | i; + } + return (result << 7) | (i & 0x7f); + } + + /** + * Reads a long value from the input stream using packed 7-bit encoding + * (variable length). + * + * @param in the input stream + * @return the long value read + * @throws IOException if an I/O error occurs or if the stream ends prematurely + */ + public static long readPack7L(final InputStream in) throws IOException { + long result = in.read(); + if (result > 0x7f) { // NOPMD + return result & 0x7fL; + } + int i; + for (i = in.read(); i < 0x80; i = in.read()) { + result = (result << 7) | i; + } + return (result << 7) | (i & 0x7f); + } + + /** + * Reads up to {@code maxSize} bytes from the given {@link InputStream}, encodes + * the total number of bytes using 7-bit variable-length encoding (similar to + * {@link #writePack7L}), and returns a single byte array containing the encoded + * length prefix followed by the actual data read. + * + *

+ * This method is optimized for performance and memory usage. Data is read in + * chunks to avoid large intermediate allocations, and a single result array is + * created only after the full size is known. + * + *

+ * The size prefix uses a packed 7-bit encoding, where the most significant bit + * of the last byte is set to 1. This format is useful for compactly + * representing lengths in binary protocols. + * + * @param in the {@link InputStream} to read data from; must not be + * {@code null} + * @param maxSize the maximum number of bytes allowed to be read from the + * stream; must be non-negative and less than or equal to + * {@code Integer.MAX_VALUE - 10} + * @return a {@code byte[]} array where the first bytes are the encoded length, + * followed by the data read + * @throws IOException if an I/O error occurs or if the data read + * exceeds {@code maxSize} + * @throws IllegalArgumentException if {@code maxSize} is negative or too large + */ + public static byte[] readWithPackedLengthPrefix(InputStream in, int maxSize) throws IOException { + // complexity + if (maxSize < 0 || maxSize > Integer.MAX_VALUE - 10) { + throw new IllegalArgumentException("Invalid maxSize: " + maxSize); + } + + List bufs = new ArrayList<>(); + int total = 0; + + byte[] buf; + int n; + while ((n = in.read(buf = new byte[DEFAULT_BUFFER_SIZE])) > 0) { // NOPMD + if (n < buf.length) { + buf = Arrays.copyOf(buf, n); + } + + if (total > maxSize - n) { + throw new IOException("Input exceeds maximum allowed size: " + maxSize); + } + + bufs.add(buf); + total += n; + } + + // Encode total size using writePack7L logic + byte[] prefixBuf = new byte[10]; + long len = total; + int idx = prefixBuf.length; + while ((len & ~0x7fL) != 0) { + prefixBuf[--idx] = (byte) (len & 0x7f); + len >>>= 7; + } + prefixBuf[--idx] = (byte) len; + prefixBuf[prefixBuf.length - 1] |= 0x80; + int prefixLen = prefixBuf.length - idx; + + // Allocate result array + int finalSize = prefixLen + total; + byte[] result = new byte[finalSize]; + + // Copy prefix + System.arraycopy(prefixBuf, idx, result, 0, prefixLen); + + // Copy content + int offset = prefixLen; + for (byte[] b : bufs) { + System.arraycopy(b, 0, result, offset, b.length); + offset += b.length; + } + + return result; + } +} diff --git a/lib/src/main/java/zeroecho/core/io/package-info.java b/lib/src/main/java/zeroecho/core/io/package-info.java new file mode 100644 index 0000000..5477c5a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/io/package-info.java @@ -0,0 +1,117 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * I/O helpers for block-based transforms, passthrough processing, and tail + * handling. + * + *

+ * This package provides building blocks for cryptographic I/O: + * chunk-transforming streams that call into a cipher, passthrough streams that + * compute or append trailers, a builder for configuring cipher-backed streams, + * a tail-stripping stream, and small I/O utilities. Components are designed to + * be allocation-conscious and to make finalization output (padding blocks, AEAD + * tags) explicit and predictable. + *

+ * + *

Key elements

+ *
    + *
  • {@link AbstractChunkTransformInputStream} - base class that reads + * upstream data in aligned chunks and lets subclasses implement + * {@code transform(...)} and {@code doFinal(...)}.
  • + *
  • {@link AbstractPassthroughInputStream} - forwards body bytes unchanged, + * invokes {@code update(...)} on each chunk, optionally emits a single trailer, + * then calls {@code onCompleted()} exactly once at EOF.
  • + *
  • {@link CipherTransformInputStreamBuilder} - fluent builder that creates + * cipher-backed streams for block-per-doFinal, left-zero-padded blocks, or + * continuous {@code update}+{@code doFinal} streaming.
  • + *
  • {@link SmartBlockStream}, {@link SmartPaddedBlockStream}, + * {@link SmartContinuousBlockStream} - concrete cipher-backed stream variants + * used by the builder.
  • + *
  • {@link TailStrippingInputStream} - withholds the last N bytes from the + * payload and delivers them to a callback at EOF (useful for tags, checksums, + * or footers).
  • + *
  • {@link Util} - static helpers for compact length-prefix I/O, UTF-8, UUID, + * and packed 7-bit integers.
  • + *
+ * + *

Typical usage

+ *

Build a streaming AES/GCM transform

{@code
+ * javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES/GCM/NOPADDING");
+ * javax.crypto.spec.GCMParameterSpec gcm = new javax.crypto.spec.GCMParameterSpec(128, ivBytes);
+ * cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, aesKey, gcm);
+ *
+ * java.io.InputStream s = zeroecho.core.io.CipherTransformInputStreamBuilder.builder()
+ *     .withUpstream(upstream)
+ *     .withCipher(cipher)
+ *     .withUpdateStreaming()          // use update(...) for bulk, doFinal() at EOF
+ *     .withInputBlockSize(16)         // logical chunking for I/O
+ *     .withOutputBlockSize(16)
+ *     .withBufferedBlocks(200)        // steady-state buffering
+ *     .withFinalizationOutputChunks(2) // room for tail bytes + 16-byte tag
+ *     .build();
+ *
+ * // Pipe transformed bytes
+ * s.transferTo(out);
+ * s.close();
+ * }
+ * + *

Strip a fixed trailer (for example, 16-byte tag) from the end of a + * stream

{@code
+ * int tagLen = 16;
+ * zeroecho.core.io.TailStrippingInputStream in =
+ *     new zeroecho.core.io.TailStrippingInputStream(raw, tagLen, 4096) {
+ *         @Override
+ *         protected void processTail(byte[] tail) throws java.io.IOException {
+ *             // verify MAC, parse footer, etc. (do not write to 'out' here)
+ *         }
+ *     };
+ *
+ * // Read payload only; the last 'tagLen' bytes are withheld and delivered to processTail(...)
+ * byte[] payload = in.readAllBytes();
+ * in.close(); // ensures processTail(...) runs even if not fully consumed
+ * }
+ * + *

Design notes

+ *
    + *
  • Streams are not thread-safe; use one instance per pipeline.
  • + *
  • Finalization headroom is explicit: reserve extra output chunks when a + * mode emits padding or tags at EOF.
  • + *
  • {@code close()} implementations drain upstream as needed to guarantee + * trailer emission and completion hooks.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.io; diff --git a/lib/src/main/java/zeroecho/core/marshal/Codec.java b/lib/src/main/java/zeroecho/core/marshal/Codec.java new file mode 100644 index 0000000..9293ec1 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/marshal/Codec.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * 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.core.marshal; + +/** + * General-purpose bidirectional codec between a domain type {@code T} and a + * serialized representation {@code R}. + * + *

+ * A {@code Codec} provides two complementary operations: + *

+ *
    + *
  • {@link #marshal(Object)} encodes a domain object into its representation + * (e.g., object → string, object → byte array).
  • + *
  • {@link #unmarshal(Object)} decodes a representation back into the domain + * object.
  • + *
+ * + *

Design notes

+ *
    + *
  • The type parameters {@code T} and {@code R} are application-defined; for + * example, {@code Codec} or {@code Codec}.
  • + *
  • Codecs are expected to be symmetric: unmarshalling the result of + * marshalling yields an equivalent domain object, subject to lossiness in the + * chosen representation.
  • + *
  • Implementations should document whether {@code null} values are + * permitted, and whether marshalling is deterministic.
  • + *
+ * + *

Example

{@code
+ * Codec intToString = new Codec<>() {
+ *     public String marshal(Integer value) { return value.toString(); }
+ *     public Integer unmarshal(String repr) { return Integer.parseInt(repr); }
+ * };
+ *
+ * String repr = intToString.marshal(42);   // "42"
+ * int val = intToString.unmarshal("42");   // 42
+ * }
+ * + * @param domain type to encode and decode + * @param representation type (serialized form) + */ +public interface Codec { + /** + * Converts a domain value into its serialized representation. + * + * @param value domain object to encode + * @return representation corresponding to {@code value} + * @throws IllegalArgumentException if {@code value} is invalid or cannot be + * encoded + */ + R marshal(T value); + + /** + * Converts a serialized representation back into the domain object. + * + * @param repr representation to decode + * @return domain object reconstructed from {@code repr} + * @throws IllegalArgumentException if {@code repr} is malformed or cannot be + * decoded + */ + T unmarshal(R repr); +} diff --git a/lib/src/main/java/zeroecho/core/marshal/PairSeq.java b/lib/src/main/java/zeroecho/core/marshal/PairSeq.java new file mode 100644 index 0000000..1396888 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/marshal/PairSeq.java @@ -0,0 +1,250 @@ +/******************************************************************************* + * 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.core.marshal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Immutable sequence of string key-value pairs with cursor-based iteration and + * simple text serialization. + * + *

+ * A {@code PairSeq} stores pairs in a flat {@code String[]} array + * representation: {@code [k0, v0, k1, v1, ...]}. It is designed for compact + * transport of structured metadata where keys and values are UTF-8–safe strings + * (binary data should be Base64 encoded before storing). + *

+ * + *

Usage

{@code
+ * PairSeq seq = PairSeq.of("type", "SntruPrimePublicKeySpec",
+ *                          "x509.b64", "BASE64ENCODED");
+ *
+ * for (PairSeq.Cursor c = seq.cursor(); c.next();) {
+ *     System.out.println(c.key() + " = " + c.value());
+ * }
+ * }
+ * + *

Serialization

+ *
    + *
  • {@link #writeTo(Appendable)} outputs each pair as {@code k=v\n} lines + * without escaping.
  • + *
  • {@link #readFrom(java.io.Reader)} parses lines in the same format, + * ignoring blank lines and comments starting with {@code #}.
  • + *
+ * + *

Thread-safety

Instances are immutable and safe to share across + * threads. The {@link Cursor} is not thread-safe and should be confined to a + * single thread. + * + * @since 1.0 + */ +public final class PairSeq { + private final String[] kv; // [k0,v0,k1,v1,...] + + private PairSeq(String... kv) { + this.kv = kv; + } + + /** + * Creates a new sequence from an even-length list of keys and values. + * + * @param kv alternating key and value strings; must have even length + * @return new {@code PairSeq} with the given contents + * @throws IllegalArgumentException if {@code kv} is {@code null} or has odd + * length + */ + public static PairSeq of(String... kv) { + if (kv == null) { + throw new IllegalArgumentException("kv must not be null"); + } + if ((kv.length & 1) != 0) { + throw new IllegalArgumentException("kv must have even length (k,v pairs)"); + } + return new PairSeq(kv); + } + + /** + * Returns the number of key-value pairs in this sequence. + * + * @return pair count + */ + public int size() { + return kv.length >>> 1; + } + + /** + * Returns the key at the given index. + * + * @param i index of the pair (0-based) + * @return key string + * @throws ArrayIndexOutOfBoundsException if {@code i} is out of range + */ + public String keyAt(int i) { + return kv[i << 1]; + } + + /** + * Returns the value at the given index. + * + * @param i index of the pair (0-based) + * @return value string + * @throws ArrayIndexOutOfBoundsException if {@code i} is out of range + */ + public String valAt(int i) { + return kv[(i << 1) + 1]; + } + + /** + * Returns a forward-only cursor for iterating the pairs. + * + * @return new cursor over this sequence + */ + public Cursor cursor() { + return new Cursor(); + } + + /** + * Forward-only iterator over the pairs of a {@link PairSeq}. + * + *

+ * The cursor maintains a current index; call {@link #next()} to advance, then + * {@link #key()} and {@link #value()} to access elements. + *

+ */ + public final class Cursor { + private int i = -1; + private final int n = size(); + + /** + * Advances to the next pair if available. + * + * @return {@code true} if advanced to a valid pair, {@code false} if end + * reached + */ + public boolean next() { + int next = i + 1; + if (next < n) { + i = next; + return true; + } + return false; + } + + /** + * Returns the current key. + * + * @return key string + * @throws IndexOutOfBoundsException if not positioned on a valid pair + */ + public String key() { + if (i < 0 || i >= n) { + throw new IndexOutOfBoundsException(); + } + return keyAt(i); + } + + /** + * Returns the current value. + * + * @return value string + * @throws IndexOutOfBoundsException if not positioned on a valid pair + */ + public String value() { + if (i < 0 || i >= n) { + throw new IndexOutOfBoundsException(); + } + return valAt(i); + } + } + + /** + * Appends all pairs to the target as {@code key=value} lines. + * + *

+ * No escaping is performed; callers must ensure keys and values do not contain + * newlines. Binary data should be stored as Base64. + *

+ * + * @param out appendable target + * @throws UncheckedIOException if the append fails + */ + public void writeTo(Appendable out) { + try { + for (int i = 0; i < size(); i++) { + out.append(keyAt(i)).append('=').append(valAt(i)).append('\n'); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Reads a {@code PairSeq} from a line-based text format. + * + *

+ * Each non-empty, non-comment line must have the form {@code key=value}. Lines + * without an equals sign or with empty keys are skipped. Comments start with + * {@code #}. + *

+ * + * @param reader character source + * @return parsed {@code PairSeq} + * @throws IOException if reading fails + */ + public static PairSeq readFrom(Reader reader) throws IOException { + BufferedReader br = (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader); + List list = new ArrayList<>(); + String line; + while ((line = br.readLine()) != null) { + if (line.isEmpty() || line.charAt(0) == '#') { + continue; + } + int eq = line.indexOf('='); + if (eq <= 0) { + continue; // skip malformed/section markers handled elsewhere + } + String k = line.substring(0, eq); + String v = line.substring(eq + 1); + list.add(k); + list.add(v); + } + return new PairSeq(list.toArray(String[]::new)); + } +} diff --git a/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java b/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java new file mode 100644 index 0000000..b1ba136 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/marshal/PairSeqCodec.java @@ -0,0 +1,205 @@ +/******************************************************************************* + * 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.core.marshal; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Objects; + +/** + * Reflection-based {@link Codec} that marshals and unmarshals domain objects to + * and from {@link PairSeq}. + * + *

+ * A {@code PairSeqCodec} adapts any domain type {@code T} to the + * {@link PairSeq} representation by convention, using reflection. The domain + * type must implement one or both of the following: + *

+ *
    + *
  • Instance method {@code marshal()} returning {@code PairSeq}.
  • + *
  • Either a public static factory {@code unmarshal(PairSeq)} or a public + * constructor {@code T(PairSeq)}.
  • + *
+ * + *

Conventions

+ *
    + *
  • Marshalling: calls {@code value.marshal()} and requires its return + * type to be {@code PairSeq}.
  • + *
  • Unmarshalling: prefers a public static {@code unmarshal(PairSeq)} + * on {@code T}; if absent, falls back to a public constructor + * {@code T(PairSeq)}.
  • + *
  • Failures in lookup or invocation are reported as + * {@link IllegalStateException} with the cause attached.
  • + *
+ * + *

Example

{@code
+ * public final class User {
+ *     private final String id;
+ *     private final String name;
+ *
+ *     public User(String id, String name) {
+ *         this.id = id; this.name = name;
+ *     }
+ *
+ *     // Required by PairSeqCodec for marshalling:
+ *     public PairSeq marshal() {
+ *         return PairSeq.of("id", id, "name", name);
+ *     }
+ *
+ *     // Preferred unmarshal entrypoint:
+ *     public static User unmarshal(PairSeq ps) {
+ *         PairSeq.Cursor c = ps.cursor();
+ *         String id = null, name = null;
+ *         while (c.next()) {
+ *             if ("id".equals(c.key())) id = c.value();
+ *             else if ("name".equals(c.key())) name = c.value();
+ *         }
+ *         return new User(id, name);
+ *     }
+ * }
+ *
+ * PairSeqCodec codec = new PairSeqCodec<>(User.class);
+ * PairSeq repr = codec.marshal(new User("u-1", "Alice"));
+ * User u = codec.unmarshal(repr);
+ * }
+ * + *

Thread-safety

Instances are immutable and thread-safe. Reflection + * lookups are performed per call and are not cached. + * + * @param domain type that follows the marshalling and unmarshalling + * conventions + * @since 1.0 + */ +public final class PairSeqCodec implements Codec { + private final Class type; + + /** + * Creates a codec bound to a specific domain type. + * + * @param type domain class that provides {@code marshal()} and either + * {@code static unmarshal(PairSeq)} or {@code T(PairSeq)} + * @throws NullPointerException if {@code type} is {@code null} + */ + public PairSeqCodec(Class type) { + this.type = Objects.requireNonNull(type, "type"); + } + + /** + * Encodes a domain object to {@link PairSeq} by invoking its {@code marshal()} + * method. + * + *

+ * The domain object's {@code marshal()} method must be public (or otherwise + * accessible) and return {@code PairSeq}. Any exception thrown by the target + * method is wrapped in {@link IllegalStateException}. + *

+ * + * @param value domain object to encode + * @return the {@code PairSeq} produced by {@code value.marshal()} + * @throws NullPointerException if {@code value} is {@code null} + * @throws IllegalStateException if no {@code marshal()} method exists, if it + * does not return {@code PairSeq}, or if the + * invocation fails + */ + @Override + public PairSeq marshal(T value) { + Objects.requireNonNull(value, "value"); + try { + Method m = value.getClass().getMethod("marshal"); + if (!PairSeq.class.isAssignableFrom(m.getReturnType())) { + throw new IllegalStateException("marshal() must return PairSeq in " + value.getClass().getName()); + } + return (PairSeq) m.invoke(value); + } catch (NoSuchMethodException e) { + throw new IllegalStateException(value.getClass().getName() + " must implement marshal():PairSeq", e); + } catch (IllegalAccessException | InvocationTargetException t) { + throw new IllegalStateException("marshal() failed for " + value.getClass().getName(), t); + } + } + + /** + * Decodes a {@link PairSeq} back into a domain object by convention. + * + *

+ * Resolution order: + *

+ *
    + *
  1. Invoke a public static {@code unmarshal(PairSeq)} on {@code type}, if + * present.
  2. + *
  3. Otherwise, invoke a public constructor {@code type(PairSeq)}.
  4. + *
+ * + *

+ * If neither entrypoint is available, or invocation fails, an + * {@link IllegalStateException} is thrown with the original cause attached for + * diagnostics. + *

+ * + * @param repr serialized representation to decode + * @return reconstructed domain object + * @throws NullPointerException if {@code repr} is {@code null} + * @throws IllegalStateException if neither a suitable static factory nor + * constructor exists, or if either invocation + * fails + */ + @SuppressWarnings("unchecked") + @Override + public T unmarshal(PairSeq repr) { + Objects.requireNonNull(repr, "repr"); + // Prefer static unmarshal(PairSeq) + try { + Method m = type.getMethod("unmarshal", PairSeq.class); + if ((m.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0) { + return (T) m.invoke(null, repr); + } + } catch (NoSuchMethodException ignore) { // NOPMD + // fall through + } catch (IllegalAccessException | InvocationTargetException t) { + throw new IllegalStateException("static unmarshal(PairSeq) failed for " + type.getName(), t); + } + // Or constructor T(PairSeq) + try { + Constructor c = type.getConstructor(PairSeq.class); + return c.newInstance(repr); + } catch (NoSuchMethodException e) { + throw new IllegalStateException(type.getName() + " must provide static unmarshal(PairSeq) or ctor(PairSeq)", + e); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException + | InstantiationException t) { + throw new IllegalStateException("ctor(PairSeq) failed for " + type.getName(), t); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/marshal/package-info.java b/lib/src/main/java/zeroecho/core/marshal/package-info.java new file mode 100644 index 0000000..755bcbb --- /dev/null +++ b/lib/src/main/java/zeroecho/core/marshal/package-info.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Marshalling utilities and conventions for compact, human-readable + * representations. + * + *

+ * This package defines a small SPI for bidirectional codecs and a lightweight + * text representation for structured metadata. It enables algorithms and + * specifications to serialize themselves into a simple key-value sequence and + * reconstruct objects from that form. + *

+ * + *

Key elements

+ *
    + *
  • {@link Codec} - a bidirectional contract between a domain type and a + * serialized representation.
  • + *
  • {@link PairSeq} - an immutable sequence of {@code String} key-value + * pairs, optimized for compact transport.
  • + *
  • {@link PairSeqCodec} - a reflection-based codec that adapts a domain type + * to {@code PairSeq} by convention.
  • + *
+ * + *

Conventions

+ *
    + *
  • Domain types that use {@link PairSeqCodec} should provide + * {@code marshal(): PairSeq} and either {@code static unmarshal(PairSeq)} or a + * public constructor {@code T(PairSeq)}.
  • + *
  • {@link PairSeq} text serialization uses one {@code key=value} pair per + * line with optional comment lines starting with {@code #}. Binary values + * should be Base64-encoded.
  • + *
+ * + *

Typical usage

{@code
+ * // A small domain type that follows the PairSeq convention.
+ * public record User(String id, String name) {
+ *     public PairSeq marshal() {
+ *         return PairSeq.of("id", id, "name", name);
+ *     }
+ *     public static User unmarshal(PairSeq ps) {
+ *         String id = null, name = null;
+ *         for (PairSeq.Cursor c = ps.cursor(); c.next();) {
+ *             if ("id".equals(c.key())) id = c.value();
+ *             else if ("name".equals(c.key())) name = c.value();
+ *         }
+ *         return new User(id, name);
+ *     }
+ * }
+ *
+ * // Use a reflection-based codec for the domain type.
+ * PairSeqCodec codec = new PairSeqCodec<>(User.class);
+ * PairSeq repr = codec.marshal(new User("u-1", "Alice"));
+ * User copy = codec.unmarshal(repr);
+ * }
+ * + * @since 1.0 + */ +package zeroecho.core.marshal; diff --git a/lib/src/main/java/zeroecho/core/package-info.java b/lib/src/main/java/zeroecho/core/package-info.java new file mode 100644 index 0000000..a532b97 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/package-info.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Core cryptography engine and registry for ZeroEcho. + * + *

+ * This package defines the low-level algorithm model, discovery and registry + * surface, capability descriptors, key usage taxonomy, and a small set of + * helpers required by concrete cryptographic algorithms. It is intentionally + * minimal and policy-agnostic so that higher layers can compose flows without + * duplicating concerns defined here. + *

+ * + *

Relationship to the overall library

+ *

+ * The core provides the foundation on which the higher-level SDK composes + * developer-facing workflows. The intent is clear separation of + * responsibilities: the core focuses on algorithms and correctness, while the + * SDK focuses on composition and ergonomics. + *

+ * + *

Algorithm model

+ *

+ * Algorithms are declared by implementing {@link CryptoAlgorithm} and exposing + * canonical identifiers, human-readable names, provider names, priorities, role + * bindings, capabilities, and key builders. Roles are enumerated by + * {@link KeyUsage} and may be grouped by {@link AlgorithmFamily}. Each role + * binding specifies the context to be created for that role, the accepted key + * type, an optional specification object, and a factory used to create the + * context instance. Capabilities are advertised through + * {@link CryptoAlgorithm#listCapabilities()} and described by immutable + * {@link Capability} descriptors. + *

+ * + *

Discovery and registry

+ *

+ * Algorithms are discovered through standard Java provider mechanisms and + * indexed by canonical identifier. The facade {@link CryptoAlgorithms} offers: + *

+ *
    + *
  • Lookup of algorithms by identifier and family.
  • + *
  • Creation of role-specific contexts using supplied keys and optional + * specifications.
  • + *
  • Enumeration utilities for tooling and documentation.
  • + *
+ * + *

Selection and catalogs

+ *

+ * Utility types such as {@link CatalogSelector} assist with filtering available + * algorithms by family and required roles. For documentation, validation, or + * export, {@link CryptoCatalog} can build a snapshot of the registry and + * serialize metadata for external consumers. + *

+ * + *

Transient parameters and sentinels

+ *

+ * Algorithms that exchange ephemeral parameters at runtime can use typed keys + * from {@link ConfluxKeys}. When an API requires a key reference but no secret + * exists, {@link NullKey#INSTANCE} can be supplied as a sentinel. Symmetric + * algorithms that need to prepend or parse a compact header for IVs, tag sizes, + * or other runtime fields may implement the optional SPI + * {@link SymmetricHeaderCodec}. + *

+ * + *

Design principles

+ *
    + *
  • Stratification: the core concentrates on algorithms and correctness; + * higher layers provide composition.
  • + *
  • Composability: predictable, chainable behavior across contexts and + * builders.
  • + *
  • Extensibility: new algorithms, formats, and flows can be added with + * minimal impact.
  • + *
  • Safety: explicit roles and clear intent around key usage and algorithm + * capabilities.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core; diff --git a/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java b/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java new file mode 100644 index 0000000..8aba05a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/policy/CryptoPolicy.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * 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.core.policy; + +import java.security.Key; + +import zeroecho.core.KeyUsage; +import zeroecho.core.spec.ContextSpec; + +/** + * A strategy interface for validating cryptographic keys, usages, and + * specifications before contexts are created. + * + *

+ * A {@code CryptoPolicy} is consulted by higher layers (such as + * {@link zeroecho.core.CryptoAlgorithms}) to enforce organizational or + * security-specific rules. Policies may range from permissive (accept + * everything) to strict (require minimum key sizes, algorithm whitelisting, + * usage separation, etc.). + *

+ * + *

Design goals

+ *
    + *
  • Allow central enforcement of key strength and usage constraints.
  • + *
  • Support extensibility without hardcoding checks into algorithms.
  • + *
  • Enable application-specific security postures while sharing the same + * underlying primitives.
  • + *
+ * + *

Typical usage

{@code
+ * // Install a global minimum-strength policy
+ * CryptoAlgorithms.setPolicy(CryptoPolicy.minStrength(128));
+ *
+ * // Later, when creating a context:
+ * EncryptionContext ctx = CryptoAlgorithms.create("AES/GCM", KeyUsage.ENCRYPT, secretKey);
+ * }
+ * + * @param context specification type + * @param key type + * @since 1.0 + */ +public interface CryptoPolicy { // NOPMD + /** + * Validates an algorithm identifier, role, key, and spec. + * + *

+ * Implementations should throw an unchecked exception (typically + * {@link IllegalArgumentException}) if the combination violates policy. + *

+ * + * @param id canonical algorithm identifier + * @param role intended key usage role + * @param key cryptographic key (may be {@code NullKey} for unkeyed flows) + * @param spec optional context specification; may be {@code null} + * @throws IllegalArgumentException if the parameters are not permitted by the + * policy + */ + void validate(String id, KeyUsage role, K key, S spec); + + /** + * Returns a permissive policy that accepts all keys, roles, and specs without + * validation. + * + *

+ * This is the default when no explicit policy is configured. + *

+ * + * @param context specification type + * @param key type + * @return a permissive {@code CryptoPolicy} + */ + static CryptoPolicy permissive() { + return (id, role, key, spec) -> { + }; + } + + /** + * Returns a policy that enforces a minimum estimated key strength. + * + *

+ * Key strength is estimated in bits using + * {@link SecurityStrengthAdvisor#estimateBits(String, Key)}. If the strength is + * below {@code minBits}, validation fails with + * {@link IllegalArgumentException}. + *

+ * + * @param minBits minimum acceptable estimated key strength, in bits + * @param context specification type + * @param key type + * @return a {@code CryptoPolicy} that enforces minimum key strength + * @throws IllegalArgumentException if {@code minBits} is negative + */ + static CryptoPolicy minStrength(int minBits) { + return (id, role, key, spec) -> { + int est = SecurityStrengthAdvisor.estimateBits(id, key); + if (est < minBits) { + throw new IllegalArgumentException( + "Algorithm " + id + " key strength " + est + " bits is below minimum " + minBits + " bits"); + } + }; + } +} diff --git a/lib/src/main/java/zeroecho/core/policy/SecurityStrengthAdvisor.java b/lib/src/main/java/zeroecho/core/policy/SecurityStrengthAdvisor.java new file mode 100644 index 0000000..dcf9aed --- /dev/null +++ b/lib/src/main/java/zeroecho/core/policy/SecurityStrengthAdvisor.java @@ -0,0 +1,419 @@ +/******************************************************************************* + * 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.core.policy; + +import java.security.Key; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.crypto.SecretKey; + +/** + * Estimates conservative security strength in bits for a given algorithm + * identifier and key. + * + *

+ * {@code SecurityStrengthAdvisor} normalizes an algorithm family name (for + * example, "RSA", "AES", "HMAC", "ML-KEM", "SPHINCS+") and inspects the + * provided key to infer an approximate strength in bits. The mappings are + * deliberately coarse and favor safe lower bounds over optimistic assumptions. + *

+ * + *

How estimation works

+ *
    + *
  • RSA / ElGamal (FF-DL): uses the modulus size to map to NIST-like + * strengths (for example, 2048 -> ~112, 3072 -> ~128, 7680 -> ~192, 15360 -> + * ~256).
  • + *
  • AES: uses the secret key size (128/192/256) when available; + * otherwise assumes 128.
  • + *
  • HMAC: strength is {@code min(key bits, hash output bits)}; + * attempts to parse the underlying SHA variant from {@code key.getAlgorithm()} + * (for example, HmacSHA256 -> 256 output bits). If unknown or opaque, caps at a + * conservative 256.
  • + *
  • Post-quantum KEMs (ML-KEM/Kyber, FrodoKEM, Saber, BIKE, HQC, CMCE, + * NTRU families): tries to extract parameters either from numeric hints in + * {@code key.getAlgorithm()} (for example, 512, 768, 1024 for ML-KEM; + * 640/976/1344 for FrodoKEM) or from NIST security levels labeled as L1/L3/L5. + * If neither is present, defaults to 128.
  • + *
  • SPHINCS+: parses the parameter size 128/192/256 from the algorithm + * string; otherwise defaults to 128.
  • + *
  • EdDSA: returns fixed strengths (Ed25519 -> 128, Ed448 -> + * 224).
  • + *
  • Digest-only and unknown families: returns a conservative 128.
  • + *
+ * + *

Inputs and parsing

+ *
    + *
  • The {@code algoId} argument selects the family mapping. It should be a + * stable id such as "RSA", "AES", "HMAC", "ML-KEM", "SPHINCS+".
  • + *
  • When applicable, {@code key.getAlgorithm()} is scanned for numeric tokens + * or level markers like "L1", "L3", "L5" to refine the estimate.
  • + *
  • Opaque keys or provider-specific names may limit visibility; in such + * cases, the result intentionally errs on the conservative side.
  • + *
+ * + *

Security notes

+ *
    + *
  • These values are heuristic, implementation-agnostic estimates for policy + * gating and coarse comparisons. They are not a substitute for a full + * cryptographic review.
  • + *
  • When in doubt, the advisor returns at least 128 bits to avoid accidental + * underestimation in modern deployments.
  • + *
+ * + *

Examples

{@code
+ * // AES-256 secret key
+ * int s1 = SecurityStrengthAdvisor.estimateBits("AES", secretKey256);   // -> 256
+ *
+ * // RSA 3072-bit public key
+ * int s2 = SecurityStrengthAdvisor.estimateBits("RSA", rsa3072Pub);     // -> 128
+ *
+ * // ML-KEM-768 (Kyber768) as indicated in key algorithm string
+ * int s3 = SecurityStrengthAdvisor.estimateBits("ML-KEM", kemKey768);   // -> 192
+ *
+ * // HmacSHA256 with a 160-bit key (not recommended)
+ * int s4 = SecurityStrengthAdvisor.estimateBits("HMAC", shortHmacKey);  // -> min(160, 256) = 160
+ * }
+ * + *

Thread-safety

This utility is stateless and thread-safe. + * + * @since 1.0 + */ +public final class SecurityStrengthAdvisor { // NOPMD + + private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d{2,5})"); + private static final Pattern LEVEL_PATTERN = Pattern.compile("(?:^|[^A-Za-z])(L[135])(?:[^A-Za-z]|$)", + Pattern.CASE_INSENSITIVE); + private static final Pattern SPHINCS_STRENGTH_PATTERN = Pattern.compile("(128|192|256)"); + private static final Pattern HMAC_SHA_PATTERN = Pattern.compile("HMAC(?:-)?SHA(?:-)?(1|224|256|384|512)", + Pattern.CASE_INSENSITIVE); + + private SecurityStrengthAdvisor() { + } + + /** + * Estimates the security strength in bits for a given algorithm family id and + * key. + * + *

+ * The method first normalizes {@code algoId} to select a family-specific + * mapping. It then inspects the {@code key} (for example, modulus bit length + * for RSA; encoded length for AES {@code SecretKey}; hash output size inferred + * from HMAC algorithm name; parameter levels for post-quantum schemes) to + * compute a conservative estimate. + *

+ * + * @param algoId algorithm family identifier such as "RSA", "AES", "HMAC", + * "ML-KEM", "SPHINCS+" + * @param key key instance to inspect; may be opaque for some providers + * @return estimated security strength in bits; never negative + */ + public static int estimateBits(String algoId, Key key) { // NOPMD + final String id = (algoId == null ? "" : algoId).toUpperCase(Locale.ROOT); + + return switch (id) { + case "RSA", "ELGAMAL" -> rsaLikeStrength(modulusBits(key)); + case "AES" -> aesStrengthBits(key); + case "HMAC" -> hmacStrengthBits(key); + case "ED25519" -> 128; + case "ED448" -> 224; + case "ML-KEM" -> kyberStrength(key); + case "BIKE" -> mapByNistLevel(key, 128, 192, 256); + case "HQC" -> mapByNistLevel(key, 128, 192, 256); + case "FRODO" -> frodoStrength(key); + case "SABER" -> saberStrength(key); + case "CMCE" -> mcelieceStrength(key); + case "NTRU" -> ntruStrength(key); + case "SNTRUPRIME" -> sntruPrimeStrength(key); + case "NTRULPRIME" -> ntruLPrimeStrength(key); + case "SPHINCS+", "SPHINCSPLUS" -> sphincsPlusStrength(key); + case "DIGEST" -> 128; + default -> 128; + }; + } + + private static int modulusBits(Key key) { + return switch (key) { + case RSAPublicKey r -> r.getModulus().bitLength(); + case RSAPrivateKey r -> r.getModulus().bitLength(); + case null, default -> 0; + }; + } + + private static int rsaLikeStrength(int nBits) { + // Coarse NIST-like mapping for RSA/FF-DL: + // 2048 -> ~112, 3072 -> ~128, 7680 -> ~192, 15360 -> ~256 + return (nBits >= 15_360) ? 256 + : (nBits >= 7680) ? 192 : (nBits >= 3072) ? 128 : (nBits >= 2048) ? 112 : (nBits >= 1024) ? 80 : 64; + } + + private static int aesStrengthBits(Key key) { + int kb = secretKeyBitsOrZero(key); + // If we cannot see the key bytes or it is non-standard, assume conservative 128 + return (kb >= 256) ? 256 : (kb >= 192) ? 192 : (kb >= 128) ? 128 : 128; + } + + private static int hmacStrengthBits(Key key) { + // Strength ≈ min(key bits, hash output bits). Try to detect the hash from + // algorithm name. + final int keyBits = secretKeyBitsOrZero(key); + + final String algo = (key != null && key.getAlgorithm() != null) ? key.getAlgorithm() : ""; + final Matcher m = HMAC_SHA_PATTERN.matcher(algo); + + int hashOut = 0; + if (m.find()) { + final String grp = m.group(1); + hashOut = switch (grp) { + case "1" -> 160; // SHA-1 + case "224" -> 224; + case "256" -> 256; + case "384" -> 384; + case "512" -> 512; + default -> 0; // unknown/other SHA size, fall back below + }; + } + + if (hashOut == 0) { + // Unknown underlying hash; assume 256-bit output for modern HMACs + hashOut = 256; + } + + if (keyBits == 0) { + // If key length is opaque, cap by 256 (typical) + return Math.min(256, hashOut); + } + return Math.min(keyBits, hashOut); + } + + private static int kyberStrength(Key key) { + // Try to parse ML-KEM-512/768/1024 from key.getAlgorithm() + String a = safeAlgo(key); + Matcher m = NUMBER_PATTERN.matcher(a); + while (m.find()) { + int v = parseIntSafe(m.group(1)); + switch (v) { + case 512: + return 128; // NIST Level 1 + case 768: + return 192; // NIST Level 3 + case 1024: + return 256; // NIST Level 5 + default: // keep searching / fall back later + } + } + + // Fall back to L-level if present + int byLevel = mapByNistLevel(key, 128, 192, 256); + if (byLevel != 0) { + return byLevel; + } + + // Conservative fallback + return 128; + } + + private static int frodoStrength(Key key) { + // Common FrodoKEM sizes: 640 (L1 ~128), 976 (L3 ~192), 1344 (L5 ~256) + String a = safeAlgo(key); + Matcher m = NUMBER_PATTERN.matcher(a); + while (m.find()) { + int v = parseIntSafe(m.group(1)); + switch (v) { + case 640: + return 128; // Level 1 + case 976: + return 192; // Level 3 + case 1344: + return 256; // Level 5 + default: // keep scanning further matches + } + } + int byLevel = mapByNistLevel(key, 128, 192, 256); + if (byLevel != 0) { + return byLevel; + } + return 128; + } + + private static int saberStrength(Key key) { + // LightSaber (L1 128), Saber (L3 192), FireSaber (L5 256) + String a = safeAlgo(key); + String lower = a.toLowerCase(Locale.ROOT); + if (lower.contains("lightsaber")) { + return 128; + } + if (lower.contains("firesaber")) { + return 256; + } + if (lower.contains("saber")) { + return 192; // plain "Saber" + } + int byLevel = mapByNistLevel(key, 128, 192, 256); + if (byLevel != 0) { + return byLevel; + } + return 128; + } + + private static int mcelieceStrength(Key key) { + // Classic McEliece parameter sets target L1/L3/L5; default 128 without detail. + int byLevel = mapByNistLevel(key, 128, 192, 256); + if (byLevel != 0) { + return byLevel; + } + return 128; + } + + private static int ntruStrength(Key key) { + // Heuristic: many NTRU variants map to L1/L3/L5 families. + int byLevel = mapByNistLevel(key, 128, 192, 256); + if (byLevel != 0) { + return byLevel; + } + + // Try to parse numeric hints like 509/677/701 (commonly L1/L3/L5-ish across + // families) + String a = safeAlgo(key); + Matcher m = NUMBER_PATTERN.matcher(a); + while (m.find()) { + int v = parseIntSafe(m.group(1)); + switch (v) { + case 509: + return 128; // L1-ish + case 677: + return 192; // L3-ish + case 701: + return 256; // L5-ish + default: // keep scanning other numbers + } + } + + // Conservative default + return 128; + } + + private static int sntruPrimeStrength(Key key) { + // SNTRU Prime families also align to L1/L3/L5; prefer explicit levels if + // present. + int byLevel = mapByNistLevel(key, 128, 192, 256); + return (byLevel == 0) ? 128 : byLevel; + } + + private static int ntruLPrimeStrength(Key key) { + // NTRU LPRime: map by L1/L3/L5 when present, else conservative. + int byLevel = mapByNistLevel(key, 128, 192, 256); + return (byLevel == 0) ? 128 : byLevel; + } + + private static int sphincsPlusStrength(Key key) { + // Try to parse 128/192/256 from the parameter set name (e.g., + // SPHINCS+-SHA2-128s, -192f, -256s) + String a = safeAlgo(key); + Matcher m = SPHINCS_STRENGTH_PATTERN.matcher(a); + if (m.find()) { + int v = parseIntSafe(m.group(1)); + if (v == 128 || v == 192 || v == 256) { + return v; + } + } + // Fallback if the parameter size is unknown + return 128; + } + + private static int mapByNistLevel(Key key, int l1, int l3, int l5) { + String a = safeAlgo(key); + + // Try explicit L1/L3/L5 markers + Matcher lm = LEVEL_PATTERN.matcher(a); + if (lm.find()) { + String lvl = lm.group(1).toUpperCase(Locale.ROOT); + switch (lvl) { + case "L1": + return l1; + case "L3": + return l3; + case "L5": + return l5; + default: + // no action, continue with numeric fallback + } + } + + // Also consider bare numbers sometimes used instead of 'Lx' + Matcher nm = NUMBER_PATTERN.matcher(a); + while (nm.find()) { + int v = parseIntSafe(nm.group(1)); + switch (v) { + case 1, 128: + return l1; + case 3, 192: + return l3; + case 5, 256: + return l5; + default: + // keep scanning + } + } + + return 0; + } + + private static int secretKeyBitsOrZero(Key key) { + if (key instanceof SecretKey) { + byte[] enc = key.getEncoded(); + if (enc != null) { + return enc.length * 8; + } + } + return 0; + } + + private static String safeAlgo(Key key) { + String algo = (key != null && key.getAlgorithm() != null) ? key.getAlgorithm() : ""; + return algo == null ? "" : algo; + } + + private static int parseIntSafe(String s) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return 0; + } + } +} diff --git a/lib/src/main/java/zeroecho/core/policy/package-info.java b/lib/src/main/java/zeroecho/core/policy/package-info.java new file mode 100644 index 0000000..665567c --- /dev/null +++ b/lib/src/main/java/zeroecho/core/policy/package-info.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Policy enforcement for algorithms, keys, and specifications. + * + *

+ * This package centralizes checks that validate algorithm identifiers, intended + * roles, keys, and optional specifications before a context is created. + * Policies allow an application to enforce organizational rules without + * hard-wiring constraints into individual algorithms. + *

+ * + *

Key elements

+ *
    + *
  • {@link CryptoPolicy} - strategy interface consulted during context + * creation to validate the combination of algorithm id, + * {@link zeroecho.core.KeyUsage} role, key, and optional spec.
  • + *
  • {@link SecurityStrengthAdvisor} - utility that estimates a conservative + * security strength in bits from an algorithm family id and a key, used by + * policies such as minimum-strength guards.
  • + *
+ * + *

Typical usage

{@code
+ * // Install a global minimum-strength policy.
+ * zeroecho.core.CryptoAlgorithms.setPolicy(
+ *     zeroecho.core.policy.CryptoPolicy.minStrength(128));
+ *
+ * // Later, when creating a context, the policy is consulted automatically.
+ * zeroecho.core.context.EncryptionContext ctx =
+ *     zeroecho.core.CryptoAlgorithms.create("AES/GCM", zeroecho.core.KeyUsage.ENCRYPT, secretKey);
+ * }
+ * + *

Design notes

+ *
    + *
  • Policies can be permissive or strict, ranging from "accept all" to + * enforcing whitelists and minimum key strengths.
  • + *
  • Strength estimation favors safe lower bounds and relies on visible key + * characteristics (for example, modulus bits for RSA, key length for AES, or + * NIST-level hints for KEMs).
  • + *
  • Validation failures are signaled using unchecked exceptions suitable for + * configuration-time errors.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.policy; diff --git a/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java b/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java new file mode 100644 index 0000000..3deb45a --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spec/AlgorithmKeySpec.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * 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.core.spec; + +import zeroecho.core.spi.SymmetricKeyBuilder; + +/** + * Marker interface for algorithm-specific key specifications. + *

+ * Implementations carry the parameters required to construct or validate a key + * for a specific algorithm family (e.g., AES, ChaCha20, HMAC, X25519). Typical + * fields include key size, algorithm identifier, or the raw key material to be + * imported. + * + *

Design

+ *
    + *
  • Separates algorithm parameters from key builders such as + * {@link SymmetricKeyBuilder}.
  • + *
  • Provides a type-safe way to pass algorithm requirements around instead of + * raw integers or opaque byte arrays.
  • + *
  • Allows higher layers to work generically with {@code AlgorithmKeySpec} + * while still permitting algorithm-specific extensions.
  • + *
+ * + *

Usage

+ *

+ * Applications never implement this interface directly. Instead, use provided + * spec classes such as {@code AesKeySpec}, {@code ChaChaKeySpec}, etc. + * Libraries may extend it when introducing new algorithm families. + * + *

+ * Thread safety: Implementations should be immutable and serializable + * only if necessary for configuration. Raw key material should be handled with + * care to avoid accidental exposure. + *

+ * + * @since 1.0 + */ +public interface AlgorithmKeySpec { +} diff --git a/lib/src/main/java/zeroecho/core/spec/ContextSpec.java b/lib/src/main/java/zeroecho/core/spec/ContextSpec.java new file mode 100644 index 0000000..194e107 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spec/ContextSpec.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.spec; + +/** + * Marker interface for per-operation parameters. + * + *

+ * Some cryptographic algorithms require additional runtime parameters that are + * distinct from the key material. For example, RSA can require padding + * parameters such as OAEP or PSS options, while an AEAD cipher might accept a + * tag length or nonce construction strategy. + *

+ * + *

+ * Implementations of this interface are provided by the library for algorithms + * that need them. Applications typically do not implement {@code ContextSpec} + * directly. + *

+ * + *

Thread safety

+ *

+ * Implementations should be immutable, as these specifications may be reused + * across multiple operations. + *

+ * + * @since 1.0 + */ +public interface ContextSpec { +} diff --git a/lib/src/main/java/zeroecho/core/spec/VoidSpec.java b/lib/src/main/java/zeroecho/core/spec/VoidSpec.java new file mode 100644 index 0000000..07ead10 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spec/VoidSpec.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.spec; + +/** + * Context specification for algorithms that require no additional parameters. + * + *

+ * Many algorithms do not define per-operation configuration beyond the key + * itself. In such cases, the {@code VoidSpec} singleton is used as a + * placeholder to satisfy generic APIs that expect a {@link ContextSpec}. + *

+ * + *

Usage

{@code
+ * CryptoContext ctx = algo.create(KeyUsage.SIGN, privateKey, VoidSpec.INSTANCE);
+ * }
+ * + *

+ * Since this type has no state, all equality checks resolve to the same single + * instance {@link #INSTANCE}. + *

+ * + * @since 1.0 + */ +public enum VoidSpec implements ContextSpec { + /** + * Singleton instance representing the absence of per-operation parameters. + */ + INSTANCE; +} diff --git a/lib/src/main/java/zeroecho/core/spec/package-info.java b/lib/src/main/java/zeroecho/core/spec/package-info.java new file mode 100644 index 0000000..6aa226e --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spec/package-info.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Specifications for keys and per-operation parameters. + * + *

+ * This package defines lightweight, strongly typed specifications used when + * creating cryptographic contexts. Key specifications describe how a key should + * be generated or imported for a particular family, while context + * specifications supply optional, per-operation parameters (for example, + * padding or nonce strategy). Algorithms that need no extra parameters use a + * singleton placeholder. + *

+ * + *

Key elements

+ *
    + *
  • {@link AlgorithmKeySpec} - marker interface for algorithm-specific key + * specs used by key builders.
  • + *
  • {@link ContextSpec} - marker interface for per-operation parameters + * supplied to context creation.
  • + *
  • {@link VoidSpec} - singleton context spec used when an algorithm requires + * no additional parameters.
  • + *
+ * + *

Typical usage

{@code
+ * // Algorithm requires no per-operation parameters.
+ * zeroecho.core.context.CryptoContext ctx =
+ *     algo.create(zeroecho.core.KeyUsage.SIGN, privateKey, zeroecho.core.spec.VoidSpec.INSTANCE);
+ *
+ * // Algorithm with parameters: pass an algorithm-specific ContextSpec implementation.
+ * // Example: RSA with OAEP/PSS, AEAD tag length, etc.
+ * // zeroecho.core.context.CryptoContext ctx =
+ * //     algo.create(zeroecho.core.KeyUsage.ENCRYPT, key, someAlgorithmSpecificSpec);
+ * }
+ * + *

Design notes

+ *
    + *
  • Specification types should be immutable and safe to share across + * operations.
  • + *
  • Key specifications and context specifications keep configuration separate + * from runtime state, improving clarity and reuse.
  • + *
  • Callers should prefer library-provided spec classes and avoid + * implementing the marker interfaces directly unless extending the library with + * new families.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.spec; diff --git a/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyBuilder.java b/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyBuilder.java new file mode 100644 index 0000000..e1c7daf --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/AsymmetricKeyBuilder.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * 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.core.spi; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Factory interface for constructing asymmetric key pairs and importing + * public/private keys from specifications. + * + *

+ * Implementations encapsulate algorithm-specific details (for example RSA, + * Ed25519, X25519) while exposing a uniform API for generation and import + * operations. This allows higher-level code to work generically with + * {@code AsymmetricKeyBuilder} without depending on algorithm internals. + *

+ * + *

Operations

+ *
    + *
  • {@link #generateKeyPair(AlgorithmKeySpec)} - creates a fresh key pair + * using the supplied algorithm spec (such as curve parameters or modulus + * length).
  • + *
  • {@link #importPublic(AlgorithmKeySpec)} - wraps externally supplied + * public key material in a {@link PublicKey}, validating that it conforms to + * the algorithm specification.
  • + *
  • {@link #importPrivate(AlgorithmKeySpec)} - wraps externally supplied + * private key material in a {@link PrivateKey}, validating that it conforms to + * the algorithm specification.
  • + *
+ * + *

Usage guidelines

+ *
    + *
  • Always prefer {@link #generateKeyPair(AlgorithmKeySpec)} when creating + * new credentials.
  • + *
  • Use {@link #importPublic(AlgorithmKeySpec)} and + * {@link #importPrivate(AlgorithmKeySpec)} for interoperability, loading from + * key stores, or migration from existing material.
  • + *
  • Implementations should reject malformed or weak keys and enforce + * algorithm-specific constraints (for example, minimum modulus length for RSA + * or disallowed small subgroup curves).
  • + *
+ * + *

Thread safety

Implementations must be stateless or otherwise safe + * for concurrent use across threads. + * + * @param algorithm-specific key specification type + * + * @since 1.0 + */ +public interface AsymmetricKeyBuilder { + /** + * Generates a new asymmetric key pair according to the given specification. + * + * @param spec algorithm parameters, such as modulus length or curve identifier + * @return a new {@link KeyPair} containing a public and private key + * @throws GeneralSecurityException if the algorithm or parameters are invalid + * or unsupported + */ + KeyPair generateKeyPair(S spec) throws GeneralSecurityException; + + /** + * Imports an externally supplied public key according to the given + * specification. + * + *

+ * Implementations must validate that the provided material is properly + * formatted, has acceptable length, and is consistent with the specified + * algorithm. + *

+ * + * @param spec algorithm parameters and encoded public key material + * @return a {@link PublicKey} validated and usable for cryptographic operations + * @throws GeneralSecurityException if the key material is invalid or does not + * match the specification + */ + PublicKey importPublic(S spec) throws GeneralSecurityException; + + /** + * Imports an externally supplied private key according to the given + * specification. + * + *

+ * Implementations must validate that the provided material is properly + * formatted, has acceptable length, and is consistent with the specified + * algorithm. + *

+ * + * @param spec algorithm parameters and encoded private key material + * @return a {@link PrivateKey} validated and usable for cryptographic + * operations + * @throws GeneralSecurityException if the key material is invalid or does not + * match the specification + */ + PrivateKey importPrivate(S spec) throws GeneralSecurityException; +} diff --git a/lib/src/main/java/zeroecho/core/spi/ContextAware.java b/lib/src/main/java/zeroecho/core/spi/ContextAware.java new file mode 100644 index 0000000..593d887 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/ContextAware.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.spi; + +import conflux.CtxInterface; + +/** + * Optional service-provider interface for cryptographic primitives that need to + * share or persist per-session parameters using a {@link CtxInterface}. + * + *

+ * Many cryptographic algorithms require auxiliary values in addition to the + * key, such as initialization vectors (IVs), nonces, salts, or additional + * authenticated data (AAD). Implementations that support external parameter + * passing can expose this interface so higher layers can provide or retrieve + * these values through a shared context. + *

+ * + *

Typical parameters

+ *
    + *
  • Initialization vectors (IV) for block cipher modes such as CBC or + * GCM.
  • + *
  • Nonces for stream ciphers or AEAD schemes.
  • + *
  • Salts for password-based key derivation functions.
  • + *
  • Additional authenticated data (AAD) for AEAD constructions.
  • + *
+ * + *

Design guidelines

+ *
    + *
  • Implementations should fail fast if required parameters are missing in + * the provided context.
  • + *
  • Contexts may be reused across operations, but values should be scoped to + * the session or algorithm instance.
  • + *
  • Implementations should treat {@link CtxInterface} as a structured + * key–value store and avoid ad-hoc parameter passing.
  • + *
+ * + *

Thread safety

The thread-safety of this interface depends on the + * provided {@link CtxInterface} implementation. Callers should not assume + * concurrent mutation is safe unless explicitly documented. + * + * @since 1.0 + */ +public interface ContextAware { + /** + * Attaches a context object that will be used to read and write per-session + * parameters. + * + *

+ * Typical values include initialization vectors (IV), nonces, salts, or + * additional authenticated data (AAD). Implementations may throw if the context + * is missing required values or contains invalid entries. + *

+ * + * @param ctx non-null parameter context + * @throws NullPointerException if {@code ctx} is {@code null} + */ + void setContext(CtxInterface ctx); + + /** + * Returns the context currently in use. + * + *

+ * This method must never return {@code null} after the first successful + * invocation of {@link #setContext(CtxInterface)} or after a context has been + * created internally by the implementation. + *

+ * + * @return the active parameter context + */ + CtxInterface context(); +} diff --git a/lib/src/main/java/zeroecho/core/spi/ContextConstructorKS.java b/lib/src/main/java/zeroecho/core/spi/ContextConstructorKS.java new file mode 100644 index 0000000..12dbeec --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/ContextConstructorKS.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * 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.core.spi; + +import java.io.IOException; +import java.security.Key; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.context.CryptoContext; +import zeroecho.core.spec.ContextSpec; + +/** + * Factory interface to construct a {@link CryptoContext} from a key and an + * optional specification. + * + *

+ * Each cryptographic algorithm binds one or more roles (for example, + * {@code ENCRYPT}, {@code SIGN}) to a corresponding context type. For each + * binding, a {@code ContextConstructorKS} is registered as the factory that + * creates the runtime context for the role. The role itself is implied by the + * registration and does not need to be passed explicitly here. + *

+ * + *

Responsibilities

+ *
    + *
  • Validate that the provided key is compatible with the expected key + * type.
  • + *
  • Interpret the {@link ContextSpec} parameters (such as IVs, padding modes, + * or curve identifiers).
  • + *
  • Construct and return a ready-to-use {@link CryptoContext} instance bound + * to the given key and spec.
  • + *
+ * + *

Usage

+ *

+ * {@code ContextConstructorKS} is primarily used internally by + * {@link CryptoAlgorithm} implementations when binding roles. Higher-level code + * should not call it directly; instead use {@link CryptoAlgorithm#create} or + * {@link zeroecho.core.CryptoAlgorithms#create}. + *

+ * + *

Thread safety

Implementations should be stateless and safe to invoke + * concurrently from multiple threads. + * + * @param context type produced + * @param key type accepted + * @param specification type accepted + * + * @since 1.0 + */ +@FunctionalInterface +public interface ContextConstructorKS { + /** + * Creates a new {@link CryptoContext} instance bound to the provided key and + * specification. + * + * @param key non-null cryptographic key suitable for the role + * @param spec role-specific parameters; may be {@code null} if defaults are + * acceptable + * @return a newly constructed context ready for cryptographic operations + * @throws IOException if context creation fails due to I/O, provider issues, or + * invalid parameters + */ + C create(K key, S spec) throws IOException; +} diff --git a/lib/src/main/java/zeroecho/core/spi/SymmetricKeyBuilder.java b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyBuilder.java new file mode 100644 index 0000000..b99157b --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/SymmetricKeyBuilder.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * 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.core.spi; + +import java.security.GeneralSecurityException; + +import javax.crypto.SecretKey; + +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Factory interface for constructing symmetric keys from algorithm-specific + * specifications. + * + *

+ * Implementations encapsulate the details of generating or importing + * {@link SecretKey} instances for a particular symmetric algorithm (for example + * AES, ChaCha20, or HMAC). This abstraction provides a uniform API for higher + * layers, independent of provider-specific implementations. + *

+ * + *

Operations

+ *
    + *
  • {@link #generateSecret(AlgorithmKeySpec)} - creates a fresh random key + * using the parameters supplied by the algorithm specification.
  • + *
  • {@link #importSecret(AlgorithmKeySpec)} - wraps externally supplied raw + * key material in a {@link SecretKey}, validating that it conforms to the + * specification.
  • + *
+ * + *

Usage guidelines

+ *
    + *
  • Prefer {@link #generateSecret(AlgorithmKeySpec)} for new credentials to + * ensure strong, random keys.
  • + *
  • Use {@link #importSecret(AlgorithmKeySpec)} only when loading existing + * keys, migrating from another system, or interoperating with external storage + * formats.
  • + *
  • Implementations must enforce algorithm constraints, including required + * key sizes and disallowing known-weak parameters.
  • + *
  • Returned {@link SecretKey} instances should be immutable and, where + * possible, wrapped in provider-specific classes that prevent serialization or + * unintended exposure.
  • + *
+ * + *

Thread safety

+ *

+ * Implementations must be stateless or otherwise safe to use concurrently + * across multiple threads. + *

+ * + * @param algorithm-specific key specification type + * + * @since 1.0 + */ +public interface SymmetricKeyBuilder { + /** + * Generates a new symmetric key according to the given specification. + * + * @param spec algorithm parameters, such as required key size or algorithm + * variant + * @return a freshly generated {@link SecretKey} containing random key material + * @throws GeneralSecurityException if key generation fails or the parameters + * are invalid or unsupported + */ + SecretKey generateSecret(S spec) throws GeneralSecurityException; + + /** + * Imports an externally provided symmetric key according to the given + * specification. + * + *

+ * Implementations must validate that the provided material matches the + * algorithm’s requirements (for example, correct length and encoding). Weak or + * truncated keys must be rejected. + *

+ * + * @param spec algorithm parameters and raw key material + * @return a validated {@link SecretKey} suitable for cryptographic use + * @throws GeneralSecurityException if the key material is invalid, corrupted, + * or inconsistent with the specification + */ + SecretKey importSecret(S spec) throws GeneralSecurityException; +} diff --git a/lib/src/main/java/zeroecho/core/spi/package-info.java b/lib/src/main/java/zeroecho/core/spi/package-info.java new file mode 100644 index 0000000..07be343 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/spi/package-info.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Service Provider Interfaces (SPI) for extending ZeroEcho with custom + * cryptographic algorithms and key builders. + * + *

+ * This package defines the provider-facing contracts used by algorithms to plug + * new primitives into the framework while keeping the public API uniform. The + * SPIs emphasize role-driven context construction, strict spec validation, and + * safe key lifecycle handling. + *

+ * + *

How algorithms plug in

+ *

+ * An algorithm publishes capabilities and binds each supported + * {@link zeroecho.core.KeyUsage role} to a factory that creates a matching + * {@link zeroecho.core.context.CryptoContext}. The binding is registered in the + * algorithm constructor using {@link ContextConstructorKS}; the role is implied + * by the binding itself. + *

+ * + *
{@code
+ * // Inside an algorithm's constructor (illustrative):
+ * capability(AlgorithmFamily.SYMMETRIC, KeyUsage.ENCRYPT,
+ *           zeroecho.core.context.EncryptionContext.class,
+ *           javax.crypto.SecretKey.class, zeroecho.core.alg.aes.AesSpec.class,
+ *           (k, s) -> new zeroecho.core.alg.aes.AesCipherContext(this, k, true, s, new java.security.SecureRandom()),
+ *           () -> zeroecho.core.alg.aes.AesSpec.gcm128(null));
+ *
+ * capability(AlgorithmFamily.SYMMETRIC, KeyUsage.DECRYPT,
+ *           zeroecho.core.context.EncryptionContext.class,
+ *           javax.crypto.SecretKey.class, zeroecho.core.alg.aes.AesSpec.class,
+ *           (k, s) -> new zeroecho.core.alg.aes.AesCipherContext(this, k, false, s, new java.security.SecureRandom()),
+ *           () -> zeroecho.core.alg.aes.AesSpec.gcm128(null));
+ * }
+ * + *

Key material builders and the spec-class keyed registry

+ *

+ * Builders are registered per spec class. The algorithm maintains a single map + * from {@code Class} to a + * builder instance; lookups are driven by the spec class, not by the high-level + * operation. This keeps registration simple while letting providers model + * different intents through different spec types. + *

+ * + *
    + *
  • {@link AsymmetricKeyBuilder} provides key pair generation and + * public/private key import for asymmetric algorithms.
  • + *
  • {@link SymmetricKeyBuilder} provides key generation and key import for + * {@link javax.crypto.SecretKey}-based algorithms.
  • + *
+ * + *

Why a single interface for both key pair generation and key import

+ *

+ * There is one cohesive builder interface because the registry keys off the + * spec class, not the operation. A single builder type per spec class gives one + * lookup path and one place to enforce spec parsing, format checks, curve or + * modulus validation, and provider constraints. The public facades in + * {@link zeroecho.core.CryptoAlgorithm} and + * {@link zeroecho.core.CryptoAlgorithms} remain compact: they resolve the + * builder by spec class and then invoke + * {@link AsymmetricKeyBuilder#generateKeyPair(zeroecho.core.spec.AlgorithmKeySpec)}, + * {@link AsymmetricKeyBuilder#importPublic(zeroecho.core.spec.AlgorithmKeySpec)}, + * {@link AsymmetricKeyBuilder#importPrivate(zeroecho.core.spec.AlgorithmKeySpec)}, + * {@link SymmetricKeyBuilder#generateSecret(zeroecho.core.spec.AlgorithmKeySpec)}, + * or + * {@link SymmetricKeyBuilder#importSecret(zeroecho.core.spec.AlgorithmKeySpec)} + * as appropriate. + *

+ *

+ * Providers express differences in capability by choosing distinct spec classes + * rather than multiplying interfaces. For example, a symmetric provider may + * register {@code AesKeyGenSpec} for generation and {@code AesKeyImportSpec} + * for import, each with its own builder. The unified interface still fits + * because the registry discriminates by spec class. + *

+ * + *

Pattern: separate specs for generation vs import with prescriptive + * exceptions

+ *

+ * It is idiomatic to register two builders for AES: one bound to a generation + * spec that implements + * {@link SymmetricKeyBuilder#generateSecret(zeroecho.core.spec.AlgorithmKeySpec)} + * and rejects import, and one bound to an import spec that implements + * {@link SymmetricKeyBuilder#importSecret(zeroecho.core.spec.AlgorithmKeySpec)} + * and rejects generation. Unsupported paths should throw + * {@link UnsupportedOperationException} with a clear, prescriptive message that + * points to the correct spec. + *

+ * + *
{@code
+ * // Generation-only builder bound to AesKeyGenSpec
+ * registerSymmetricKeyBuilder(zeroecho.core.alg.aes.AesKeyGenSpec.class, new SymmetricKeyBuilder<>() {
+ *   @Override public javax.crypto.SecretKey generateSecret(zeroecho.core.alg.aes.AesKeyGenSpec spec)
+ *       throws java.security.GeneralSecurityException {
+ *     // generate according to spec.keySizeBits()
+ *     throw new UnsupportedOperationException("example");
+ *   }
+ *   @Override public javax.crypto.SecretKey importSecret(zeroecho.core.alg.aes.AesKeyGenSpec spec) {
+ *     throw new UnsupportedOperationException("Use AesKeyImportSpec for importing AES keys");
+ *   }
+ * }, zeroecho.core.alg.aes.AesKeyGenSpec::aes256);
+ *
+ * // Import-only builder bound to AesKeyImportSpec
+ * registerSymmetricKeyBuilder(zeroecho.core.alg.aes.AesKeyImportSpec.class, new SymmetricKeyBuilder<>() {
+ *   @Override public javax.crypto.SecretKey generateSecret(zeroecho.core.alg.aes.AesKeyImportSpec spec) {
+ *     throw new UnsupportedOperationException("Use AesKeyGenSpec to generate AES keys");
+ *   }
+ *   @Override public javax.crypto.SecretKey importSecret(zeroecho.core.alg.aes.AesKeyImportSpec spec) {
+ *     return new javax.crypto.spec.SecretKeySpec(spec.key(), "AES");
+ *   }
+ * }, null);
+ * }
+ * + *

+ * The same separation can be applied to asymmetric algorithms by using, for + * example, {@code RsaKeyGenSpec} and {@code RsaKeyImportSpec}. Each spec class + * maps to exactly one {@link AsymmetricKeyBuilder} in the algorithm's registry. + *

+ * + *

Context sharing and parameter flow

+ *

+ * Contexts that need per-session values (IV, nonce, salt, AAD) may implement + * {@link ContextAware} to read and write through a shared + * {@link conflux.CtxInterface}. For symmetric streams that carry lightweight + * headers, algorithms in {@code zeroecho.core} can use + * {@link zeroecho.core.SymmetricHeaderCodec}. + *

+ * + *

Error handling and validation

+ *
    + *
  • Fail fast when keys or specs are incompatible with the bound role.
  • + *
  • Use {@link java.security.GeneralSecurityException} for key generation or + * import failures.
  • + *
  • Use {@link java.io.IOException} from + * {@link ContextConstructorKS#create(java.security.Key, zeroecho.core.spec.ContextSpec)} + * when context setup performs I/O.
  • + *
  • Use {@link UnsupportedOperationException} with a precise message when an + * operation is intentionally unsupported by the spec or provider, for example + * "Use AesKeyImportSpec for importing AES keys".
  • + *
+ * + *

Thread safety

+ *

+ * SPI implementations should be stateless or otherwise safe for concurrent use. + * Created contexts are not necessarily thread-safe unless explicitly documented + * by the provider. + *

+ * + * @since 1.0 + */ +package zeroecho.core.spi; diff --git a/lib/src/main/java/zeroecho/core/storage/KeyringStore.java b/lib/src/main/java/zeroecho/core/storage/KeyringStore.java new file mode 100644 index 0000000..37238f7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/storage/KeyringStore.java @@ -0,0 +1,631 @@ +/******************************************************************************* + * 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.core.storage; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.crypto.SecretKey; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.marshal.PairSeq; +import zeroecho.core.spec.AlgorithmKeySpec; + +/** + * Human-editable keyring persisted in a simple UTF-8 text format. + * + *

File format

Each file starts with a magic header followed by one or + * more entries. Lines starting with the hash character are comments. A blank + * line terminates the current entry. Keys belonging to the key-spec payload are + * prefixed with "s." to avoid collisions with top-level metadata. + * + *
{@code
+ * # KeyringStore v1
+ * @entry
+ * alias=my-rsa
+ * algorithm=RSA
+ * kind=public|private|secret
+ * spec=zeroecho.core.alg.rsa.RsaPublicKeySpec
+ * s.x509B64=MIIBIjANBgkqh...
+ * s.note=optional
+ *
+ * @entry
+ * ...
+ * }
+ * + *

Reading and writing

Use {@link #save(java.nio.file.Path)} to write + * the keyring to disk and {@link #load(java.nio.file.Path)} to read it back. + * The loader tolerates the presence of the header and comment lines but + * requires the magic header for the v1 format. + * + *

Spec marshaling

A record holds a fully qualified spec class name and + * a key-value payload. The store relies on a pair of static utility methods + * that must be implemented by each spec type: + *
    + *
  • {@code static PairSeq marshal(SpecType spec)}
  • + *
  • {@code static SpecType unmarshal(PairSeq pairs)}
  • + *
+ * These are discovered and invoked via reflection. See + * {@link #marshalSpec(AlgorithmKeySpec)} and + * {@link #unmarshalSpec(String, PairSeq)} for details. + * + *

Basic usage

{@code
+ * KeyringStore ks = new KeyringStore();
+ * ks.putPublic("site-signing", "Ed25519", myEd25519PublicSpec);
+ * ks.putPrivate("site-signing", "Ed25519", myEd25519PrivateSpec);
+ * ks.save(Path.of("keyring.txt"));
+ *
+ * KeyringStore reloaded = KeyringStore.load(Path.of("keyring.txt"));
+ * PublicKey pub = reloaded.getPublic("site-signing");
+ * }
+ */ +public final class KeyringStore { + + private static final String MAGIC_HEADER = "# KeyringStore v1"; + private static final String ENTRY_MARKER = "@entry"; + private static final String PREFIX_SPEC = "s."; // marks spec payload keys + + private final Map byAlias = new LinkedHashMap<>(); + + /** + * Immutable entry in a {@link KeyringStore}. + * + *

+ * Each record represents one alias-bound key with its algorithm metadata and + * serialized specification payload. Records are immutable value objects and + * provide structural equality and stable hashing. + *

+ * + * @param alias alias that identifies the entry. It must be unique within + * the store. + * @param algorithm algorithm identifier such as {@code "RSA"}, + * {@code "Ed25519"}, {@code "HMAC"}, or {@code "AES"}. The + * value is passed to the crypto catalog when materializing + * keys. + * @param kind type of key stored in this record + * @param specClass fully qualified class name of the key-spec used to + * materialize the key + * @param specPayload specification payload represented as alternating key and + * value pairs. Keys appear in the file with the {@code "s."} + * prefix but are stored here without the prefix. + */ + public record Record(String alias, String algorithm, Kind kind, String specClass, PairSeq specPayload) { + + /** + * Enumeration of supported key kinds. + * + *
    + *
  • {@link #PUBLIC_KEY} - public key material
  • + *
  • {@link #PRIVATE_KEY} - private key material
  • + *
  • {@link #SECRET_KEY} - symmetric key material
  • + *
+ */ + public enum Kind { + PUBLIC_KEY, PRIVATE_KEY, SECRET_KEY + } + } + + /** + * Adds or replaces a public-key entry under the given alias. + * + * @param alias the unique alias to store under; must not be null or blank + * @param algorithmId algorithm identifier understood by the crypto catalog + * @param importSpec the algorithm-specific spec carrying public key material + * @throws IllegalArgumentException if any argument is invalid + */ + public void putPublic(String alias, String algorithmId, AlgorithmKeySpec importSpec) { + put(alias, algorithmId, Record.Kind.PUBLIC_KEY, importSpec); + } + + /** + * Adds or replaces a private-key entry under the given alias. + * + * @param alias the unique alias to store under; must not be null or blank + * @param algorithmId algorithm identifier understood by the crypto catalog + * @param importSpec the algorithm-specific spec carrying private key material + * @throws IllegalArgumentException if any argument is invalid + */ + public void putPrivate(String alias, String algorithmId, AlgorithmKeySpec importSpec) { + put(alias, algorithmId, Record.Kind.PRIVATE_KEY, importSpec); + } + + /** + * Adds or replaces a secret-key entry under the given alias. + * + * @param alias the unique alias to store under; must not be null or blank + * @param algorithmId algorithm identifier understood by the crypto catalog + * @param importSpec the algorithm-specific spec carrying secret key material + * @throws IllegalArgumentException if any argument is invalid + */ + public void putSecret(String alias, String algorithmId, AlgorithmKeySpec importSpec) { + put(alias, algorithmId, Record.Kind.SECRET_KEY, importSpec); + } + + /** + * Checks whether an entry with the given alias exists. + * + * @param alias the alias to test + * @return true if an entry exists for the alias, false otherwise + */ + public boolean contains(String alias) { + return byAlias.containsKey(alias); + } + + /** + * Returns all aliases currently present in insertion order. + * + * @return a new list of aliases; the returned list is mutable but independent + */ + public List aliases() { + return new ArrayList<>(byAlias.keySet()); + } + + /** + * PublicWithId pairs the algorithm identifier with a resolved public key. + * + *

Usage

{@code
+     * KeyringStore ks = KeyringStore.load(path);
+     * KeyringStore.PublicWithId r = ks.getPublicWithId("alice");
+     * String algId = r.algorithm();
+     * PublicKey pub = r.key();
+     * }
+ */ + public record PublicWithId(String algorithm, PublicKey key) { + } + + /** + * PrivateWithId pairs the algorithm identifier with a resolved private key. + * + *

Usage

{@code
+     * KeyringStore ks = KeyringStore.load(path);
+     * KeyringStore.PrivateWithId r = ks.getPrivateWithId("alice");
+     * String algId = r.algorithm();
+     * PrivateKey prv = r.key();
+     * }
+ */ + public record PrivateWithId(String algorithm, PrivateKey key) { + } + + /** + * SecretWithId pairs the algorithm identifier with a resolved secret key. + * + *

Usage

{@code
+     * KeyringStore ks = KeyringStore.load(path);
+     * KeyringStore.SecretWithId r = ks.getSecretWithId("hmac-key");
+     * String algId = r.algorithm();
+     * SecretKey sk = r.key();
+     * }
+ */ + public record SecretWithId(String algorithm, SecretKey key) { + } + + /** + * Resolves a public key together with its algorithm identifier. + * + *

+ * The method mirrors {@link #getPublic(String)} but returns the algorithm id + * stored in the record along with the materialized key so that callers can + * route to algorithm-specific processing without asking the user to repeat it. + *

+ * + * @param alias the alias of a {@link Record.Kind#PUBLIC_KEY} entry + * @return a pair containing the algorithm id and the resolved public key + * @throws GeneralSecurityException if spec unmarshaling or key construction + * fails + * @throws IllegalArgumentException if alias is missing or not a public key + */ + public PublicWithId getPublicWithId(String alias) throws GeneralSecurityException { + Record r = require(alias, Record.Kind.PUBLIC_KEY); + AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload); + PublicKey key = CryptoAlgorithms.publicKey(r.algorithm, spec); + return new PublicWithId(r.algorithm, key); + } + + /** + * Resolves a private key together with its algorithm identifier. + * + * @param alias the alias of a {@link Record.Kind#PRIVATE_KEY} entry + * @return a pair containing the algorithm id and the resolved private key + * @throws GeneralSecurityException if spec unmarshaling or key construction + * fails + * @throws IllegalArgumentException if alias is missing or not a private key + */ + public PrivateWithId getPrivateWithId(String alias) throws GeneralSecurityException { + Record r = require(alias, Record.Kind.PRIVATE_KEY); + AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload); + PrivateKey key = CryptoAlgorithms.privateKey(r.algorithm, spec); + return new PrivateWithId(r.algorithm, key); + } + + /** + * Resolves a secret key together with its algorithm identifier. + * + * @param alias the alias of a {@link Record.Kind#SECRET_KEY} entry + * @return a pair containing the algorithm id and the resolved secret key + * @throws GeneralSecurityException if spec unmarshaling or key construction + * fails + * @throws IllegalArgumentException if alias is missing or not a secret key + */ + public SecretWithId getSecretWithId(String alias) throws GeneralSecurityException { + Record r = require(alias, Record.Kind.SECRET_KEY); + AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload); + SecretKey key = CryptoAlgorithms.secretKey(r.algorithm, spec); + return new SecretWithId(r.algorithm, key); + } + + /** + * Resolves the public key bound to the alias. + * + *

+ * The stored spec class is loaded and unmarshaled via + * {@link #unmarshalSpec(String, PairSeq)}, and the key is materialized via the + * crypto catalog. + *

+ * + * @param alias the alias of a {@link Record.Kind#PUBLIC_KEY} entry + * @return a public key instance created from the stored spec + * @throws GeneralSecurityException if spec unmarshaling or key construction + * fails + * @throws IllegalArgumentException if the alias is missing or the entry is not + * a public key + */ + public PublicKey getPublic(String alias) throws GeneralSecurityException { + Record r = require(alias, Record.Kind.PUBLIC_KEY); + AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload); + return CryptoAlgorithms.publicKey(r.algorithm, spec); + } + + /** + * Resolves the private key bound to the alias. + * + * @param alias the alias of a {@link Record.Kind#PRIVATE_KEY} entry + * @return a private key instance created from the stored spec + * @throws GeneralSecurityException if spec unmarshaling or key construction + * fails + * @throws IllegalArgumentException if the alias is missing or the entry is not + * a private key + */ + public PrivateKey getPrivate(String alias) throws GeneralSecurityException { + Record r = require(alias, Record.Kind.PRIVATE_KEY); + AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload); + return CryptoAlgorithms.privateKey(r.algorithm, spec); + } + + /** + * Resolves the secret key bound to the alias. + * + * @param alias the alias of a {@link Record.Kind#SECRET_KEY} entry + * @return a secret key instance created from the stored spec + * @throws GeneralSecurityException + * @throws IllegalArgumentException if the alias is missing or the entry is not + * a secret key + * @throws GeneralSecurityException if spec unmarshaling or key construction + * fails + */ + public SecretKey getSecret(String alias) throws GeneralSecurityException { + Record r = require(alias, Record.Kind.SECRET_KEY); + AlgorithmKeySpec spec = unmarshalSpec(r.specClass, r.specPayload); + return CryptoAlgorithms.secretKey(r.algorithm, spec); + } + + /** + * Saves the entire keyring to a UTF-8 text file. + * + * @param path destination path + * @throws IOException if writing fails + */ + public void save(Path path) throws IOException { + try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { + writeAll(w, byAlias.values()); + } + } + + /** + * Loads a keyring from a UTF-8 text file. + * + * @param path source path + * @return a new store populated with entries from the file + * @throws IOException if reading fails or the format is not supported + */ + public static KeyringStore load(Path path) throws IOException { + try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + List recs = readAll(r, /* requireHeader */ true); + KeyringStore store = new KeyringStore(); + recs.forEach(rec -> store.byAlias.put(rec.alias, rec)); + return store; + } + } + + /** + * Exports the specified aliases as a versioned, line-oriented text snippet. + * + * @param aliases aliases to export; each alias must exist in this store + * @return UTF-8 text beginning with the magic header + * @throws IllegalArgumentException if an alias is not present + */ + public String exportText(Collection aliases) { + StringWriter out = new StringWriter(256); + List recs = new ArrayList<>(aliases.size()); + for (String a : aliases) { + Record r = byAlias.get(a); + if (r == null) { + throw new IllegalArgumentException("Missing alias: " + a); + } + recs.add(r); + } + try { + writeAll(out, recs); + } catch (IOException impossible) { + throw new AssertionError(impossible); + } + return out.toString(); + } + + /** + * Imports a versioned, line-oriented snippet into this store. + * + * @param text snippet text + * @param overwrite whether collisions are allowed + * @throws IOException if the header is missing/unsupported or the + * body is malformed + * @throws IllegalArgumentException if {@code overwrite} is false and a + * collision occurs + */ + public void importText(String text, boolean overwrite) throws IOException { + if (text == null) { + throw new IOException("Snippet is null"); + } + try (BufferedReader r = new BufferedReader(new StringReader(text))) { + List recs = readAll(r, /* requireHeader */ true); + for (Record rec : recs) { + if (!overwrite && byAlias.containsKey(rec.alias)) { + throw new IllegalArgumentException("Alias already exists: " + rec.alias); + } + byAlias.put(rec.alias, rec); + } + } + } + + // --------------------------------------------------------------------- + // Shared IO helpers (used by save/load and export/import) + // --------------------------------------------------------------------- + + /** + * Writes the magic header and all records to {@code w}. + */ + private static void writeAll(Writer w, Collection records) throws IOException { + w.write(MAGIC_HEADER); + w.write('\n'); + for (Record r : records) { + writeRecord(w, r); + } + } + + /** + * Writes a single record in the canonical line-oriented format. + */ + private static void writeRecord(Writer w, Record r) throws IOException { + w.write(ENTRY_MARKER); + w.write('\n'); + w.write("alias="); + w.write(r.alias); + w.write('\n'); + w.write("algorithm="); + w.write(r.algorithm); + w.write('\n'); + w.write("kind="); + w.write(r.kind.name()); + w.write('\n'); + w.write("spec="); + w.write(r.specClass); + w.write('\n'); + + PairSeq.Cursor c = r.specPayload.cursor(); + while (c.next()) { + w.write(PREFIX_SPEC); + w.write(c.key()); + w.write('='); + w.write(c.value()); + w.write('\n'); + } + w.write('\n'); // end of entry + } + + /** + * Reads and returns all records from {@code r}. When {@code requireHeader} is + * true the first non-empty line must equal the magic header. + */ + private static List readAll(BufferedReader r, boolean requireHeader) throws IOException { // NOPMD + List recs = new ArrayList<>(); + String line; + + // header + while ((line = r.readLine()) != null && line.isBlank()) { // NOPMD + /* skip leading blanks */ + } + if (requireHeader && line == null || !MAGIC_HEADER.equals(line.trim())) { + throw new IOException("Unsupported keyring header. Expected: " + MAGIC_HEADER); + } + + String alias; + String algorithm; + Record.Kind kind; + String spec; + List payload; + + alias = algorithm = spec = null; + kind = null; + payload = new ArrayList<>(); + + while ((line = r.readLine()) != null) { + if (line.isEmpty()) { + if (alias != null) { + recs.add(new Record(alias, algorithm, kind, spec, PairSeq.of(payload.toArray(String[]::new)))); + } + alias = algorithm = spec = null; + kind = null; + payload = new ArrayList<>(); // NOPMD + continue; + } + if (line.startsWith("#")) { + continue; + } + + if (ENTRY_MARKER.equals(line)) { + if (alias != null) { + recs.add(new Record(alias, algorithm, kind, spec, PairSeq.of(payload.toArray(String[]::new)))); + } + alias = algorithm = spec = null; + kind = null; + payload = new ArrayList<>(); // NOPMD + continue; + } + + int eq = line.indexOf('='); + if (eq <= 0) { + throw new IOException("Malformed line: " + line); + } + String k = line.substring(0, eq); + String v = line.substring(eq + 1); + + switch (k) { + case "alias" -> alias = v; + case "algorithm" -> algorithm = v; + case "kind" -> kind = Record.Kind.valueOf(v); + case "spec" -> spec = v; + default -> { + if (k.startsWith(PREFIX_SPEC)) { + String sk = k.substring(PREFIX_SPEC.length()); + payload.add(sk); + payload.add(v); + } + } + } + } + + if (alias != null) { + recs.add(new Record(alias, algorithm, kind, spec, PairSeq.of(payload.toArray(String[]::new)))); + } + return recs; + } + + private void put(String alias, String algorithmId, Record.Kind kind, AlgorithmKeySpec importSpec) { + if (alias == null || alias.isBlank()) { + throw new IllegalArgumentException("alias"); + } + if (algorithmId == null || algorithmId.isBlank()) { + throw new IllegalArgumentException("algorithmId"); + } + if (importSpec == null) { + throw new IllegalArgumentException("importSpec"); + } + + PairSeq payload = marshalSpec(importSpec); + + Record r = new Record(alias, algorithmId, kind, importSpec.getClass().getName(), payload); + + byAlias.put(alias, r); + } + + private Record require(String alias, Record.Kind kind) { + Record r = byAlias.get(alias); + if (r == null) { + throw new IllegalArgumentException("No entry: " + alias); + } + if (r.kind != kind) { + throw new IllegalArgumentException("Alias '" + alias + "' is not a " + kind); + } + return r; + } + + /** + * Calls a static {@code marshal(SpecType)} method on the spec class. + * + * @param spec the spec instance to marshal + * @return a pair sequence representing the spec payload + * @throws IllegalStateException if the spec class does not expose a compatible + * static method + */ + private static PairSeq marshalSpec(AlgorithmKeySpec spec) { + try { + Method m = spec.getClass().getMethod("marshal", spec.getClass()); + Object out = m.invoke(null, spec); + return (PairSeq) out; + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Spec class lacks static marshal(Spec): " + spec.getClass().getName(), e); + } catch (IllegalAccessException | InvocationTargetException | SecurityException e) { + throw new IllegalStateException("Spec marshal failed: " + spec.getClass().getName(), e); + } + } + + /** + * Calls a static {@code unmarshal(PairSeq)} method on the spec class. + * + * @param spec type + * @param specClass fully qualified spec class name + * @param p the pair sequence to unmarshal + * @return the reconstructed spec instance + * @throws IllegalStateException if reflection fails or the method is absent + */ + @SuppressWarnings("unchecked") + private static S unmarshalSpec(String specClass, PairSeq p) { + try { + Class cls = Class.forName(specClass); + Method m = cls.getMethod("unmarshal", PairSeq.class); + Object out = m.invoke(null, p); + return (S) out; + } catch (IllegalAccessException | InvocationTargetException | ClassNotFoundException | NoSuchMethodException + | SecurityException e) { + throw new IllegalStateException("Spec unmarshal failed for " + specClass, e); + } + } +} diff --git a/lib/src/main/java/zeroecho/core/storage/package-info.java b/lib/src/main/java/zeroecho/core/storage/package-info.java new file mode 100644 index 0000000..23384d7 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/storage/package-info.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Human-editable key storage persisted in a compact UTF-8 text format. + * + *

+ * This package provides a lightweight keyring that applications can read and + * write without specialized tooling. The store keeps entries in insertion + * order, allows simple alias-based lookups, and materializes keys through the + * core catalog. + *

+ * + *

Key elements

+ *
    + *
  • {@link KeyringStore} - in-memory map of aliases to immutable records with + * helpers to add, load, save, import, export, and resolve keys.
  • + *
  • {@link KeyringStore.Record} - value object describing one entry: alias, + * algorithm id, {@link KeyringStore.Record.Kind kind}, spec class, and a + * {@link zeroecho.core.marshal.PairSeq} payload.
  • + *
  • Resolution helpers - {@link KeyringStore#getPublic(String)}, + * {@link KeyringStore#getPrivate(String)}, and + * {@link KeyringStore#getSecret(String)} plus + * {@link KeyringStore.PublicWithId}, {@link KeyringStore.PrivateWithId}, + * {@link KeyringStore.SecretWithId} to return the algorithm id together with + * the key.
  • + *
+ * + *

File format

+ *

+ * Files begin with a magic header followed by one or more @entry + * blocks. Keys that belong to the spec payload are prefixed with + * s. to avoid collisions with top-level fields; in-memory they are + * stored without the prefix. Lines beginning with # are comments. + * A blank line terminates an entry. + *

+ * + *
{@code
+ * # KeyringStore v1
+ * @entry
+ * alias=my-rsa
+ * algorithm=RSA
+ * kind=PUBLIC_KEY
+ * spec=zeroecho.core.alg.rsa.RsaPublicKeySpec
+ * s.x509B64=MIIBIjANBgkqh...
+ * }
+ * + *

Spec marshaling contract

+ *

+ * Each spec class named in the spec field must expose two public + * static methods discovered by reflection: + *

+ *
    + *
  • static PairSeq marshal(SpecType spec)
  • + *
  • static SpecType unmarshal(PairSeq pairs)
  • + *
+ *

+ * See {@link KeyringStore#marshalSpec(zeroecho.core.spec.AlgorithmKeySpec)} and + * {@link KeyringStore#unmarshalSpec(String, zeroecho.core.marshal.PairSeq)} for + * details. + *

+ * + *

Typical usage

{@code
+ * // Create and persist a keyring.
+ * KeyringStore ks = new KeyringStore();
+ * ks.putPublic("site-signing", "Ed25519", myEd25519PublicSpec);
+ * ks.putPrivate("site-signing", "Ed25519", myEd25519PrivateSpec);
+ * ks.save(java.nio.file.Path.of("keyring.txt"));
+ *
+ * // Load and resolve a key later.
+ * KeyringStore reloaded = KeyringStore.load(java.nio.file.Path.of("keyring.txt"));
+ * java.security.PublicKey pub = reloaded.getPublic("site-signing");
+ * }
+ * + *

Notes and recommendations

+ *
    + *
  • Persistence is plaintext. Treat files as sensitive, protect with OS + * permissions, and avoid committing to VCS.
  • + *
  • Resolution delegates to {@link zeroecho.core.CryptoAlgorithms}; the + * algorithm id must be one that the catalog recognizes.
  • + *
  • Records are immutable; + * {@link KeyringStore#putPublic(String, String, zeroecho.core.spec.AlgorithmKeySpec)}, + * {@link KeyringStore#putPrivate(String, String, zeroecho.core.spec.AlgorithmKeySpec)}, + * and + * {@link KeyringStore#putSecret(String, String, zeroecho.core.spec.AlgorithmKeySpec)} + * replace entries by alias.
  • + *
  • Lookups validate kind; for example, + * {@link KeyringStore#getPublic(String)} fails if the alias stores a private + * key.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.core.storage; diff --git a/lib/src/main/java/zeroecho/core/tag/ByteVerificationStrategy.java b/lib/src/main/java/zeroecho/core/tag/ByteVerificationStrategy.java new file mode 100644 index 0000000..0af62ef --- /dev/null +++ b/lib/src/main/java/zeroecho/core/tag/ByteVerificationStrategy.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * 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.core.tag; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.core.err.VerificationException; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; +import zeroecho.core.util.Strings; + +/** + * Verification strategy for comparing two byte arrays in constant time. + * + *

+ * This implementation is intended for cryptographic use cases such as verifying + * signatures, digests, or message authentication codes. The comparison runs in + * constant time with respect to the contents of the arrays, in order to avoid + * timing side-channel leaks that might otherwise reveal partial information. + *

+ * + *

+ * Logging at {@link Level#FINE} is available to report pass or fail outcomes + * with truncated array contents using {@link Strings#toShortString(byte[])}. + *

+ */ +public class ByteVerificationStrategy extends VerificationBiPredicate { + private static final Logger LOG = Logger.getLogger(ByteVerificationStrategy.class.getName()); + + /** + * Verifies that two byte arrays are equal using a constant-time comparison. + * + *

+ * The method compares the arrays byte by byte, accumulating differences without + * early exit. This ensures the runtime depends only on the length of the arrays + * and not on their contents, which is critical in cryptographic contexts. If + * the arrays differ in length or either is {@code null}, the result is + * {@code false}. + *

+ * + *

+ * Example: + *

+ * + *
+     * {@code
+     * ByteVerificationStrategy verifier = new ByteVerificationStrategy();
+     * boolean ok = verifier.verify(signature1, signature2);
+     * if (!ok) {
+     *     throw new VerificationException(signature2, signature1);
+     * }
+     * }
+     * 
+ * + * @param a the first byte array to compare, may be {@code null} + * @param b the second byte array to compare, may be {@code null} + * @return {@code true} if both arrays are non-null, of the same length, and + * have identical contents; {@code false} otherwise + * @throws VerificationException if the comparison process itself fails (not + * typically expected in this implementation) + */ + @Override + public boolean verify(byte[] a, byte[] b) throws VerificationException { + if (a == null || b == null || a.length != b.length) { + return false; + } + int r = 0; + for (int i = 0; i < a.length; i++) { + r |= (a[i] ^ b[i]); + } + + if (LOG.isLoggable(Level.FINE)) { + if (r == 0) { + LOG.log(Level.FINE, "PASS {0} == {1}", // NOPMD + new Object[] { Strings.toShortString(a), Strings.toShortString(b) }); + } else { + LOG.log(Level.FINE, "FAIL {0} != {1}", // NOPMD + new Object[] { Strings.toShortString(a), Strings.toShortString(b) }); + } + } + + return r == 0; + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/tag/SignatureVerificationStrategy.java b/lib/src/main/java/zeroecho/core/tag/SignatureVerificationStrategy.java new file mode 100644 index 0000000..88e037e --- /dev/null +++ b/lib/src/main/java/zeroecho/core/tag/SignatureVerificationStrategy.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * 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.core.tag; + +import java.security.Signature; +import java.security.SignatureException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.core.err.VerificationException; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; +import zeroecho.core.util.Strings; + +/** + * Verification strategy that delegates to a JCA {@link Signature} object. + * + *

+ * This strategy invokes {@link Signature#verify(byte[])} on the supplied + * signature instance to validate the given data (typically a detached signature + * or digest). Logging at {@link Level#FINE} provides pass/fail outcomes using + * {@link Strings#toShortString(byte[])} for concise output. + *

+ * + *

+ * The wrapped {@link SignatureException} is converted into a + * {@link VerificationException} to unify error reporting in ZeroEcho + * verification flows. + *

+ * + *

+ * Example: + *

+ *
+ * {@code
+ * Signature jcaSig = Signature.getInstance("Ed25519");
+ * jcaSig.initVerify(publicKey);
+ * jcaSig.update(message);
+ *
+ * SignatureVerificationStrategy verifier = new SignatureVerificationStrategy();
+ * boolean ok = verifier.verify(jcaSig, detachedSignature);
+ * if (!ok) {
+ *     // handle verification failure
+ * }
+ * }
+ * 
+ */ +public class SignatureVerificationStrategy extends VerificationBiPredicate { + private static final Logger LOG = Logger.getLogger(SignatureVerificationStrategy.class.getName()); + + /** + * Verifies that the provided {@link Signature} object validates the given + * bytes. + * + *

+ * The method calls {@link Signature#verify(byte[])} and logs the outcome at + * {@link Level#FINE}. If verification fails due to a + * {@link SignatureException}, the error is wrapped in a + * {@link VerificationException}. + *

+ * + * @param s the signature object to use for verification, already initialized + * for verification + * @param b the detached signature or digest to validate against + * @return {@code true} if verification succeeds, {@code false} otherwise + * @throws VerificationException if the underlying signature operation fails + */ + @Override + public boolean verify(Signature s, byte[] b) throws VerificationException { + try { + boolean result = s.verify(b); + + if (LOG.isLoggable(Level.FINE)) { + if (result) { + LOG.log(Level.FINE, "PASS {0}", Strings.toShortString(b)); // NOPMD + } else { + LOG.log(Level.FINE, "FAIL {0}", Strings.toShortString(b)); // NOPMD + } + } + + return result; + + } catch (SignatureException e) { + throw new VerificationException(e, b); + } + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/core/tag/TagEngine.java b/lib/src/main/java/zeroecho/core/tag/TagEngine.java new file mode 100644 index 0000000..3f8ca41 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/tag/TagEngine.java @@ -0,0 +1,210 @@ +/******************************************************************************* + * 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.core.tag; + +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.Logger; + +import zeroecho.core.alg.common.sig.GenericJcaSignatureContext; +import zeroecho.core.alg.digest.JcaDigestContext; +import zeroecho.core.alg.rsa.RsaSignatureContext; +import zeroecho.core.tag.ThrowingBiPredicate.VerificationBiPredicate; + +/** + * Minimal streaming engine for producing or verifying authentication tags on + * byte streams. + * + *

+ * A {@code TagEngine} adapts stateful primitives (for example + * {@link java.security.MessageDigest}, {@link java.security.Signature}, or HMAC + * engines) into a streaming pipeline: a single-use wrapper consumes all bytes + * from an upstream {@link java.io.InputStream} and, depending on mode, either + * appends a trailer (produce mode) or verifies a caller-supplied tag (verify + * mode). + *

+ * + *

Operation modes

+ *
    + *
  • Produce mode: {@link #wrap(InputStream)} returns a passthrough + * stream. All upstream bytes are emitted unchanged; when end-of-stream is + * reached, the computed tag is appended as a trailer to the output.
  • + *
  • Verify mode: The caller first supplies the expected tag via + * {@link #setExpectedTag(byte[])} and ensures the upstream does not include the + * trailer (for example, by using a tail-stripping wrapper). The returned stream + * emits only the body. At EOF, the engine compares the computed tag with the + * expected tag and reports the outcome using the configured verification + * approach.
  • + *
+ * + *

Verification approaches

+ *

+ * Verification outcome handling is delegated to a + * {@link VerificationBiPredicate}. Implementations should provide a sensible + * default (for example, constant-time equality on byte arrays). Callers may + * replace or decorate the approach with helpers such as + * {@link VerificationBiPredicate#getThrowOnMismatch()} or + * {@link VerificationBiPredicate#getFlagOkInCtx(conflux.CtxInterface, conflux.Key)}. + *

+ * + *

Implementation notes

+ *
    + *
  • Trailer emission must happen exactly once and only in produce mode. No + * trailer is ever emitted in verify mode.
  • + *
  • Engines are stateful and not thread-safe. Each instance is intended for + * one pipeline run.
  • + *
  • Typical adapters include: + *
      + *
    • {@link JcaDigestContext} for SHA-2 and SHA-3 digests
    • + *
    • {@link GenericJcaSignatureContext} for JCA + * {@link java.security.Signature}
    • + *
    • {@link RsaSignatureContext} for RSA-PSS and PKCS#1 signatures
    • + *
    + *
  • + *
+ * + *

+ * Direct access to the raw tag is not exposed; engines either append it + * (produce mode) or check it internally (verify mode). + *

+ * + *

+ * Example: + *

+ *
+ * {@code
+ * TagEngine digestEngine = TagEngineBuilder.digest(DigestSpec.sha256()).get();
+ * InputStream wrapped = digestEngine.wrap(plaintextIn);
+ * // Read all bytes from wrapped; the SHA-256 trailer is appended automatically.
+ *
+ * TagEngine verifyEngine =
+ *         TagEngineBuilder.ed25519Verify(publicKey).get();
+ * verifyEngine.setVerificationApproach(
+ *         verifyEngine.getVerificationCore().getThrowOnMismatch());
+ * verifyEngine.setExpectedTag(expectedTag);
+ * InputStream verifyWrapped = verifyEngine.wrap(bodyWithoutTrailer);
+ * // Read to EOF; throws on mismatch due to the configured approach.
+ * }
+ * 
+ * + * @param auxiliary type used by the concrete engine (for example + * {@link java.security.Signature} for signature engines or + * {@code byte[]} for digest/HMAC engines) + * + * @see JcaDigestContext + * @see GenericJcaSignatureContext + * @see RsaSignatureContext + */ +public interface TagEngine { + Logger LOG = Logger.getLogger(TagEngine.class.getName()); + + /** + * Wraps the given upstream stream with a filtering stream that both passes + * bytes through and updates the tag engine state. + * + *

+ * In produce mode, the wrapper emits the original data followed by the + * generated tag trailer when EOF is reached. In verify mode, the wrapper + * validates the expected tag either immediately or at EOF, depending on the + * engine and the configured verification approach. + *

+ * + * @param upstream source of plaintext or ciphertext; must not be {@code null} + * @return a new stream that applies tag computation or verification + * @throws IOException if wrapping fails or the engine cannot allocate required + * resources + */ + InputStream wrap(InputStream upstream) throws IOException; + + /** + * Returns the fixed length in bytes of the final authentication tag. + * + *

+ * Higher layers use this value to strip or append trailers during stream + * processing. + *

+ * + * @return the tag length in bytes + */ + int tagLength(); + + /** + * Supplies the expected tag for verify mode. + * + *

+ * Engines may verify immediately if the computed tag is already known, or they + * may defer verification until EOF. This method is invoked at most once per + * wrapped stream. + *

+ * + *

+ * The default implementation throws {@link UnsupportedOperationException}. + * Engines that support verification must override this method. + *

+ * + * @param expected expected tag value; implementations may defensively copy this + * array + * @throws UnsupportedOperationException if the engine does not support + * verification + */ + default void setExpectedTag(byte[] expected) { + // default: engines that don't support verify can refuse + throw new UnsupportedOperationException("Verification not supported by this TagEngine"); + } + + /** + * Sets the verification approach used to compare computed and expected tags. + * + *

+ * Typical implementations will default to a constant-time comparison for + * {@code byte[]} values. Callers may replace or decorate the strategy, for + * example with {@link VerificationBiPredicate#getThrowOnMismatch()} to raise an + * exception on mismatch, or + * {@link VerificationBiPredicate#getFlagOkInCtx(conflux.CtxInterface, conflux.Key)} + * to record the outcome in a context. + *

+ * + * @param strategy the verification predicate to use; must not be {@code null} + */ + void setVerificationApproach(VerificationBiPredicate strategy); + + /** + * Returns the current verification approach. + * + * @return the verification predicate in use; never {@code null} after + * initialization + */ + VerificationBiPredicate getVerificationCore(); +} diff --git a/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java b/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java new file mode 100644 index 0000000..57aa534 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/tag/TagEngineBuilder.java @@ -0,0 +1,357 @@ +/******************************************************************************* + * 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.core.tag; + +import java.io.IOException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.util.Objects; +import java.util.function.Supplier; + +import javax.crypto.SecretKey; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.NullKey; +import zeroecho.core.alg.digest.DigestSpec; +import zeroecho.core.alg.ecdsa.EcdsaCurveSpec; +import zeroecho.core.alg.hmac.HmacSpec; +import zeroecho.core.alg.rsa.RsaSigSpec; +import zeroecho.core.spec.ContextSpec; +import zeroecho.core.spec.VoidSpec; + +/** + * Factory-style builder that supplies fresh {@link TagEngine} instances on + * demand. + * + *

+ * Each {@code TagEngineBuilder} holds a factory that typically delegates to + * {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, ContextSpec)} + * so that global policy and auditing are consistently enforced. A new engine + * instance is created for each call to {@link #get()}. + *

+ * + *

+ * The type parameter {@code T} reflects the auxiliary type used by the concrete + * engine (for example {@link java.security.Signature} for signature engines, or + * {@code byte[]} for digest and HMAC engines). + *

+ * + *

Usage

+ * {@code
+ * // Digest-based tag
+ * TagTrailerDataContentBuilder tag =
+ *     new TagTrailerDataContentBuilder(
+ *         TagEngineBuilder.digest(DigestSpec.sha256()));
+ *
+ * // Digital signature tag (Ed25519)
+ * TagTrailerDataContentBuilder sig =
+ *     new TagTrailerDataContentBuilder(
+ *         TagEngineBuilder.ed25519Sign(privateKey));
+ * }
+ * 
+ * + * @param auxiliary type used by the engine (for example + * {@link java.security.Signature}) + * @since 1.0 + */ +public final class TagEngineBuilder implements Supplier> { + private final Supplier> factory; + + private TagEngineBuilder(Supplier> factory) { + this.factory = Objects.requireNonNull(factory, "factory"); + } + + /** + * Returns a new {@link TagEngine} produced by the underlying factory. + * + * @return a fresh tag engine instance + */ + @Override + public TagEngine get() { + return factory.get(); + } + + /** + * Creates a builder for an unkeyed digest engine. + * + *

+ * If {@code spec} is {@code null}, {@link DigestSpec#sha256()} is used. + *

+ * + * @param spec digest specification; may be {@code null} to select the default + * @return a builder that produces digest-based {@link TagEngine} instances + */ + public static TagEngineBuilder digest(final DigestSpec spec) { + final DigestSpec s = spec == null ? DigestSpec.sha256() : spec; + return new TagEngineBuilder<>(() -> { + try { + // JcaDigestContext implements DigestContext extends TagEngine + return CryptoAlgorithms.create("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, s); + } catch (IOException e) { + throw new IllegalStateException("Failed to create DIGEST TagEngine", e); + } + }); + } + + /** + * Creates a builder for a keyed HMAC engine. + * + *

+ * If {@code spec} is {@code null}, {@link HmacSpec#sha256()} is used. + *

+ * + * @param key secret key used for MAC computation; must not be {@code null} + * @param spec HMAC specification; may be {@code null} to select the default + * @return a builder that produces HMAC-based {@link TagEngine} instances + * @throws NullPointerException if {@code key} is {@code null} + */ + public static TagEngineBuilder hmac(final SecretKey key, final HmacSpec spec) { + Objects.requireNonNull(key, "key"); + final HmacSpec s = spec == null ? HmacSpec.sha256() : spec; + return new TagEngineBuilder<>(() -> { + try { + // HmacMacContext implements MacContext extends TagEngine + return CryptoAlgorithms.create("HMAC", KeyUsage.MAC, key, s); + } catch (IOException e) { + throw new IllegalStateException("Failed to create HMAC TagEngine", e); + } + }); + } + + /** + * Creates a builder for a signature engine of the given algorithm. + * + *

+ * The key type determines the role: a {@link PrivateKey} selects + * {@link KeyUsage#SIGN}, and a {@link PublicKey} selects + * {@link KeyUsage#VERIFY}. + *

+ * + *

+ * If {@code spec} is {@code null}, {@link VoidSpec#INSTANCE} is used. + *

+ * + * @param id canonical algorithm identifier (for example "Ed25519", "RSA", + * "ECDSA"); must not be {@code null} + * @param key private or public key to bind the engine; must not be + * {@code null} + * @param spec context specification; may be {@code null} to select the default + * @return a builder that produces signature-based {@link TagEngine} instances + * @throws IllegalArgumentException if {@code key} is not a supported type + * @throws NullPointerException if {@code id} or {@code key} is {@code null} + */ + public static TagEngineBuilder signature(final String id, final java.security.Key key, + final ContextSpec spec) { + Objects.requireNonNull(id, "id"); + Objects.requireNonNull(key, "key"); + + final KeyUsage role = (key instanceof PrivateKey) ? KeyUsage.SIGN + : (key instanceof PublicKey) ? KeyUsage.VERIFY : null; + if (role == null) { + throw new IllegalArgumentException("Unsupported key type for signature engine: " + key.getClass()); + } + + final ContextSpec s = spec == null ? VoidSpec.INSTANCE : spec; + return new TagEngineBuilder<>(() -> { + try { + // RsaSignatureContext / Ed25519SignatureContext implement SignatureContext + // extends TagEngine + return CryptoAlgorithms.create(id, role, key, s); + } catch (IOException e) { + throw new IllegalStateException("Failed to create " + id + " signature TagEngine", e); + } + }); + } + + /** + * Creates a builder for an Ed25519 signing engine. + * + * @param privateKey private signing key; must not be {@code null} + * @return a builder that produces Ed25519 signature engines in SIGN mode + * @throws NullPointerException if {@code privateKey} is {@code null} + */ + public static TagEngineBuilder ed25519Sign(final PrivateKey privateKey) { + Objects.requireNonNull(privateKey, "privateKey"); + return signature("Ed25519", privateKey, VoidSpec.INSTANCE); + } + + /** + * Creates a builder for an Ed25519 verification engine. + * + * @param publicKey public verification key; must not be {@code null} + * @return a builder that produces Ed25519 signature engines in VERIFY mode + * @throws NullPointerException if {@code publicKey} is {@code null} + */ + public static TagEngineBuilder ed25519Verify(final PublicKey publicKey) { + Objects.requireNonNull(publicKey, "publicKey"); + return signature("Ed25519", publicKey, VoidSpec.INSTANCE); + } + + /** + * Creates a builder for an RSA signing engine. + * + *

+ * If {@code spec} is {@code null}, PKCS#1 PSS with SHA-256 and a 32-byte salt + * is used. + *

+ * + * @param privateKey private signing key; must not be {@code null} + * @param spec RSA signature specification; may be {@code null} to select + * the default + * @return a builder that produces RSA signature engines in SIGN mode + * @throws NullPointerException if {@code privateKey} is {@code null} + */ + public static TagEngineBuilder rsaSign(final PrivateKey privateKey, final RsaSigSpec spec) { + Objects.requireNonNull(privateKey, "privateKey"); + return signature("RSA", privateKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec); + } + + /** + * Creates a builder for an RSA verification engine. + * + *

+ * If {@code spec} is {@code null}, PKCS#1 PSS with SHA-256 and a 32-byte salt + * is used. + *

+ * + * @param publicKey public verification key; must not be {@code null} + * @param spec RSA signature specification; may be {@code null} to select + * the default + * @return a builder that produces RSA signature engines in VERIFY mode + * @throws NullPointerException if {@code publicKey} is {@code null} + */ + public static TagEngineBuilder rsaVerify(final PublicKey publicKey, final RsaSigSpec spec) { + Objects.requireNonNull(publicKey, "publicKey"); + return signature("RSA", publicKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec); + } + + /** + * Creates a builder for an ECDSA signing engine with the specified curve. + * + *

+ * If {@code spec} is {@code null}, {@link EcdsaCurveSpec#P256} is used. + *

+ * + * @param privateKey private signing key; must not be {@code null} + * @param spec curve specification; may be {@code null} to select the + * default + * @return a builder that produces ECDSA signature engines in SIGN mode + * @throws NullPointerException if {@code privateKey} is {@code null} + */ + public static TagEngineBuilder ecdsaSign(final PrivateKey privateKey, final EcdsaCurveSpec spec) { + Objects.requireNonNull(privateKey, "privateKey"); + final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec; + return signature("ECDSA", privateKey, s); + } + + /** + * Creates a builder for an ECDSA verification engine with the specified curve. + * + *

+ * If {@code spec} is {@code null}, {@link EcdsaCurveSpec#P256} is used. + *

+ * + * @param publicKey public verification key; must not be {@code null} + * @param spec curve specification; may be {@code null} to select the + * default + * @return a builder that produces ECDSA signature engines in VERIFY mode + * @throws NullPointerException if {@code publicKey} is {@code null} + */ + public static TagEngineBuilder ecdsaVerify(final PublicKey publicKey, final EcdsaCurveSpec spec) { + Objects.requireNonNull(publicKey, "publicKey"); + final EcdsaCurveSpec s = spec == null ? EcdsaCurveSpec.P256 : spec; + return signature("ECDSA", publicKey, s); + } + + /** + * Creates a builder for an ECDSA P-256 signing engine. + * + * @param privateKey private signing key; must not be {@code null} + * @return a builder that produces ECDSA signature engines in SIGN mode + * @throws NullPointerException if {@code privateKey} is {@code null} + */ + public static TagEngineBuilder ecdsaP256Sign(final PrivateKey privateKey) { + Objects.requireNonNull(privateKey, "privateKey"); + return signature("ECDSA", privateKey, EcdsaCurveSpec.P256); + } + + /** + * Creates a builder for an ECDSA P-256 verification engine. + * + * @param publicKey public verification key; must not be {@code null} + * @return a builder that produces ECDSA signature engines in VERIFY mode + * @throws NullPointerException if {@code publicKey} is {@code null} + */ + public static TagEngineBuilder ecdsaP256Verify(final PublicKey publicKey) { + Objects.requireNonNull(publicKey, "publicKey"); + return signature("ECDSA", publicKey, EcdsaCurveSpec.P256); + } + + /** + * Creates a builder for a SPHINCS+ signing engine. + * + *

+ * Requires the BouncyCastle PQC provider and a registered "SPHINCS+" algorithm + * in {@link CryptoAlgorithms}. + *

+ * + * @param privateKey private signing key; must not be {@code null} + * @return a builder that produces SPHINCS+ signature engines in SIGN mode + * @throws NullPointerException if {@code privateKey} is {@code null} + */ + public static TagEngineBuilder sphincsPlusSign(final PrivateKey privateKey) { + Objects.requireNonNull(privateKey, "privateKey"); + return signature("SPHINCS+", privateKey, VoidSpec.INSTANCE); + } + + /** + * Creates a builder for a SPHINCS+ verification engine. + * + *

+ * Requires the BouncyCastle PQC provider and a registered "SPHINCS+" algorithm + * in {@link CryptoAlgorithms}. + *

+ * + * @param publicKey public verification key; must not be {@code null} + * @return a builder that produces SPHINCS+ signature engines in VERIFY mode + * @throws NullPointerException if {@code publicKey} is {@code null} + */ + public static TagEngineBuilder sphincsPlusVerify(final PublicKey publicKey) { + Objects.requireNonNull(publicKey, "publicKey"); + return signature("SPHINCS+", publicKey, VoidSpec.INSTANCE); + } +} diff --git a/lib/src/main/java/zeroecho/core/tag/ThrowingBiPredicate.java b/lib/src/main/java/zeroecho/core/tag/ThrowingBiPredicate.java new file mode 100644 index 0000000..2ce0dcd --- /dev/null +++ b/lib/src/main/java/zeroecho/core/tag/ThrowingBiPredicate.java @@ -0,0 +1,306 @@ +/******************************************************************************* + * 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.core.tag; + +import java.util.Objects; + +import conflux.CtxInterface; +import conflux.Key; +import zeroecho.core.err.VerificationException; + +/** + * A functional predicate of two arguments that may throw a checked exception. + * + *

+ * This contract mirrors {@code java.util.function.BiPredicate} but allows the + * {@link #verify(Object, Object)} method to throw a checked exception. It is + * intended for verification and comparison routines that may fail due to + * validation or cryptographic errors, while still enabling use in lambda + * expressions and method references. + *

+ * + *

+ * Example: + *

+ *
+ * {@code
+ * ThrowingBiPredicate constantTimeEq =
+ *         new ByteVerificationStrategy()::verify;
+ *
+ * boolean ok = constantTimeEq.verify(actualSignature, expectedSignature);
+ * }
+ * 
+ * + * @param the type of the first input to the predicate + * @param the type of the second input to the predicate + * @param the type of exception that may be thrown + */ +@FunctionalInterface +public interface ThrowingBiPredicate { + /** + * Evaluates this predicate on the given arguments. + * + * @param t the first input value + * @param u the second input value + * @return {@code true} if the predicate holds for the supplied values; + * {@code false} otherwise + * @throws E if evaluation fails and the implementation chooses to signal the + * failure via an exception + */ + boolean verify(T t, U u) throws E; + + /** + * Base type for verification strategies that compare a value of type {@code T} + * to a byte array and may throw {@link VerificationException}. + * + *

+ * Implementations define the {@link #verify(Object, byte[])} method to perform + * the actual check (for example, constant-time byte comparison). This base type + * also provides adapters to compose behavior, such as throwing on mismatch or + * recording the result in an execution context. + *

+ * + *

+ * Example: + *

+ *
+     * {@code
+     * VerificationBiPredicate verifier = new ByteVerificationStrategy();
+     * boolean ok = verifier.verify(actual, expected);
+     *
+     * // Decorate to throw an exception when verification fails:
+     * verifier.getThrowOnMismatch().verify(actual, expected);
+     * }
+     * 
+ * + * @param the type of the first value being verified + */ + abstract class VerificationBiPredicate implements ThrowingBiPredicate { + /** + * Verifies that {@code t} matches {@code u} according to the implementation + * contract. + * + * @param t the first value to verify + * @param u the byte array to verify against + * @return {@code true} if the values are considered a match; {@code false} + * otherwise + * @throws VerificationException if verification cannot be completed or must be + * signaled as an error + */ + @Override + public abstract boolean verify(T t, byte[] u) throws VerificationException; + + /** + * Returns a decorator that throws {@link VerificationException} if verification + * fails. + * + *

+ * The returned predicate delegates to this instance. When the delegated + * verification returns {@code false}, the decorator throws a + * {@link VerificationException} carrying the most relevant values for + * diagnostics. + *

+ * + *

+ * Example: + *

+ *
+         * {@code
+         * VerificationBiPredicate mustMatch =
+         *         new ByteVerificationStrategy().getThrowOnMismatch();
+         *
+         * // Will throw VerificationException if arrays do not match
+         * mustMatch.verify(actual, expected);
+         * }
+         * 
+ * + * @return a verification predicate that throws on mismatch + */ + public VerificationBiPredicate getThrowOnMismatch() { + return new ThrowOnMismatch<>(this); + } + + /** + * Returns a decorator that records the verification outcome into the supplied + * context. + * + *

+ * The returned predicate delegates to this instance, stores the boolean result + * under {@code verifyKey} in {@code ctx}, and then returns that result. This is + * useful when callers need to access the outcome outside the immediate call + * path. + *

+ * + *

+ * Example: + *

+ *
+         * {@code
+         * VerificationBiPredicate withFlag =
+         *         new ByteVerificationStrategy().getFlagOkInCtx(ctx, VERIFY_KEY);
+         *
+         * boolean ok = withFlag.verify(actual, expected);
+         * // Later: ctx.get(VERIFY_KEY) yields the same boolean
+         * }
+         * 
+ * + * @param ctx the context into which the result is stored; must not be + * {@code null} + * @param verifyKey the key under which the boolean result is stored; must not + * be {@code null} + * @return a verification predicate that records the outcome in the provided + * context + */ + public VerificationBiPredicate getFlagOkInCtx(CtxInterface ctx, Key verifyKey) { + return new FlagOkInCtx<>(this, ctx, verifyKey); + } + } + + /** + * Decorator that throws {@link VerificationException} when the delegated + * verification fails. + * + *

+ * If the value {@code t} being verified is a {@code byte[]} instance, the + * thrown exception includes both the expected and calculated arrays for clearer + * diagnostics. Otherwise the exception includes the byte array argument. + *

+ * + * @param the type of the first value being verified + */ + final class ThrowOnMismatch extends VerificationBiPredicate { + /** + * The underlying verification strategy to delegate to. + */ + private final VerificationBiPredicate delegate; + + /** + * Creates a new throwing decorator. + * + * @param delegate the underlying verifier to call; must not be {@code null} + */ + public ThrowOnMismatch(VerificationBiPredicate delegate) { + super(); + + this.delegate = Objects.requireNonNull(delegate); + } + + /** + * Verifies that {@code t} matches {@code u}; throws on mismatch. + * + * @param t the first value to verify + * @param u the byte array to verify against + * @return {@code true} if the values match + * @throws VerificationException if the values do not match, or if the delegate + * throws + */ + @Override + public boolean verify(T t, byte[] u) throws VerificationException { + boolean equality = delegate.verify(t, u); + + if (!equality) { + if (t instanceof byte[] array) { + throw new VerificationException(array, u); + } else { + throw new VerificationException(u); + } + } + return equality; + } + + } + + /** + * Decorator that records the verification outcome in a context before returning + * it. + * + *

+ * The result of {@link #verify(Object, byte[])} is stored under the provided + * {@code verifyKey} in {@code ctx}. The recorded value is {@code true} for a + * successful verification and {@code false} otherwise. + *

+ * + * @param the type of the first value being verified + */ + final class FlagOkInCtx extends VerificationBiPredicate { + /** + * The underlying verification strategy to delegate to. + */ + private final VerificationBiPredicate delegate; + /** + * The context that will receive the verification outcome. + */ + private final CtxInterface ctx; + /** + * The key under which the boolean verification outcome is stored. + */ + private final Key verifyKey; + + /** + * Creates a new context-flagging decorator. + * + * @param delegate the underlying verifier to call; must not be {@code null} + * @param ctx the context that will store the boolean outcome; must not be + * {@code null} + * @param verifyKey the key under which the outcome is recorded; must not be + * {@code null} + */ + public FlagOkInCtx(VerificationBiPredicate delegate, CtxInterface ctx, Key verifyKey) { + super(); + + this.delegate = Objects.requireNonNull(delegate); + this.ctx = Objects.requireNonNull(ctx, "context cannot be null"); + this.verifyKey = Objects.requireNonNull(verifyKey, "verification-key cannot be null"); + } + + /** + * Verifies that {@code s} matches {@code b}, records the outcome, and returns + * it. + * + * @param s the first value to verify + * @param b the byte array to verify against + * @return {@code true} if the values match; {@code false} otherwise + * @throws VerificationException if the delegate throws during verification + */ + @Override + public boolean verify(T s, byte[] b) throws VerificationException { + boolean equality = delegate.verify(s, b); + + ctx.put(verifyKey, equality); + + return equality; + } + } +} diff --git a/lib/src/main/java/zeroecho/core/tag/package-info.java b/lib/src/main/java/zeroecho/core/tag/package-info.java new file mode 100644 index 0000000..b5e9c80 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/tag/package-info.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Streaming tag computation and verification for digests, MACs, and digital + * signatures. + * + *

+ * This package defines a minimal engine interface for tag-producing primitives + * and a small builder that instantiates engines backed by registered + * algorithms. Engines operate in a single-use, pipeline-friendly style: they + * wrap an upstream stream, update internal state as bytes flow, and then either + * append a fixed-length trailer (produce mode) or verify an expected tag + * (verify mode) according to a configurable verification approach. + *

+ * + *

Engines and verification

+ *

+ * The core abstraction is {@link TagEngine}, which adapts stateful primitives + * such as {@link java.security.MessageDigest}, {@link java.security.Signature}, + * or HMAC engines to a streaming model. Verification behavior is delegated to a + * strategy implementing {@link ThrowingBiPredicate.VerificationBiPredicate}, + * enabling constant-time equality for byte arrays or delegation to a JCA + * {@link java.security.Signature}. Callers can swap or decorate the strategy, + * for example to throw on mismatch or to record the result in a context object. + *

+ * + *

Operation modes

+ *
    + *
  • Produce mode: {@link TagEngine#wrap(java.io.InputStream)} returns + * a pass-through stream that emits the original data and, at end-of-stream, + * appends the computed tag as a trailer.
  • + *
  • Verify mode: The caller supplies the expected tag via + * {@link TagEngine#setExpectedTag(byte[])} and ensures the upstream body does + * not include a trailer. At end-of-stream, the engine compares the computed tag + * with the expected tag and reports the outcome using the configured + * verification approach set via + * {@link TagEngine#setVerificationApproach(ThrowingBiPredicate.VerificationBiPredicate)}. + *
  • + *
+ * + *

Typical usage

+ *

Produce an HMAC trailer

+ * {@code
+ * javax.crypto.SecretKey key = ...;
+ * HmacSpec spec = HmacSpec.sha256();
+ *
+ * TagEngine<byte[]> eng = TagEngineBuilder.hmac(key, spec).get();
+ * try (java.io.InputStream in = eng.wrap(upstream)) {
+ *     in.transferTo(out); // body bytes, then HMAC trailer are written to 'out'
+ * }
+ * }
+ * 
+ * + *

Verify a detached signature and throw on mismatch

+ * {@code
+ * java.security.PublicKey pub = ...;
+ * byte[] expectedSig = ...; // obtained out-of-band
+ *
+ * TagEngine<java.security.Signature> eng =
+ *     TagEngineBuilder.ed25519Verify(pub).get();
+ *
+ * // Throw on mismatch at EOF:
+ * eng.setVerificationApproach(eng.getVerificationCore().getThrowOnMismatch());
+ * eng.setExpectedTag(expectedSig);
+ *
+ * try (java.io.InputStream in = eng.wrap(bodyWithoutTrailer)) {
+ *     in.transferTo(java.io.OutputStream.nullOutputStream());
+ * } // VerificationException wrapped in IOException may be raised by the engine at EOF
+ * }
+ * 
+ * + *

Record verification outcome in a context

+ * {@code
+ * CtxInterface ctx = ...;
+ * Key<Boolean> VERIFY_OK = ...;
+ *
+ * TagEngine<byte[]> eng = TagEngineBuilder.digest(DigestSpec.sha256()).get();
+ * eng.setVerificationApproach(eng.getVerificationCore().getFlagOkInCtx(ctx, VERIFY_OK));
+ * eng.setExpectedTag(expectedDigest);
+ *
+ * try (java.io.InputStream in = eng.wrap(bodyWithoutTrailer)) {
+ *     in.transferTo(java.io.OutputStream.nullOutputStream());
+ * }
+ * Boolean ok = ctx.get(VERIFY_OK);
+ * }
+ * 
+ * + *

Design notes

+ *
    + *
  • Engines are stateful, not thread-safe, and intended for one pipeline run + * per instance.
  • + *
  • {@link TagEngine#tagLength()} is fixed for a given engine and is used by + * higher layers to size or strip trailers when composing pipelines.
  • + *
  • Verification approaches include constant-time byte comparison + * ({@link ByteVerificationStrategy}) and JCA-signature-based verification + * ({@link SignatureVerificationStrategy}). Applications may provide custom + * strategies.
  • + *
  • No direct accessor to raw tags is exposed. Engines either append tags + * (produce mode) or verify them internally (verify mode).
  • + *
+ * + *

Builders

+ *

+ * {@link TagEngineBuilder} provides factories for common primitives, including + * digest, HMAC, and signature engines (Ed25519, RSA, ECDSA, SPHINCS+). Each + * call to {@link TagEngineBuilder#get()} returns a fresh {@link TagEngine} + * instance. + *

+ * + * @since 1.0 + */ +package zeroecho.core.tag; diff --git a/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java b/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java new file mode 100644 index 0000000..e56a033 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/util/GenerateCryptoCatalogTable.java @@ -0,0 +1,247 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.util; + +import java.io.File; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import zeroecho.core.Capability; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.sdk.util.BouncyCastleActivator; + +/** + * Emits an HTML fragment with a table that summarizes discovered cryptographic + * algorithms. The table includes subcolumns for algorithm families and key + * usages found in capabilities. + * + *

Columns

+ *
    + *
  • Algorithm ID
  • + *
  • Display Name
  • + *
  • Family:* (one column per family present)
  • + *
  • Key usage:* (one column per role present)
  • + *
  • Default spec
  • + *
+ * + *

+ * Usage: + *

+ *
{@code
+ *   java zeroecho.tools.GenerateCryptoCatalogTable /abs/path/to/table.html
+ * }
+ */ +public final class GenerateCryptoCatalogTable { + + /** + * Generates a standalone HTML fragment (no <html> or <head>) to the + * given file. + * + * @param args the first argument must be the output file path for the generated + * fragment + * @throws Exception if writing fails or discovery finds invalid providers + */ + public static void main(String[] args) throws Exception { // NOPMD + if (args.length != 1) { // NOPMD + System.err.println("Expected 1 argument: output fragment path"); + System.exit(2); + } + + BouncyCastleActivator.init(); + + File out = new File(args[0]); + out.getParentFile().mkdirs(); + + // Discover providers directly so we do not rely on CryptoCatalog internals. + Map algos = loadAlgorithms(); + validate(algos); + + // Enumerate subcolumns actually present. + SortedSet families = new TreeSet<>(); + SortedSet roles = new TreeSet<>(); + for (CryptoAlgorithm a : algos.values()) { + for (Capability c : a.listCapabilities()) { + families.add(c.family().name()); + roles.add(c.role().name()); + } + } + + try (Writer w = Files.newBufferedWriter(Paths.get(out.toURI()))) { + // Optional minimal style and anchor for local TOC links. + w.write("\n"); + w.write("

"); + w.write("Table of registered cryptographic algorithms with grouped columns for families and key usages."); + w.write("

\n"); + + // Open table + w.write(""); + w.write(""); + + // THEAD row 1: group headers + w.write(""); + w.write(""); + w.write(""); + w.write(""); + w.write(""); + w.write(""); + w.write(""); + + // THEAD row 2: subheaders (vertical labels) + w.write(""); + int fi = 0; + for (String f : families) { + String cls = "vcol narrow fam" + (fi == 0 ? " fam-first" : ""); + w.write(""); + fi++; + } + int ri = 0; + for (String r : roles) { + String cls = "vcol narrow usage" + (ri == 0 ? " usage-first" : ""); + w.write(""); + ri++; + } + w.write(""); + + // TBODY same as before, but make family/role cells narrow and centered + w.write(""); + for (String id : new TreeSet<>(algos.keySet())) { + CryptoAlgorithm a = algos.get(id); + + Set famHit = new HashSet<>(); // NOPMD + Set roleHit = new HashSet<>(); // NOPMD + LinkedHashSet defaultSpecs = new LinkedHashSet<>(); // NOPMD + + for (Capability c : a.listCapabilities()) { + famHit.add(c.family().name()); + roleHit.add(c.role().name()); + if (c.defaultSpec() != null) { + try { + Object ds = c.defaultSpec().get(); + if (ds != null) { + defaultSpecs.add(esc(labelOf(ds))); + } + } catch (Exception ignore) { // NOPMD + } + } + } + + w.write(""); + w.write(""); + w.write(""); + + int i = 0; + for (String f : families) { + String cls = "zecenter narrow fam-td" + (i == 0 ? " fam-first-td" : ""); + w.write(""); + i++; + } + int j = 0; + for (String r : roles) { + String cls = "zecenter narrow usage-td" + (j == 0 ? " usage-first-td" : ""); + w.write(""); + j++; + } + w.write(""); + w.write(""); + } + w.write("
Registered cryptographic algorithms
Algorithm IDDisplay NameFamilyKey usageDefault spec
" + esc(f) + "" + esc(r) + "
" + esc(a.id()) + "" + esc(a.displayName()) + "" + (famHit.contains(f) ? "✓" : "") + "" + (roleHit.contains(r) ? "✓" : "") + "" + String.join("
", defaultSpecs) + "
\n"); + w.write("\n"); + } + } + + private static Map loadAlgorithms() { + Map m = new HashMap<>(); + ServiceLoader.load(CryptoAlgorithm.class).forEach(a -> { + CryptoAlgorithm prev = m.put(a.id(), a); + if (prev != null) { + throw new IllegalStateException("Duplicate algorithm id: " + a.id()); + } + }); + return m; + } + + private static void validate(Map algos) { + StringBuilder sb = null; + for (CryptoAlgorithm a : algos.values()) { + boolean hasCaps = !a.listCapabilities().isEmpty(); + boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty(); + boolean hasSym = !a.symmetricBuildersInfo().isEmpty(); + if (!hasCaps && !hasAsym && !hasSym) { + if (sb == null) { + sb = new StringBuilder(); // NOPMD + } + sb.append("Algorithm ").append(a.id()).append(" has no capabilities nor key builders.\n"); + } + } + if (sb != null) { + throw new IllegalStateException(sb.toString()); + } + } + + private static String labelOf(Object o) { + if (o == null) { + return "null"; + } + if (o instanceof zeroecho.core.annotation.Describable) { + return ((zeroecho.core.annotation.Describable) o).description(); + } + Class c = (o instanceof Class) ? (Class) o : o.getClass(); + // Match CryptoCatalog.labelOf semantics as closely as possible. + zeroecho.core.annotation.DisplayName dn = c.getAnnotation(zeroecho.core.annotation.DisplayName.class); + if (dn != null) { + return dn.value(); + } + return c.getSimpleName(); + } + + private static String esc(String v) { + if (v == null) { + return ""; + } + return v.replace("&", "&").replace("<", "<").replace(">", ">"); + } + + private GenerateCryptoCatalogTable() { + /* no instances */ } +} diff --git a/lib/src/main/java/zeroecho/core/util/Strings.java b/lib/src/main/java/zeroecho/core/util/Strings.java new file mode 100644 index 0000000..36dc193 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/util/Strings.java @@ -0,0 +1,152 @@ +/******************************************************************************* + * 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.core.util; + +/** + * Utility class providing string conversion helpers for debugging and logging. + * + *

+ * This class focuses on safe and concise representations of binary data and + * other values that are commonly needed in diagnostic messages. It is not + * intended for cryptographic formatting or encoding tasks such as Base64 or + * hexadecimal. + *

+ */ +public final class Strings { + + private Strings() { + // empty + } + + /** + * Converts a byte array into a short, human-readable string representation. + * + *

+ * The representation is a comma-separated list of decimal byte values enclosed + * in square brackets. For arrays longer than 32 elements, the output is + * truncated after 32 elements, followed by an ellipsis marker: {@code [...]}. + *

+ * + *

+ * Examples: + *

+ * + *
+     * {@code
+     * Strings.toShortString(null)        -> "null"
+     * Strings.toShortString(new byte[0]) -> "[]"
+     * Strings.toShortString(new byte[]{1, 2, 3})
+     *     -> "[1, 2, 3]"
+     * Strings.toShortString(new byte[40])
+     *     -> "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...]"
+     * }
+     * 
+ * + * @param a the byte array to format, may be {@code null} + * @return a short string representation of the byte array, never {@code null} + */ + public static String toShortString(byte[] a) { + if (a == null) { + return "null"; + } + int iMax = a.length - 1; + if (iMax == -1) { + return "[]"; + } + + StringBuilder b = new StringBuilder(); + b.append('['); + for (int i = 0;; i++) { + if (i == 32) { // NOPMD + return b.append("...]").toString(); + } + b.append(a[i]); + if (i == iMax) { + return b.append(']').toString(); + } + b.append(", "); + } + } + + /** + * Converts a byte array into a compact hexadecimal string representation. + * + *

+ * The output starts with {@code "[0x"}, concatenates each byte as a two-digit + * lowercase hexadecimal value with no separators, and ends with {@code "]"}. If + * the array contains more than 32 bytes, only the first 32 are included, + * followed by {@code "...]"} to indicate truncation. + *

+ * + *

+ * Special cases: + *

+ *
    + *
  • If {@code a} is {@code null}, the string {@code "null"} is returned.
  • + *
  • If {@code a} is empty, the string {@code "[]"} is returned.
  • + *
+ * + *

Examples

{@code
+     * toShortHexString(null)                   -> "null"
+     * toShortHexString(new byte[0])            -> "[]"
+     * toShortHexString(new byte[]{0x01})       -> "[0x01]"
+     * toShortHexString(new byte[]{0x01,0x2a})  -> "[0x012a]"
+     * }
+ * + * @param a the byte array to convert, may be {@code null} + * @return a compact hexadecimal string representation + */ + public static String toShortHexString(byte[] a) { + if (a == null) { + return "null"; + } + int iMax = a.length - 1; + if (iMax == -1) { + return "[]"; + } + + final char[] hexArray = "0123456789abcdef".toCharArray(); + StringBuilder b = new StringBuilder("[0x"); + for (int i = 0;; i++) { + if (i == 32) { // NOPMD + return b.append("...]").toString(); + } + b.append(hexArray[(a[i] >> 4) & 0xf]).append(hexArray[a[i] & 0xf]); + if (i == iMax) { + return b.append(']').toString(); + } + } + } +} diff --git a/lib/src/main/java/zeroecho/core/util/package-info.java b/lib/src/main/java/zeroecho/core/util/package-info.java new file mode 100644 index 0000000..881fec9 --- /dev/null +++ b/lib/src/main/java/zeroecho/core/util/package-info.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * General-purpose helper classes and utility routines. + * + *

+ * This package provides support functionality intended to simplify diagnostics, + * logging, and internal housekeeping tasks across the ZeroEcho codebase. The + * focus is on lightweight, dependency-free utilities that avoid pulling in + * heavy external frameworks for simple needs. + *

+ * + *

+ * Current functionality includes: + *

+ *
    + *
  • Concise string conversion for byte arrays, suitable for debug messages + * and error reporting without exposing full data contents.
  • + *
+ * + *

+ * Future additions may include helpers for array comparison, safe formatting, + * compact object summaries, or other low-level routines that assist in building + * higher-level cryptographic and I/O functionality. + *

+ */ +package zeroecho.core.util; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/logging/ClasspathJulConfig.java b/lib/src/main/java/zeroecho/logging/ClasspathJulConfig.java new file mode 100644 index 0000000..f906b36 --- /dev/null +++ b/lib/src/main/java/zeroecho/logging/ClasspathJulConfig.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * 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.logging; + +import java.io.IOException; +import java.io.InputStream; +import java.util.logging.LogManager; + +/** + * Configures {@link java.util.logging} (JUL) from a {@code jul.properties} + * resource on the classpath. + * + *

+ * When constructed, this class attempts to locate {@code jul.properties} using + * the current thread’s context class loader. If none is available, it falls + * back to the defining class loader. The located resource is then parsed by + * {@link java.util.logging.LogManager#readConfiguration(InputStream)} to + * install JUL handlers, formatters, and levels as specified. + *

+ * + *

Error handling

+ *
    + *
  • If the resource is not found, construction fails with + * {@link IllegalStateException}.
  • + *
  • If a {@link SecurityException} or {@link IOException} occurs while + * applying the configuration, the exception stack trace is printed directly to + * {@code System.err}. This ensures diagnostic visibility even before JUL itself + * is configured.
  • + *
+ * + *

Usage

{@code
+ * // Trigger JUL initialization from classpath
+ * new ClasspathJulConfig();
+ *
+ * // Afterwards, logging is configured per jul.properties
+ * Logger log = Logger.getLogger("my.app");
+ * log.info("Configured via classpath resource");
+ * }
+ * + * @since 1.0 + */ +public final class ClasspathJulConfig { + /** + * Loads {@code jul.properties} from the classpath and applies it as the current + * {@link java.util.logging.LogManager} configuration. + * + * @throws IllegalStateException if the resource {@code jul.properties} is not + * found + */ + public ClasspathJulConfig() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = ClasspathJulConfig.class.getClassLoader(); // NOPMD + } + try (InputStream in = cl.getResourceAsStream("jul.properties")) { + if (in == null) { + throw new IllegalStateException("jul.properties not found on classpath"); + } + LogManager.getLogManager().readConfiguration(in); + } catch (SecurityException | IOException e) { + // As a last resort, print — logging may not be configured yet. + e.printStackTrace(); // NOPMD + } + } +} diff --git a/lib/src/main/java/zeroecho/logging/CompactOneLineFormatter.java b/lib/src/main/java/zeroecho/logging/CompactOneLineFormatter.java new file mode 100644 index 0000000..508fc5f --- /dev/null +++ b/lib/src/main/java/zeroecho/logging/CompactOneLineFormatter.java @@ -0,0 +1,260 @@ +/******************************************************************************* + * 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.logging; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; + +/** + * A compact single-line {@link java.util.logging.Formatter} for JUL log + * records. + * + *

+ * Each record is rendered into one line containing: + *

+ *
    + *
  • timestamp in {@code yyyy-MM-dd HH:mm:ss.SSS} format (local time + * zone),
  • + *
  • log level name,
  • + *
  • current thread name,
  • + *
  • source class and method (abbreviated),
  • + *
  • the formatted message with whitespace collapsed,
  • + *
  • and, if present, exception information: exception type, message, and + * first stack frame, rendered inline.
  • + *
+ * + *

+ * This formatter is designed for console or file logs where compactness and + * readability in a single line per record are preferred over multi-line stack + * traces. Exception stack traces are summarized to avoid overwhelming output; + * only the first frame is shown. + *

+ * + *

Example output

{@code
+ * 2025-09-10 22:15:32.184 INFO [main] c.e.d.p.MyService#start - Started service
+ * 2025-09-10 22:15:33.201 SEVERE [worker-1] c.e.d.p.Worker#run - Task failed
+ *   | ex=IllegalStateException: bad state @ Worker.java:42
+ * }
+ * + *

Abbreviation rules

+ *
    + *
  • Package names are reduced to their initials (e.g., + * {@code com.example.deep.package.MyService} → {@code c.e.d.p.MyService}).
  • + *
  • Simple class names are shortened by keeping uppercase letters before the + * last uppercase, and then the last uppercase plus the suffix (e.g., + * {@code MyServiceManager} → {@code MSManager}, {@code XmlHttpRequest} → + * {@code XHR}, {@code UserDAO} → {@code UDAO}, {@code FooBar} → + * {@code FBar}).
  • + *
+ * + * @since 1.0 + */ +public final class CompactOneLineFormatter extends Formatter { + private static final DateTimeFormatter TS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") + .withZone(ZoneId.systemDefault()); + + /** + * Formats a {@link LogRecord} into a single line. + * + *

+ * The message text has runs of whitespace collapsed to a single space. If + * {@link LogRecord#getThrown()} is non-null, the formatter appends a compact + * suffix in the form {@code " | ex=: @ "}. + *

+ * + * @param r the log record to format + * @return formatted line ending with the platform line separator + */ + @Override + public String format(LogRecord r) { + String ts = TS.format(Instant.ofEpochMilli(r.getMillis())); + String level = r.getLevel().getName(); + String clazz = compactFast(r.getSourceClassName() != null ? r.getSourceClassName() : r.getLoggerName()); + String method = r.getSourceMethodName() != null ? r.getSourceMethodName() : "-"; + String thread = Thread.currentThread().getName(); // NOPMD + String msg = collapseWhitespace(formatMessage(r)).trim(); + + String thrown = ""; + if (r.getThrown() != null) { + Throwable t = r.getThrown(); + StringBuilder sb = new StringBuilder(64); + sb.append(" | ex=").append(t.getClass().getSimpleName()); + String m = t.getMessage(); + if (m != null && !m.isEmpty()) { + sb.append(": ").append(collapseWhitespace(m)); + } + StackTraceElement[] st = t.getStackTrace(); + if (st.length > 0) { + sb.append(" @ ").append(st[0]); + } + thrown = sb.toString(); + } + + return ts + " " + level + " [" + thread + "] " + clazz + "#" + method + " - " + msg + thrown + + System.lineSeparator(); + } + + /** + * Collapses runs of Unicode whitespace into a single ASCII space without using + * regex. + * + *

+ * Null or empty input yields an empty string. The algorithm is linear in the + * input length and minimizes allocations for use on logging hot paths. + *

+ * + * @param s input text + * @return text with runs of whitespace replaced by a single space + */ + private static String collapseWhitespace(String s) { + if (s == null || s.isEmpty()) { + return ""; + } + StringBuilder out = new StringBuilder(s.length()); + boolean inWs = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + boolean ws = Character.isWhitespace(c); + if (ws) { + if (!inWs) { + out.append(' '); + inWs = true; + } + } else { + out.append(c); + inWs = false; + } + } + return out.toString(); + } + + /** + * Compacts a fully qualified class name to package initials plus a shortened + * simple name. + * + *

+ * Example: {@code com.example.deep.package.MyService} becomes + * {@code c.e.d.p.MyService}. If the input has no package, only the shortened + * simple name is returned. + *

+ * + * @param fqcn fully qualified class name, or null + * @return compact representation or "-" if {@code fqcn} is null or empty + */ + private static String compactFast(String fqcn) { + if (fqcn == null || fqcn.isEmpty()) { + return "-"; + } + int lastDot = fqcn.lastIndexOf('.'); + if (lastDot < 0) { + return shorten(fqcn); + } + + StringBuilder sb = new StringBuilder(fqcn.length()); + // Package initials + int start = 0; + while (true) { + int dot = fqcn.indexOf('.', start); + if (dot < 0 || dot >= lastDot) { + break; + } + if (dot > start) { + sb.append(fqcn.charAt(start)).append('.'); + } + start = dot + 1; + } + // Simple class + sb.append(shorten(fqcn.substring(lastDot + 1))); + return sb.toString(); + } + + /** + * Shortens a simple class name according to abbreviation rules. + * + *

+ * Rule: keep all uppercase letters before the last uppercase, then keep the + * last uppercase and everything after it. + *

+ * + *

Examples

+ *
    + *
  • {@code Service} → {@code Service}
  • + *
  • {@code MyServiceManager} → {@code MSManager}
  • + *
  • {@code XmlHttpRequest} → {@code XHR}
  • + *
  • {@code UserDAO} → {@code UDAO}
  • + *
  • {@code FooBar} → {@code FBar}
  • + *
+ * + * @param simple the simple class name + * @return shortened representation of the class name + */ + private static String shorten(String simple) { + if (simple.isEmpty()) { + return simple; + } + + char[] chars = simple.toCharArray(); + + // find index of last uppercase + int lastUpper = -1; + for (int i = chars.length - 1; i >= 0; i--) { + if (Character.isUpperCase(chars[i])) { + lastUpper = i; + break; + } + } + + if (lastUpper == -1) { + return simple; // no uppercase at all + } + + StringBuilder out = new StringBuilder(); + + // keep uppercase letters before the last one + for (int i = 0; i < lastUpper; i++) { + if (Character.isUpperCase(chars[i])) { + out.append(chars[i]); + } + } + + // keep the last uppercase and the tail + out.append(simple.substring(lastUpper)); + + return out.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/logging/package-info.java b/lib/src/main/java/zeroecho/logging/package-info.java new file mode 100644 index 0000000..b04bab3 --- /dev/null +++ b/lib/src/main/java/zeroecho/logging/package-info.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Provides lightweight logging utilities and configuration helpers for Java + * Util Logging (JUL). + * + *

Overview

+ *

+ * This package complements the standard {@code java.util.logging} framework + * with tools for streamlined configuration and more compact log output. It is + * designed for applications that prefer JUL over third-party logging frameworks + * but still require concise, production-ready log formatting. + *

+ * + *

Components

+ *
    + *
  • {@code ClasspathJulConfig} – loads a {@code jul.properties} configuration + * file from the application classpath and applies it to the global + * {@link java.util.logging.LogManager}. This allows declarative setup of + * handlers, levels, and formatters without manual bootstrap code.
  • + *
  • {@code CompactOneLineFormatter} – a formatter that produces single-line + * log records with timestamps, thread names, class/method context, and optional + * exception summaries. The output is compact enough for terminals and log + * aggregation systems, while still preserving diagnostic detail.
  • + *
+ * + *

Design goals

+ *
    + *
  • Simplicity – avoid external dependencies and keep JUL setup + * minimal.
  • + *
  • Compactness – formatters collapse whitespace and abbreviate fully + * qualified class names to keep logs dense but readable.
  • + *
  • Safety – exception details are reduced to single-line summaries, + * preventing multi-line stack traces from overwhelming logs in production.
  • + *
+ * + *

Usage example

{@code
+ * // Automatically loads jul.properties from the classpath
+ * new ClasspathJulConfig();
+ *
+ * Logger logger = Logger.getLogger("demo");
+ * logger.info("Hello world");
+ * }
+ * + * @since 1.0 + */ +package zeroecho.logging; diff --git a/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java new file mode 100644 index 0000000..f075658 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/TagTrailerDataContentBuilder.java @@ -0,0 +1,333 @@ +/******************************************************************************* + * 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.sdk.builders; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import java.util.function.Supplier; + +import conflux.CtxInterface; +import conflux.Key; +import zeroecho.core.io.TailStrippingInputStream; +import zeroecho.core.tag.TagEngine; +import zeroecho.core.tag.TagEngineBuilder; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * Builder for {@link DataContent} pipelines that attach or verify an + * authentication tag at end of stream. + * + *

+ * A {@code TagTrailerDataContentBuilder} composes with a {@link TagEngine} to + * produce {@link DataContent} instances for two complementary flows: + *

+ *
    + *
  • Encrypt (produce): the wrapped engine emits {@code [body][tag]} + * when EOF is reached.
  • + *
  • Decrypt (verify): the trailer is stripped from input, supplied to + * {@link TagEngine#setExpectedTag(byte[])}, and the body is streamed through + * the engine for verification.
  • + *
+ * + *

Construction

+ *
    + *
  • Use the single-engine constructor if only one stream will be built from + * this instance.
  • + *
  • Prefer the factory constructor (for example, with + * {@link TagEngineBuilder}) to obtain a fresh {@link TagEngine} per stream and + * avoid state reuse.
  • + *
+ * + *

Verification behavior

+ *
    + *
  • {@link #throwOnMismatch()} - default; verification failures surface as + * {@link java.io.IOException} from the returned stream when EOF finalizes + * verification.
  • + *
  • {@link #flagInContext(CtxInterface, Key)} - instead of throwing, the + * engine records a {@code Boolean} under the provided {@link Key} in the + * supplied {@link CtxInterface}.
  • + *
+ * + * @param the engine-specific verification type parameter used by + * {@link TagEngine} + * @since 1.0 + */ +public final class TagTrailerDataContentBuilder implements DataContentBuilder { + + /** + * Factory that creates a new {@link TagEngine} per use. + */ + private final Supplier> engineFactory; + + /** + * Buffer size for the tail-stripper (default 8192). + */ + private int bufferSize = 8192; + + /** + * Optional context and key for verification flagging. + */ + private CtxInterface ctx; + private Key verifyKey; + + /** + * Creates a builder bound to a fixed engine instance. + * + *

+ * This constructor is backward compatible but ties the builder to a single-use + * engine. Prefer {@link #TagTrailerDataContentBuilder(Supplier)} when multiple + * streams are expected. + *

+ * + * @param engine preconstructed engine instance; must not be {@code null} + * @throws NullPointerException if {@code engine} is {@code null} + */ + public TagTrailerDataContentBuilder(TagEngine engine) { + Objects.requireNonNull(engine, "engine"); + this.engineFactory = () -> engine; + } + + /** + * Creates a builder with an engine factory. + * + *

+ * A fresh engine is obtained for each built stream. See + * {@link TagEngineBuilder} for standard factories. + *

+ * + * @param engineFactory supplier producing a new {@link TagEngine}; must not be + * {@code null} + * @throws NullPointerException if {@code engineFactory} is {@code null} + */ + public TagTrailerDataContentBuilder(Supplier> engineFactory) { + this.engineFactory = Objects.requireNonNull(engineFactory, "engineFactory"); + } + + /** + * Sets the internal I/O buffer size used when stripping the trailer. + * + * @param bytes buffer size in bytes; must be at least 1 + * @return this builder for chaining + * @throws IllegalArgumentException if {@code bytes < 1} + */ + public TagTrailerDataContentBuilder bufferSize(int bytes) { + if (bytes < 1) { // NOPMD + throw new IllegalArgumentException("bufferSize must be >= 1"); + } + this.bufferSize = bytes; + return this; + } + + /** + * Configures decryption to throw on verification mismatch. + * + *

+ * This is the default. The underlying engine is set up to raise + * {@link java.io.IOException} at EOF when the computed tag does not equal the + * expected tag. + *

+ * + * @return this builder for chaining + */ + public TagTrailerDataContentBuilder throwOnMismatch() { + this.ctx = null; + this.verifyKey = null; + return this; + } + + /** + * Configures decryption to flag the verification outcome in the provided + * context instead of throwing. + * + *

+ * On mismatch, {@code Boolean.FALSE} is stored under {@code verifyKey}; on + * success, {@code Boolean.TRUE} is stored. + *

+ * + * @param ctx context for recording the result; must not be {@code null} + * @param verifyKey key under which to record the verification result; must not + * be {@code null} + * @return this builder for chaining + * @throws NullPointerException if {@code ctx} or {@code verifyKey} is + * {@code null} + */ + public TagTrailerDataContentBuilder flagInContext(CtxInterface ctx, Key verifyKey) { + this.ctx = ctx; + this.verifyKey = verifyKey; + return this; + } + + /** + * Builds a {@link DataContent} pipeline for the selected direction. + * + *
    + *
  • Encrypt: the engine appends the tag trailer after body bytes.
  • + *
  • Decrypt: the trailer is stripped, supplied to the engine, and the + * body is streamed and verified.
  • + *
+ * + * @param encrypt {@code true} to build the encrypting path, {@code false} for + * the decrypting path + * @return the configured {@link DataContent} pipeline + */ + @Override + public DataContent build(boolean encrypt) { + return encrypt ? new Encrypting<>(engineFactory) : new Decrypting<>(engineFactory, bufferSize, ctx, verifyKey); + } + + /** + * Encryption pipeline stage that appends a tag trailer. + * + *

+ * The engine itself emits {@code [body][tag]} via + * {@link TagEngine#wrap(InputStream)}. + *

+ */ + private static final class Encrypting implements PlainContent { + private final Supplier> engineFactory; + private DataContent input; + + private Encrypting(Supplier> engineFactory) { + this.engineFactory = engineFactory; + } + + /** + * Sets the upstream content to be processed. + * + * @param input upstream data; must not be {@code null} + * @throws NullPointerException if {@code input} is {@code null} + */ + @Override + public void setInput(DataContent input) { + this.input = input; + } + + /** + * Returns a stream that forwards the body and appends the tag on EOF. + * + * @return a wrapped stream that emits {@code [body][tag]} + * @throws IOException if no upstream is set or the engine fails to wrap + */ + @Override + public InputStream getStream() throws IOException { + if (input == null) { + throw new IOException("TagTrailer[encrypt] has no upstream input"); + } + // Fresh engine per stream; engine must emit [body][tag] itself. + TagEngine engine = engineFactory.get(); + return engine.wrap(input.getStream()); + } + } + + /** + * Decryption pipeline stage that strips and verifies a trailing tag. + * + *

+ * The final {@code tagLength()} bytes are removed from the upstream, supplied + * to {@link TagEngine#setExpectedTag(byte[])}, and the remaining body is + * streamed through {@link TagEngine#wrap(InputStream)}. The engine performs + * verification according to the configured strategy. + *

+ */ + private static final class Decrypting implements PlainContent { + private final Supplier> engineFactory; + private final int bufferSize; + private final CtxInterface ctx; + private final Key verifyKey; + + private DataContent input; + + private Decrypting(Supplier> engineFactory, int bufferSize, CtxInterface ctx, + Key verifyKey) { + this.engineFactory = engineFactory; + this.bufferSize = bufferSize; + this.ctx = ctx; + this.verifyKey = verifyKey; + } + + /** + * Sets the upstream content to be processed. + * + * @param input upstream data; must not be {@code null} + * @throws NullPointerException if {@code input} is {@code null} + */ + @Override + public void setInput(DataContent input) { + this.input = input; + } + + /** + * Returns a stream that yields only the body while the tag is verified at EOF. + * + * @return a wrapped stream that verifies the detached trailer + * @throws IOException if no upstream is set, the declared tag length is + * invalid, or wrapping fails + */ + @Override + public InputStream getStream() throws IOException { + if (input == null) { + throw new IOException("TagTrailer[decrypt] has no upstream input"); + } + + // Fresh engine per stream + TagEngine engine = engineFactory.get(); + + final int tagLen = engine.tagLength(); + if (tagLen <= 0) { + throw new IOException("Invalid tagLength: " + tagLen); + } + + if (ctx == null) { + engine.setVerificationApproach(engine.getVerificationCore().getThrowOnMismatch()); + } else { + engine.setVerificationApproach(engine.getVerificationCore().getFlagOkInCtx(ctx, verifyKey)); + } + + // Strip the final tag from the plaintext and pass it to the engine immediately. + InputStream bodyOnly = new TailStrippingInputStream(input.getStream(), tagLen, bufferSize) { + @Override + protected void processTail(byte[] tail) throws IOException { + engine.setExpectedTag(tail); // engine may verify now or defer until EOF + } + }; + + // Feed the engine only the body. The engine handles verification on its own. + return engine.wrap(bodyOnly); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/AbstractStreamingSignatureDataBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/AbstractStreamingSignatureDataBuilder.java new file mode 100644 index 0000000..c64df11 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/AbstractStreamingSignatureDataBuilder.java @@ -0,0 +1,1303 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Base64; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import conflux.CtxInterface; +import conflux.Key; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.tag.SignatureVerificationStrategy; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; +import zeroecho.sdk.io.SignatureTrailerInputStream; + +/** + * A reusable streaming signature builder that signs or verifies data as it + * flows through an {@link java.io.InputStream}. + * + *

+ * This abstract builder composes {@link PlainContent} pipelines that either + * produce a signature while streaming the original input (sign passthrough), + * emit the signature itself in various encodings, or verify an expected + * signature while streaming or emit a boolean result. Algorithms are supplied + * by {@link CryptoAlgorithms}; concrete subclasses provide algorithm-specific + * details through protected abstract hooks. + *

+ * + *

What this builder does

+ *
    + *
  • Obtains an algorithm instance and keys (direct, imported, or + * generated).
  • + *
  • Creates a {@link SignatureContext} in SIGN or VERIFY mode on demand.
  • + *
  • Builds a streaming pipeline that either passes data through or emits a + * detached artifact.
  • + *
  • Lets callers plug in a verification approach (constant-time compare, + * throw-on-mismatch, flag-in-context, etc.).
  • + *
+ * + *

Typical usage

{@code
+ * // Signing while passing the original bytes downstream:
+ * PlainContent content = new MyAlgStreamingSignatureDataBuilder()
+ *     .sign()
+ *     .withPrivateKey(privateKey)
+ *     .passThrough()
+ *     .build(true);
+ *
+ * // Emitting a hex-encoded detached signature:
+ * PlainContent sigOut = new MyAlgStreamingSignatureDataBuilder()
+ *     .sign()
+ *     .withPrivateKey(privateKey)
+ *     .emitHexSignature()
+ *     .build(true);
+ *
+ * // Verifying against an expected signature and passing data through:
+ * PlainContent verified = new MyAlgStreamingSignatureDataBuilder()
+ *     .verify()
+ *     .withPublicKey(publicKey)
+ *     .expectedSignatureBase64(b64Sig)
+ *     .passThrough()
+ *     .build(true);
+ *
+ * // Verifying and emitting a boolean ("true" or "false"):
+ * PlainContent ok = new MyAlgStreamingSignatureDataBuilder()
+ *     .verify()
+ *     .emitVerificationBoolean()
+ *     .withPublicKey(publicKey)
+ *     .expectedSignature(rawSig)
+ *     .build(true);
+ * }
+ * + *

+ * The type parameters represent algorithm-specific key specifications: + *

+ *
    + *
  • {@code KG} - key generation specification type
  • + *
  • {@code PUB} - public key import specification type
  • + *
  • {@code PRIV} - private key import specification type
  • + *
+ * + *

+ * Subclasses supply algorithm name, key spec classes, default key generation + * supplier, factories for import specs, and creation of + * {@link SignatureContext} instances for signing and verification. + *

+ * + *

Thread-safety

+ *

+ * Builders are mutable and not thread-safe. Create and use an instance on a + * single thread. + *

+ * + * @param key generation spec type for the algorithm + * @param public key import spec type for the algorithm + * @param private key import spec type for the algorithm + */ +public abstract class AbstractStreamingSignatureDataBuilder + implements DataContentBuilder { + private Mode mode = Mode.SIGN; + + private PrivateKey privateKey; + private PublicKey publicKey; + + private boolean genKeyPair; + + private byte[] _importPrivatePkcs8; + private String importPrivateProvider; // optional + + private byte[] _importPublicX509; + private String importPublicProvider; // optional + + private byte[] _expectedSignature; // VERIFY: raw + private Key _expectedSignatureFromCtx; // optional ctx fetch + + private Output out = Output.PASSTHROUGH; + + private CtxInterface ctx; // optional + private Key storeSigKey; // optional + private SignatureVerificationStrategy _strategy; // = null optional + + private Consumer sigCallback; // optional + + private int _bufferSize = 8192; + + private CryptoAlgorithm algorithm; // resolved in resolveKeys() + + /** + * Returns the canonical algorithm name used to resolve an implementation from + * {@link CryptoAlgorithms}. + * + *

+ * Examples include "Ed25519" or "SPHINCS+". + *

+ * + * @return the algorithm name understood by + * {@link CryptoAlgorithms#require(String)} + */ + protected abstract String algorithmName(); + + /** + * Creates a {@link SignatureContext} in sign mode for the given algorithm and + * private key. + * + *

+ * Implementations should instantiate a signing context configured for streaming + * updates and producing the final signature tag. + *

+ * + * @param alg the resolved algorithm instance + * @param key the private key used to generate signatures + * @return a new signature context in sign mode + * @throws GeneralSecurityException if the context cannot be created for the + * provided algorithm or key + */ + protected abstract SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) + throws GeneralSecurityException; + + /** + * Creates a {@link SignatureContext} in verify mode for the given algorithm and + * public key. + * + *

+ * Implementations should instantiate a verification context configured for + * streaming updates and validating the final signature tag. + *

+ * + * @param alg the resolved algorithm instance + * @param key the public key used to verify signatures + * @return a new signature context in verify mode + * @throws GeneralSecurityException if the context cannot be created for the + * provided algorithm or key + */ + protected abstract SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) + throws GeneralSecurityException; + + /** + * Returns the key generation specification class for the algorithm. + * + * @return the class object representing {@code KG} + */ + protected abstract Class keyGenSpecClass(); + + /** + * Returns the public key import specification class for the algorithm. + * + * @return the class object representing {@code PUB} + */ + protected abstract Class publicKeySpecClass(); + + /** + * Returns the private key import specification class for the algorithm. + * + * @return the class object representing {@code PRIV} + */ + protected abstract Class privateKeySpecClass(); + + /** + * Returns a supplier of default key generation specifications. + * + *

+ * The supplier must not return null when invoked. + *

+ * + * @return a non-null supplier of default {@code KG} instances + */ + protected abstract Supplier defaultKeyGenSpecSupplier(); + + /** + * Returns the currently configured key generation specification or null if the + * default should be used. + * + *

+ * Subclasses typically provide a public setter to allow users to set a + * non-default specification. + *

+ * + * @return the current key generation spec or null to indicate default should be + * used + */ + protected abstract KG currentKeyGenSpecOrNull(); + + /** + * Builds a public key import specification from X.509-encoded bytes. + * + *

+ * The {@code providerHint} may be ignored if not applicable to the + * implementation. + *

+ * + * @param x509 the X.509 SubjectPublicKeyInfo bytes + * @param providerHint an optional provider name hint, may be null + * @return the public key import spec instance + */ + protected abstract PUB makePublicKeySpec(byte[] x509, String providerHint); + + /** + * Builds a private key import specification from PKCS#8-encoded bytes. + * + *

+ * The {@code providerHint} may be ignored if not applicable to the + * implementation. + *

+ * + * @param pkcs8 the PKCS#8 PrivateKeyInfo bytes + * @param providerHint an optional provider name hint, may be null + * @return the private key import spec instance + */ + protected abstract PRIV makePrivateKeySpec(byte[] pkcs8, String providerHint); + + /** + * Returns the default provider name hint used when importing keys if no + * explicit provider was set. + * + * @return the provider hint or null if there is no preference + */ + protected abstract String defaultProviderHint(); + + /** + * Operating mode for the builder. + */ + public enum Mode { + /** + * Sign mode produces a signature using a private key. + */ + SIGN, + /** + * Verify mode checks an expected signature using a public key. + */ + VERIFY + } + + /** + * Declares the output mode for a cryptographic operation. + * + *

+ * Each constant specifies how the result of an operation such as signing, + * verification, or transformation should be returned or rendered. + *

+ * + *

Modes

+ *
    + *
  • {@link #PASSTHROUGH} - Return the original data without + * modification.
  • + *
  • {@link #SIG_RAW} - Return the raw signature bytes produced by the + * algorithm.
  • + *
  • {@link #SIG_HEX} - Return the signature encoded as a hexadecimal + * string.
  • + *
  • {@link #SIG_BASE64} - Return the signature encoded in Base64.
  • + *
  • {@link #VERIFY_BOOL} - Return a boolean result of verification + * ({@code true} if valid, {@code false} otherwise).
  • + *
+ * + *

Thread-safety

Enum constants are immutable and inherently + * thread-safe. + */ + private enum Output { + /** Return the original data without modification. */ + PASSTHROUGH, + /** Return raw signature bytes. */ + SIG_RAW, + /** Return the signature as a hexadecimal string. */ + SIG_HEX, + /** Return the signature as a Base64-encoded string. */ + SIG_BASE64, + /** Return a boolean verification result. */ + VERIFY_BOOL + } + + /** + * Switches the builder to sign mode. + * + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder sign() { + this.mode = Mode.SIGN; + return this; + } + + /** + * Switches the builder to verify mode. + * + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder verify() { + this.mode = Mode.VERIFY; + return this; + } + + /** + * Configures the output to pass the original data through unchanged. + * + *

+ * In sign mode this computes the signature but appends it only when using the + * internal trailer format. In verify mode this verifies as the data is consumed + * while passing it through. + *

+ * + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder passThrough() { + this.out = Output.PASSTHROUGH; + return this; + } + + /** + * Configures the output to emit the raw detached signature bytes. + * + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder emitRawSignature() { + this.out = Output.SIG_RAW; + return this; + } + + /** + * Configures the output to emit the detached signature as lowercase hexadecimal + * text. + * + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder emitHexSignature() { + this.out = Output.SIG_HEX; + return this; + } + + /** + * Configures the output to emit the detached signature as Base64 text. + * + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder emitBase64Signature() { + this.out = Output.SIG_BASE64; + return this; + } + + /** + * Configures the builder to verify and emit a boolean result encoded as ASCII + * "true" or "false". + * + *

+ * This method also switches the mode to {@link Mode#VERIFY}. + *

+ * + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder emitVerificationBoolean() { + this.mode = Mode.VERIFY; + this.out = Output.VERIFY_BOOL; + return this; + } + + /** + * Sets the internal streaming buffer size used when consuming input. + * + * @param bytes buffer size in bytes, must be greater than or equal to 1 + * @return {@code this} builder for chaining + * @throws IllegalArgumentException if {@code bytes < 1} + */ + public AbstractStreamingSignatureDataBuilder bufferSize(int bytes) { + if (bytes < 1) { // NOPMD + throw new IllegalArgumentException("bufferSize must be >= 1"); + } + this._bufferSize = bytes; + return this; + } + + /** + * Sets the private key to be used in sign mode. + * + * @param k the private key, must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code k} is null + */ + public AbstractStreamingSignatureDataBuilder withPrivateKey(PrivateKey k) { + this.privateKey = Objects.requireNonNull(k); + return this; + } + + /** + * Sets the public key to be used in verify mode. + * + * @param k the public key, must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code k} is null + */ + public AbstractStreamingSignatureDataBuilder withPublicKey(PublicKey k) { + this.publicKey = Objects.requireNonNull(k); + return this; + } + + /** + * Requests generation of a fresh key pair using the algorithm-specific key + * generation spec. + * + *

+ * If called, a key pair will be generated during {@link #build(boolean)} using + * either the current key generation spec or a default one from + * {@link #defaultKeyGenSpecSupplier()}. + *

+ * + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder generateKeyPair() { + this.genKeyPair = true; + return this; + } + + /** + * Provides a PKCS#8-encoded private key to import using the default provider + * hint. + * + * @param pkcs8 PKCS#8 PrivateKeyInfo bytes, must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code pkcs8} is null + */ + public AbstractStreamingSignatureDataBuilder importPrivatePkcs8(byte[] pkcs8) { + this._importPrivatePkcs8 = Objects.requireNonNull(pkcs8).clone(); + this.importPrivateProvider = null; + return this; + } + + /** + * Provides a PKCS#8-encoded private key to import using the given provider + * name. + * + * @param pkcs8 PKCS#8 PrivateKeyInfo bytes, must not be null + * @param providerName provider name hint to use, may be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code pkcs8} is null + */ + public AbstractStreamingSignatureDataBuilder importPrivatePkcs8(byte[] pkcs8, String providerName) { + this._importPrivatePkcs8 = Objects.requireNonNull(pkcs8).clone(); + this.importPrivateProvider = providerName; + return this; + } + + /** + * Provides an X.509-encoded public key to import using the default provider + * hint. + * + * @param x509 X.509 SubjectPublicKeyInfo bytes, must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code x509} is null + */ + public AbstractStreamingSignatureDataBuilder importPublicX509(byte[] x509) { + this._importPublicX509 = Objects.requireNonNull(x509).clone(); + this.importPublicProvider = null; + return this; + } + + /** + * Provides an X.509-encoded public key to import using the given provider name. + * + * @param x509 X.509 SubjectPublicKeyInfo bytes, must not be null + * @param providerName provider name hint to use, may be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code x509} is null + */ + public AbstractStreamingSignatureDataBuilder importPublicX509(byte[] x509, String providerName) { + this._importPublicX509 = Objects.requireNonNull(x509).clone(); + this.importPublicProvider = providerName; + return this; + } + + /** + * Sets the expected signature for verification as raw bytes. + * + * @param raw the expected signature bytes, must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code raw} is null + */ + public AbstractStreamingSignatureDataBuilder expectedSignature(byte[] raw) { + this._expectedSignature = Objects.requireNonNull(raw).clone(); + return this; + } + + /** + * Sets the expected signature for verification from a hexadecimal string. + * + * @param hex lowercase or uppercase hexadecimal string, must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code hex} is null + * @throws IllegalArgumentException if {@code hex} is not valid hexadecimal + */ + public AbstractStreamingSignatureDataBuilder expectedSignatureHex(String hex) { + this._expectedSignature = java.util.HexFormat.of().parseHex(Objects.requireNonNull(hex)); + return this; + } + + /** + * Sets the expected signature for verification from a Base64 string. + * + * @param b64 Base64-encoded signature text, must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code b64} is null + * @throws IllegalArgumentException if {@code b64} is not valid Base64 + */ + public AbstractStreamingSignatureDataBuilder expectedSignatureBase64(String b64) { + this._expectedSignature = Base64.getDecoder().decode(Objects.requireNonNull(b64)); + return this; + } + + /** + * Configures verification to fetch the expected signature bytes from a context + * when building the stream. + * + * @param key the context key under which the expected signature is stored + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code key} is null + */ + public AbstractStreamingSignatureDataBuilder expectedSignatureFromCtx(Key key) { + this._expectedSignatureFromCtx = Objects.requireNonNull(key); + return this; + } + + /** + * Sets the optional runtime context to read or write auxiliary values such as a + * generated signature or verification result. + * + * @param c the context instance, may be null to disable context integration + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder context(CtxInterface c) { + this.ctx = c; + return this; + } + + /** + * Configures the context key under which a generated signature will be stored + * after signing. + * + * @param key the context key to store the signature under, may be null to + * disable storage + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder storeSignature(Key key) { + this.storeSigKey = key; + return this; + } + + /** + * Registers a callback that receives the generated signature bytes after + * signing completes. + * + * @param cb the callback to invoke with a defensive copy of the signature, may + * be null + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder onSignature(Consumer cb) { + this.sigCallback = cb; + return this; + } + + /** + * Sets a custom verification approach to be applied by verify-mode pipelines. + * + *

+ * The strategy defines how the computed and expected tags are compared and how + * failures are surfaced. For example, callers may supply + * {@code getVerificationCore().getThrowOnMismatch()} to raise on mismatch, or a + * decorated variant that records a boolean flag in a context. + *

+ * + *

Default behavior

+ *

+ * If this method is not called, verify-mode pipelines use the default core with + * throw-on-mismatch semantics. + *

+ * + * @param strategy verification strategy; if {@code null}, the default core is + * used + * @return {@code this} builder for chaining + */ + public AbstractStreamingSignatureDataBuilder withStrategy(SignatureVerificationStrategy strategy) { + this._strategy = strategy; + return this; + } + + /** + * Builds the configured streaming signature pipeline as {@link PlainContent}. + * + *

+ * This method resolves the algorithm and keys according to the current + * configuration, then returns a {@link PlainContent} that will perform signing + * or verification when its stream is consumed. + *

+ * + *

+ * The boolean parameter is ignored and present only to satisfy the + * {@link DataContentBuilder} interface. + *

+ * + *
{@code
+     * PlainContent pipeline = builder
+     *     .sign()
+     *     .withPrivateKey(pk)
+     *     .passThrough()
+     *     .build(true);
+     * }
+ * + * @param ignored not used + * @return a {@link PlainContent} instance that performs the requested operation + * on stream consumption + * @throws IllegalStateException if required keys are missing for the selected + * mode or if key setup fails + */ + @Override + public PlainContent build(boolean ignored) { + try { + resolveKeys(); + } catch (GeneralSecurityException e) { + throw new IllegalStateException(algorithmName() + " key setup failed", e); + } + return switch (mode) { + case SIGN -> { + if (privateKey == null) { + throw new IllegalStateException("SIGN mode needs a PrivateKey"); + } + yield (out == Output.PASSTHROUGH) + ? new SignPassthrough(algorithm, privateKey, ctx, storeSigKey, sigCallback, _bufferSize) + : new SignEmit(algorithm, privateKey, out, ctx, storeSigKey, sigCallback, _bufferSize); + } + case VERIFY -> { + if (publicKey == null) { + throw new IllegalStateException("VERIFY mode needs a PublicKey"); + } + yield (out == Output.VERIFY_BOOL) + ? new VerifyEmit(algorithm, publicKey, _expectedSignature, _expectedSignatureFromCtx, ctx, + _strategy) + : new VerifyPassthrough(algorithm, publicKey, _expectedSignature, _expectedSignatureFromCtx, + ctx, _strategy); + } + }; + } + + private void resolveKeys() throws GeneralSecurityException { + this.algorithm = CryptoAlgorithms.require(algorithmName()); + + if (genKeyPair) { + final Supplier sup = Objects.requireNonNull(defaultKeyGenSpecSupplier(), "defaultKeyGenSpecSupplier"); + final KG spec = (currentKeyGenSpecOrNull() != null) ? currentKeyGenSpecOrNull() : sup.get(); + final AsymmetricKeyBuilder b = algorithm.asymmetricKeyBuilder(keyGenSpecClass()); + final KeyPair kp = b.generateKeyPair(spec); + this.privateKey = kp.getPrivate(); + this.publicKey = kp.getPublic(); + } + if (_importPrivatePkcs8 != null) { + final String prov = (importPrivateProvider != null) ? importPrivateProvider : defaultProviderHint(); + final PRIV privSpec = makePrivateKeySpec(_importPrivatePkcs8, prov); + final AsymmetricKeyBuilder b = algorithm.asymmetricKeyBuilder(privateKeySpecClass()); + this.privateKey = b.importPrivate(privSpec); + } + if (_importPublicX509 != null) { + final String prov = (importPublicProvider != null) ? importPublicProvider : defaultProviderHint(); + final PUB pubSpec = makePublicKeySpec(_importPublicX509, prov); + final AsymmetricKeyBuilder b = algorithm.asymmetricKeyBuilder(publicKeySpecClass()); + this.publicKey = b.importPublic(pubSpec); + } + } + + /** + * Pass-through content that signs the streamed bytes and emits the final + * signature. + * + *

+ * {@code SignPassthrough} attaches to an upstream {@link DataContent}, returns + * an {@link InputStream} that forwards all bytes unchanged, and computes a + * digital signature as the stream is consumed. When the stream reaches EOF, the + * signature is finalized and can be stored in a {@link CtxInterface} and/or + * delivered to a callback if configured. + *

+ * + *

Behavior

+ *
    + *
  • Signing context is created lazily in {@link #getStream()} via + * {@code newSignContext(alg, key)}.
  • + *
  • The returned stream is read-only and must be consumed to EOF to produce a + * signature.
  • + *
  • Failures while storing or delivering the signature are swallowed to avoid + * disrupting the caller's read loop.
  • + *
+ */ + private final class SignPassthrough implements PlainContent { + private final CryptoAlgorithm alg; + private final PrivateKey key; + private final CtxInterface ctx; + private final Key storeKey; + private final Consumer cb; + private final int bufferSize; + private volatile DataContent upstream; // NOPMD + + /** + * Creates a new pass-through signer. + * + * @param alg the algorithm that provides a {@link SignatureContext}; + * must not be {@code null} + * @param key the private key used for signing; must not be {@code null} + * @param ctx optional context used to store the produced signature; may + * be {@code null} + * @param storeKey optional key under which the signature is stored in + * {@code ctx}; may be {@code null} + * @param cb optional callback invoked with a defensive copy of the + * signature; may be {@code null} + * @param bufferSize the internal buffer size used by the trailer stream + * @throws NullPointerException if {@code alg} or {@code key} is {@code null} + */ + private SignPassthrough(CryptoAlgorithm alg, PrivateKey key, CtxInterface ctx, Key storeKey, + Consumer cb, int bufferSize) { + this.alg = Objects.requireNonNull(alg); + this.key = Objects.requireNonNull(key); + this.ctx = ctx; + this.storeKey = storeKey; + this.cb = cb; + this.bufferSize = bufferSize; + } + + /** + * Sets the upstream content that will be passed through and signed. + * + * @param input the upstream data source; must not be {@code null} + * @throws NullPointerException if {@code input} is {@code null} + */ + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input); + } + + /** + * Returns a stream that forwards upstream bytes unmodified and computes a + * signature. + * + *

+ * The signature is finalized when the returned stream reaches EOF and is then: + *

+ *
    + *
  • stored into {@code ctx} under {@code storeKey} if both are non-null, + * and
  • + *
  • delivered to {@code cb} if non-null (the byte array passed to the + * callback is a clone).
  • + *
+ * + * @return an input stream that signs data while passing it through unchanged + * @throws IOException if the signing context cannot be initialized or + * if the underlying upstream stream throws an I/O + * error + * @throws NullPointerException if {@link #setInput(DataContent)} was not called + * before invocation + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "sign: missing input"); + final SignatureContext sc; + try { + sc = newSignContext(alg, key); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to init sign context", e); + } + + return new SignatureTrailerInputStream(sc, upstream.getStream(), bufferSize, sig -> { + if (ctx != null && storeKey != null) { + try { + ctx.put(storeKey, sig); + } catch (RuntimeException ignore) { // NOPMD + } + } + if (cb != null) { + try { + cb.accept(sig.clone()); + } catch (RuntimeException ignore) { // NOPMD + } + } + }); + } + } + + /** + * Eager-signing content that emits only the signature in a chosen encoding. + * + *

+ * {@code SignEmit} consumes the entire upstream {@link DataContent}, computes a + * digital signature, optionally stores and/or publishes the signature, and + * returns an {@link InputStream} over the encoded signature bytes. Unlike a + * pass-through variant, the payload is fully drained internally and is not + * forwarded to the caller; the resulting stream contains only the signature + * material in the requested format. + *

+ * + *

Behavior

+ *
    + *
  • Only {@code SIG_*} output modes are accepted.
  • + *
  • Signing context is created in {@link #getStream()} via + * {@code newSignContext(alg, key)}.
  • + *
  • Upstream is read to EOF inside {@link #getStream()} to finalize the + * signature.
  • + *
  • On success, the signature is optionally stored and/or passed to a + * callback; side-effect failures are swallowed.
  • + *
+ */ + private final class SignEmit implements PlainContent { + private final CryptoAlgorithm alg; + private final PrivateKey key; + private final Output out; + private final CtxInterface ctx; + private final Key storeKey; + private final Consumer cb; + private final int bufferSize; + private volatile DataContent upstream; // NOPMD + + /** + * Creates a new signer that emits the signature in the requested format. + * + * @param alg the algorithm used to create a {@link SignatureContext}; + * must not be {@code null} + * @param key the private key used for signing; must not be {@code null} + * @param out the desired signature output format; must be one of + * {@link Output#SIG_RAW}, {@link Output#SIG_HEX}, or + * {@link Output#SIG_BASE64} + * @param ctx optional context used to store the produced signature; may + * be {@code null} + * @param storeKey optional key under which the signature is stored in + * {@code ctx}; may be {@code null} + * @param cb optional callback invoked with a defensive copy of the + * signature; may be {@code null} + * @param bufferSize the internal buffer size used by the trailer stream + * @throws IllegalArgumentException if {@code out} is not a {@code SIG_*} + * variant + * @throws NullPointerException if {@code alg} or {@code key} is + * {@code null} + */ + private SignEmit(CryptoAlgorithm alg, PrivateKey key, Output out, CtxInterface ctx, Key storeKey, + Consumer cb, int bufferSize) { + if (out != Output.SIG_RAW && out != Output.SIG_HEX && out != Output.SIG_BASE64) { + throw new IllegalArgumentException("SignEmit requires SIG_* output"); + } + this.alg = Objects.requireNonNull(alg); + this.key = Objects.requireNonNull(key); + this.out = out; + this.ctx = ctx; + this.storeKey = storeKey; + this.cb = cb; + this.bufferSize = bufferSize; + } + + /** + * Sets the upstream content to be read and signed. + * + * @param input the upstream data source; must not be {@code null} + * @throws NullPointerException if {@code input} is {@code null} + */ + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input); + } + + /** + * Drains the upstream to compute the signature and returns a stream over the + * signature bytes. + * + *

+ * The method creates a {@link SignatureContext}, reads the entire upstream + * stream to EOF in order to finalize the signature, and then returns an + * {@link InputStream} over the encoded signature according to {@link #out}: + *

+ *
    + *
  • {@link Output#SIG_RAW} - raw signature bytes,
  • + *
  • {@link Output#SIG_HEX} - hexadecimal string encoded as UTF-8 bytes,
  • + *
  • {@link Output#SIG_BASE64} - Base64-encoded bytes.
  • + *
+ * + *

+ * If provided, the signature is stored in {@code ctx} under {@code storeKey} + * and passed to {@code cb}. Both operations use a defensive copy and swallow + * runtime exceptions to avoid interrupting the primary flow. + *

+ * + * @return an input stream that yields only the encoded signature bytes + * @throws IOException if the signing context cannot be initialized, + * the upstream cannot be read, or no signature + * trailer is produced + * @throws NullPointerException if {@link #setInput(DataContent)} was not + * invoked prior to this call + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "sign: missing input"); + + final SignatureContext sc; + try { + sc = newSignContext(alg, key); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to init sign context", e); + } + + final byte[][] sigHolder = new byte[1][]; + + try (SignatureTrailerInputStream in = new SignatureTrailerInputStream(sc, upstream.getStream(), bufferSize, + new Consumer<>() { + @Override + public void accept(byte[] sig) { + sigHolder[0] = (sig == null ? null : sig.clone()); + } + })) { + in.transferTo(OutputStream.nullOutputStream()); + } catch (IOException ioe) { + try { + sc.close(); + } catch (RuntimeException ignore) { // NOPMD + } + throw ioe; + } + + final byte[] sig = sigHolder[0]; + if (sig == null) { + throw new IOException("Missing signature trailer"); + } + + if (ctx != null && storeKey != null) { + try { + ctx.put(storeKey, sig.clone()); + } catch (RuntimeException ignore) { // NOPMD + } + } + if (cb != null) { + try { + cb.accept(sig.clone()); + } catch (RuntimeException ignore) { // NOPMD + } + } + + byte[] outBytes = switch (out) { + case SIG_RAW -> sig; + case SIG_HEX -> java.util.HexFormat.of().formatHex(sig).getBytes(StandardCharsets.UTF_8); + case SIG_BASE64 -> Base64.getEncoder().encode(sig); + default -> throw new IllegalStateException("Unexpected output: " + out); + }; + return new ByteArrayInputStream(outBytes); + } + } + + /** + * Pass-through verifier that forwards bytes unchanged while verifying a + * signature at EOF. + * + *

+ * {@code VerifyPassthrough} attaches to an upstream {@link DataContent}, + * returns an {@link InputStream} that yields the original payload, and performs + * signature verification as the stream is consumed. The expected signature is + * provided directly or fetched from a {@link CtxInterface}. When the stream + * reaches EOF, the configured verification strategy determines whether a + * mismatch raises an error or is handled differently (for example, by flagging + * in a context if the supplied strategy implements that). + *

+ * + *

Behavior

+ *
    + *
  • Expected signature is taken from the {@code expected} field or from + * {@code ctx.get(expectedKey)}.
  • + *
  • The returned stream must be fully drained or closed to finalize + * verification.
  • + *
+ */ + private final class VerifyPassthrough implements PlainContent { + private final CryptoAlgorithm alg; + private final PublicKey key; + private final byte[] expected; + private final Key expectedKey; + private final CtxInterface ctx; + private final SignatureVerificationStrategy strategy; + private volatile DataContent upstream; // NOPMD + + /** + * Creates a pass-through verifier that reads from upstream and verifies at EOF. + * + * @param alg algorithm used to obtain a {@link SignatureContext}; must + * not be {@code null} + * @param key public key used for verification; must not be {@code null} + * @param expected expected signature bytes (defensively copied), or + * {@code null} to fetch from {@code ctx} + * @param expectedKey key in {@code ctx} under which the expected signature may + * be stored; may be {@code null} + * @param ctx optional context used to fetch the expected signature; may + * be {@code null} + * @param strategy verification approach; if {@code null}, a default + * throw-on-mismatch strategy is used + * @throws NullPointerException if {@code alg} or {@code key} is {@code null} + */ + private VerifyPassthrough(CryptoAlgorithm alg, PublicKey key, byte[] expected, Key expectedKey, + CtxInterface ctx, SignatureVerificationStrategy strategy) { + this.alg = Objects.requireNonNull(alg); + this.key = Objects.requireNonNull(key); + this.expected = (expected == null ? null : expected.clone()); + this.expectedKey = expectedKey; + this.ctx = ctx; + this.strategy = strategy; + } + + /** + * Sets the upstream content that will be passed through and verified. + * + * @param input upstream data source; must not be {@code null} + * @throws NullPointerException if {@code input} is {@code null} + */ + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input); + } + + /** + * Returns a stream that forwards upstream bytes and performs signature + * verification. + * + *

+ * The method creates a {@link SignatureContext}, configures the expected + * signature and verification policy, and wraps the upstream stream. The + * returned stream must be consumed to EOF (or closed) to finalize verification + * and, if configured, to store/emit the result. + *

+ * + * @return an input stream that yields the original bytes while verifying at EOF + * @throws IOException if the verify context cannot be initialized or + * the wrapping fails + * @throws IllegalStateException if no expected signature is available via + * constructor or context + * @throws NullPointerException if {@link #setInput(DataContent)} was not + * invoked prior to this call + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "verify: missing input"); + + byte[] exp = expected; + if (exp == null && ctx != null && expectedKey != null) { + exp = ctx.get(expectedKey); + } + if (exp == null) { + throw new IllegalStateException("VERIFY requires expectedSignature (or ctx+key)"); + } + + final SignatureContext sc; // NOPMD + try { + sc = newVerifyContext(alg, key); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to init verify context", e); + } + + sc.setExpectedTag(exp); + if (strategy == null) { + sc.setVerificationApproach(sc.getVerificationCore().getThrowOnMismatch()); + } else { + sc.setVerificationApproach(strategy); + } + + try { + return sc.wrap(upstream.getStream()); + } catch (IOException initFail) { + try { + sc.close(); + } catch (RuntimeException ignore) { // NOPMD + } + throw initFail; + } + } + } + + /** + * Streaming content wrapper that verifies a digital signature and emits the + * result as a boolean value. + * + *

+ * {@code VerifyEmit} consumes an upstream {@link DataContent}, initializes a + * {@link SignatureContext} with the supplied public key and expected signature, + * and verifies the signature while streaming the input. The verification + * outcome ({@code true} or {@code false}) is then returned as a one-shot + * {@link java.io.InputStream} containing the UTF-8 encoded string + * {@code "true"} or {@code "false"}. + *

+ * + *

Notes

+ *
    + *
  • The expected signature may be provided directly or looked up from a + * {@link CtxInterface} via {@code expectedKey}.
  • + *
  • Outcome computation follows the configured verification strategy; I/O or + * verification failures result in {@code "false"}.
  • + *
+ */ + private final class VerifyEmit implements PlainContent { + private final CryptoAlgorithm alg; + private final PublicKey key; + private final byte[] expected; + private final Key expectedKey; + private final CtxInterface ctx; + private final SignatureVerificationStrategy strategy; + private volatile DataContent upstream; // NOPMD + + /** + * Creates a streaming verifier that consumes upstream data and emits a boolean + * result. + * + * @param alg algorithm used to create a {@link SignatureContext}; must + * not be {@code null} + * @param key public key used for verification; must not be {@code null} + * @param expected expected signature bytes; may be {@code null} when + * {@code ctx} and {@code expectedKey} are provided + * @param expectedKey context key from which to read the expected signature when + * {@code expected} is {@code null}; may be {@code null} + * @param ctx optional context used to read the expected signature; may + * be {@code null} + * @param strategy verification approach; if {@code null}, a default + * throw-on-mismatch strategy is used + * @throws NullPointerException if {@code alg} or {@code key} is {@code null} + */ + private VerifyEmit(CryptoAlgorithm alg, PublicKey key, byte[] expected, Key expectedKey, + CtxInterface ctx, SignatureVerificationStrategy strategy) { + this.alg = Objects.requireNonNull(alg); + this.key = Objects.requireNonNull(key); + this.expected = (expected == null ? null : expected.clone()); + this.expectedKey = expectedKey; + this.ctx = ctx; + this.strategy = strategy; + } + + /** + * Sets the upstream content that will be consumed and verified. + * + *

+ * This method must be called exactly once before {@link #getStream()}. The + * provided {@link DataContent} is stored and later used to obtain the readable + * stream that is fed into the verification context. + *

+ * + * @param input upstream data source; must not be {@code null} + * @throws NullPointerException if {@code input} is {@code null} + */ + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input); + } + + /** + * Returns a one-shot stream that yields the UTF-8 text {@code "true"} or + * {@code "false"} after verifying the upstream content against the expected + * signature. + * + *

+ * On entry, this method initializes a {@link SignatureContext}, configures the + * expected tag and a strict verification policy, and then drains the upstream + * stream. Any I/O failures or verification mismatches result in the boolean + * outcome {@code false}; successful verification results in {@code true}. The + * outcome is optionally stored into {@code ctx} under {@code storeOk} and + * passed to the callback {@code cb}. + *

+ * + *

+ * Resource management is handled via try-with-resources: the verification + * context is always closed. If closing the context fails, the method reports + * {@code false} rather than propagating the close exception, allowing callers + * to reliably consume the result. + *

+ * + *

Usage

{@code
+         * verifyEmit.setInput(data);
+         * try (InputStream result = verifyEmit.getStream()) {
+         *     boolean ok = Boolean.parseBoolean(new String(result.readAllBytes(), StandardCharsets.UTF_8));
+         *     // use ok
+         * }
+         * }
+ * + * @return a new input stream that produces {@code "true"} or {@code "false"} in + * UTF-8 + * @throws IOException if the verify context cannot be initialized or + * the upstream stream cannot be obtained + * @throws IllegalStateException if no expected signature is available from the + * constructor arguments or the context + * @throws NullPointerException if {@link #setInput(DataContent)} was not + * called before invocation + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "verify: missing input"); + + byte[] exp = expected; + if (exp == null && ctx != null && expectedKey != null) { + exp = ctx.get(expectedKey); + } + if (exp == null) { + throw new IllegalStateException("VERIFY requires expectedSignature (or ctx+key)"); + } + + final SignatureContext sc; + try { + sc = newVerifyContext(alg, key); + } catch (GeneralSecurityException e) { + throw new IOException("Failed to init verify context", e); + } + + sc.setExpectedTag(exp); + if (strategy == null) { + sc.setVerificationApproach(sc.getVerificationCore().getThrowOnMismatch()); + } else { + sc.setVerificationApproach(strategy); + } + + try (sc) { // closes sc; if it throws, that exception propagates + try (InputStream in = sc.wrap(upstream.getStream())) { + in.transferTo(OutputStream.nullOutputStream()); + return new ByteArrayInputStream("true".getBytes(StandardCharsets.UTF_8)); + } catch (IOException fail) { + // wrap/read/transfer/close(in) failures land here and are swallowed -> ok = + // false + return new ByteArrayInputStream("false".getBytes(StandardCharsets.UTF_8)); + } + } + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java new file mode 100644 index 0000000..24f08b5 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/AesDataContentBuilder.java @@ -0,0 +1,641 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.util.Base64; +import java.util.Objects; + +import javax.crypto.SecretKey; + +import conflux.Ctx; +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.alg.aes.AesHeaderCodec; +import zeroecho.core.alg.aes.AesKeyGenSpec; +import zeroecho.core.alg.aes.AesKeyImportSpec; +import zeroecho.core.alg.aes.AesSpec; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spi.ContextAware; +import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.EncryptedContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * Fluent builder that exposes AES as a + * {@link zeroecho.sdk.content.api.DataContent} in pull‑mode. + *

+ * This is the simplest way to integrate AES into pipelines. Supply the key with + * {@link #withKey(javax.crypto.SecretKey)}, choose a mode (e.g., + * {@link #modeGcm(int)}), optionally install a header codec via + * {@link #withHeader()}, and call {@link #build(boolean)} for encryption + * ({@code true}) or decryption ({@code false}). Connect your upstream source + * with + * {@link zeroecho.sdk.content.api.DataContent#setInput(zeroecho.sdk.content.api.DataContent)} + * and consume the resulting {@link java.io.InputStream}. + *

+ * + *

Examples

{@code
+ * // Encrypt (pull-mode)
+ * DataContent enc = AesDataContentBuilder.builder()
+ *     .withKey(secret)
+ *     .modeGcm(128)
+ *     .withHeader()
+ *     .build(true);
+ * enc.setInput(PlainFileBuilder.ofPath(inPath).build());
+ * try (InputStream s = enc.getStream()) { s.transferTo(Files.newOutputStream(ctPath)); }
+ *
+ * // Decrypt (pull-mode)
+ * DataContent dec = AesDataContentBuilder.builder()
+ *     .withKey(secret)
+ *     .modeGcm(128)
+ *     .withHeader()
+ *     .build(false);
+ * dec.setInput(PlainFileBuilder.ofPath(ctPath).build());
+ * try (InputStream s = dec.getStream()) { s.transferTo(Files.newOutputStream(ptPath)); }
+ * }
+ * + *

Runtime parameters

Optional IV and AAD can be provided via + * {@link #withIv(byte[])} and {@link #withAad(byte[])}. If a Conflux context is + * present (set via {@link #context(conflux.CtxInterface)} or implied by a + * header), a fresh IV is generated on encrypt when absent and stored back into + * the context. For GCM, AAD defaults to empty. + * + *

Thread‑safety

The builder is not thread‑safe. Built + * {@code DataContent} instances are independent. + * + * @since 1.0 + */ +public final class AesDataContentBuilder implements DataContentBuilder { + private SecretKey secretKey; + private AesKeyGenSpec genSpec; + private AesKeyImportSpec importSpec; + + private final AesSpec.Builder _spec = AesSpec.builder(); + + private byte[] iv; // optional + private byte[] aad; // optional + + private SymmetricHeaderCodec headerCodec; // optional; carried by AesSpec + + private CtxInterface ctx; // may be null (caller opts out of ctx features) + + private SecretKey generatedKey; // if we generate a key, it is stored here + + /** + * Creates a new AES builder with default spec (GCM/128, no header) and no key + * selected. Use {@link #withKey(SecretKey)} (recommended), or import/generate a + * key, then select a mode. + * + * @return a new builder instance + */ + public static AesDataContentBuilder builder() { + return new AesDataContentBuilder(); + } + + private AesDataContentBuilder() { + } + + /** + * Sets the key to use. Overrides any previous generate/import choice. + * + * @param k the AES {@link SecretKey}; must be 128/192/256-bit + * @return this builder + * @throws NullPointerException if {@code k} is null + */ + public AesDataContentBuilder withKey(SecretKey k) { + this.secretKey = Objects.requireNonNull(k); + this.genSpec = null; + this.importSpec = null; + return this; + } + + /** + * Requests generation of a fresh AES key of the given size. + *

+ * Note: prefer {@link #withKey(SecretKey)} for pipelines that need + * predictable key reuse across encrypt/decrypt runs. + *

+ * + * @param bits 128, 192, or 256 + * @return this builder + * @throws IllegalArgumentException if size is invalid + */ + public AesDataContentBuilder generateKey(int bits) { + this.genSpec = new AesKeyGenSpec(bits); + this.secretKey = null; + this.importSpec = null; + return this; + } + + /** + * Imports a raw AES key (16/24/32 bytes). + * + * @param raw key material; copied defensively + * @return this builder + * @throws IllegalArgumentException if length is not 16/24/32 + */ + public AesDataContentBuilder importKeyRaw(byte[] raw) { + this.importSpec = AesKeyImportSpec.fromRaw(raw); + this.secretKey = null; + this.genSpec = null; + return this; + } + + /** + * Imports a hex‑encoded AES key (32/48/64 hex chars). + * + * @param hex hex string; not null + * @return this builder + * @throws NullPointerException if {@code hex} is null + * @throws IllegalArgumentException if the decoded length is not 16/24/32 + */ + public AesDataContentBuilder importKeyHex(String hex) { + return importKeyRaw(java.util.HexFormat.of().parseHex(hex)); + } + + /** + * Imports a Base64‑encoded AES key (no padding required). + * + * @param b64 Base64 text; not null + * @return this builder + * @throws NullPointerException if {@code b64} is null + * @throws IllegalArgumentException if the decoded length is not 16/24/32 + */ + public AesDataContentBuilder importKeyBase64(String b64) { + return importKeyRaw(Base64.getDecoder().decode(b64)); + } + + /** + * Copies mode/padding/tagBits/header from a fully built {@link AesSpec}. + * + * @param s the spec to apply + * @return this builder + * @throws NullPointerException if {@code s} is null + */ + public AesDataContentBuilder spec(AesSpec s) { + this._spec.mode(s.mode()).padding(s.padding()).tagLenBits(s.tagLenBits()).header(s.header()); + return this; + } + + /** + * Selects GCM and sets the authentication tag length in bits. + * + * @param tagBits 96..128 (step 8) + * @return this builder + * @throws IllegalArgumentException if {@code tagBits} is out of range + */ + public AesDataContentBuilder modeGcm(int tagBits) { + this._spec.mode(AesSpec.Mode.GCM).tagLenBits(tagBits); + return this; + } + + /** + * Selects CTR with NOPADDING. Pair with a MAC if integrity is required. + * + * @return this builder + */ + public AesDataContentBuilder modeCtr() { + this._spec.mode(AesSpec.Mode.CTR).padding(AesSpec.Padding.NOPADDING); + return this; + } + + /** + * Selects CBC/PKCS5PADDING. + * + * @return this builder + */ + public AesDataContentBuilder modeCbcPkcs5() { + this._spec.mode(AesSpec.Mode.CBC).padding(AesSpec.Padding.PKCS5PADDING); + return this; + } + + /** + * Selects CBC/NOPADDING. The plaintext length must be a multiple of the block + * size. + * + * @return this builder + */ + public AesDataContentBuilder modeCbcNoPadding() { + this._spec.mode(AesSpec.Mode.CBC).padding(AesSpec.Padding.NOPADDING); + return this; + } + + /** + * Supplies an explicit IV/nonce. If omitted during ENCRYPT and a context is + * present, a fresh IV is generated and stored in the context. + * + * @param iv the IV (12 bytes for GCM, 16 bytes for CBC/CTR) + * @return this builder + * @throws IllegalArgumentException if the length does not match the mode + */ + public AesDataContentBuilder withIv(byte[] iv) { + this.iv = iv; + return this; + } + + /** + * Supplies Additional Authenticated Data (GCM). AAD is not encrypted; it is + * bound to the ciphertext. If a header is used, only a SHA‑256 hash of AAD is + * written. + * + * @param aad AAD bytes (may be empty) + * @return this builder + */ + public AesDataContentBuilder withAad(byte[] aad) { + this.aad = aad; + return this; + } + + /** + * Installs a specific header codec into the spec so IV/tagBits/AAD hash can be + * persisted in‑band. + * + * @param codec the codec to use (e.g., {@link AesHeaderCodec}) + * @return this builder + */ + public AesDataContentBuilder withHeaderCodec(SymmetricHeaderCodec codec) { + this.headerCodec = codec; + return this; + } + + /** + * Convenience for {@link #withHeaderCodec(SymmetricHeaderCodec)} using + * {@link AesHeaderCodec}. + * + * @return this builder + */ + public AesDataContentBuilder withHeader() { + return withHeaderCodec(new AesHeaderCodec()); + } + + /** + * Provides a Conflux session context for exchanging IV/AAD/tagBits and enabling + * header I/O. When null, no context features are used and no header is + * written/read. + * + * @param ctx the context, or {@code null} + * @return this builder + */ + public AesDataContentBuilder context(CtxInterface ctx) { + this.ctx = ctx; + return this; + } + + /** + * Builds an encrypting or decrypting {@link DataContent}. + *
    + *
  • Resolves the key from {@link #withKey(SecretKey)} or the selected + * generate/import spec.
  • + *
  • Finalizes {@link AesSpec} (injecting the header if configured).
  • + *
  • Propagates IV/AAD into the context if supplied.
  • + *
+ * + * @param encrypt {@code true} for encryption; {@code false} for decryption + * @return a {@link DataContent} that transforms its upstream stream + * @throws IllegalStateException on key construction errors or provider + * misconfiguration + */ + @Override + public DataContent build(boolean encrypt) { + final SecretKey key = resolveKey(); + + // finalize AesSpec from builder + header + if (this.headerCodec != null) { + if (ctx == null) { + ctx = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); + } + + this._spec.header(this.headerCodec); + } + final AesSpec aesSpec = _spec.build(); + + if (aad != null && aad.length > 0) { + if (ctx == null) { + ctx = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); + } + ctx.put(ConfluxKeys.aad("AES"), aad); + } + if (iv != null && iv.length > 0) { + if (ctx == null) { + ctx = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); + } + ctx.put(ConfluxKeys.iv("AES"), iv); + } + + return encrypt ? new EncryptContent(key, aesSpec, ctx) : new DecryptContent(key, aesSpec, ctx); + } + + /** + * Returns the generated secret key associated with this instance. + * + *

+ * The value is typically produced during a key generation process and stored + * internally for later retrieval. The returned reference is the actual + * {@link SecretKey} object; callers should avoid modifying or exposing it + * unnecessarily to preserve security. + *

+ * + * @return the generated {@link SecretKey}, or {@code null} if no key has been + * generated + */ + public SecretKey generatedKey() { + return generatedKey; + } + + private SecretKey resolveKey() { + if (secretKey != null) { + return secretKey; + } + try { + CryptoAlgorithm algo = CryptoAlgorithms.require("AES"); // registry lookup + if (genSpec != null) { + SymmetricKeyBuilder b = algo.symmetricKeyBuilder(AesKeyGenSpec.class); + generatedKey = b.generateSecret(genSpec); + return generatedKey; + } + if (importSpec != null) { + SymmetricKeyBuilder b = algo.symmetricKeyBuilder(AesKeyImportSpec.class); + return b.importSecret(importSpec); + } + SymmetricKeyBuilder b = algo.symmetricKeyBuilder(AesKeyGenSpec.class); + generatedKey = b.generateSecret(AesKeyGenSpec.aes256()); + return generatedKey; + } catch (GeneralSecurityException e) { + throw new IllegalStateException("AES key construction failed", e); + } + } + + /** + * Streaming AES encryptor that adapts an upstream {@link DataContent} into a + * ciphertext stream. + * + *

+ * An instance of {@code EncryptContent} produces an {@link java.io.InputStream} + * that encrypts bytes on demand using the provided {@link SecretKey} and + * {@link AesSpec}. The encryption is performed by an + * {@link zeroecho.core.context.EncryptionContext} created via + * {@link CryptoAlgorithms}. If the created context implements a context-aware + * extension, the optional {@link CtxInterface} is forwarded to it to enable + * header emission and parameter exchange (for example IV, tag bits, or AAD + * digests). + *

+ * + *

Lifetime semantics

+ *
    + *
  • Independence of the returned stream: The {@link #getStream()} + * method obtains an {@link zeroecho.core.context.EncryptionContext}, calls its + * {@code attach(InputStream)} method, and returns the resulting stream. By + * contract, the returned stream is usable even if the temporary encryption + * context is closed immediately after {@code attach}. In other words, the + * stream does not retain an operational dependency on the context object.
  • + *
  • Caller responsibility: Callers must close the stream returned by + * {@link #getStream()} to ensure encryption finalization and resource + * release.
  • + *
+ * + *

Thread-safety

Instances are not thread-safe. Each + * {@code EncryptContent} should be configured once via + * {@link #setInput(DataContent)} and then consumed by a single reader. + * + *

Example

{@code
+     * EncryptContent enc = new EncryptContent(secretKey, aesSpec, ctx);
+     * enc.setInput(plainContent);
+     * try (InputStream cipher = enc.getStream()) {
+     *     // consume ciphertext
+     *     cipher.transferTo(out);
+     * }
+     * }
+ */ + private static final class EncryptContent implements EncryptedContent { + private final SecretKey key; + private final AesSpec spec; + private final CtxInterface ctx; // may be null + private DataContent upstream; + + /** + * Creates a new encrypting content adapter for AES. + * + *

+ * The supplied {@link SecretKey} and {@link AesSpec} determine the algorithm + * parameters (mode, tag length, nonce semantics, etc.). If a + * {@link CtxInterface} is provided, the underlying implementation may emit a + * small header and/or exchange ephemeral parameters via the context. Passing + * {@code null} disables such header-based coordination. + *

+ * + * @param key symmetric AES key; must not be {@code null} + * @param spec AES specification describing mode and parameters; must not be + * {@code null} + * @param ctx optional context used to exchange transient parameters; may be + * {@code null} + * @throws NullPointerException if {@code key} or {@code spec} is {@code null} + */ + private EncryptContent(SecretKey key, AesSpec spec, CtxInterface ctx) { + this.key = key; + this.spec = spec; + this.ctx = ctx; + } + + /** + * Sets the upstream source of plaintext bytes to be encrypted. + * + *

+ * This method must be invoked exactly once before {@link #getStream()}. The + * upstream stream supplied by {@code input} is not opened here; it is resolved + * lazily when {@link #getStream()} is called. + *

+ * + * @param input upstream plaintext provider; must not be {@code null} + * @throws NullPointerException if {@code input} is {@code null} + */ + @Override + public void setInput(DataContent input) { + this.upstream = input; + } + + /** + * Returns a stream that yields the AES-encrypted form of the upstream content. + * + *

+ * This method creates an {@link zeroecho.core.context.EncryptionContext} for + * AES in the {@link KeyUsage#ENCRYPT} role and attaches it to the upstream + * stream. If the context is {@code ContextAware}, the optional + * {@link CtxInterface} is passed to it; if not, an + * {@link IllegalStateException} is thrown because header-based coordination + * would be unavailable. + *

+ * + *

+ * The returned stream is independent of the temporary encryption context; + * closing the context is not required for the stream to function. Callers must + * close the returned stream to finalize encryption and release resources. + *

+ * + * @return an input stream that produces ciphertext derived from the upstream + * bytes + * @throws IOException if the AES context cannot be created or if + * stream attachment fails + * @throws NullPointerException if no upstream content has been set via + * {@link #setInput(DataContent)} + * @throws IllegalStateException if the AES context does not implement + * {@code ContextAware} + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "encrypt: missing input content"); + try (EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec)) { + if (!(enc instanceof ContextAware)) { + throw new IllegalStateException("AES context is not ContextAware; cannot pass conflux Ctx"); + } + ((ContextAware) enc).setContext(ctx); // ctx may be null → header disabled; IV generated only if ctx + // present + return enc.attach(upstream.getStream()); + } + } + } + + /** + * Streaming AES decryptor that adapts an upstream {@link DataContent} into a + * plaintext stream. + * + *

+ * {@code DecryptContent} obtains an + * {@link zeroecho.core.context.EncryptionContext} in the + * {@link KeyUsage#DECRYPT} role and returns an {@link java.io.InputStream} that + * decrypts bytes on demand using the provided {@link SecretKey} and + * {@link AesSpec}. The implementation passes an optional {@link CtxInterface} + * to the context when supported. + *

+ * + *

Lifetime semantics

The {@link #getStream()} method creates the + * decryption context in a try-with-resources block and immediately returns the + * stream produced by {@code attach}. By contract, the returned stream is + * independent of the temporary context, so the context can be closed before the + * caller consumes the stream. Callers are responsible for closing the returned + * stream. + * + *

Thread-safety

Instances are not thread-safe and are intended for + * single-use pipelines. + */ + private static final class DecryptContent implements PlainContent { + private final SecretKey key; + private final AesSpec spec; + private final CtxInterface ctx; // may be null + private DataContent upstream; + + /** + * Creates a new decrypting content adapter for AES. + * + *

+ * The supplied {@link SecretKey} and {@link AesSpec} define algorithm + * parameters. If a {@link CtxInterface} is provided and the underlying context + * is {@code ContextAware}, runtime parameters (for example IV or tag length) + * may be exchanged through the context. Passing {@code null} disables such + * coordination. + *

+ * + * @param key symmetric AES key; must not be {@code null} + * @param spec AES specification describing mode and parameters; must not be + * {@code null} + * @param ctx optional context for exchanging ephemeral parameters; may be + * {@code null} + * @throws NullPointerException if {@code key} or {@code spec} is {@code null} + */ + private DecryptContent(SecretKey key, AesSpec spec, CtxInterface ctx) { + this.key = key; + this.spec = spec; + this.ctx = ctx; + } + + /** + * Sets the upstream source of ciphertext bytes to be decrypted. + * + *

+ * This must be called exactly once before {@link #getStream()} is invoked. The + * upstream stream is acquired lazily during {@link #getStream()}. + *

+ * + * @param input upstream ciphertext provider; must not be {@code null} + * @throws NullPointerException if {@code input} is {@code null} + */ + @Override + public void setInput(DataContent input) { + this.upstream = input; + } + + /** + * Returns a stream that yields the AES-decrypted form of the upstream content. + * + *

+ * The method creates a decryption + * {@link zeroecho.core.context.EncryptionContext} via + * {@link CryptoAlgorithms#create(String, KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}, + * forwards the optional {@link CtxInterface} when the context is + * {@code ContextAware}, and returns the stream produced by {@code attach}. The + * returned stream is independent of the temporary context and remains usable + * after the context is closed by try-with-resources. + *

+ * + * @return an input stream that produces plaintext derived from the upstream + * bytes + * @throws IOException if the decryption context cannot be created or + * if stream attachment fails + * @throws NullPointerException if no upstream content has been set via + * {@link #setInput(DataContent)} + * @throws IllegalStateException if the AES context does not implement + * {@code ContextAware} + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "decrypt: missing input content"); + try (EncryptionContext dec = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec)) { + if (!(dec instanceof ContextAware)) { + throw new IllegalStateException("AES context is not ContextAware; cannot pass conflux Ctx"); + } + ((ContextAware) dec).setContext(ctx); + return dec.attach(upstream.getStream()); + } + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/ChaChaDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/ChaChaDataContentBuilder.java new file mode 100644 index 0000000..945aee2 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/ChaChaDataContentBuilder.java @@ -0,0 +1,697 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.util.Objects; + +import javax.crypto.SecretKey; + +import conflux.Ctx; +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.SymmetricHeaderCodec; +import zeroecho.core.alg.chacha.ChaCha20Poly1305HeaderCodec; +import zeroecho.core.alg.chacha.ChaCha20Poly1305Spec; +import zeroecho.core.alg.chacha.ChaChaHeaderCodec; +import zeroecho.core.alg.chacha.ChaChaKeyGenSpec; +import zeroecho.core.alg.chacha.ChaChaKeyImportSpec; +import zeroecho.core.alg.chacha.ChaChaSpec; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spec.ContextSpec; +import zeroecho.core.spi.ContextAware; +import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.EncryptedContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * ChaChaDataContentBuilder builds streaming ChaCha20 and ChaCha20-Poly1305 + * pipelines that encrypt or decrypt as an {@link java.io.InputStream} is + * consumed. + * + *

Variants

The builder selects between two cipher variants: + *
    + *
  • Stream (ChaCha20): chosen by default or when {@link #initialCounter(int)} + * or {@link #withCounter(int)} is used.
  • + *
  • AEAD (ChaCha20-Poly1305): chosen when {@link #withAad(byte[])} is + * provided.
  • + *
+ * Supplying both AAD and counter options is a configuration error and results + * in an {@link IllegalStateException}. + * + *

Key material

A secret key is resolved in the following order: + *
    + *
  1. Explicit key via {@link #withKey(javax.crypto.SecretKey)}.
  2. + *
  3. Key generation via {@link #generateKey()} with a default 256-bit + * spec.
  4. + *
  5. Key import via {@link #importKeyRaw(byte[])}, + * {@link #importKeyHex(String)}, or {@link #importKeyBase64(String)}.
  6. + *
  7. Fallback to default 256-bit generation if none of the above is + * provided.
  8. + *
+ * + *

Context and headers

A runtime {@link conflux.CtxInterface} can be + * attached via {@link #context(conflux.CtxInterface)} to exchange parameters + * such as nonce, AAD, or counter. When a context is present, the builder writes + * values under algorithm-specific keys from {@code ConfluxKeys}. If a header is + * requested with {@link #withHeader()} or a custom {@link SymmetricHeaderCodec} + * is supplied via {@link #withHeaderCodec(SymmetricHeaderCodec)}, a context is + * required; if none was provided, a temporary one is created internally. A + * default header codec is chosen based on the variant when no custom codec is + * set. The nonce is typically 12 bytes; if absent during encryption and a + * context is available, an implementation-specific nonce may be generated and + * stored in the context. + * + *

Usage examples

{@code
+ * // 1) ChaCha20 stream encryption with generated key and header
+ * DataContent enc = ChaChaDataContentBuilder.builder()
+ *     .generateKey()
+ *     .withHeader()
+ *     .build(true);
+ *
+ * // 2) ChaCha20-Poly1305 decryption with supplied key, nonce, and AAD
+ * DataContent dec = ChaChaDataContentBuilder.builder()
+ *     .withKey(secretKey)
+ *     .withNonce(nonce12)                  // 12 bytes
+ *     .withAad("meta".getBytes(StandardCharsets.UTF_8))
+ *     .build(false);
+ *
+ * // 3) ChaCha20 stream encryption with explicit counter propagated via context
+ * DataContent enc2 = ChaChaDataContentBuilder.builder()
+ *     .withKey(secretKey)
+ *     .context(ctx)
+ *     .withCounter(7)                      // overrides initialCounter in ctx
+ *     .build(true);
+ *
+ * // 4) Import a key from hex and decrypt without headers
+ * DataContent dec2 = ChaChaDataContentBuilder.builder()
+ *     .importKeyHex(hexKey)
+ *     .build(false);
+ * }
+ * + *

Behavior of {@link #build(boolean)}

Calling {@code build(true)} + * returns a pipeline that encrypts as the stream is read; {@code build(false)} + * returns a pipeline that decrypts. In stream mode, + * {@link #initialCounter(int)} sets the spec's initial counter (default is 1), + * while {@link #withCounter(int)} stores an override in the context. In AEAD + * mode, {@link #withAad(byte[])} supplies additional authenticated data. + * + *

Thread-safety

Instances are mutable and not thread-safe. Configure + * and use each builder instance from a single thread. + * + * @see zeroecho.sdk.content.api.DataContent + * @see zeroecho.sdk.builders.core.DataContentBuilder + * @see zeroecho.core.CryptoAlgorithms + * @see zeroecho.core.context.EncryptionContext + * @see SymmetricHeaderCodec + */ +public final class ChaChaDataContentBuilder implements DataContentBuilder { + + private SecretKey secretKey; + private ChaChaKeyGenSpec genSpec; + private ChaChaKeyImportSpec importSpec; + + private CtxInterface ctx; + private byte[] nonce; + + private int initialCounter = 1; + private boolean initialCounterSet; // = false; + private Integer counter; // = null; + + private byte[] aad; // = null; + + private SymmetricHeaderCodec headerCodec; // = null; + private boolean headerRequested; // = false; + + /** + * Returns a new builder instance for constructing ChaCha20 or ChaCha20-Poly1305 + * streaming pipelines. + * + *

Examples

{@code
+     * DataContent enc = ChaChaDataContentBuilder.builder()
+     *     .generateKey()
+     *     .withHeader()
+     *     .build(true);
+     * }
+ * + * @return a new {@code ChaChaDataContentBuilder} + */ + public static ChaChaDataContentBuilder builder() { + return new ChaChaDataContentBuilder(); + } + + private ChaChaDataContentBuilder() { + } + + /** + * Sets an explicit secret key to use for encryption or decryption. + * + *

+ * Supplying a key clears any previously configured key generation or import + * specification. + *

+ * + * @param k the secret key to use; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code k} is null + */ + public ChaChaDataContentBuilder withKey(SecretKey k) { + this.secretKey = Objects.requireNonNull(k); + this.genSpec = null; + this.importSpec = null; + return this; + } + + /** + * Requests generation of a fresh ChaCha key using a default 256-bit + * specification. + * + *

+ * Enabling key generation clears any previously configured explicit key or + * import specification. + *

+ * + * @return {@code this} builder for chaining + */ + public ChaChaDataContentBuilder generateKey() { + this.genSpec = ChaChaKeyGenSpec.chacha256(); + this.secretKey = null; + this.importSpec = null; + return this; + } + + /** + * Imports a secret key from raw key bytes appropriate for the selected ChaCha + * variant. + * + *

+ * Supplying an import spec clears any previously configured explicit key or key + * generation request. + *

+ * + * @param raw the raw key bytes; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code raw} is null + * @throws IllegalArgumentException if {@code raw} is not a valid key length + */ + public ChaChaDataContentBuilder importKeyRaw(byte[] raw) { + this.importSpec = ChaChaKeyImportSpec.fromRaw(raw); + this.secretKey = null; + this.genSpec = null; + return this; + } + + /** + * Imports a secret key from a hexadecimal string. + * + *

+ * Supplying an import spec clears any previously configured explicit key or key + * generation request. + *

+ * + * @param hex the hex-encoded key; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code hex} is null + * @throws IllegalArgumentException if {@code hex} is not valid hexadecimal or + * not a valid key length + */ + public ChaChaDataContentBuilder importKeyHex(String hex) { + this.importSpec = ChaChaKeyImportSpec.fromHex(hex); + this.secretKey = null; + this.genSpec = null; + return this; + } + + /** + * Imports a secret key from a Base64 string. + * + *

+ * Supplying an import spec clears any previously configured explicit key or key + * generation request. + *

+ * + * @param b64 the Base64-encoded key; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code b64} is null + * @throws IllegalArgumentException if {@code b64} is not valid Base64 or not a + * valid key length + */ + public ChaChaDataContentBuilder importKeyBase64(String b64) { + this.importSpec = ChaChaKeyImportSpec.fromBase64(b64); + this.secretKey = null; + this.genSpec = null; + return this; + } + + /** + * Sets an optional runtime context used to read or write parameters such as + * nonce, AAD, or counter. + * + *

+ * When a header is requested and no context is provided, a temporary context is + * created internally. + *

+ * + * @param ctx the context instance to use; may be null + * @return {@code this} builder for chaining + */ + public ChaChaDataContentBuilder context(CtxInterface ctx) { + this.ctx = ctx; + return this; + } + + /** + * Sets an optional nonce value. + * + *

+ * For ChaCha20 and ChaCha20-Poly1305 this is typically 12 bytes. If absent + * during encryption and a context is in use, an implementation-specific nonce + * may be generated and placed into the context. + *

+ * + * @param nonce the nonce bytes; may be null + * @return {@code this} builder for chaining + */ + public ChaChaDataContentBuilder withNonce(byte[] nonce) { + this.nonce = nonce; + return this; + } + + /** + * Sets the initial counter for ChaCha20 stream mode. + * + *

+ * Calling this method selects stream mode unless AEAD is selected via + * {@link #withAad(byte[])}. The default initial counter is 1. + *

+ * + * @param c the initial counter value + * @return {@code this} builder for chaining + */ + public ChaChaDataContentBuilder initialCounter(int c) { + this.initialCounter = c; + this.initialCounterSet = true; + return this; + } + + /** + * Sets an explicit counter override to be propagated through the context. + * + *

+ * This value is stored under an algorithm-specific key in the provided context + * and overrides {@link #initialCounter(int)} if present. + *

+ * + * @param c the counter value to use + * @return {@code this} builder for chaining + */ + public ChaChaDataContentBuilder withCounter(int c) { + this.counter = c; + return this; + } + + /** + * Sets additional authenticated data for ChaCha20-Poly1305. + * + *

+ * Providing AAD selects AEAD mode. Passing {@code null} or an empty array + * leaves the decision to other configuration such as stream counters. + *

+ * + * @param aad the AAD bytes; may be null + * @return {@code this} builder for chaining + */ + public ChaChaDataContentBuilder withAad(byte[] aad) { + this.aad = aad; + return this; + } + + /** + * Sets a custom header codec to emit or parse algorithm headers. + * + *

+ * Supplying a codec clears the implicit header request flag. If no codec is + * supplied but a header is requested via {@link #withHeader()}, a default is + * chosen based on the selected variant. + *

+ * + * @param codec the header codec to use; may be null to clear + * @return {@code this} builder for chaining + */ + public ChaChaDataContentBuilder withHeaderCodec(SymmetricHeaderCodec codec) { + this.headerCodec = codec; + this.headerRequested = false; + return this; + } + + /** + * Requests that a header be emitted (on encrypt) or parsed (on decrypt). + * + *

+ * If no custom codec is set, a default codec is chosen depending on whether + * stream or AEAD mode is inferred. + *

+ * + * @return {@code this} builder for chaining + */ + public ChaChaDataContentBuilder withHeader() { + this.headerRequested = true; + return this; + } + + /** + * Builds a streaming encryption or decryption pipeline according to the current + * configuration. + * + *

+ * AEAD mode is selected if AAD is provided via {@link #withAad(byte[])}, using + * the algorithm id "CHACHA20-POLY1305". Otherwise stream mode is used, with the + * algorithm id "CHACHA20". + *

+ * + *

+ * The key is resolved in this order: explicit key, key generation spec, key + * import spec, default 256-bit generation. + *

+ * + *

+ * If a header is requested and no context is provided, a temporary context is + * created. When encrypting with a context, if no nonce is supplied, the + * implementation may generate one and store it into the context. + *

+ * + *
{@code
+     * DataContent enc = ChaChaDataContentBuilder.builder()
+     *     .generateKey()
+     *     .withHeader()
+     *     .build(true);
+     * }
+ * + * @param encrypt {@code true} to build an encrypting pipeline, {@code false} + * for a decrypting pipeline + * @return a {@link zeroecho.sdk.content.api.DataContent} instance that performs + * the requested operation when its stream is consumed + * @throws IllegalStateException if conflicting configuration is detected or key + * construction fails + */ + @Override + public DataContent build(boolean encrypt) { // NOPMD + final Variant v = inferVariant(); + final String algId = v == Variant.AEAD ? "CHACHA20-POLY1305" : "CHACHA20"; + + final SecretKey key = resolveKey(algId); + + // header → ensure ctx exists + if ((headerRequested || headerCodec != null) && ctx == null) { + ctx = Ctx.INSTANCE + .getContext("chacha-" + (v == Variant.AEAD ? "aead" : "stream") + "-" + System.nanoTime()); + } + // fill ctx with runtime params + if (ctx != null) { + if (nonce != null && nonce.length > 0) { + ctx.put(ConfluxKeys.iv(algId), nonce); + } + if (v == Variant.AEAD) { + if (aad != null) { + ctx.put(ConfluxKeys.aad(algId), aad); + } + } else { + if (counter != null) { + ctx.put(ConfluxKeys.tagBits(algId), counter); + } + } + } + + // pick default header now that variant is known (unless user supplied one) + final SymmetricHeaderCodec hdr = (headerCodec != null) ? headerCodec + : headerRequested ? (v == Variant.AEAD ? new ChaCha20Poly1305HeaderCodec() : new ChaChaHeaderCodec()) + : null; + + if (v == Variant.AEAD) { + final ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(hdr).build(); + return encrypt ? new EncryptContent<>(algId, key, spec, ctx) : new DecryptContent<>(algId, key, spec, ctx); + } else { + final ChaChaSpec spec = ChaChaSpec.builder().initialCounter(initialCounter).header(hdr).build(); + return encrypt ? new EncryptContent<>(algId, key, spec, ctx) : new DecryptContent<>(algId, key, spec, ctx); + } + } + + /** + * Internal selector for the cipher variant inferred from the builder + * configuration. + * + *

Selection rules

+ *
    + *
  • {@code AEAD} is selected when non-empty AAD is supplied via + * {@link #withAad(byte[])}.
  • + *
  • {@code STREAM} is selected when a counter is configured via + * {@link #initialCounter(int)} or {@link #withCounter(int)}, or when neither + * AAD nor counter options are provided (default).
  • + *
  • Supplying both AAD and counter options is invalid and results in + * {@link IllegalStateException} at build time.
  • + *
+ * + *

+ * The selected variant determines the algorithm id ({@code "CHACHA20"} vs + * {@code "CHACHA20-POLY1305"}), the required parameters, and the finalization + * behavior (AEAD verification at EOF). + *

+ */ + private enum Variant { + + /** + * ChaCha20 stream mode. + * + *

+ * Chosen by default or when a counter is configured. The initial counter + * defaults to 1 unless overridden by {@link #initialCounter(int)}; an explicit + * override can be propagated via {@link #withCounter(int)}. + *

+ */ + STREAM, + /** + * ChaCha20-Poly1305 authenticated encryption mode. + * + *

+ * Selected when non-empty AAD is provided via {@link #withAad(byte[])}. The + * pipeline produces and verifies an authentication tag; decryption failure must + * be treated as fatal. + *

+ */ + AEAD + } + + private Variant inferVariant() { + final boolean wantsAead = aad != null && aad.length > 0; + final boolean wantsStream = initialCounterSet || counter != null; + + if (wantsAead && wantsStream) { + throw new IllegalStateException( + "Conflicting ChaCha configuration: AAD was provided (AEAD) and counter options were set (STREAM). " + + "Remove either AAD or counter/initialCounter."); + } + // If neither explicitly set, default STREAM (matches common expectation) + return wantsAead ? Variant.AEAD : Variant.STREAM; + } + + private SecretKey resolveKey(String algId) { + if (secretKey != null) { + return secretKey; + } + try { + CryptoAlgorithm algo = CryptoAlgorithms.require(algId); + if (genSpec != null) { + SymmetricKeyBuilder b = algo.symmetricKeyBuilder(ChaChaKeyGenSpec.class); + return b.generateSecret(genSpec); + } + if (importSpec != null) { + SymmetricKeyBuilder b = algo.symmetricKeyBuilder(ChaChaKeyImportSpec.class); + return b.importSecret(importSpec); + } + SymmetricKeyBuilder b = algo.symmetricKeyBuilder(ChaChaKeyGenSpec.class); + return b.generateSecret(ChaChaKeyGenSpec.chacha256()); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("ChaCha key construction failed for " + algId, e); + } + } + + /** + * Streaming encrypting content that pulls bytes from an upstream + * {@link DataContent} and exposes the encrypted view as an + * {@link java.io.InputStream}. + * + *

+ * An instance is configured with the algorithm id, key, spec, and optional + * context. The upstream content is provided via {@link #setInput(DataContent)} + * and the transformed stream is obtained from {@link #getStream()}. + *

+ * + *

+ * The actual cipher work is delegated to an + * {@link zeroecho.core.context.EncryptionContext} created through + * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}. + * If the created context implements {@code ContextAware}, the configured + * context is injected before the stream is attached. + *

+ */ + private static final class EncryptContent implements EncryptedContent { + private final String algId; + private final SecretKey key; + private final S spec; + private final CtxInterface ctx; + private DataContent upstream; + + private EncryptContent(String algId, SecretKey key, S spec, CtxInterface ctx) { + this.algId = algId; + this.key = key; + this.spec = spec; + this.ctx = ctx; + } + + /** + * Sets the upstream content that will be encrypted. + * + * @param input the source content whose bytes will be read and encrypted + */ + @Override + public void setInput(DataContent input) { + this.upstream = input; + } + + /** + * Returns a stream that provides the encrypted view of the upstream content. + * + *

+ * The method creates an {@link zeroecho.core.context.EncryptionContext} for the + * configured algorithm id and encryption role, injects the optional context if + * applicable, and attaches the upstream stream to it. The returned stream + * performs encryption on-the-fly as it is read. + *

+ * + * @return an input stream that yields encrypted bytes + * @throws IOException if the upstream stream cannot be obtained or if + * context creation or attachment fails + * @throws NullPointerException if no upstream content has been set via + * {@link #setInput(DataContent)} + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "encrypt: missing input content"); + final EncryptionContext enc = CryptoAlgorithms.create(algId, KeyUsage.ENCRYPT, key, spec); // NOPMD + if (enc instanceof ContextAware ca) { + ca.setContext(ctx); + } + return enc.attach(upstream.getStream()); + } + } + + /** + * Streaming decrypting content that pulls bytes from an upstream + * {@link DataContent} and exposes the decrypted view as an + * {@link java.io.InputStream}. + * + *

+ * An instance is configured with the algorithm id, key, spec, and optional + * context. The upstream content is provided via {@link #setInput(DataContent)} + * and the transformed stream is obtained from {@link #getStream()}. + *

+ * + *

+ * The actual cipher work is delegated to an + * {@link zeroecho.core.context.EncryptionContext} created through + * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}. + * If the created context implements {@code ContextAware}, the configured + * context is injected before the stream is attached. + *

+ */ + private static final class DecryptContent implements PlainContent { + private final String algId; + private final SecretKey key; + private final S spec; + private final CtxInterface ctx; + private DataContent upstream; + + private DecryptContent(String algId, SecretKey key, S spec, CtxInterface ctx) { + this.algId = algId; + this.key = key; + this.spec = spec; + this.ctx = ctx; + } + + /** + * Sets the upstream content that will be decrypted. + * + * @param input the source content whose bytes will be read and decrypted + */ + @Override + public void setInput(DataContent input) { + this.upstream = input; + } + + /** + * Returns a stream that provides the decrypted view of the upstream content. + * + *

+ * The method creates an {@link zeroecho.core.context.EncryptionContext} for the + * configured algorithm id and decryption role, injects the optional context if + * applicable, and attaches the upstream stream to it. The returned stream + * performs decryption on-the-fly as it is read; in AEAD mode it will also + * verify the authentication tag when the stream is fully consumed. + *

+ * + * @return an input stream that yields decrypted bytes + * @throws IOException if the upstream stream cannot be obtained or if + * context creation or attachment fails + * @throws NullPointerException if no upstream content has been set via + * {@link #setInput(DataContent)} + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "decrypt: missing input content"); + final EncryptionContext dec = CryptoAlgorithms.create(algId, KeyUsage.DECRYPT, key, spec); // NOPMD + if (dec instanceof ContextAware ca) { + ca.setContext(ctx); + } + return dec.attach(upstream.getStream()); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/DigestDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/DigestDataContentBuilder.java new file mode 100644 index 0000000..62a4e37 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/DigestDataContentBuilder.java @@ -0,0 +1,569 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; +import java.util.function.Consumer; + +import conflux.CtxInterface; +import conflux.Key; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.NullKey; +import zeroecho.core.alg.digest.DigestSpec; +import zeroecho.core.context.DigestContext; +import zeroecho.core.io.TailStrippingInputStream; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * DigestDataContentBuilder constructs streaming pipelines that compute message + * digests while reading an InputStream. + * + *

Overview

The builder creates {@link PlainContent} that either passes + * the original bytes through and strips the trailing digest tag from the + * underlying engine, or emits the finalized digest itself in raw, hexadecimal, + * or Base64 form. Algorithm selection is performed via convenience methods such + * as {@link #sha256()} or {@link #shake256(int)} which configure an internal + * {@link DigestSpec}. + * + *

Output modes

+ *
    + *
  • Pass-through: the output stream is identical to the input stream + * while the digest is computed under the hood. The trailing tag produced by the + * engine is stripped off transparently.
  • + *
  • Emit digest: the output stream contains only the digest, encoded + * as raw bytes, hex, or Base64.
  • + *
+ * + *

Context integration

A runtime context can be supplied via + * {@link #context(conflux.CtxInterface)}. When provided together with + * {@link #storeInCtx(conflux.Key)}, the finalized digest bytes are stored in + * the context. A callback may also be registered via + * {@link #onDigest(java.util.function.Consumer)} to receive a defensive copy of + * the digest. + * + *

Examples

{@code
+ * // 1) Pass-through SHA-256 that stores the digest in a context:
+ * PlainContent content = DigestDataContentBuilder.builder()
+ *     .sha256()
+ *     .context(ctx)
+ *     .storeInCtx(digestKey)
+ *     .passThrough()
+ *     .build(true);
+ *
+ * // 2) Emit hex-encoded SHA3-512 digest:
+ * PlainContent hexOut = DigestDataContentBuilder.builder()
+ *     .sha3_512()
+ *     .emitHexDigest()
+ *     .build(true);
+ *
+ * // 3) Emit 32-byte SHAKE128 digest in Base64 and also receive it via a callback:
+ * PlainContent b64Out = DigestDataContentBuilder.builder()
+ *     .shake128(32)
+ *     .onDigest(bytes -> System.out.println("len=" + bytes.length))
+ *     .emitBase64Digest()
+ *     .build(true);
+ * }
+ * + *

Thread-safety

Instances are mutable and not thread-safe. Configure + * and use each builder instance from a single thread. + * + * @see DigestSpec + * @see DigestContext + * @see zeroecho.sdk.builders.core.DataContentBuilder + * @see zeroecho.sdk.content.api.PlainContent + */ +public final class DigestDataContentBuilder implements DataContentBuilder { + /** + * OutputMode selects how the digest-computing pipeline presents its result to + * callers. + * + *

+ * A mode controls whether the produced stream passes the original bytes through + * (while computing the digest transparently) or emits only the finalized digest + * in a chosen encoding. Modes are set by the builder methods + * {@link #passThrough()}, {@link #emitRawDigest()}, {@link #emitHexDigest()}, + * and {@link #emitBase64Digest()}. + *

+ * + *

+ * Context storage via {@link #storeInCtx(conflux.Key)} and callbacks registered + * with {@link #onDigest(java.util.function.Consumer)} are honored in all modes + * after the digest has been finalized. + *

+ * + * @since 1.0 + */ + private enum OutputMode { + /** + * Pass-through mode emits the original input bytes and strips the trailing tag. + * + *

+ * The underlying {@link DigestContext} appends a digest tag after the body. The + * pipeline transparently removes that tag from the stream, leaving the body + * unchanged. The finalized digest is still made available to the optional + * context and callback. + *

+ */ + PASSTHROUGH, + /** + * Emit-raw mode outputs only the finalized digest bytes. + * + *

+ * The upstream content is fully consumed to finalize the digest, then the + * pipeline exposes a stream over the raw digest bytes with no additional + * encoding. This is suitable when a binary digest is required by the caller. + *

+ */ + EMIT_RAW, + /** + * Emit-hex mode outputs the finalized digest as lowercase hexadecimal text. + * + *

+ * The hex string is ASCII-encoded with no separators or prefixes. This mode is + * convenient for logs, configuration files, and protocols that prefer textual + * digests. + *

+ */ + EMIT_HEX, + /** + * Emit-Base64 mode outputs the finalized digest as Base64 text without padding. + * + *

+ * The output is the RFC 4648 Base64 alphabet with padding omitted. This mode + * provides a compact textual representation suitable for HTTP headers and JSON. + *

+ */ + EMIT_BASE64 + } + + // ---- configuration ---- + private DigestSpec spec = DigestSpec.sha256(); + private OutputMode mode = OutputMode.PASSTHROUGH; + + private CtxInterface ctx; // optional + private Key ctxStoreKey; // optional + private Consumer callback; // optional + private int bufferSize = 8192; // internal I/O buffer for tail-stripper + + private DigestDataContentBuilder() { + } + + /** + * Creates a new builder for constructing digest-computing {@link PlainContent} + * pipelines. + * + *

Example

{@code
+     * PlainContent pc = DigestDataContentBuilder.builder()
+     *     .sha256()
+     *     .emitRawDigest()
+     *     .build(true);
+     * }
+ * + * @return a new {@code DigestDataContentBuilder} instance + */ + public static DigestDataContentBuilder builder() { + return new DigestDataContentBuilder(); + } + + /** + * Selects SHA-256 as the digest algorithm. + * + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder sha256() { + this.spec = DigestSpec.sha256(); + return this; + } + + /** + * Selects SHA-384 as the digest algorithm. + * + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder sha384() { + this.spec = DigestSpec.sha384(); + return this; + } + + /** + * Selects SHA-512 as the digest algorithm. + * + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder sha512() { + this.spec = DigestSpec.sha512(); + return this; + } + + /** + * Selects SHA3-256 as the digest algorithm. + * + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder sha3_256() { + this.spec = DigestSpec.sha3_256(); + return this; + } + + /** + * Selects SHA3-512 as the digest algorithm. + * + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder sha3_512() { + this.spec = DigestSpec.sha3_512(); + return this; + } + + /** + * Selects SHAKE128 with a given output length. + * + * @param outLenBytes the desired digest length in bytes + * @return {@code this} builder for chaining + * @throws IllegalArgumentException if {@code outLenBytes} is invalid for the + * implementation + */ + public DigestDataContentBuilder shake128(int outLenBytes) { + this.spec = DigestSpec.shake128(outLenBytes); + return this; + } + + /** + * Selects SHAKE256 with a given output length. + * + * @param outLenBytes the desired digest length in bytes + * @return {@code this} builder for chaining + * @throws IllegalArgumentException if {@code outLenBytes} is invalid for the + * implementation + */ + public DigestDataContentBuilder shake256(int outLenBytes) { + this.spec = DigestSpec.shake256(outLenBytes); + return this; + } + + /** + * Configures the builder to pass the input through unchanged and strip the + * trailing tag. + * + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder passThrough() { + this.mode = OutputMode.PASSTHROUGH; + return this; + } + + /** + * Configures the builder to emit the finalized digest as raw bytes. + * + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder emitRawDigest() { + this.mode = OutputMode.EMIT_RAW; + return this; + } + + /** + * Configures the builder to emit the finalized digest as lowercase hexadecimal + * text. + * + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder emitHexDigest() { + this.mode = OutputMode.EMIT_HEX; + return this; + } + + /** + * Configures the builder to emit the finalized digest as Base64 text without + * padding. + * + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder emitBase64Digest() { + this.mode = OutputMode.EMIT_BASE64; + return this; + } + + /** + * Sets the internal buffer size used by the tail-stripping wrapper. + * + * @param bytes buffer size in bytes; must be greater than or equal to 1 + * @return {@code this} builder for chaining + * @throws IllegalArgumentException if {@code bytes < 1} + */ + public DigestDataContentBuilder bufferSize(int bytes) { + if (bytes < 1) { // NOPMD + throw new IllegalArgumentException("bufferSize must be >= 1"); + } + this.bufferSize = bytes; + return this; + } + + /** + * Sets an optional runtime context used to store the finalized digest when + * requested. + * + * @param ctx the context instance; may be null to disable context storage + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder context(CtxInterface ctx) { + this.ctx = ctx; + return this; + } + + /** + * Requests that the finalized digest be written into the context under the + * provided key. + * + *

+ * This setting is only effective if a non-null context is provided via + * {@link #context(CtxInterface)}. + *

+ * + * @param key the context key used to store the digest; may be null to clear + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder storeInCtx(Key key) { + this.ctxStoreKey = key; + return this; + } + + /** + * Registers a callback to receive a defensive copy of the finalized digest. + * + * @param cb the callback to invoke after the digest is computed; may be null to + * clear + * @return {@code this} builder for chaining + */ + public DigestDataContentBuilder onDigest(Consumer cb) { + this.callback = cb; + return this; + } + + /** + * Builds a digest-computing {@link PlainContent} according to the current + * configuration. + * + *

+ * The boolean parameter is ignored and present only to satisfy the + * {@link zeroecho.sdk.builders.core.DataContentBuilder} interface. + *

+ * + *

Result

+ *
    + *
  • Pass-through mode returns content that outputs the original bytes while + * computing the digest and stripping the trailing tag.
  • + *
  • Emit modes return content whose output is only the digest in the + * requested encoding.
  • + *
+ * + * @param encryptFlagIgnored ignored parameter from the interface + * @return a {@link PlainContent} pipeline that performs the selected digest + * behavior when its stream is consumed + */ + @Override + public PlainContent build(boolean encryptFlagIgnored) { + return switch (mode) { + case PASSTHROUGH -> new PassthroughDigestContent(spec, ctx, ctxStoreKey, callback, bufferSize); + case EMIT_RAW, EMIT_HEX, EMIT_BASE64 -> + new DigestBytesContent(spec, ctx, ctxStoreKey, callback, mode, bufferSize); + }; + } + + /** + * Pass-through: output == input; digest is computed by DigestContext and + * captured from the produced trailer using + * TailStrippingInputStream#processTail(...). + */ + private static final class PassthroughDigestContent implements PlainContent { + private final DigestSpec spec; + private final CtxInterface ctx; + private final Key storeKey; + private final Consumer callback; + private final int bufferSize; + + private DataContent input; + + private PassthroughDigestContent(DigestSpec spec, CtxInterface ctx, Key storeKey, + Consumer callback, int bufferSize) { + this.spec = Objects.requireNonNull(spec, "spec"); + this.ctx = ctx; + this.storeKey = storeKey; + this.callback = callback; + this.bufferSize = bufferSize; + } + + @Override + public void setInput(DataContent input) { + this.input = Objects.requireNonNull(input, "input must not be null"); + } + + @Override + public InputStream getStream() throws IOException { + if (input == null) { + throw new IllegalStateException("Upstream input is not set."); + } + + // Create a DigestContext via registry (DIGEST / DIGEST / NullKey + spec). + final DigestContext dctx = CryptoAlgorithms.create("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, spec); // NOPMD + + // Engine emits [body][tag]; we strip the tag and capture it. + final InputStream produced = dctx.wrap(input.getStream()); + final int tagLen = dctx.tagLength(); + + return new TailStrippingInputStream(produced, tagLen, bufferSize) { + @Override + protected void processTail(byte[] tail) throws IOException { + if (tail == null) { + return; + } + byte[] copy = tail.clone(); + if (ctx != null && storeKey != null) { + try { + ctx.put(storeKey, copy); + } catch (RuntimeException ignore) { // NOPMD + } + } + if (callback != null) { + try { + callback.accept(copy.clone()); + } catch (RuntimeException ignore) { // NOPMD + } + } + } + }; + } + } + + /** + * Emit-only: consumes upstream so the engine appends the tag; we capture the + * tag via TailStrippingInputStream and then return a stream over the encoded + * tag. + */ + private static final class DigestBytesContent implements PlainContent { + private final DigestSpec spec; + private final CtxInterface ctx; + private final Key storeKey; + private final Consumer callback; + private final OutputMode out; + private final int bufferSize; + + private DataContent input; + + private DigestBytesContent(DigestSpec spec, CtxInterface ctx, Key storeKey, Consumer callback, + OutputMode out, int bufferSize) { + this.spec = Objects.requireNonNull(spec, "spec"); + this.ctx = ctx; + this.storeKey = storeKey; + this.callback = callback; + this.out = Objects.requireNonNull(out, "out"); + if (out == OutputMode.PASSTHROUGH) { + throw new IllegalArgumentException("DigestBytesContent must use an emit-* mode"); + } + this.bufferSize = bufferSize; + } + + @Override + public void setInput(DataContent input) { + this.input = Objects.requireNonNull(input, "input must not be null"); + } + + @Override + public InputStream getStream() throws IOException { + if (input == null) { + throw new IllegalStateException("Upstream input is not set."); + } + + // Create engine and its output (which appends [tag] after the body). + final DigestContext dctx = CryptoAlgorithms.create("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, spec); // NOPMD + final InputStream produced = dctx.wrap(input.getStream()); + final int tagLen = dctx.tagLength(); + + // Capture trailer while reading the produced stream to EOF. + final byte[][] holder = new byte[1][]; + try (InputStream in = new TailStrippingInputStream(produced, tagLen, bufferSize) { + @Override + protected void processTail(byte[] tail) throws IOException { + holder[0] = (tail == null ? null : tail.clone()); + } + }) { + byte[] buf = new byte[8192]; + while (true) { + int n = in.read(buf); + if (n < 0) { + break; // discard body (we only need the digest) + } + } + } + + byte[] digest = holder[0]; + if (digest == null) { + digest = new byte[0]; + } + + // Store / callback (raw bytes) + byte[] toStore = digest.clone(); + if (ctx != null && storeKey != null) { + try { + ctx.put(storeKey, toStore); + } catch (RuntimeException ignore) { // NOPMD + } + } + if (callback != null) { + try { + callback.accept(toStore.clone()); + } catch (RuntimeException ignore) { // NOPMD + } + } + + // Encode as requested + byte[] outBytes = switch (out) { + case EMIT_RAW -> toStore; + case EMIT_HEX -> java.util.HexFormat.of().formatHex(toStore).getBytes(StandardCharsets.UTF_8); + case EMIT_BASE64 -> Base64.getEncoder().withoutPadding().encode(toStore); + default -> throw new IllegalStateException("Unexpected output mode: " + out); + }; + return new ByteArrayInputStream(outBytes); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/EcdsaDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/EcdsaDataContentBuilder.java new file mode 100644 index 0000000..5bb1b98 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/EcdsaDataContentBuilder.java @@ -0,0 +1,316 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Objects; +import java.util.function.Supplier; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.alg.common.sig.GenericJcaSignatureContext; +import zeroecho.core.alg.ecdsa.EcdsaCurveSpec; +import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec; +import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec; +import zeroecho.core.context.SignatureContext; + +/** + * EcdsaDataContentBuilder configures and builds streaming ECDSA signature + * pipelines over an InputStream. + * + *

Overview

This builder specializes + * {@link AbstractStreamingSignatureDataBuilder} for ECDSA and lets callers + * choose a named curve before constructing a signing or verification pipeline. + * The actual signing and verification work is performed by + * {@link SignatureContext} instances created via JCA-backed factories. + * + *

Typical usage

{@code
+ * // Sign while passing the original bytes through:
+ * PlainContent signed = EcdsaDataContentBuilder.builder()
+ *     .withCurveP256()
+ *     .sign()
+ *     .withPrivateKey(privateKey)
+ *     .passThrough()
+ *     .build(true);
+ *
+ * // Emit a detached Base64 signature:
+ * PlainContent sigOut = EcdsaDataContentBuilder.builder()
+ *     .withCurve(EcdsaCurveSpec.P384)
+ *     .sign()
+ *     .withPrivateKey(privateKey)
+ *     .emitBase64Signature()
+ *     .build(true);
+ *
+ * // Verify while passing the original bytes through:
+ * PlainContent verified = EcdsaDataContentBuilder.builder()
+ *     .withCurveP256()
+ *     .verify()
+ *     .withPublicKey(publicKey)
+ *     .expectedSignature(rawSig)
+ *     .passThrough()
+ *     .build(true);
+ * }
+ * + *

Curve selection

If no curve is selected explicitly, {@code P256} is + * used. Convenience methods are provided for P-256, P-384, and P-512 + * (implementation-specific name used by {@link EcdsaCurveSpec}). + * + *

Thread-safety

Instances are mutable and not thread-safe. Configure + * and use each builder instance from a single thread. + * + * @see AbstractStreamingSignatureDataBuilder + * @see EcdsaCurveSpec + * @see EcdsaPublicKeySpec + * @see EcdsaPrivateKeySpec + * @see SignatureContext + * @see GenericJcaSignatureContext + */ +public final class EcdsaDataContentBuilder + extends AbstractStreamingSignatureDataBuilder { + + private final static EcdsaCurveSpec DEFAULT = EcdsaCurveSpec.P256; + + private EcdsaCurveSpec selected = DEFAULT; + + /** + * Creates a new builder instance with the default curve selection. + * + *

Example

{@code
+     * EcdsaDataContentBuilder b = EcdsaDataContentBuilder.builder();
+     * }
+ * + * @return a new {@code EcdsaDataContentBuilder} + */ + public static EcdsaDataContentBuilder builder() { + return new EcdsaDataContentBuilder(); + } + + /** + * Selects the curve to be used for key generation and signature processing. + * + * @param spec the ECDSA curve specification; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code spec} is null + */ + public EcdsaDataContentBuilder withCurve(final EcdsaCurveSpec spec) { + Objects.requireNonNull(spec, "EcdsaCurveSpec cannot be null"); + this.selected = spec; + return this; + } + + /** + * Selects the P-256 curve. + * + * @return {@code this} builder for chaining + */ + public EcdsaDataContentBuilder withCurveP256() { + this.selected = EcdsaCurveSpec.P256; + return this; + } + + /** + * Selects the P-384 curve. + * + * @return {@code this} builder for chaining + */ + public EcdsaDataContentBuilder withCurveP384() { + this.selected = EcdsaCurveSpec.P384; + return this; + } + + /** + * Selects the P-512 curve as defined by {@link EcdsaCurveSpec}. + * + * @return {@code this} builder for chaining + */ + public EcdsaDataContentBuilder withCurveP512() { + this.selected = EcdsaCurveSpec.P512; + return this; + } + + /** + * Returns the algorithm name used to resolve an implementation from + * {@link CryptoAlgorithms}. + * + * @return the string {@code "ECDSA"} + */ + @Override + protected String algorithmName() { + return "ECDSA"; + } + + private EcdsaCurveSpec activeSpec() { + return selected; + } + + /** + * Creates a signing {@link SignatureContext} for the active curve using the + * provided algorithm and key. + * + *

+ * The returned context is configured with a JCA signature factory derived from + * the curve's {@link EcdsaCurveSpec#jcaFactory()} and a fixed-length resolver + * based on {@link EcdsaCurveSpec#signFixedLength()}. + *

+ * + * @param alg the resolved crypto algorithm + * @param key the private key to use for signing + * @return a new signature context in sign mode + * @throws GeneralSecurityException if the context cannot be created + */ + @Override + protected SignatureContext newSignContext(final CryptoAlgorithm alg, final PrivateKey key) + throws GeneralSecurityException { + EcdsaCurveSpec s = activeSpec(); + return new GenericJcaSignatureContext(alg, key, GenericJcaSignatureContext.jcaFactory(s.jcaFactory(), null), + GenericJcaSignatureContext.SignLengthResolver.fixed(s.signFixedLength())); + } + + /** + * Creates a verification {@link SignatureContext} for the active curve using + * the provided algorithm and key. + * + *

+ * The returned context is configured with a JCA signature factory derived from + * the curve's {@link EcdsaCurveSpec#jcaFactory()} and a fixed-length resolver + * based on {@link EcdsaCurveSpec#signFixedLength()}. + *

+ * + * @param alg the resolved crypto algorithm + * @param key the public key to use for verification + * @return a new signature context in verify mode + * @throws GeneralSecurityException if the context cannot be created + */ + @Override + protected SignatureContext newVerifyContext(final CryptoAlgorithm alg, final PublicKey key) + throws GeneralSecurityException { + EcdsaCurveSpec s = activeSpec(); + return new GenericJcaSignatureContext(alg, key, GenericJcaSignatureContext.jcaFactory(s.jcaFactory(), null), + GenericJcaSignatureContext.VerifyLengthResolver.fixed(s.signFixedLength())); + } + + /** + * Returns the class object of the key generation specification used by this + * builder. + * + * @return {@code EcdsaCurveSpec.class} + */ + @Override + protected Class keyGenSpecClass() { + return EcdsaCurveSpec.class; + } + + /** + * Returns the class object of the public key import specification used by this + * builder. + * + * @return {@code EcdsaPublicKeySpec.class} + */ + @Override + protected Class publicKeySpecClass() { + return EcdsaPublicKeySpec.class; + } + + /** + * Returns the class object of the private key import specification used by this + * builder. + * + * @return {@code EcdsaPrivateKeySpec.class} + */ + @Override + protected Class privateKeySpecClass() { + return EcdsaPrivateKeySpec.class; + } + + /** + * Supplies the default key generation specification for this builder. + * + * @return a supplier that returns {@link EcdsaCurveSpec#P256} + */ + @Override + protected Supplier defaultKeyGenSpecSupplier() { + return () -> DEFAULT; + } + + /** + * Returns the currently selected key generation specification, or null to + * indicate that the default should be used. + * + * @return the active {@link EcdsaCurveSpec} or {@code null} + */ + @Override + protected EcdsaCurveSpec currentKeyGenSpecOrNull() { + return selected; + } + + /** + * Creates a public key import specification from X.509-encoded bytes. + * + * @param x509 the SubjectPublicKeyInfo bytes + * @param provider an optional provider name hint, ignored by this + * implementation + * @return a new {@link EcdsaPublicKeySpec} wrapping the provided bytes + */ + @Override + protected EcdsaPublicKeySpec makePublicKeySpec(final byte[] x509, final String provider) { + return new EcdsaPublicKeySpec(x509); + } + + /** + * Creates a private key import specification from PKCS#8-encoded bytes. + * + * @param pkcs8 the PrivateKeyInfo bytes + * @param provider an optional provider name hint, ignored by this + * implementation + * @return a new {@link EcdsaPrivateKeySpec} wrapping the provided bytes + */ + @Override + protected EcdsaPrivateKeySpec makePrivateKeySpec(final byte[] pkcs8, final String provider) { + return new EcdsaPrivateKeySpec(pkcs8); + } + + /** + * Returns the default provider hint to use when importing keys if none is + * explicitly set. + * + * @return {@code null} to indicate no preference + */ + @Override + protected String defaultProviderHint() { + return null; + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/Ed25519DataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/Ed25519DataContentBuilder.java new file mode 100644 index 0000000..5ffbb9f --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/Ed25519DataContentBuilder.java @@ -0,0 +1,254 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.function.Supplier; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec; +import zeroecho.core.alg.ed25519.Ed25519PrivateKeySpec; +import zeroecho.core.alg.ed25519.Ed25519PublicKeySpec; +import zeroecho.core.alg.ed25519.Ed25519SignatureContext; +import zeroecho.core.context.SignatureContext; + +/** + * Ed25519DataContentBuilder builds streaming Ed25519 signature pipelines that + * sign or verify as an InputStream is consumed. + * + *

Overview

This builder specializes + * {@link AbstractStreamingSignatureDataBuilder} for the Ed25519 algorithm. It + * constructs {@link zeroecho.sdk.content.api.PlainContent} that either passes + * the original bytes through while computing or checking a detached signature, + * or emits the signature or verification result directly, depending on the + * configuration provided by the fluent API defined in the superclass. + * + *

Typical usage

{@code
+ * // Sign while passing the original bytes through:
+ * PlainContent signed = Ed25519DataContentBuilder.builder()
+ *     .sign()
+ *     .withPrivateKey(privateKey)
+ *     .passThrough()
+ *     .build(true);
+ *
+ * // Emit a Base64 detached signature:
+ * PlainContent sigOut = Ed25519DataContentBuilder.builder()
+ *     .sign()
+ *     .withPrivateKey(privateKey)
+ *     .emitBase64Signature()
+ *     .build(true);
+ *
+ * // Verify against an expected signature while passing data through:
+ * PlainContent verified = Ed25519DataContentBuilder.builder()
+ *     .verify()
+ *     .withPublicKey(publicKey)
+ *     .expectedSignature(rawSig)
+ *     .passThrough()
+ *     .build(true);
+ * }
+ * + *

Key handling

Ed25519 has no tunable parameters for key generation in + * this builder. A default generation supplier is provided, and imports from + * X.509 (public) and PKCS#8 (private) encodings are supported. + * + *

Thread-safety

Instances are mutable and not thread-safe. Configure + * and use each builder instance from a single thread. + * + * @see AbstractStreamingSignatureDataBuilder + * @see zeroecho.core.CryptoAlgorithms + * @see zeroecho.core.context.SignatureContext + */ +public final class Ed25519DataContentBuilder + extends AbstractStreamingSignatureDataBuilder { + + private static final Supplier DEFAULT_GEN = Ed25519KeyGenSpec::defaultSpec; + + /** + * Creates a new builder instance for constructing Ed25519 streaming signature + * pipelines. + * + *

Example

{@code
+     * Ed25519DataContentBuilder b = Ed25519DataContentBuilder.builder();
+     * }
+ * + * @return a new {@code Ed25519DataContentBuilder} + */ + public static Ed25519DataContentBuilder builder() { + return new Ed25519DataContentBuilder(); + } + + /** + * Returns the canonical algorithm name used to resolve an implementation from + * {@link zeroecho.core.CryptoAlgorithms}. + * + * @return the string {@code "Ed25519"} + */ + @Override + protected String algorithmName() { + return "Ed25519"; + } + + /** + * Creates a signing {@link SignatureContext} for Ed25519 using the provided + * algorithm instance and private key. + * + * @param alg the resolved algorithm instance used to create the context + * @param key the private key for signing + * @return a new {@link SignatureContext} configured for Ed25519 signing + * @throws GeneralSecurityException if the context cannot be created for the + * given key or algorithm + */ + @Override + protected SignatureContext newSignContext(final CryptoAlgorithm alg, final PrivateKey key) + throws GeneralSecurityException { + return new Ed25519SignatureContext(alg, key); + } + + /** + * Creates a verification {@link SignatureContext} for Ed25519 using the + * provided algorithm instance and public key. + * + * @param alg the resolved algorithm instance used to create the context + * @param key the public key for verification + * @return a new {@link SignatureContext} configured for Ed25519 verification + * @throws GeneralSecurityException if the context cannot be created for the + * given key or algorithm + */ + @Override + protected SignatureContext newVerifyContext(final CryptoAlgorithm alg, final PublicKey key) + throws GeneralSecurityException { + return new Ed25519SignatureContext(alg, key); + } + + /** + * Returns the key generation specification class used by this builder. + * + * @return {@code Ed25519KeyGenSpec.class} + */ + @Override + protected Class keyGenSpecClass() { + return Ed25519KeyGenSpec.class; + } + + /** + * Returns the public key import specification class used by this builder. + * + * @return {@code Ed25519PublicKeySpec.class} + */ + @Override + protected Class publicKeySpecClass() { + return Ed25519PublicKeySpec.class; + } + + /** + * Returns the private key import specification class used by this builder. + * + * @return {@code Ed25519PrivateKeySpec.class} + */ + @Override + protected Class privateKeySpecClass() { + return Ed25519PrivateKeySpec.class; + } + + /** + * Supplies a default key generation specification for Ed25519. + * + * @return a supplier returning {@link Ed25519KeyGenSpec#defaultSpec()} + */ + @Override + protected Supplier defaultKeyGenSpecSupplier() { + return DEFAULT_GEN; + } + + /** + * Returns the currently configured key generation specification or null to + * indicate the default should be used. + * + *

+ * Ed25519 has no tunables in this builder, so this method returns {@code null}. + *

+ * + * @return {@code null} + */ + @Override + protected Ed25519KeyGenSpec currentKeyGenSpecOrNull() { + return null; // no tunables for Ed25519 + } + + /** + * Builds a public key import specification from X.509-encoded + * SubjectPublicKeyInfo bytes. + * + * @param x509 the X.509 public key bytes + * @param ignoredProvider an optional provider hint, ignored by this + * implementation + * @return a new {@link Ed25519PublicKeySpec} wrapping the provided bytes + */ + @Override + protected Ed25519PublicKeySpec makePublicKeySpec(final byte[] x509, final String ignoredProvider) { + return new Ed25519PublicKeySpec(x509); + } + + /** + * Builds a private key import specification from PKCS#8-encoded PrivateKeyInfo + * bytes. + * + * @param pkcs8 the PKCS#8 private key bytes + * @param ignoredProvider an optional provider hint, ignored by this + * implementation + * @return a new {@link Ed25519PrivateKeySpec} wrapping the provided bytes + */ + @Override + protected Ed25519PrivateKeySpec makePrivateKeySpec(final byte[] pkcs8, final String ignoredProvider) { + return new Ed25519PrivateKeySpec(pkcs8); + } + + /** + * Returns the default provider hint used for key imports when none is + * explicitly supplied. + * + *

+ * This builder relies on the JDK default provider for Ed25519. + *

+ * + * @return {@code null} to indicate no provider preference + */ + @Override + protected String defaultProviderHint() { + return null; // use JDK default provider + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/Ed448DataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/Ed448DataContentBuilder.java new file mode 100644 index 0000000..ab786a6 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/Ed448DataContentBuilder.java @@ -0,0 +1,275 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.function.Supplier; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.alg.ed448.Ed448KeyGenSpec; +import zeroecho.core.alg.ed448.Ed448PrivateKeySpec; +import zeroecho.core.alg.ed448.Ed448PublicKeySpec; +import zeroecho.core.alg.ed448.Ed448SignatureContext; +import zeroecho.core.context.SignatureContext; + +/** + * Builder for constructing streaming Ed448 signature and verification + * {@link zeroecho.sdk.content.api.DataContent} pipelines. + * + *

+ * {@code Ed448DataContentBuilder} is a thin adapter around + * {@link AbstractStreamingSignatureDataBuilder} that binds the generic + * streaming signature framework to the Ed448 algorithm. It supports both + * signing and verification flows, with key material provided via + * {@link Ed448KeyGenSpec}, {@link Ed448PublicKeySpec}, and + * {@link Ed448PrivateKeySpec}. + *

+ * + *

Usage example

{@code
+ * // Create a builder for signing
+ * Ed448DataContentBuilder builder = Ed448DataContentBuilder.builder()
+ *     .sign()
+ *     .generateKeyPair()
+ *     .emitBase64Signature();
+ *
+ * // Build a content pipeline
+ * PlainContent content = builder.build(true);
+ * content.setInput(originalContent);
+ *
+ * try (InputStream in = content.getStream()) {
+ *     byte[] signature = in.readAllBytes();
+ * }
+ * }
+ * + *

Design notes

+ *
    + *
  • Uses {@link Ed448SignatureContext} for both signing and + * verification.
  • + *
  • Key generation is parameterized by {@link Ed448KeyGenSpec}, but Ed448 + * exposes no runtime tunables; the default is always used.
  • + *
  • Public/private key imports are supported via X.509 and PKCS#8 wrappers + * respectively.
  • + *
  • No provider hints are necessary; the JDK default is assumed.
  • + *
+ * + * @see Ed448SignatureContext + * @see Ed448KeyGenSpec + * @see Ed448PublicKeySpec + * @see Ed448PrivateKeySpec + * @since 1.0 + */ +public final class Ed448DataContentBuilder + extends AbstractStreamingSignatureDataBuilder { + + private static final Supplier DEFAULT_GEN = Ed448KeyGenSpec::defaultSpec; + + /** + * Creates a new builder instance for constructing Ed448 streaming signature + * pipelines. + * + *

Example

{@code
+     * Ed448DataContentBuilder b = Ed448DataContentBuilder.builder();
+     * }
+ * + * @return a new {@code Ed448DataContentBuilder} + */ + public static Ed448DataContentBuilder builder() { + return new Ed448DataContentBuilder(); + } + + /** + * Returns the canonical algorithm name used to resolve an implementation from + * {@link zeroecho.core.CryptoAlgorithms}. + * + * @return the string {@code "Ed448"} + */ + @Override + protected String algorithmName() { + return "Ed448"; + } + + /** + * Creates a signing {@link SignatureContext} for Ed448 using the provided + * algorithm instance and private key. + * + *

+ * The returned context is configured to accept streaming updates and to produce + * a detached signature tag at end-of-stream. + *

+ * + *

Example

{@code
+     * SignatureContext sc = newSignContext(alg, privateKey);
+     * try (InputStream in = sc.wrap(upstream)) {
+     *     in.transferTo(OutputStream.nullOutputStream());
+     * }
+     * }
+ * + * @param alg the resolved algorithm instance + * @param key the private key used for signing + * @return a new signature context in sign mode + * @throws GeneralSecurityException if the context cannot be created for the + * given key or algorithm + */ + @Override + protected SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) throws GeneralSecurityException { + return new Ed448SignatureContext(alg, key); + } + + /** + * Creates a verification {@link SignatureContext} for Ed448 using the provided + * algorithm instance and public key. + * + *

+ * The returned context is configured to accept streaming updates and to + * validate the expected detached signature tag at end-of-stream. + *

+ * + * @param alg the resolved algorithm instance + * @param key the public key used for verification + * @return a new signature context in verify mode + * @throws GeneralSecurityException if the context cannot be created for the + * given key or algorithm + */ + @Override + protected SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) throws GeneralSecurityException { + return new Ed448SignatureContext(alg, key); + } + + /** + * Returns the key generation specification class used by this builder. + * + * @return {@code Ed448KeyGenSpec.class} + */ + @Override + protected Class keyGenSpecClass() { + return Ed448KeyGenSpec.class; + } + + /** + * Returns the public key import specification class used by this builder. + * + * @return {@code Ed448PublicKeySpec.class} + */ + @Override + protected Class publicKeySpecClass() { + return Ed448PublicKeySpec.class; + } + + /** + * Returns the private key import specification class used by this builder. + * + * @return {@code Ed448PrivateKeySpec.class} + */ + @Override + protected Class privateKeySpecClass() { + return Ed448PrivateKeySpec.class; + } + + /** + * Supplies the default key generation specification for Ed448. + * + * @return a supplier returning {@link Ed448KeyGenSpec#defaultSpec()} + */ + @Override + protected Supplier defaultKeyGenSpecSupplier() { + return DEFAULT_GEN; + } + + /** + * Returns the currently configured key generation specification or null to + * indicate that the default should be used. + * + *

+ * Ed448 has no tunables in this builder, so this method returns {@code null}. + *

+ * + * @return {@code null} + */ + @Override + protected Ed448KeyGenSpec currentKeyGenSpecOrNull() { + return null; // no options + } + + /** + * Builds a public key import specification from X.509-encoded + * SubjectPublicKeyInfo bytes. + * + *

Example

{@code
+     * Ed448PublicKeySpec spec = makePublicKeySpec(spkiBytes, null);
+     * }
+ * + * @param x509 X.509 public key bytes (SubjectPublicKeyInfo) + * @param ignoredProvider optional provider hint, ignored by this implementation + * @return a new {@link Ed448PublicKeySpec} wrapping the provided bytes + */ + @Override + protected Ed448PublicKeySpec makePublicKeySpec(byte[] x509, String ignoredProvider) { + return new Ed448PublicKeySpec(x509); + } + + /** + * Builds a private key import specification from PKCS#8-encoded PrivateKeyInfo + * bytes. + * + *

Example

{@code
+     * Ed448PrivateKeySpec spec = makePrivateKeySpec(pkcs8Bytes, null);
+     * }
+ * + * @param pkcs8 PKCS#8 private key bytes (PrivateKeyInfo) + * @param ignoredProvider optional provider hint, ignored by this implementation + * @return a new {@link Ed448PrivateKeySpec} wrapping the provided bytes + */ + @Override + protected Ed448PrivateKeySpec makePrivateKeySpec(byte[] pkcs8, String ignoredProvider) { + return new Ed448PrivateKeySpec(pkcs8); + } + + /** + * Returns the default provider hint used for key imports when none is + * explicitly supplied. + * + *

+ * This builder relies on the JDK default provider for Ed448. + *

+ * + * @return {@code null} to indicate no provider preference + */ + @Override + protected String defaultProviderHint() { + return null; // JDK default + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java new file mode 100644 index 0000000..2e768b6 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/ElgamalEncDataContentBuilder.java @@ -0,0 +1,484 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Objects; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.elgamal.ElgamalEncSpec; +import zeroecho.core.alg.elgamal.ElgamalKeyGenSpec; +import zeroecho.core.alg.elgamal.ElgamalParamSpec; +import zeroecho.core.alg.elgamal.ElgamalPrivateKeySpec; +import zeroecho.core.alg.elgamal.ElgamalPublicKeySpec; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.EncryptedContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * ElgamalEncDataContentBuilder builds streaming ElGamal encryption or + * decryption pipelines that operate as an InputStream is consumed. + * + *

Overview

This builder creates + * {@link zeroecho.sdk.content.api.DataContent} for ElGamal public key + * encryption. In encrypt mode it returns content that transforms upstream + * plaintext into ciphertext using a + * {@link zeroecho.core.context.EncryptionContext}; in decrypt mode it produces + * plaintext from ciphertext. Keys may be supplied, generated, or imported from + * standard encodings. Padding mode is selected via {@link ElgamalEncSpec}. + * + *

Typical usage

{@code
+ * // Encrypt with a generated 2048-bit key pair using PKCS#1-style padding:
+ * DataContent enc = ElgamalEncDataContentBuilder.builder()
+ *     .generateKeyPair(ElgamalKeyGenSpec.elgamal2048())
+ *     .pkcs1()
+ *     .build(true);
+ *
+ * // Decrypt with an imported private key (PKCS#8) and no padding:
+ * DataContent dec = ElgamalEncDataContentBuilder.builder()
+ *     .noPadding()
+ *     .importPrivatePkcs8(pkcs8Bytes)
+ *     .build(false);
+ *
+ * // Encrypt with an existing public key and parameter preset:
+ * DataContent enc2 = ElgamalEncDataContentBuilder.builder()
+ *     .withPublicKey(pub)
+ *     .spec(ElgamalEncSpec.pkcs1())
+ *     .build(true);
+ * }
+ * + *

Key material

You can: + *
    + *
  • Provide keys directly via {@link #withPublicKey(PublicKey)} or + * {@link #withPrivateKey(PrivateKey)}.
  • + *
  • Generate a pair using a size-based {@link ElgamalKeyGenSpec} or a preset + * {@link ElgamalParamSpec}.
  • + *
  • Import from encodings with {@link #importPublicX509(byte[])} and + * {@link #importPrivatePkcs8(byte[])}.
  • + *
+ * + *

Thread-safety

Instances are mutable and not thread-safe. Configure + * and use each builder instance from a single thread. + * + * @see zeroecho.sdk.builders.core.DataContentBuilder + * @see zeroecho.core.CryptoAlgorithms + * @see zeroecho.core.context.EncryptionContext + * @see ElgamalEncSpec + * @see ElgamalKeyGenSpec + * @see ElgamalParamSpec + */ +public final class ElgamalEncDataContentBuilder implements DataContentBuilder { + private PublicKey publicKey; + private PrivateKey privateKey; + + private boolean genKeyPairPredef; + private ElgamalParamSpec paramSpec; + + private boolean genKeyPair; + private ElgamalKeyGenSpec keyGen = ElgamalKeyGenSpec.elgamal2048(); + + private byte[] importPrivatePkcs8; + private byte[] importPublicX509; + + private ElgamalEncSpec spec = ElgamalEncSpec.pkcs1(); + + private ElgamalEncDataContentBuilder() { + } + + /** + * Creates a new builder for constructing ElGamal encryption or decryption + * pipelines. + * + *

Example

{@code
+     * ElgamalEncDataContentBuilder b = ElgamalEncDataContentBuilder.builder();
+     * }
+ * + * @return a new {@code ElgamalEncDataContentBuilder} instance + */ + public static ElgamalEncDataContentBuilder builder() { + return new ElgamalEncDataContentBuilder(); + } + + /** + * Sets the encryption specification such as padding mode. + * + * @param s the ElGamal encryption spec to use; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code s} is null + */ + public ElgamalEncDataContentBuilder spec(ElgamalEncSpec s) { + this.spec = Objects.requireNonNull(s); + return this; + } + + /** + * Selects the no-padding variant of ElGamal encryption. + * + *

+ * Use with caution. No padding may be appropriate only for specific protocols + * that add their own randomness and structure. + *

+ * + * @return {@code this} builder for chaining + */ + public ElgamalEncDataContentBuilder noPadding() { + this.spec = ElgamalEncSpec.noPadding(); + return this; + } + + /** + * Selects the PKCS#1-style padding variant of ElGamal encryption. + * + * @return {@code this} builder for chaining + */ + public ElgamalEncDataContentBuilder pkcs1() { + this.spec = ElgamalEncSpec.pkcs1(); + return this; + } + + /** + * Supplies the public key to use for encryption. + * + * @param k the public key; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code k} is null + */ + public ElgamalEncDataContentBuilder withPublicKey(PublicKey k) { + this.publicKey = Objects.requireNonNull(k); + return this; + } + + /** + * Supplies the private key to use for decryption. + * + * @param k the private key; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code k} is null + */ + public ElgamalEncDataContentBuilder withPrivateKey(PrivateKey k) { + this.privateKey = Objects.requireNonNull(k); + return this; + } + + /** + * Requests generation of a fresh ElGamal key pair using the provided key + * generation specification. + * + *

+ * This method clears any preset-parameter request. The generated keys will be + * used for subsequent {@link #build(boolean)} calls unless overridden by + * imports or explicit keys. + *

+ * + * @param spec the key generation spec; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code spec} is null + */ + public ElgamalEncDataContentBuilder generateKeyPair(ElgamalKeyGenSpec spec) { + this.genKeyPair = true; + this.genKeyPairPredef = false; + this.keyGen = Objects.requireNonNull(spec); + return this; + } + + /** + * Requests generation of a fresh ElGamal key pair with the specified modulus + * length and default certainty. + * + * @param bits modulus size in bits + * @return {@code this} builder for chaining + */ + public ElgamalEncDataContentBuilder generateKeyPair(int bits) { + return generateKeyPair(new ElgamalKeyGenSpec(bits, 128)); + } + + /** + * Requests generation of a fresh ElGamal key pair using a predefined parameter + * set. + * + *

+ * This method clears any size-based generation request. The generated keys will + * be used for subsequent {@link #build(boolean)} calls unless overridden. + *

+ * + * @param spec the predefined parameter set; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code spec} is null + */ + public ElgamalEncDataContentBuilder generateKeyPair(ElgamalParamSpec spec) { + this.genKeyPairPredef = true; + this.genKeyPair = false; + this.paramSpec = Objects.requireNonNull(spec); + return this; + } + + /** + * Imports a private key from PKCS#8-encoded bytes. + * + * @param pkcs8 PKCS#8 PrivateKeyInfo bytes; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code pkcs8} is null + */ + public ElgamalEncDataContentBuilder importPrivatePkcs8(byte[] pkcs8) { + this.importPrivatePkcs8 = Objects.requireNonNull(pkcs8).clone(); + return this; + } + + /** + * Imports a public key from X.509 SubjectPublicKeyInfo bytes. + * + * @param x509 X.509 public key bytes; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code x509} is null + */ + public ElgamalEncDataContentBuilder importPublicX509(byte[] x509) { + this.importPublicX509 = Objects.requireNonNull(x509).clone(); + return this; + } + + /** + * Builds a streaming encryption or decryption pipeline according to the current + * configuration. + * + *

+ * When {@code encrypt} is true, a public key must be available either from + * prior configuration, generation, or import. When {@code encrypt} is false, a + * private key must be available. + *

+ * + *

Key resolution order

During build, the builder may generate keys, + * import keys, or use explicitly provided keys based on the configuration. + * Generation by size and generation by predefined parameters are mutually + * exclusive; the last selected mode wins. + * + *

Examples

{@code
+     * DataContent enc = ElgamalEncDataContentBuilder.builder()
+     *     .generateKeyPair(2048)
+     *     .pkcs1()
+     *     .build(true);
+     *
+     * DataContent dec = ElgamalEncDataContentBuilder.builder()
+     *     .importPrivatePkcs8(pkcs8)
+     *     .build(false);
+     * }
+ * + * @param encrypt {@code true} to build an encrypting pipeline, {@code false} + * for a decrypting pipeline + * @return a {@link zeroecho.sdk.content.api.DataContent} that performs the + * requested operation when its stream is consumed + * @throws IllegalStateException if a required key is missing or if key setup + * fails + */ + @Override + public DataContent build(boolean encrypt) { + try { + resolveKeys(); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("ElGamal key setup failed", e); + } + if (encrypt) { + if (publicKey == null) { + throw new IllegalStateException("Encrypt requires a PublicKey"); + } + return new Encrypt(publicKey, spec); + } else { + if (privateKey == null) { + throw new IllegalStateException("Decrypt requires a PrivateKey"); + } + return new Decrypt(privateKey, spec); + } + } + + private void resolveKeys() throws GeneralSecurityException { + CryptoAlgorithm alg = CryptoAlgorithms.require("ElGamal"); + if (genKeyPair) { + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(ElgamalKeyGenSpec.class); + KeyPair kp = b.generateKeyPair(keyGen); + this.publicKey = kp.getPublic(); + this.privateKey = kp.getPrivate(); + } else if (genKeyPairPredef) { + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(ElgamalParamSpec.class); + KeyPair kp = b.generateKeyPair(paramSpec); + this.publicKey = kp.getPublic(); + this.privateKey = kp.getPrivate(); + } + if (importPrivatePkcs8 != null) { + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(ElgamalPrivateKeySpec.class); + this.privateKey = b.importPrivate(new ElgamalPrivateKeySpec(importPrivatePkcs8)); + } + if (importPublicX509 != null) { + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(ElgamalPublicKeySpec.class); + this.publicKey = b.importPublic(new ElgamalPublicKeySpec(importPublicX509)); + } + } + + /** + * Encrypt is a streaming content adapter that reads plaintext from an upstream + * {@link zeroecho.sdk.content.api.DataContent} and exposes the + * ElGamal-encrypted view as an {@link java.io.InputStream}. + * + *

+ * The class is configured with a {@link java.security.PublicKey} and an + * {@link ElgamalEncSpec}. When {@link #getStream()} is called, it creates an + * {@link zeroecho.core.context.EncryptionContext} for the "ElGamal" algorithm + * in {@link zeroecho.core.KeyUsage#ENCRYPT} role via + * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}, + * attaches the upstream stream, and returns a pull-based stream that encrypts + * on the fly. + *

+ * + *

Thread-safety

Instances are not thread-safe and must be used from a + * single thread. + */ + private static final class Encrypt implements EncryptedContent { + private final PublicKey key; + private final ElgamalEncSpec spec; + private DataContent upstream; + + private Encrypt(PublicKey key, ElgamalEncSpec spec) { + this.key = key; + this.spec = spec; + } + + /** + * Sets the upstream content that supplies plaintext bytes. + * + * @param in the source content to be encrypted; must not be null + * @throws NullPointerException if {@code in} is null + */ + @Override + public void setInput(DataContent in) { + this.upstream = Objects.requireNonNull(in); + } + + /** + * Returns a stream that yields the encrypted view of the upstream content. + * + *

+ * This method creates an {@link zeroecho.core.context.EncryptionContext} for + * the configured key and spec, then attaches the upstream stream. The returned + * stream performs ElGamal encryption as bytes are read. + *

+ * + * @return an input stream producing ElGamal ciphertext + * @throws IOException if the upstream stream cannot be obtained or if + * the encryption context fails to attach + * @throws IllegalStateException if no upstream has been set via + * {@link #setInput(DataContent)} + */ + @Override + public InputStream getStream() throws IOException { + if (upstream == null) { + throw new IllegalStateException("elgamal encrypt: upstream not set"); + } + EncryptionContext ctx = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, key, spec); // NOPMD + return ctx.attach(upstream.getStream()); + } + } + + /** + * Decrypt is a streaming content adapter that reads ciphertext from an upstream + * {@link zeroecho.sdk.content.api.DataContent} and exposes the + * ElGamal-decrypted plaintext as an {@link java.io.InputStream}. + * + *

+ * The class is configured with a {@link java.security.PrivateKey} and an + * {@link ElgamalEncSpec}. When {@link #getStream()} is called, it creates an + * {@link zeroecho.core.context.EncryptionContext} for the "ElGamal" algorithm + * in {@link zeroecho.core.KeyUsage#DECRYPT} role via + * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}, + * attaches the upstream stream, and returns a pull-based stream that decrypts + * on the fly. + *

+ * + *

Thread-safety

Instances are not thread-safe and must be used from a + * single thread. + */ + private static final class Decrypt implements PlainContent { + private final PrivateKey key; + private final ElgamalEncSpec spec; + private DataContent upstream; + + private Decrypt(PrivateKey key, ElgamalEncSpec spec) { + this.key = key; + this.spec = spec; + } + + /** + * Sets the upstream content that supplies ciphertext bytes. + * + * @param in the source content to be decrypted; must not be null + * @throws NullPointerException if {@code in} is null + */ + @Override + public void setInput(DataContent in) { + this.upstream = Objects.requireNonNull(in); + } + + /** + * Returns a stream that yields the decrypted view of the upstream content. + * + *

+ * This method creates an {@link zeroecho.core.context.EncryptionContext} for + * the configured key and spec, then attaches the upstream stream. The returned + * stream performs ElGamal decryption as bytes are read. + *

+ * + * @return an input stream producing plaintext bytes + * @throws IOException if the upstream stream cannot be obtained or if + * the decryption context fails to attach + * @throws IllegalStateException if no upstream has been set via + * {@link #setInput(DataContent)} + */ + @Override + public InputStream getStream() throws IOException { + if (upstream == null) { + throw new IllegalStateException("elgamal decrypt: upstream not set"); + } + EncryptionContext ctx = CryptoAlgorithms.create("ElGamal", KeyUsage.DECRYPT, key, spec); // NOPMD + return ctx.attach(upstream.getStream()); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/HmacDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/HmacDataContentBuilder.java new file mode 100644 index 0000000..90a1c9b --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/HmacDataContentBuilder.java @@ -0,0 +1,856 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Base64; +import java.util.Objects; + +import javax.crypto.SecretKey; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.hmac.HmacKeyGenSpec; +import zeroecho.core.alg.hmac.HmacKeyImportSpec; +import zeroecho.core.alg.hmac.HmacSpec; +import zeroecho.core.context.MacContext; +import zeroecho.core.io.TailStrippingInputStream; +import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * HmacDataContentBuilder constructs streaming pipelines that compute or verify + * HMAC tags while an InputStream is consumed. + * + *

Overview

The builder produces + * {@link zeroecho.sdk.content.api.PlainContent} that either passes the original + * bytes through while computing or verifying an HMAC trailer, or emits only the + * tag or a boolean verification result. The HMAC variant is selected via + * {@link #spec(HmacSpec)} or convenience methods such as {@link #sha256()}. + * + *

Modes and outputs

+ *
    + *
  • MAC mode: compute an HMAC over the incoming stream and either pass the + * body through with the trailer stripped or emit the finalized tag in raw, hex, + * or Base64 form.
  • + *
  • VERIFY mode: verify an expected HMAC. The expected tag can be provided + * directly or taken from the input trailer. The output can be pass-through of + * the body or a textual boolean.
  • + *
+ * + *

Key handling

Keys may be supplied directly, generated for the + * configured MAC name, or imported from raw, hex, or Base64 text. If no key + * material is provided, a default key suitable for the selected MAC is + * generated. + * + *

Thread-safety

Instances are mutable and not thread-safe. Configure + * and use each builder instance from a single thread. + * + * @see zeroecho.sdk.builders.core.DataContentBuilder + * @see zeroecho.core.CryptoAlgorithms + * @see zeroecho.core.context.MacContext + * @see HmacSpec + */ +public final class HmacDataContentBuilder implements DataContentBuilder { + /** + * Mode selects whether the pipeline computes an HMAC tag or verifies one. + * + *

+ * The active mode is set by {@link #mac()} or {@link #verify()} and governs how + * the builder interprets the selected output. In {@code MAC} mode, the pipeline + * produces or strips an HMAC trailer computed over the input. In {@code VERIFY} + * mode, the pipeline checks an expected tag supplied explicitly or taken from + * the input trailer. + *

+ * + *

Typical flows

{@code
+     * // Compute HMAC and emit hex tag:
+     * PlainContent c1 = HmacDataContentBuilder.builder()
+     *     .mac()
+     *     .sha256()
+     *     .emitHexTag()
+     *     .build(true);
+     *
+     * // Verify HMAC carried as input trailer and pass the body through:
+     * PlainContent c2 = HmacDataContentBuilder.builder()
+     *     .verify()
+     *     .sha256()
+     *     .passThrough()
+     *     .build(true);
+     * }
+ * + * @since 1.0 + */ + public enum Mode { + /** + * Compute an HMAC tag over the input stream. + * + *

+ * Use with {@link #passThrough()} to output the body while stripping the tag, + * or with one of {@link #emitRawTag()}, {@link #emitHexTag()}, or + * {@link #emitBase64Tag()} to output only the finalized tag. + *

+ */ + MAC, + /** + * Verify an expected HMAC tag against the input stream. + * + *

+ * The expected tag can be provided via {@link #expectedTag(byte[])}, + * {@link #expectedTagHex(String)}, or {@link #expectedTagBase64(String)}. If no + * expected tag is supplied, the pipeline reads it from the input trailer. Use + * {@link #passThrough()} to output the body and throw on mismatch, or + * {@link #emitVerificationBoolean()} to output a textual {@code "true"} or + * {@code "false"} result. + *

+ */ + VERIFY + } + + /** + * Out selects the concrete output representation produced by the pipeline. + * + *

+ * In {@link Mode#MAC} the {@code TAG_*} outputs emit only the computed tag and + * {@link #PASSTHROUGH} emits the original body with the trailer stripped. In + * {@link Mode#VERIFY}, {@link #VERIFY_BOOL} emits a textual boolean and + * {@link #PASSTHROUGH} emits the body, throwing on mismatch. Any other + * mode/output combination is rejected at build time. + *

+ * + *

Mapping from builder methods

+ *
    + *
  • {@link #passThrough()} -> {@code PASSTHROUGH}
  • + *
  • {@link #emitRawTag()} -> {@code TAG_RAW}
  • + *
  • {@link #emitHexTag()} -> {@code TAG_HEX}
  • + *
  • {@link #emitBase64Tag()} -> {@code TAG_BASE64}
  • + *
  • {@link #emitVerificationBoolean()} -> {@code VERIFY_BOOL}
  • + *
+ * + * @since 1.0 + */ + private enum Out { + /** + * Pass-through body output. + * + *

+ * In {@link Mode#MAC}, the HMAC trailer produced by the engine is stripped and + * the body is forwarded unchanged. In {@link Mode#VERIFY}, the body is + * forwarded and verification is performed; a mismatch results in an exception + * at EOF. + *

+ */ + PASSTHROUGH, + /** + * Emit the finalized HMAC tag as raw bytes. + * + *

+ * Valid only with {@link Mode#MAC}. + *

+ */ + TAG_RAW, + /** + * Emit the finalized HMAC tag as lowercase hexadecimal text. + * + *

+ * Valid only with {@link Mode#MAC}. + *

+ */ + TAG_HEX, + /** + * Emit the finalized HMAC tag as Base64 text. + * + *

+ * Valid only with {@link Mode#MAC}. + *

+ */ + TAG_BASE64, + /** + * Emit a textual boolean representing verification success. + * + *

+ * Outputs {@code "true"} when verification succeeds and {@code "false"} on + * failure. Valid only with {@link Mode#VERIFY}. + *

+ */ + VERIFY_BOOL + } + + private static final int IO_BUF = 8192; + + private Mode mode = Mode.MAC; + + private HmacSpec spec = HmacSpec.sha256(); // default + private SecretKey key; + private Integer genKeyBits; // if set, generate for this spec.macName() + + private byte[] importRaw; + private String importHex; + private String importBase64; + + private byte[] expectedTag; // raw bytes + private String expectedTagHex; + private String expectedTagBase64; + + private Out out = Out.PASSTHROUGH; + + private HmacDataContentBuilder() { + } + + /** + * Creates a new builder for constructing HMAC computing or verifying pipelines. + * + *

Example

{@code
+     * PlainContent pc = HmacDataContentBuilder.builder()
+     *     .sha256()
+     *     .emitHexTag()
+     *     .build(true);
+     * }
+ * + * @return a new {@code HmacDataContentBuilder} instance + */ + public static HmacDataContentBuilder builder() { + return new HmacDataContentBuilder(); + } + + /** + * Switches the builder to MAC mode. + * + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder mac() { + this.mode = Mode.MAC; + return this; + } + + /** + * Switches the builder to VERIFY mode. + * + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder verify() { + this.mode = Mode.VERIFY; + return this; + } + + /** + * Sets the HMAC specification to use for this pipeline. + * + * @param s the HMAC spec; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code s} is null + */ + public HmacDataContentBuilder spec(HmacSpec s) { + this.spec = Objects.requireNonNull(s); + return this; + } + + /** + * Selects HmacSHA256. + * + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder sha256() { + return spec(HmacSpec.sha256()); + } + + /** + * Selects HmacSHA384. + * + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder sha384() { + return spec(HmacSpec.sha384()); + } + + /** + * Selects HmacSHA512. + * + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder sha512() { + return spec(HmacSpec.sha512()); + } + + /** + * Supplies an explicit secret key for the HMAC operation. + * + *

+ * Calling this clears any prior key generation or import configuration. + *

+ * + * @param k the secret key; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code k} is null + */ + public HmacDataContentBuilder withKey(SecretKey k) { + this.key = Objects.requireNonNull(k); + this.genKeyBits = null; + this.importRaw = null; + this.importHex = null; + this.importBase64 = null; + return this; + } + + /** + * Requests generation of a new secret key sized for the configured MAC. + * + *

+ * The key is generated for the MAC name returned by {@link HmacSpec#macName()}. + *

+ * + * @param bits desired key length in bits + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder generateKey(int bits) { + this.genKeyBits = bits; + this.key = null; + this.importRaw = null; + this.importHex = null; + this.importBase64 = null; + return this; + } + + /** + * Imports a secret key from raw bytes. + * + *

+ * Calling this clears any prior explicit key or generation request. + *

+ * + * @param raw the raw key bytes; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code raw} is null + */ + public HmacDataContentBuilder importKeyRaw(byte[] raw) { + this.importRaw = Objects.requireNonNull(raw).clone(); + this.key = null; + this.genKeyBits = null; + return this; + } + + /** + * Imports a secret key from a hexadecimal string. + * + *

+ * Calling this clears any prior explicit key or generation request. + *

+ * + * @param hex hex-encoded key text; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code hex} is null + * @throws IllegalArgumentException if {@code hex} is not valid hexadecimal + */ + public HmacDataContentBuilder importKeyHex(String hex) { + this.importHex = Objects.requireNonNull(hex); + this.key = null; + this.genKeyBits = null; + return this; + } + + /** + * Imports a secret key from a Base64 string. + * + *

+ * Calling this clears any prior explicit key or generation request. + *

+ * + * @param b64 Base64-encoded key text; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code b64} is null + * @throws IllegalArgumentException if {@code b64} is not valid Base64 + */ + public HmacDataContentBuilder importKeyBase64(String b64) { + this.importBase64 = Objects.requireNonNull(b64); + this.key = null; + this.genKeyBits = null; + return this; + } + + /** + * Configures the pipeline to pass the input body through unchanged and strip + * the engine's trailer. + * + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder passThrough() { + this.out = Out.PASSTHROUGH; + return this; + } + + /** + * Configures the pipeline to emit the finalized HMAC tag as raw bytes. + * + *

+ * Switches the mode to {@link Mode#MAC}. + *

+ * + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder emitRawTag() { + this.mode = Mode.MAC; + this.out = Out.TAG_RAW; + return this; + } + + /** + * Configures the pipeline to emit the finalized HMAC tag as lowercase + * hexadecimal text. + * + *

+ * Switches the mode to {@link Mode#MAC}. + *

+ * + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder emitHexTag() { + this.mode = Mode.MAC; + this.out = Out.TAG_HEX; + return this; + } + + /** + * Configures the pipeline to emit the finalized HMAC tag as Base64 text. + * + *

+ * Switches the mode to {@link Mode#MAC}. + *

+ * + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder emitBase64Tag() { + this.mode = Mode.MAC; + this.out = Out.TAG_BASE64; + return this; + } + + /** + * Configures the pipeline to verify and emit a textual boolean result. + * + *

+ * Switches the mode to {@link Mode#VERIFY}. + *

+ * + * @return {@code this} builder for chaining + */ + public HmacDataContentBuilder emitVerificationBoolean() { + this.mode = Mode.VERIFY; + this.out = Out.VERIFY_BOOL; + return this; + } + + /** + * Supplies the expected HMAC tag as raw bytes for verification. + * + *

+ * Providing a value here clears any prior hex or Base64 expected tag. + *

+ * + * @param raw the expected tag bytes; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code raw} is null + */ + public HmacDataContentBuilder expectedTag(byte[] raw) { + this.expectedTag = Objects.requireNonNull(raw).clone(); + this.expectedTagHex = null; + this.expectedTagBase64 = null; + return this; + } + + /** + * Supplies the expected HMAC tag as a hexadecimal string for verification. + * + *

+ * Providing a value here clears any prior raw or Base64 expected tag. + *

+ * + * @param hex the expected tag in hexadecimal; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code hex} is null + * @throws IllegalArgumentException if {@code hex} is not valid hexadecimal + */ + public HmacDataContentBuilder expectedTagHex(String hex) { + this.expectedTagHex = Objects.requireNonNull(hex); + this.expectedTag = null; + this.expectedTagBase64 = null; + return this; + } + + /** + * Supplies the expected HMAC tag as a Base64 string for verification. + * + *

+ * Providing a value here clears any prior raw or hex expected tag. + *

+ * + * @param b64 the expected tag in Base64; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code b64} is null + * @throws IllegalArgumentException if {@code b64} is not valid Base64 + */ + public HmacDataContentBuilder expectedTagBase64(String b64) { + this.expectedTagBase64 = Objects.requireNonNull(b64); + this.expectedTag = null; + this.expectedTagHex = null; + return this; + } + + /** + * Builds a digesting or verifying {@link PlainContent} pipeline according to + * the current configuration. + * + *

+ * The boolean parameter is ignored and exists only to satisfy the + * {@link zeroecho.sdk.builders.core.DataContentBuilder} interface. The actual + * behavior is controlled by {@link #mac()} or {@link #verify()} and the + * selected output. + *

+ * + *

Result

+ *
    + *
  • MAC mode: returns a pass-through content or a tag-emitting content based + * on {@link #passThrough()}, {@link #emitRawTag()}, {@link #emitHexTag()}, or + * {@link #emitBase64Tag()}.
  • + *
  • VERIFY mode: returns a pass-through content that throws on mismatch or an + * emitter of "true"/"false" based on {@link #emitVerificationBoolean()}.
  • + *
+ * + * @param ignoredEncryptFlag unused parameter from the interface + * @return a {@link PlainContent} pipeline performing the configured HMAC + * operation + * @throws IllegalStateException if an invalid mode/output combination is + * selected or key construction fails + */ + @Override + public PlainContent build(boolean ignoredEncryptFlag) { + final SecretKey secret = resolveKey(); + + return switch (mode) { + case MAC -> switch (out) { + case PASSTHROUGH -> new MacPassThrough(secret, spec); + case TAG_RAW, TAG_HEX, TAG_BASE64 -> new MacEmit(secret, spec, out); + default -> throw new IllegalStateException("Invalid output for MAC: " + out); + }; + case VERIFY -> switch (out) { + case PASSTHROUGH -> + new VerifyPassThrough(secret, spec, decodeExpected(expectedTag, expectedTagHex, expectedTagBase64)); + case VERIFY_BOOL -> + new VerifyEmit(secret, spec, decodeExpected(expectedTag, expectedTagHex, expectedTagBase64)); + default -> throw new IllegalStateException("Invalid output for VERIFY: " + out); + }; + }; + } + + private SecretKey resolveKey() { + if (key != null) { + return key; + } + final String mac = spec.macName(); // e.g., "HmacSHA256" + try { + final CryptoAlgorithm algo = CryptoAlgorithms.require("HMAC"); // registered in HmacAlgorithm + if (genKeyBits != null) { + SymmetricKeyBuilder b = algo.symmetricKeyBuilder(HmacKeyGenSpec.class); + return b.generateSecret(new HmacKeyGenSpec(mac, genKeyBits)); // builder honors macName + } + if (importRaw != null || importHex != null || importBase64 != null) { + SymmetricKeyBuilder b = algo.symmetricKeyBuilder(HmacKeyImportSpec.class); + HmacKeyImportSpec ispec; + if (importRaw != null) { + ispec = HmacKeyImportSpec.fromRaw(mac, importRaw); + } else if (importHex != null) { + ispec = HmacKeyImportSpec.fromHex(mac, importHex); + } else { + ispec = HmacKeyImportSpec.fromBase64(mac, importBase64); + } + return b.importSecret(ispec); + } + // default keygen if nothing provided + SymmetricKeyBuilder b = algo.symmetricKeyBuilder(HmacKeyGenSpec.class); + return b.generateSecret(HmacKeyGenSpec.sha256(256)); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("HMAC key construction failed", e); + } + } + + private static byte[] decodeExpected(byte[] raw, String hex, String b64) { + if (raw != null) { + return raw.clone(); + } + if (hex != null) { + return java.util.HexFormat.of().parseHex(hex); + } + if (b64 != null) { + return Base64.getDecoder().decode(b64); + } + return null; // NOPMD means: expect the tag to be present as a trailer in the input stream + } + + /** + * MAC + pass-through: return body only (trailer stripped). + */ + private static final class MacPassThrough implements PlainContent { + private final SecretKey key; + private final HmacSpec spec; + + private DataContent upstream; + + private MacPassThrough(SecretKey key, HmacSpec spec) { + this.key = key; + this.spec = spec; + } + + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input); + } + + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "mac: missing input"); + final MacContext m = CryptoAlgorithms.create("HMAC", KeyUsage.MAC, key, spec); // HmacMacContext + final InputStream produced = m.wrap(upstream.getStream()); // emits [body][tag] + final int tagLen = m.tagLength(); + return withContextClose(new TailStrippingInputStream(produced, tagLen, IO_BUF) { + @Override + protected void processTail(byte[] tail) throws IOException { + /* drop */ } + }, m); + } + } + + /** + * MAC + emit tag (raw/hex/base64). + */ + private static final class MacEmit implements PlainContent { + private final SecretKey key; + private final HmacSpec spec; + private final Out out; + private DataContent upstream; + + private MacEmit(SecretKey key, HmacSpec spec, Out out) { + if (out != Out.TAG_RAW && out != Out.TAG_HEX && out != Out.TAG_BASE64) { + throw new IllegalArgumentException("MacEmit requires TAG_* output"); + } + this.key = key; + this.spec = spec; + this.out = out; + } + + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input); + } + + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "mac: missing input"); + + final byte[][] holder = new byte[1][]; + try (MacContext m = CryptoAlgorithms.create("HMAC", KeyUsage.MAC, key, spec)) { + final int tagLen = m.tagLength(); + try (InputStream in = new TailStrippingInputStream(m.wrap(upstream.getStream()), tagLen, IO_BUF) { + @Override + protected void processTail(byte[] tail) throws IOException { + holder[0] = (tail == null ? null : tail.clone()); + } + }) { + byte[] buf = new byte[8192]; + while (true) { + int n = in.read(buf); + if (n < 0) { + break; + } + } + } + } + + byte[] tag = holder[0]; + if (tag == null) { + tag = new byte[0]; + } + + byte[] outBytes; + switch (out) { + case TAG_RAW: + outBytes = tag; + break; + case TAG_HEX: + outBytes = java.util.HexFormat.of().formatHex(tag).getBytes(StandardCharsets.UTF_8); + break; + case TAG_BASE64: + outBytes = Base64.getEncoder().encode(tag); + break; + default: + throw new IllegalStateException("Unexpected TAG output: " + out); + } + return new ByteArrayInputStream(outBytes); + } + } + + /** + * VERIFY + pass-through: - If expected tag provided: verify against it. - Else: + * read expected from the input trailer (and strip it). Returns body only; + * throws on mismatch. + */ + private static final class VerifyPassThrough implements PlainContent { + private final SecretKey key; + private final HmacSpec spec; + private final byte[] expectedOrNull; + + private DataContent upstream; + + private VerifyPassThrough(SecretKey key, HmacSpec spec, byte[] expectedOrNull) { + this.key = key; + this.spec = spec; + this.expectedOrNull = (expectedOrNull == null) ? null : expectedOrNull.clone(); + } + + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input); + } + + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "verify: missing input"); + + final MacContext m = CryptoAlgorithms.create("HMAC", KeyUsage.MAC, key, spec); + + final InputStream body; + if (expectedOrNull != null) { + // expected supplied by caller; body is the upstream as-is + m.setExpectedTag(expectedOrNull); + body = m.wrap(upstream.getStream()); + } else { + // expected is the input trailer; strip it and feed body to the engine + final int tagLen = m.tagLength(); + InputStream bodyOnly = new TailStrippingInputStream(upstream.getStream(), tagLen, IO_BUF) { // NOPMD + @Override + protected void processTail(byte[] tail) throws IOException { + m.setExpectedTag(tail == null ? null : tail.clone()); + } + }; + body = m.wrap(bodyOnly); + } + + // Engine will throw on mismatch at EOF (THROW_ON_MISMATCH by default). + return withContextClose(body, m); + } + } + + /** + * VERIFY + emit boolean ("true"/"false"). + */ + private static final class VerifyEmit implements PlainContent { + private final SecretKey key; + private final HmacSpec spec; + private final byte[] expectedOrNull; + + private DataContent upstream; + + private VerifyEmit(SecretKey key, HmacSpec spec, byte[] expectedOrNull) { + this.key = key; + this.spec = spec; + this.expectedOrNull = (expectedOrNull == null) ? null : expectedOrNull.clone(); + } + + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input); + } + + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "verify: missing input"); + + boolean ok; + try (MacContext m = CryptoAlgorithms.create("HMAC", KeyUsage.MAC, key, spec)) { + if (expectedOrNull != null) { + m.setExpectedTag(expectedOrNull); + try (InputStream in = m.wrap(upstream.getStream())) { + byte[] buf = new byte[8192]; + while (true) { + int n = in.read(buf); + if (n < 0) { + break; + } + } + } + } else { + final int tagLen = m.tagLength(); + try (InputStream in = m.wrap(new TailStrippingInputStream(upstream.getStream(), tagLen, IO_BUF) { + @Override + protected void processTail(byte[] tail) throws IOException { + m.setExpectedTag(tail == null ? null : tail.clone()); + } + })) { + byte[] buf = new byte[8192]; + while (true) { + int n = in.read(buf); + if (n < 0) { + break; + } + } + } + } + ok = true; // no exception thrown by engine → verified OK + } catch (IOException fail) { + ok = false; + } + + byte[] out = ok ? "true".getBytes(StandardCharsets.UTF_8) : "false".getBytes(StandardCharsets.UTF_8); + return new ByteArrayInputStream(out); + } + } + + private static InputStream withContextClose(final InputStream in, final MacContext ctx) { + return new FilterInputStream(in) { + @Override + public void close() throws IOException { + try (ctx) { + super.close(); + } + } + }; + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/KemDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/KemDataContentBuilder.java new file mode 100644 index 0000000..545b860 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/KemDataContentBuilder.java @@ -0,0 +1,619 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Objects; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.KemContext.KemResult; +import zeroecho.core.io.Util; +import zeroecho.core.spec.VoidSpec; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.EncryptedContent; + +/** + * KEM-based envelope builder that first collects KEM parameters and only then + * delegates to an explicit symmetric builder for the payload. + * + *

+ * This builder fixes the earlier unintuitive flow where each symmetric builder + * duplicated KEM settings. With {@code KemDataContentBuilder} you configure the + * KEM once (algorithm id, recipient key, KDF) and then select which payload + * cipher to use by supplying an {@link AesDataContentBuilder} or a + * {@link ChaChaDataContentBuilder}. The derived symmetric key is injected into + * the provided symmetric builder prior to building the payload stage. + *

+ * + *

Header format

The produced envelope prepends the KEM encapsulation + * ciphertext using the following simple framing:
+ *   length L in Pack7 format
+ *   L bytes of KEM ciphertext
+ *   [payload stream produced by the symmetric builder]
+ * 
The symmetric builder (AES or ChaCha) remains fully responsible for + * its own optional header (IV, AAD tag, etc.) which follows immediately after + * the KEM prefix. + * + *

Usage

{@code
+ * // Encryption
+ * DataContent content = DataContentChainBuilder.encrypt()
+ *     .add(PlainStringBuilder.builder().value("hello"))
+ *     .add(KemDataContentBuilder.builder()
+ *            .kem("ML-KEM")
+ *            .recipientPublic(recipientPub)
+ *            .hkdfSha256("KEM-demo".getBytes(StandardCharsets.US_ASCII))
+ *            .withAes(AesDataContentBuilder.builder().modeGcm(128).withHeader()))
+ *     .build();
+ *
+ * // Decryption
+ * DataContent plain = DataContentChainBuilder.decrypt()
+ *     .add(PlainBytesBuilder.builder().bytes(cipherBytes))
+ *     .add(KemDataContentBuilder.builder()
+ *            .kem("ML-KEM")
+ *            .recipientPrivate(recipientPriv)
+ *            .hkdfSha256("KEM-demo".getBytes(StandardCharsets.US_ASCII))
+ *            .withAes(AesDataContentBuilder.builder().modeGcm(128).withHeader()))
+ *     .build();
+ * }
+ * + *

Thread-safety

Instances are not thread-safe. Create a new builder + * per pipeline/use. + * + * @since 1.0 + */ +public final class KemDataContentBuilder implements DataContentBuilder { + // ---------- KEM configuration ---------- + private String kemId; + private PublicKey recipientPublic; + private PrivateKey recipientPrivate; + + // Simple HKDF/SHA-256 or identity KDF. Default HKDF with info "ZeroEcho-KEM". + private boolean useHkdf = true; + private byte[] hkdfInfo = "ZeroEcho-KEM".getBytes(StandardCharsets.US_ASCII); + + // Maximum accepted KEM ciphertext length during decryption to avoid abuse. + private int maxKemCiphertextLen = 1 << 16; // 64 KiB default + + // Derived key length in bytes for the symmetric payload. 32 = AES-256/ChaCha. + private int derivedKeyBytes = 32; + + // ---------- Symmetric payload selection ---------- + private AesDataContentBuilder aesBuilder; + private ChaChaDataContentBuilder chachaBuilder; + + private KemDataContentBuilder() { + } + + /** + * Returns a new {@code KemDataContentBuilder}. + * + * @return fresh builder instance + */ + public static KemDataContentBuilder builder() { + return new KemDataContentBuilder(); + } + + /** + * Sets the canonical KEM algorithm identifier to use (e.g., "ML-KEM"). + * + * @param id algorithm identifier resolved by + * {@link CryptoAlgorithms#require(String)} + * @return this builder + * @throws NullPointerException if {@code id} is null + */ + public KemDataContentBuilder kem(String id) { + this.kemId = Objects.requireNonNull(id, "kem id"); + return this; + } + + /** + * Sets the recipient public key for encapsulation (encryption path). + * + * @param k recipient public key + * @return this builder + * @throws NullPointerException if {@code k} is null + */ + public KemDataContentBuilder recipientPublic(PublicKey k) { + this.recipientPublic = Objects.requireNonNull(k, "recipientPublic"); + return this; + } + + /** + * Sets the recipient private key for decapsulation (decryption path). + * + * @param k recipient private key + * @return this builder + * @throws NullPointerException if {@code k} is null + */ + public KemDataContentBuilder recipientPrivate(PrivateKey k) { + this.recipientPrivate = Objects.requireNonNull(k, "recipientPrivate"); + return this; + } + + /** + * Configures HKDF-SHA256 as the key derivation function using the provided + * {@code info} context string. If not set, a default constant is used. + * + * @param info application/context info; copied defensively; may be empty but + * must not be null + * @return this builder + * @throws NullPointerException if {@code info} is null + */ + public KemDataContentBuilder hkdfSha256(byte[] info) { + this.useHkdf = true; + this.hkdfInfo = Objects.requireNonNull(info, "info").clone(); + return this; + } + + /** + * Disables HKDF and uses the shared secret directly (or truncated) for the + * symmetric key. This is usually discouraged unless the KEM already outputs a + * uniformly random key. + * + * @return this builder + */ + public KemDataContentBuilder directSecret() { + this.useHkdf = false; + return this; + } + + /** + * Sets a safety limit for the KEM ciphertext length accepted in decryption. + * Values up to the algorithm's maximum should be allowed. + * + * @param maxBytes maximum number of ciphertext bytes; must be positive + * @return this builder + * @throws IllegalArgumentException if {@code maxBytes} is not positive + */ + public KemDataContentBuilder maxKemCiphertextLen(int maxBytes) { + if (maxBytes <= 0) { + throw new IllegalArgumentException("maxKemCiphertextLen must be > 0"); + } + this.maxKemCiphertextLen = maxBytes; + return this; + } + + /** + * Selects AES as the payload cipher and supplies a configured + * {@link AesDataContentBuilder} that will receive the derived key. + * + *

+ * The builder instance will be mutated to inject the derived key. Supply a + * fresh instance per pipeline to avoid cross-use. + *

+ * + * @param b AES payload builder to use + * @return this builder + * @throws NullPointerException if {@code b} is null + * @see #derivedKeyBytes(int) + */ + public KemDataContentBuilder withAes(AesDataContentBuilder b) { + this.aesBuilder = Objects.requireNonNull(b, "aesBuilder"); + this.chachaBuilder = null; + return this; + } + + /** + * Selects ChaCha20/ChaCha20-Poly1305 as the payload cipher and supplies a + * configured {@link ChaChaDataContentBuilder} that will receive the derived + * key. + * + *

+ * The builder instance will be mutated to inject the derived key. Supply a + * fresh instance per pipeline to avoid cross-use. + *

+ * + * @param b ChaCha payload builder to use + * @return this builder + * @throws NullPointerException if {@code b} is null + * @see #derivedKeyBytes(int) + */ + public KemDataContentBuilder withChaCha(ChaChaDataContentBuilder b) { + this.chachaBuilder = Objects.requireNonNull(b, "chachaBuilder"); + this.aesBuilder = null; + return this; + } + + /** + * Sets the number of derived key bytes to feed into the selected symmetric + * builder. Defaults to 32 (AES-256 or ChaCha20). + * + * @param bytes number of key bytes; typical values are 16, 24, or 32 + * @return this builder + * @throws IllegalArgumentException if {@code bytes} is not positive + */ + public KemDataContentBuilder derivedKeyBytes(int bytes) { + if (bytes <= 0) { + throw new IllegalArgumentException("derivedKeyBytes must be > 0"); + } + this.derivedKeyBytes = bytes; + return this; + } + + /** + * Builds the KEM envelope stage in either encryption or decryption mode. + * + *

+ * On encryption, the returned {@link DataContent} will: + *

    + *
  1. encapsulate with the configured KEM and recipient public key,
  2. + *
  3. derive a symmetric key via HKDF-SHA256 (or directly),
  4. + *
  5. inject the key into the selected symmetric builder,
  6. + *
  7. prepend the KEM encapsulation ciphertext, and
  8. + *
  9. stream the symmetric payload output.
  10. + *
+ * On decryption, it will: + *
    + *
  1. read and decapsulate the KEM ciphertext using the recipient private + * key,
  2. + *
  3. derive the symmetric key,
  4. + *
  5. inject it into the symmetric builder, and
  6. + *
  7. stream-decrypt the remaining payload bytes.
  8. + *
+ * + * @param encrypt {@code true} to build the encryption stage, {@code false} for + * decryption + * @return a {@link DataContent} representing the configured KEM envelope + * @throws IllegalStateException if required parameters are missing or invalid + */ + @Override + public DataContent build(boolean encrypt) { + validateConfiguration(encrypt); + + return encrypt + ? new EncryptContent(kemId, recipientPublic, useHkdf, hkdfInfo.clone(), derivedKeyBytes, + maxKemCiphertextLen, aesBuilder, chachaBuilder) + : new DecryptContent(kemId, recipientPrivate, useHkdf, hkdfInfo.clone(), derivedKeyBytes, + maxKemCiphertextLen, aesBuilder, chachaBuilder); + + } + + private void validateConfiguration(boolean encrypt) { + if (kemId == null || kemId.isEmpty()) { + throw new IllegalStateException("KEM algorithm id not set. Call kem(id)."); + } + if (aesBuilder == null && chachaBuilder == null) { + throw new IllegalStateException("No payload cipher selected. Call withAes(...) or withChaCha(...)."); + } + if (encrypt && recipientPublic == null) { + throw new IllegalStateException("Encryption requires recipientPublic(...)"); + } + if (!encrypt && recipientPrivate == null) { + throw new IllegalStateException("Decryption requires recipientPrivate(...)"); + } + } + + // ====================================================================== + // Static final content implementations (no reference to outer builder) + // ====================================================================== + + /** + * EncryptContent is a streaming envelope stage that performs KEM encapsulation, + * derives a symmetric key, injects it into the selected payload builder, and + * emits a concatenation of the KEM prefix and the payload stream. + * + *

+ * The produced stream has the following structure: + *

+ *
+     *   [Pack7 length][KEM ciphertext][payload bytes...]
+     * 
The payload bytes are generated by the supplied + * {@code AesDataContentBuilder} or {@code ChaChaDataContentBuilder} configured + * with the derived key. + * + *

Lifecycle

+ *
    + *
  1. Encapsulate with {@link zeroecho.core.context.KemContext} using the + * recipient public key.
  2. + *
  3. Derive a symmetric key via HKDF-SHA256 (or use the shared secret + * directly), sized by {@code derivedKeyBytes}.
  4. + *
  5. Inject the key into the chosen symmetric builder and build the payload + * stage in encrypt mode.
  6. + *
  7. Prefix the KEM ciphertext using the builder's length framing and then + * stream the payload.
  8. + *
+ * + *

Thread-safety

Instances are not thread-safe. Each instance must be + * configured and used from a single thread. + */ + private static final class EncryptContent implements EncryptedContent { + private final String kemId; + private final PublicKey recipientPublic; + private final boolean useHkdf; + private final byte[] hkdfInfo; + private final int derivedKeyBytes; + private final int maxKemCtLen; // used for sanity check on produced ct + private final AesDataContentBuilder aesBuilder; + private final ChaChaDataContentBuilder chachaBuilder; + + private DataContent upstream; + + private EncryptContent(String kemId, PublicKey recipientPublic, boolean useHkdf, byte[] hkdfInfo, + int derivedKeyBytes, int maxKemCtLen, AesDataContentBuilder aesBuilder, + ChaChaDataContentBuilder chachaBuilder) { + this.kemId = kemId; + this.recipientPublic = recipientPublic; + this.useHkdf = useHkdf; + this.hkdfInfo = hkdfInfo; + this.derivedKeyBytes = derivedKeyBytes; + this.maxKemCtLen = maxKemCtLen; + this.aesBuilder = aesBuilder; + this.chachaBuilder = chachaBuilder; + } + + /** + * Sets the upstream content that supplies the plaintext for the payload stage. + * + * @param input upstream content; must not be null + * @throws NullPointerException if {@code input} is null + */ + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input, "upstream"); + } + + /** + * Returns a stream that emits the KEM length-prefixed ciphertext followed by + * the symmetric payload ciphertext. + * + *

+ * This method constructs a {@link zeroecho.core.context.KemContext} for the + * configured algorithm id and performs encapsulation; it then configures and + * builds the selected symmetric builder in encrypt mode and concatenates the + * KEM prefix with the payload stream. + *

+ * + * @return input stream producing the envelope bytes + * @throws IOException if KEM encapsulation, key derivation, or stream + * setup fails + * @throws IllegalStateException if no upstream content has been set + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "encrypt: missing input"); + + final KemContext kem = CryptoAlgorithms.create(kemId, KeyUsage.ENCAPSULATE, recipientPublic, // NOPMD + VoidSpec.INSTANCE); + final KemResult kr = kem.encapsulate(); + final byte[] ct = Objects.requireNonNull(kr.ciphertext(), "kem ciphertext"); + final byte[] shared = Objects.requireNonNull(kr.sharedSecret(), "kem sharedSecret"); + + if (ct.length <= 0 || ct.length > maxKemCtLen) { + throw new IOException("KEM ciphertext length invalid: " + ct.length); + } + + // Configure symmetric algorithm + DataContent symmetric; + + if (aesBuilder != null) { + // aes + final SecretKey payloadKey = deriveKey(shared, "AES", derivedKeyBytes, useHkdf, hkdfInfo); + symmetric = aesBuilder.withKey(payloadKey).build(true); + } else { + // chacha20 + final SecretKey payloadKey = deriveKey(shared, "ChaCha20", derivedKeyBytes, useHkdf, hkdfInfo); + symmetric = chachaBuilder.withKey(payloadKey).build(true); + } + + symmetric.setInput(upstream); + + final InputStream kemPrefix = writeLenPrefixedAsStream(ct); + final InputStream payloadStream = symmetric.getStream(); + return new SequenceInputStream(kemPrefix, payloadStream); + } + } + + /** + * DecryptContent is a streaming envelope stage that parses the KEM prefix, + * derives a symmetric key via the configured KDF, and streams the remaining + * bytes through the selected payload builder in decrypt mode. + * + *

+ * The input must have the structure: + *

+ *
+     *   [Pack7 length][KEM ciphertext][payload bytes...]
+     * 
The KEM ciphertext is consumed to recover the shared secret and derive + * the payload key; the payload bytes are then decrypted by the supplied + * symmetric builder. + *

+ * + *

Thread-safety

Instances are not thread-safe. Each instance must be + * configured and used from a single thread. + */ + private static final class DecryptContent implements DataContent { // PlainContent would also be fine + private final String kemId; + private final PrivateKey recipientPrivate; + private final boolean useHkdf; + private final byte[] hkdfInfo; + private final int derivedKeyBytes; + private final int maxKemCtLen; + private final AesDataContentBuilder aesBuilder; + private final ChaChaDataContentBuilder chachaBuilder; + + private DataContent upstream; + + private DecryptContent(String kemId, PrivateKey recipientPrivate, boolean useHkdf, byte[] hkdfInfo, + int derivedKeyBytes, int maxKemCtLen, AesDataContentBuilder aesBuilder, + ChaChaDataContentBuilder chachaBuilder) { + this.kemId = kemId; + this.recipientPrivate = recipientPrivate; + this.useHkdf = useHkdf; + this.hkdfInfo = hkdfInfo; + this.derivedKeyBytes = derivedKeyBytes; + this.maxKemCtLen = maxKemCtLen; + this.aesBuilder = aesBuilder; + this.chachaBuilder = chachaBuilder; + } + + /** + * Sets the upstream content that supplies the envelope bytes to be decrypted. + * + * @param input upstream content; must not be null + * @throws NullPointerException if {@code input} is null + */ + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input, "upstream"); + } + + /** + * Returns a stream that yields the plaintext produced by the symmetric payload + * stage after KEM decapsulation and key derivation. + * + *

+ * This method reads and validates the length-prefixed KEM ciphertext, performs + * decapsulation using a {@link zeroecho.core.context.KemContext}, derives the + * symmetric key via HKDF-SHA256 (or directly), configures and builds the + * selected symmetric builder in decrypt mode, and then streams the remainder of + * the input through that payload stage. + *

+ * + * @return input stream producing plaintext bytes + * @throws IOException if the prefix is malformed, decapsulation + * fails, key derivation fails, or the payload + * stage cannot be created + * @throws IllegalStateException if no upstream content has been set + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "decrypt: missing input"); + + final InputStream in = upstream.getStream(); // NOPMD + + // Read the KEM encapsulation prefix [Pack7 len][ct...] + final byte[] ct = readLenPrefixed(in, maxKemCtLen); + + final KemContext kem = CryptoAlgorithms.create(kemId, KeyUsage.DECAPSULATE, recipientPrivate, // NOPMD + VoidSpec.INSTANCE); + final byte[] shared = kem.decapsulate(ct); + + // Configure symmetric algorithm + DataContent symmetric; + + if (aesBuilder != null) { + // aes + final SecretKey payloadKey = deriveKey(shared, "AES", derivedKeyBytes, useHkdf, hkdfInfo); + symmetric = aesBuilder.withKey(payloadKey).build(false); + } else { + // chacha20 + final SecretKey payloadKey = deriveKey(shared, "ChaCha20", derivedKeyBytes, useHkdf, hkdfInfo); + symmetric = chachaBuilder.withKey(payloadKey).build(false); + } + + // The symmetric stage must consume exactly the remaining bytes of 'in'. + symmetric.setInput(new BridgeDataContent(in)); + + return symmetric.getStream(); + } + } + + /** + * BridgeDataContent is a lightweight adapter that exposes an existing + * {@link java.io.InputStream} as {@link zeroecho.sdk.content.api.DataContent}. + * + *

+ * This is used internally to hand the remaining bytes of an already-open input + * stream to a downstream payload builder, without copying. + *

+ * + *

Thread-safety

Instances are not thread-safe and should be used by a + * single consumer. + */ + private final static class BridgeDataContent implements DataContent { + private final InputStream stream; + + private BridgeDataContent(InputStream stream) { + this.stream = stream; + } + + /** + * Returns the underlying stream without additional wrapping. + * + * @return the provided input stream + * @throws IOException never thrown by this implementation + */ + @Override + public InputStream getStream() throws IOException { + return stream; + } + + } + + private static SecretKey deriveKey(byte[] shared, String algo, int keyBytes, boolean hkdf, byte[] info) + throws IOException { + final byte[] material; + if (hkdf) { + // HKDF-SHA256 with empty salt; replace with your KDF utility if preferred. + try { + material = zeroecho.sdk.util.Kdf.hkdfSha256(shared, null, info, keyBytes); + } catch (GeneralSecurityException e) { + throw new IOException("HKDF-SHA256 failed", e); + } + } else { + // Direct use or truncation. + if (shared.length < keyBytes) { + throw new IOException("Shared secret too short for direct use: " + shared.length); + } + material = new byte[keyBytes]; + System.arraycopy(shared, 0, material, 0, keyBytes); + } + return new SecretKeySpec(material, algo); + } + + private static InputStream writeLenPrefixedAsStream(byte[] ct) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(ct.length + 16); + Util.write(baos, ct); + return new ByteArrayInputStream(baos.toByteArray()); + } + + private static byte[] readLenPrefixed(InputStream in, int maxLen) throws IOException { + return Util.read(in, maxLen); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/RsaEncDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/RsaEncDataContentBuilder.java new file mode 100644 index 0000000..7c9cd08 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/RsaEncDataContentBuilder.java @@ -0,0 +1,463 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Objects; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.rsa.RsaEncSpec; +import zeroecho.core.alg.rsa.RsaKeyGenSpec; +import zeroecho.core.alg.rsa.RsaPrivateKeySpec; +import zeroecho.core.alg.rsa.RsaPublicKeySpec; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.EncryptedContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * RsaEncDataContentBuilder builds streaming RSA encryption or decryption + * pipelines that operate while an InputStream is consumed. + * + *

Overview

This builder produces + * {@link zeroecho.sdk.content.api.DataContent} backed by an + * {@link zeroecho.core.context.EncryptionContext} created from + * {@code CryptoAlgorithms}. In encrypt mode it transforms upstream plaintext + * into ciphertext; in decrypt mode it transforms ciphertext back to plaintext. + * Padding and OAEP parameters are configured via {@link RsaEncSpec}. + * + *

Key material

Keys can be supplied directly, generated from + * {@link RsaKeyGenSpec}, or imported from standard encodings: PKCS#8 for + * private keys and X.509 SubjectPublicKeyInfo for public keys. If both + * generation and import are requested, the last configuration applied before + * {@link #build(boolean)} takes effect. + * + *

Examples

{@code
+ * // Encrypt with OAEP-SHA-256 and a generated 2048-bit key pair
+ * DataContent enc = RsaEncDataContentBuilder.builder()
+ *     .oaepSha256()
+ *     .generateKeyPair(RsaKeyGenSpec.rsa2048())
+ *     .build(true);
+ *
+ * // Decrypt with an imported PKCS#8 private key
+ * DataContent dec = RsaEncDataContentBuilder.builder()
+ *     .importPrivatePkcs8(pkcs8Bytes)
+ *     .build(false);
+ *
+ * // Encrypt with OAEP-SHA-512 and an application label
+ * DataContent enc2 = RsaEncDataContentBuilder.builder()
+ *     .oaep(RsaEncSpec.Hash.SHA512)
+ *     .oaepLabelUtf8("session-1")
+ *     .withPublicKey(pub)
+ *     .build(true);
+ * }
+ * + *

Thread-safety

Instances are mutable and not thread-safe. Configure + * and use each builder instance from a single thread. + * + * @see zeroecho.sdk.builders.core.DataContentBuilder + * @see zeroecho.core.CryptoAlgorithms + * @see zeroecho.core.context.EncryptionContext + * @see RsaEncSpec + * @see RsaKeyGenSpec + */ +public final class RsaEncDataContentBuilder implements DataContentBuilder { + + private PublicKey publicKey; + private PrivateKey privateKey; + + private boolean genKeyPair; + private RsaKeyGenSpec keyGen = RsaKeyGenSpec.rsa2048(); + + private byte[] importPrivatePkcs8; + private byte[] importPublicX509; + + private RsaEncSpec spec = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256); // default + + private RsaEncDataContentBuilder() { + } + + /** + * Creates a new builder for constructing RSA encryption or decryption + * pipelines. + * + *
{@code
+     * RsaEncDataContentBuilder b = RsaEncDataContentBuilder.builder();
+     * }
+ * + * @return a new {@code RsaEncDataContentBuilder} instance + */ + public static RsaEncDataContentBuilder builder() { + return new RsaEncDataContentBuilder(); + } + + /** + * Sets the RSA encryption specification, including padding and OAEP parameters. + * + * @param s the RSA encryption spec; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code s} is null + */ + public RsaEncDataContentBuilder spec(RsaEncSpec s) { + this.spec = Objects.requireNonNull(s); + return this; + } + + /** + * Selects RSA-OAEP with SHA-256 for MGF1 and hash. + * + * @return {@code this} builder for chaining + */ + public RsaEncDataContentBuilder oaepSha256() { + this.spec = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256); + return this; + } + + /** + * Selects RSA-OAEP with the given hash algorithm for MGF1 and hash. + * + * @param h the OAEP hash function + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code h} is null + */ + public RsaEncDataContentBuilder oaep(RsaEncSpec.Hash h) { + this.spec = RsaEncSpec.oaep(h); + return this; + } + + /** + * Sets the OAEP label as arbitrary bytes. + * + *

+ * The label is included in the OAEP parameter set. It is not encrypted and must + * be supplied identically during decryption. + *

+ * + * @param label the OAEP label bytes; may be null to clear + * @return {@code this} builder for chaining + */ + public RsaEncDataContentBuilder oaepLabel(byte[] label) { + this.spec = this.spec.withLabel(label); + return this; + } + + /** + * Sets the OAEP label from a UTF-8 string. + * + * @param s the label text; may be null to clear + * @return {@code this} builder for chaining + */ + public RsaEncDataContentBuilder oaepLabelUtf8(String s) { + this.spec = this.spec.withLabelUtf8(s); + return this; + } + + /** + * Selects PKCS#1 v1.5 padding for RSA. + * + * @return {@code this} builder for chaining + */ + public RsaEncDataContentBuilder pkcs1v15() { + this.spec = RsaEncSpec.pkcs1v15(); + return this; + } + + /** + * Supplies the public key to use for encryption. + * + * @param k the public key; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code k} is null + */ + public RsaEncDataContentBuilder withPublicKey(PublicKey k) { + this.publicKey = Objects.requireNonNull(k); + return this; + } + + /** + * Supplies the private key to use for decryption. + * + * @param k the private key; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code k} is null + */ + public RsaEncDataContentBuilder withPrivateKey(PrivateKey k) { + this.privateKey = Objects.requireNonNull(k); + return this; + } + + /** + * Requests generation of a fresh RSA key pair with the provided key generation + * specification. + * + * @param spec the RSA key generation spec; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code spec} is null + */ + public RsaEncDataContentBuilder generateKeyPair(RsaKeyGenSpec spec) { + this.genKeyPair = true; + this.keyGen = Objects.requireNonNull(spec); + return this; + } + + /** + * Requests generation of a fresh RSA key pair using the given modulus size and + * public exponent. + * + * @param bits modulus size in bits + * @param publicExponent public exponent (for example 65537) + * @return {@code this} builder for chaining + */ + public RsaEncDataContentBuilder generateKeyPair(int bits, int publicExponent) { + return generateKeyPair(new RsaKeyGenSpec(bits, publicExponent)); + } + + /** + * Imports a private key from PKCS#8-encoded bytes. + * + * @param pkcs8 PKCS#8 PrivateKeyInfo bytes; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code pkcs8} is null + */ + public RsaEncDataContentBuilder importPrivatePkcs8(byte[] pkcs8) { + this.importPrivatePkcs8 = Objects.requireNonNull(pkcs8).clone(); + return this; + } + + /** + * Imports a public key from X.509 SubjectPublicKeyInfo bytes. + * + * @param x509 X.509 public key bytes; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code x509} is null + */ + public RsaEncDataContentBuilder importPublicX509(byte[] x509) { + this.importPublicX509 = Objects.requireNonNull(x509).clone(); + return this; + } + + /** + * Builds a streaming encryption or decryption pipeline according to the current + * configuration. + * + *

+ * When {@code encrypt} is true, a public key must be available from prior + * configuration, generation, or import. When {@code encrypt} is false, a + * private key must be available. + *

+ * + *
{@code
+     * DataContent enc = RsaEncDataContentBuilder.builder()
+     *     .oaepSha256()
+     *     .generateKeyPair(2048, 65537)
+     *     .build(true);
+     * }
+ * + * @param encrypt {@code true} to build an encrypting pipeline, {@code false} + * for a decrypting pipeline + * @return a {@link zeroecho.sdk.content.api.DataContent} that performs the + * requested operation when its stream is consumed + * @throws IllegalStateException if a required key is missing or if key setup + * fails + */ + @Override + public DataContent build(boolean encrypt) { + try { + resolveKeys(); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("RSA key setup failed", e); + } + if (encrypt) { + if (publicKey == null) { + throw new IllegalStateException("Encrypt requires a PublicKey"); + } + return new Encrypt(publicKey, spec); + } else { + if (privateKey == null) { + throw new IllegalStateException("Decrypt requires a PrivateKey"); + } + return new Decrypt(privateKey, spec); + } + } + + private void resolveKeys() throws GeneralSecurityException { + CryptoAlgorithm alg = CryptoAlgorithms.require("RSA"); + if (genKeyPair) { + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(RsaKeyGenSpec.class); + KeyPair kp = b.generateKeyPair(keyGen); + this.publicKey = kp.getPublic(); + this.privateKey = kp.getPrivate(); + } + if (importPrivatePkcs8 != null) { + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(RsaPrivateKeySpec.class); + this.privateKey = b.importPrivate(new RsaPrivateKeySpec(importPrivatePkcs8)); + } + if (importPublicX509 != null) { + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(RsaPublicKeySpec.class); + this.publicKey = b.importPublic(new RsaPublicKeySpec(importPublicX509)); + } + } + + /** + * Encrypt is a streaming adapter that reads plaintext from an upstream + * {@link zeroecho.sdk.content.api.DataContent} and exposes the RSA-encrypted + * view as an {@link java.io.InputStream}. + * + *

+ * When {@link #getStream()} is invoked, this class creates an + * {@link zeroecho.core.context.EncryptionContext} for the "RSA" algorithm in + * {@link zeroecho.core.KeyUsage#ENCRYPT} role via + * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}, + * attaches the upstream stream, and returns a pull-based stream that encrypts + * on-the-fly using the configured {@link RsaEncSpec}. + *

+ * + *

Thread-safety

Instances are not thread-safe and must be used from a + * single thread. + */ + private static final class Encrypt implements EncryptedContent { + private final PublicKey key; + private final RsaEncSpec spec; + private DataContent upstream; + + private Encrypt(PublicKey key, RsaEncSpec spec) { + this.key = key; + this.spec = spec; + } + + /** + * Sets the upstream content that supplies plaintext bytes. + * + * @param in the source content to be encrypted; must not be null + * @throws NullPointerException if {@code in} is null + */ + @Override + public void setInput(DataContent in) { + this.upstream = Objects.requireNonNull(in); + } + + /** + * Returns a stream that yields the RSA-encrypted view of the upstream content. + * + *

+ * The method constructs an {@link zeroecho.core.context.EncryptionContext} for + * the configured public key and {@link RsaEncSpec}, then attaches the upstream + * stream. The returned stream performs encryption as bytes are read. + *

+ * + * @return an input stream producing RSA ciphertext bytes + * @throws IOException if the upstream stream cannot be obtained or if + * the encryption context fails to attach + * @throws IllegalStateException if no upstream has been set via + * {@link #setInput(DataContent)} + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "rsa encrypt: upstream not set"); + EncryptionContext ctx = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, key, spec); // NOPMD + return ctx.attach(upstream.getStream()); + } + } + + /** + * Decrypt is a streaming adapter that reads ciphertext from an upstream + * {@link zeroecho.sdk.content.api.DataContent} and exposes the RSA-decrypted + * plaintext as an {@link java.io.InputStream}. + * + *

+ * When {@link #getStream()} is invoked, this class creates an + * {@link zeroecho.core.context.EncryptionContext} for the "RSA" algorithm in + * {@link zeroecho.core.KeyUsage#DECRYPT} role via + * {@link zeroecho.core.CryptoAlgorithms#create(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}, + * attaches the upstream stream, and returns a pull-based stream that decrypts + * on-the-fly using the configured {@link RsaEncSpec}. + *

+ * + *

Thread-safety

Instances are not thread-safe and must be used from a + * single thread. + */ + private static final class Decrypt implements PlainContent { + private final PrivateKey key; + private final RsaEncSpec spec; + private DataContent upstream; + + private Decrypt(PrivateKey key, RsaEncSpec spec) { + this.key = key; + this.spec = spec; + } + + /** + * Sets the upstream content that supplies ciphertext bytes. + * + * @param in the source content to be decrypted; must not be null + * @throws NullPointerException if {@code in} is null + */ + @Override + public void setInput(DataContent in) { + this.upstream = Objects.requireNonNull(in); + } + + /** + * Returns a stream that yields the RSA-decrypted view of the upstream content. + * + *

+ * The method constructs an {@link zeroecho.core.context.EncryptionContext} for + * the configured private key and {@link RsaEncSpec}, then attaches the upstream + * stream. The returned stream performs decryption as bytes are read. + *

+ * + * @return an input stream producing plaintext bytes + * @throws IOException if the upstream stream cannot be obtained or if + * the decryption context fails to attach + * @throws IllegalStateException if no upstream has been set via + * {@link #setInput(DataContent)} + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "rsa decrypt: upstream not set"); + EncryptionContext ctx = CryptoAlgorithms.create("RSA", KeyUsage.DECRYPT, key, spec); // NOPMD + return ctx.attach(upstream.getStream()); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/RsaSigDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/RsaSigDataContentBuilder.java new file mode 100644 index 0000000..e978d18 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/RsaSigDataContentBuilder.java @@ -0,0 +1,788 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Base64; +import java.util.Objects; +import java.util.function.Consumer; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.rsa.RsaKeyGenSpec; +import zeroecho.core.alg.rsa.RsaPrivateKeySpec; +import zeroecho.core.alg.rsa.RsaPublicKeySpec; +import zeroecho.core.alg.rsa.RsaSigSpec; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.io.TailStrippingInputStream; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * RsaSigDataContentBuilder builds streaming RSA signature pipelines that sign + * or verify as an InputStream is consumed. + * + *

Overview

The builder produces + * {@link zeroecho.sdk.content.api.PlainContent} that either passes the original + * bytes through while appending or checking a detached signature trailer, or + * emits only the signature or a boolean verification result. The signature + * variant (PKCS#1 v1.5 or PSS) is configured via {@link #spec(RsaSigSpec)}, + * {@link #pkcs1v15(RsaSigSpec.Hash)}, or {@link #pss(RsaSigSpec.Hash, int)}. + * Keys may be supplied, generated, or imported from standard encodings. + * + *

Usage examples

{@code
+ * // Sign while passing the body through; capture signature via callback:
+ * PlainContent sign = RsaSigDataContentBuilder.builder()
+ *     .pss(RsaSigSpec.Hash.SHA256, 32)
+ *     .withPrivateKey(priv)
+ *     .onSignature(sig -> System.out.println(sig.length))
+ *     .passThrough()
+ *     .sign()
+ *     .build(true);
+ *
+ * // Emit a Base64 detached signature:
+ * PlainContent sigOnly = RsaSigDataContentBuilder.builder()
+ *     .pkcs1v15(RsaSigSpec.Hash.SHA512)
+ *     .withPrivateKey(priv)
+ *     .emitBase64Signature()
+ *     .build(true);
+ *
+ * // Verify against an expected signature and emit "true"/"false":
+ * PlainContent verdict = RsaSigDataContentBuilder.builder()
+ *     .withPublicKey(pub)
+ *     .pss(RsaSigSpec.Hash.SHA256, 32)
+ *     .expectedSignatureBase64(b64)
+ *     .emitVerificationBoolean()
+ *     .build(true);
+ * }
+ * + *

Thread-safety

Instances are mutable and not thread-safe. Configure + * and use each builder instance from a single thread. + * + * @see zeroecho.sdk.builders.core.DataContentBuilder + * @see zeroecho.core.CryptoAlgorithms + * @see zeroecho.core.context.SignatureContext + * @see RsaSigSpec + * @see RsaKeyGenSpec + */ +public final class RsaSigDataContentBuilder implements DataContentBuilder { + /** + * Mode selects whether the builder signs or verifies. + */ + public enum Mode { + /** + * Sign mode produces an RSA signature over the input stream. + */ + SIGN, + /** + * Verify mode checks an RSA signature against the input stream. + */ + VERIFY + } + + /** + * Output selector controlling what the pipeline produces after processing the + * body. + * + *

Semantics

+ *
    + *
  • {@link #PASSTHROUGH}: return only the input body; in sign mode the + * signature is consumed as a trailer (optionally delivered via callback); in + * verify mode the expected signature is checked and a mismatch throws.
  • + *
  • {@link #SIG_RAW}/{@link #SIG_HEX}/{@link #SIG_BASE64}: sign mode only; + * return the finalized signature bytes encoded as specified. The input body is + * consumed but not returned.
  • + *
  • {@link #VERIFY_BOOL}: verify mode only; return either {@code "true"} or + * {@code "false"} after consuming the input body, without throwing on + * mismatch.
  • + *
+ * + *

+ * Selecting a signature-emitting option implicitly sets {@link Mode#SIGN}. + * Selecting {@link #VERIFY_BOOL} implicitly sets {@link Mode#VERIFY}. + *

+ */ + private enum Out { + /** + * Return only the input body. Sign: the signature trailer is stripped; Verify: + * throws on mismatch. + */ + PASSTHROUGH, + /** + * Emit the computed signature as raw bytes (sign mode only). + */ + SIG_RAW, + /** + * Emit the computed signature as lowercase hexadecimal text (sign mode only). + */ + SIG_HEX, + /** + * Emit the computed signature as Base64 text (sign mode only). + */ + SIG_BASE64, + /** + * Emit a textual boolean result: {@code "true"} on success, {@code "false"} on + * failure (verify mode only). + */ + VERIFY_BOOL + } + + private static final int IO_BUF = 8192; + + private Mode mode = Mode.SIGN; + private RsaSigSpec spec = RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32); // default (PSS, salt 32) + + private PrivateKey privateKey; + private PublicKey publicKey; + + private boolean genKeyPair; + private RsaKeyGenSpec keyGen = RsaKeyGenSpec.rsa2048(); + private byte[] importPrivatePkcs8; + private byte[] importPublicX509; + + private byte[] expected; // verify input (raw) + private Out out = Out.PASSTHROUGH; + + private Consumer onSignature; + private Consumer onVerified; + + private RsaSigDataContentBuilder() { + } + + /** + * Creates a new builder for constructing RSA signing or verification pipelines. + * + *
{@code
+     * RsaSigDataContentBuilder b = RsaSigDataContentBuilder.builder();
+     * }
+ * + * @return a new {@code RsaSigDataContentBuilder} instance + */ + public static RsaSigDataContentBuilder builder() { + return new RsaSigDataContentBuilder(); + } + + /** + * Switches the builder to sign mode. + * + * @return {@code this} builder for chaining + */ + public RsaSigDataContentBuilder sign() { + this.mode = Mode.SIGN; + return this; + } + + /** + * Switches the builder to verify mode. + * + * @return {@code this} builder for chaining + */ + public RsaSigDataContentBuilder verify() { + this.mode = Mode.VERIFY; + return this; + } + + /** + * Configures the pipeline to pass the input body through unchanged while + * appending or checking a trailer. + * + * @return {@code this} builder for chaining + */ + public RsaSigDataContentBuilder passThrough() { + this.out = Out.PASSTHROUGH; + return this; + } + + /** + * Configures sign mode to emit the finalized signature as raw bytes. + * + * @return {@code this} builder for chaining + */ + public RsaSigDataContentBuilder emitRawSignature() { + this.mode = Mode.SIGN; + this.out = Out.SIG_RAW; + return this; + } + + /** + * Configures sign mode to emit the finalized signature as lowercase hexadecimal + * text. + * + * @return {@code this} builder for chaining + */ + public RsaSigDataContentBuilder emitHexSignature() { + this.mode = Mode.SIGN; + this.out = Out.SIG_HEX; + return this; + } + + /** + * Configures sign mode to emit the finalized signature as Base64 text. + * + * @return {@code this} builder for chaining + */ + public RsaSigDataContentBuilder emitBase64Signature() { + this.mode = Mode.SIGN; + this.out = Out.SIG_BASE64; + return this; + } + + /** + * Configures verify mode to emit a textual boolean result. + * + * @return {@code this} builder for chaining + */ + public RsaSigDataContentBuilder emitVerificationBoolean() { + this.mode = Mode.VERIFY; + this.out = Out.VERIFY_BOOL; + return this; + } + + /** + * Sets the RSA signature specification, including padding mode and hash + * options. + * + * @param s the RSA signature spec; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code s} is null + */ + public RsaSigDataContentBuilder spec(RsaSigSpec s) { + this.spec = Objects.requireNonNull(s); + return this; + } + + /** + * Selects the PKCS#1 v1.5 signature scheme with the given hash function. + * + * @param h the hash function to use + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code h} is null + */ + public RsaSigDataContentBuilder pkcs1v15(RsaSigSpec.Hash h) { + this.spec = RsaSigSpec.pkcs1v15(h); + return this; + } + + /** + * Selects the RSASSA-PSS signature scheme with the given hash function and salt + * length. + * + * @param h the hash function to use for both message hash and MGF1 + * @param saltLen the salt length in bytes + * @return {@code this} builder for chaining + */ + public RsaSigDataContentBuilder pss(RsaSigSpec.Hash h, int saltLen) { + this.spec = RsaSigSpec.pss(h, saltLen); + return this; + } + + /** + * Supplies the private key to use for signing. + * + * @param k the private key; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code k} is null + */ + public RsaSigDataContentBuilder withPrivateKey(PrivateKey k) { + this.privateKey = Objects.requireNonNull(k); + return this; + } + + /** + * Supplies the public key to use for verification. + * + * @param k the public key; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code k} is null + */ + public RsaSigDataContentBuilder withPublicKey(PublicKey k) { + this.publicKey = Objects.requireNonNull(k); + return this; + } + + /** + * Requests generation of a fresh RSA key pair with the provided key generation + * specification. + * + * @param spec the RSA key generation spec; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code spec} is null + */ + public RsaSigDataContentBuilder generateKeyPair(RsaKeyGenSpec spec) { + this.genKeyPair = true; + this.keyGen = Objects.requireNonNull(spec); + return this; + } + + /** + * Imports a private key from PKCS#8-encoded bytes. + * + * @param pkcs8 PKCS#8 PrivateKeyInfo bytes; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code pkcs8} is null + */ + public RsaSigDataContentBuilder importPrivatePkcs8(byte[] pkcs8) { + this.importPrivatePkcs8 = Objects.requireNonNull(pkcs8).clone(); + return this; + } + + /** + * Imports a public key from X.509 SubjectPublicKeyInfo bytes. + * + * @param x509 X.509 public key bytes; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code x509} is null + */ + public RsaSigDataContentBuilder importPublicX509(byte[] x509) { + this.importPublicX509 = Objects.requireNonNull(x509).clone(); + return this; + } + + /** + * Supplies the expected signature as raw bytes for verification. + * + * @param raw the expected signature bytes; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code raw} is null + */ + public RsaSigDataContentBuilder expectedSignature(byte[] raw) { + this.expected = Objects.requireNonNull(raw).clone(); + return this; + } + + /** + * Supplies the expected signature as a hexadecimal string for verification. + * + * @param hex the expected signature in hexadecimal; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code hex} is null + * @throws IllegalArgumentException if {@code hex} is not valid hexadecimal + */ + public RsaSigDataContentBuilder expectedSignatureHex(String hex) { + this.expected = java.util.HexFormat.of().parseHex(Objects.requireNonNull(hex)); + return this; + } + + /** + * Supplies the expected signature as a Base64 string for verification. + * + * @param b64 the expected signature in Base64; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code b64} is null + * @throws IllegalArgumentException if {@code b64} is not valid Base64 + */ + public RsaSigDataContentBuilder expectedSignatureBase64(String b64) { + this.expected = Base64.getDecoder().decode(Objects.requireNonNull(b64)); + return this; + } + + /** + * Registers a callback that receives a defensive copy of the finalized + * signature bytes in sign mode. + * + * @param cb the callback to invoke; may be null to clear + * @return {@code this} builder for chaining + */ + public RsaSigDataContentBuilder onSignature(Consumer cb) { + this.onSignature = cb; + return this; + } + + /** + * Registers a callback that receives the verification result in verify mode. + * + * @param cb the callback to invoke with {@code true} on success and + * {@code false} on failure; may be null to clear + * @return {@code this} builder for chaining + */ + public RsaSigDataContentBuilder onVerified(Consumer cb) { + this.onVerified = cb; + return this; + } + + /** + * Builds a streaming signing or verification pipeline according to the current + * configuration. + * + *

+ * The boolean parameter is ignored and exists only to satisfy the + * {@link zeroecho.sdk.builders.core.DataContentBuilder} interface; behavior is + * determined by {@link #sign()} or {@link #verify()} and the selected output. + *

+ * + *

Result

+ *
    + *
  • Sign mode: returns pass-through content that strips the trailer and + * optionally invokes {@link #onSignature(Consumer)} or an emitter of the + * signature in raw, hex, or Base64.
  • + *
  • Verify mode: returns pass-through content that throws on mismatch and can + * invoke {@link #onVerified(Consumer)}, or an emitter of "true"/"false".
  • + *
+ * + * @param ignored ignored parameter from the interface + * @return a {@link zeroecho.sdk.content.api.PlainContent} pipeline performing + * the configured RSA signature operation + * @throws IllegalStateException if required key material is missing or key + * setup fails + */ + @Override + public PlainContent build(boolean ignored) { + try { + resolveKeys(); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("RSA key setup failed", e); + } + switch (mode) { + case SIGN: + if (privateKey == null) { + throw new IllegalStateException("SIGN requires a PrivateKey"); + } + return (out == Out.PASSTHROUGH) ? new SignPass(privateKey, spec, onSignature) + : new SignEmit(privateKey, spec, out); + case VERIFY: + if (publicKey == null) { + throw new IllegalStateException("VERIFY requires a PublicKey"); + } + return (out == Out.VERIFY_BOOL) ? new VerifyEmit(publicKey, spec, expected, onVerified) + : new VerifyPass(publicKey, spec, expected, onVerified); + } + throw new IllegalStateException("Unsupported mode: " + mode); + } + + private void resolveKeys() throws GeneralSecurityException { + CryptoAlgorithm alg = CryptoAlgorithms.require("RSA"); + if (genKeyPair) { + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(RsaKeyGenSpec.class); + KeyPair kp = b.generateKeyPair(keyGen); + this.privateKey = kp.getPrivate(); + this.publicKey = kp.getPublic(); + } + if (importPrivatePkcs8 != null) { + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(RsaPrivateKeySpec.class); + this.privateKey = b.importPrivate(new RsaPrivateKeySpec(importPrivatePkcs8)); + } + if (importPublicX509 != null) { + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(RsaPublicKeySpec.class); + this.publicKey = b.importPublic(new RsaPublicKeySpec(importPublicX509)); + } + } + + /** + * SIGN + pass-through: return body only; capture signature from trailer. + */ + private static final class SignPass implements PlainContent { + private final PrivateKey key; + private final RsaSigSpec spec; + private final Consumer cb; + private DataContent upstream; + + private SignPass(PrivateKey k, RsaSigSpec s, Consumer cb) { + this.key = k; + this.spec = s; + this.cb = cb; + } + + @Override + public void setInput(DataContent in) { + this.upstream = Objects.requireNonNull(in); + } + + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "rsa sign: upstream not set"); + + SignatureContext sc = CryptoAlgorithms.create("RSA", KeyUsage.SIGN, key, spec); + final InputStream produced = sc.wrap(upstream.getStream()); // NOPMD emits [body][signature] + final int sigLen = sc.tagLength(); + + InputStream stripped = new TailStrippingInputStream(produced, sigLen, IO_BUF) { + @Override + protected void processTail(byte[] tail) throws IOException { + if (cb != null && tail != null) { + try { + cb.accept(tail.clone()); + } catch (RuntimeException ignore) { // NOPMD + + } + } + } + }; + return withContextClose(stripped, sc); + } + } + + /** + * SIGN + emit-only (raw/hex/base64). + */ + private static final class SignEmit implements PlainContent { + private final PrivateKey key; + private final RsaSigSpec spec; + private final Out out; + private DataContent upstream; + + private SignEmit(PrivateKey k, RsaSigSpec s, Out o) { + if (o == Out.PASSTHROUGH || o == Out.VERIFY_BOOL) { + throw new IllegalArgumentException("Emit mode requires SIG_* output"); + } + this.key = k; + this.spec = s; + this.out = o; + } + + @Override + public void setInput(DataContent in) { + this.upstream = Objects.requireNonNull(in); + } + + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "rsa sign: upstream not set"); + + final byte[][] sigHolder = new byte[1][]; + try (SignatureContext sc = CryptoAlgorithms.create("RSA", KeyUsage.SIGN, key, spec)) { + final int sigLen = sc.tagLength(); + + try (InputStream in = new TailStrippingInputStream(sc.wrap(upstream.getStream()), sigLen, IO_BUF) { + @Override + protected void processTail(byte[] tail) throws IOException { + sigHolder[0] = (tail == null ? null : tail.clone()); + } + }) { + byte[] buf = new byte[8192]; + while (true) { + int n = in.read(buf); + if (n < 0) { + break; + } + } + } + } + + byte[] sig = sigHolder[0]; + if (sig == null) { + sig = new byte[0]; + } + + byte[] outBytes = switch (out) { + case SIG_RAW -> sig; + case SIG_HEX -> java.util.HexFormat.of().formatHex(sig).getBytes(StandardCharsets.UTF_8); + case SIG_BASE64 -> Base64.getEncoder().encode(sig); + default -> throw new IllegalStateException("Unexpected output: " + out); + }; + return new ByteArrayInputStream(outBytes); + } + } + + /** + * VERIFY + pass-through: verify against expected; return body (throws on + * mismatch). + */ + private static final class VerifyPass implements PlainContent { + private final PublicKey key; + private final RsaSigSpec spec; + private final byte[] expected; + private final Consumer cb; + private DataContent upstream; + + private VerifyPass(PublicKey k, RsaSigSpec s, byte[] exp, Consumer cb) { + this.key = k; + this.spec = s; + this.expected = (exp == null ? null : exp.clone()); + this.cb = cb; + } + + @Override + public void setInput(DataContent in) { + this.upstream = Objects.requireNonNull(in); + } + + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "rsa verify: upstream not set"); + if (expected == null) { + throw new IllegalStateException("VERIFY requires expectedSignature(...)"); + } + SignatureContext sc = CryptoAlgorithms.create("RSA", KeyUsage.VERIFY, key, spec); + sc.setExpectedTag(expected); + InputStream body = sc.wrap(upstream.getStream()); // throws at EOF on mismatch + + if (cb != null) { + body = new EofCallbackInputStream(body, new Runnable() { + @Override + public void run() { + try { + cb.accept(Boolean.TRUE); + } catch (RuntimeException ignore) { // NOPMD + } + } + }); + } + return withContextClose(body, sc); + } + } + + /** + * VERIFY + emit boolean ("true"/"false"). + */ + private static final class VerifyEmit implements PlainContent { + private final PublicKey key; + private final RsaSigSpec spec; + private final byte[] expected; + private final Consumer cb; + private DataContent upstream; + + private VerifyEmit(PublicKey k, RsaSigSpec s, byte[] exp, Consumer cb) { + this.key = k; + this.spec = s; + this.expected = (exp == null ? null : exp.clone()); + this.cb = cb; + } + + @Override + public void setInput(DataContent in) { + this.upstream = Objects.requireNonNull(in); + } + + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "rsa verify: upstream not set"); + if (expected == null) { + throw new IllegalStateException("VERIFY requires expectedSignature(...)"); + } + + boolean ok; + try (SignatureContext sc = CryptoAlgorithms.create("RSA", KeyUsage.VERIFY, key, spec)) { + sc.setExpectedTag(expected); + + try (InputStream in = sc.wrap(upstream.getStream())) { + byte[] buf = new byte[8192]; + while (true) { + int n = in.read(buf); + if (n < 0) { + break; + } + } + ok = true; // no exception => verified + } catch (IOException fail) { + ok = false; + } + } + + if (cb != null) { + try { + cb.accept(ok); + } catch (RuntimeException ignore) { // NOPMD + } + } + + byte[] outBytes = ok ? "true".getBytes(StandardCharsets.UTF_8) : "false".getBytes(StandardCharsets.UTF_8); + return new ByteArrayInputStream(outBytes); + } + } + + /** + * Ensure the SignatureContext is closed when the stream is closed. + */ + private static InputStream withContextClose(final InputStream in, final SignatureContext sc) { + return new FilterInputStream(in) { + @Override + public void close() throws IOException { + try (sc) { + super.close(); + } + } + }; + } + + /** + * Run a callback exactly once at EOF of the underlying stream. + */ + private static final class EofCallbackInputStream extends FilterInputStream { + private final Runnable onEof; + private boolean done; + + private EofCallbackInputStream(InputStream in, Runnable onEof) { + super(in); + this.onEof = onEof; + } + + @Override + public int read() throws IOException { + int r = super.read(); + if (r == -1) { + runOnce(); + } + return r; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int n = super.read(b, off, len); + if (n == -1) { + runOnce(); + } + return n; + } + + @Override + public void close() throws IOException { + try { // NOPMD + transferTo(OutputStream.nullOutputStream()); + runOnce(); + } finally { + super.close(); + } + } + + private void runOnce() { + if (!done) { + done = true; + onEof.run(); + } + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/SphincsPlusDataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/alg/SphincsPlusDataContentBuilder.java new file mode 100644 index 0000000..dd91f5c --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/SphincsPlusDataContentBuilder.java @@ -0,0 +1,301 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Objects; +import java.util.function.Supplier; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec; +import zeroecho.core.alg.sphincsplus.SphincsPlusPrivateKeySpec; +import zeroecho.core.alg.sphincsplus.SphincsPlusPublicKeySpec; +import zeroecho.core.alg.sphincsplus.SphincsPlusSignatureContext; +import zeroecho.core.context.SignatureContext; + +/** + * Builder for streaming signature operations using the SPHINCS+ post-quantum + * signature scheme. + * + *

+ * This builder follows the {@link AbstractStreamingSignatureDataBuilder} + * pattern and provides configuration options to generate keys, import keys, and + * perform signing or verification in a streaming pipeline. SPHINCS+ is a + * hash-based, stateless signature scheme designed for post-quantum security, + * standardized by NIST PQC Round 3. + *

+ * + *

Key management

+ *
    + *
  • Keys can be generated using a custom {@link SphincsPlusKeyGenSpec} + * supplied via {@link #withKeyGenSpec(SphincsPlusKeyGenSpec)}, or with the + * algorithm's default specification.
  • + *
  • Public and private keys may be imported from encoded formats + * ({@code X.509} and {@code PKCS#8}, respectively).
  • + *
  • A provider hint may optionally be given. If not provided, the builder + * will prefer the provider specified in {@link SphincsPlusKeyGenSpec}, or fall + * back to {@link #defaultProviderHint()}.
  • + *
+ * + *

Contexts

+ *
    + *
  • {@link #newSignContext(CryptoAlgorithm, java.security.PrivateKey)} + * produces a {@link SphincsPlusSignatureContext} in sign mode.
  • + *
  • {@link #newVerifyContext(CryptoAlgorithm, java.security.PublicKey)} + * produces a {@link SphincsPlusSignatureContext} in verify mode.
  • + *
  • Contexts are bound to the {@link CryptoAlgorithm} instance returned by + * {@link CryptoAlgorithms#require(String)} with id {@code "SPHINCS+"}.
  • + *
+ * + *

Thread-safety

Instances of this builder are not thread-safe. Each + * builder should be configured and built within a single thread or with + * external synchronization. + * + *

Example

{@code
+ * // Signing with SPHINCS+
+ * SphincsPlusDataContentBuilder builder = SphincsPlusDataContentBuilder.builder()
+ *      .sign()
+ *      .withKeyGenSpec(SphincsPlusKeyGenSpec.sphincsSha256128s());
+ *
+ * PlainContent content = builder.build(true);
+ * content.setInput(new FileContent("document.txt"));
+ * try (InputStream in = content.getStream()) {
+ *     // Consume input to trigger signing
+ * }
+ * }
+ * + * @see SphincsPlusKeyGenSpec + * @see SphincsPlusPublicKeySpec + * @see SphincsPlusPrivateKeySpec + * @see SphincsPlusSignatureContext + * @since 1.0 + */ +public final class SphincsPlusDataContentBuilder extends + AbstractStreamingSignatureDataBuilder { + /** + * Optional custom key generation specification to be used when + * {@link #generateKeyPair()} is selected in the superclass API. + */ + private SphincsPlusKeyGenSpec keyGenSpec; // optional custom spec + + /** + * Creates a new builder for constructing SPHINCS+ streaming signature + * pipelines. + * + *

Example

{@code
+     * SphincsPlusDataContentBuilder b = SphincsPlusDataContentBuilder.builder();
+     * }
+ * + * @return a new {@code SphincsPlusDataContentBuilder} instance + */ + public static SphincsPlusDataContentBuilder builder() { + return new SphincsPlusDataContentBuilder(); + } + + /** + * Sets a custom key generation specification to be used for SPHINCS+ key pair + * generation. + * + *

+ * If no specification is provided, the builder falls back to the supplier + * returned by {@link #defaultKeyGenSpecSupplier()}. + *

+ * + * @param spec the SPHINCS+ key generation specification; must not be null + * @return {@code this} builder for chaining + * @throws NullPointerException if {@code spec} is null + */ + public SphincsPlusDataContentBuilder withKeyGenSpec(SphincsPlusKeyGenSpec spec) { + this.keyGenSpec = Objects.requireNonNull(spec); + return this; + } + + /** + * Returns the algorithm identifier used to resolve the implementation from + * {@link zeroecho.core.CryptoAlgorithms}. + * + * @return the string {@code "SPHINCS+"} + */ + @Override + protected String algorithmName() { + return "SPHINCS+"; + } + + /** + * Creates a signing {@link zeroecho.core.context.SignatureContext} for SPHINCS+ + * using the provided algorithm instance and private key. + * + * @param alg the resolved algorithm instance used to create the context + * @param key the private key for signing + * @return a new {@link SphincsPlusSignatureContext} configured for signing + * @throws GeneralSecurityException if the context cannot be created for the + * given key or algorithm + */ + @Override + protected SignatureContext newSignContext(CryptoAlgorithm alg, PrivateKey key) throws GeneralSecurityException { + return new SphincsPlusSignatureContext(alg, key); + } + + /** + * Creates a verification {@link zeroecho.core.context.SignatureContext} for + * SPHINCS+ using the provided algorithm instance and public key. + * + * @param alg the resolved algorithm instance used to create the context + * @param key the public key for verification + * @return a new {@link SphincsPlusSignatureContext} configured for verification + * @throws GeneralSecurityException if the context cannot be created for the + * given key or algorithm + */ + @Override + protected SignatureContext newVerifyContext(CryptoAlgorithm alg, PublicKey key) throws GeneralSecurityException { + return new SphincsPlusSignatureContext(alg, key); + } + + /** + * Returns the key generation specification class used by this builder. + * + * @return {@code SphincsPlusKeyGenSpec.class} + */ + @Override + protected Class keyGenSpecClass() { + return SphincsPlusKeyGenSpec.class; + } + + /** + * Returns the public key import specification class used by this builder. + * + * @return {@code SphincsPlusPublicKeySpec.class} + */ + @Override + protected Class publicKeySpecClass() { + return SphincsPlusPublicKeySpec.class; + } + + /** + * Returns the private key import specification class used by this builder. + * + * @return {@code SphincsPlusPrivateKeySpec.class} + */ + @Override + protected Class privateKeySpecClass() { + return SphincsPlusPrivateKeySpec.class; + } + + /** + * Supplies a default key generation specification for SPHINCS+. + * + * @return a supplier returning {@link SphincsPlusKeyGenSpec#defaultSpec()} + */ + @Override + protected Supplier defaultKeyGenSpecSupplier() { + return SphincsPlusKeyGenSpec::defaultSpec; + } + + /** + * Returns the currently configured key generation specification or null to + * indicate that the default should be used. + * + * @return the active {@link SphincsPlusKeyGenSpec} or {@code null} if none has + * been set + */ + @Override + protected SphincsPlusKeyGenSpec currentKeyGenSpecOrNull() { + return keyGenSpec; + } + + /** + * Builds a public key import specification from X.509-encoded + * SubjectPublicKeyInfo bytes and an optional provider hint. + * + *

+ * Provider selection follows this precedence: + *

    + *
  1. Use {@code providerHint} if non-null.
  2. + *
  3. Else, if a custom key generation spec is present, use + * {@link SphincsPlusKeyGenSpec#providerName()}.
  4. + *
  5. Else, use {@link #defaultProviderHint()}.
  6. + *
+ * + * @param x509 the X.509 public key bytes + * @param providerHint an optional provider name hint; may be null + * @return a new {@link SphincsPlusPublicKeySpec} wrapping the provided bytes + * and provider choice + */ + @Override + protected SphincsPlusPublicKeySpec makePublicKeySpec(byte[] x509, String providerHint) { + String p = providerHint != null ? providerHint + : keyGenSpec != null ? keyGenSpec.providerName() : defaultProviderHint(); + return new SphincsPlusPublicKeySpec(x509, p); + } + + /** + * Builds a private key import specification from PKCS#8-encoded PrivateKeyInfo + * bytes and an optional provider hint. + * + *

+ * Provider selection follows this precedence: + *

    + *
  1. Use {@code providerHint} if non-null.
  2. + *
  3. Else, if a custom key generation spec is present, use + * {@link SphincsPlusKeyGenSpec#providerName()}.
  4. + *
  5. Else, use {@link #defaultProviderHint()}.
  6. + *
+ * + * @param pkcs8 the PKCS#8 private key bytes + * @param providerHint an optional provider name hint; may be null + * @return a new {@link SphincsPlusPrivateKeySpec} wrapping the provided bytes + * and provider choice + */ + @Override + protected SphincsPlusPrivateKeySpec makePrivateKeySpec(byte[] pkcs8, String providerHint) { + String p = providerHint != null ? providerHint + : keyGenSpec != null ? keyGenSpec.providerName() : defaultProviderHint(); + return new SphincsPlusPrivateKeySpec(pkcs8, p); + } + + /** + * Returns the default provider hint to use when importing SPHINCS+ keys if none + * is explicitly specified. + * + * @return the default provider hint string, typically {@code "BCPQC"} + */ + @Override + protected String defaultProviderHint() { + return "BCPQC"; + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/alg/package-info.java b/lib/src/main/java/zeroecho/sdk/builders/alg/package-info.java new file mode 100644 index 0000000..e20b395 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/alg/package-info.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Streaming cryptographic builders for symmetric encryption, signatures, MACs, + * digests, and KEM envelopes. + * + *

+ * Classes in this package provide small, composable builders that configure + * cryptographic operations and produce streaming {@code DataContent} stages. + * The emphasis is on zero-copy streaming where feasible, clear separation + * between configuration (builders) and execution (content stages), and + * provider-agnostic algorithm lookup via the central registry. + *

+ * + *

Design goals

+ *
    + *
  • Enable large-payload processing without materializing data in + * memory.
  • + *
  • Keep configuration fluent and explicit; keep execution + * pipeline-friendly.
  • + *
  • Resolve algorithms through the registry so applications can swap JCA + * providers easily.
  • + *
  • Offer explicit control of headers, trailers, and verification behavior + * where applicable.
  • + *
+ * + *

Builders overview

+ *
    + *
  • {@link AesDataContentBuilder} and {@link ChaChaDataContentBuilder} - + * stream symmetric encryption and decryption.
  • + *
  • {@link HmacDataContentBuilder} - compute or verify MAC tags in a + * streaming pipeline.
  • + *
  • {@link DigestDataContentBuilder} - compute digests/XOFs and, where + * supported, emit textual encodings.
  • + *
  • {@link RsaEncDataContentBuilder} and {@link ElgamalEncDataContentBuilder} + * - wrap asymmetric encryption.
  • + *
  • {@link RsaSigDataContentBuilder}, {@link EcdsaDataContentBuilder}, + * {@link Ed25519DataContentBuilder}, {@link Ed448DataContentBuilder}, and + * {@link SphincsPlusDataContentBuilder} - perform streaming signatures and + * verification.
  • + *
  • {@link KemDataContentBuilder} - implement KEM-first envelopes and inject + * the derived key into a chosen symmetric payload builder.
  • + *
+ * + *

Streaming model

+ *

+ * Each builder implements a content-builder contract and produces a + * {@code DataContent} stage when built. The stage uses the registry to obtain a + * core context and attaches to an upstream {@link java.io.InputStream}; output + * is produced lazily as the stream is read. Signature and MAC stages can pass + * the body through while appending or verifying a trailing tag; symmetric + * stages may emit a compact header with non-secret parameters such as IV, + * nonce, tag length, or AAD hints. + *

+ * + *

Signatures and MACs

+ *

+ * Signature builders share common mechanics via + * {@link AbstractStreamingSignatureDataBuilder}. They support: + *

+ *
    + *
  • Pass-through: output the message body while a trailing signature + * is captured or verified.
  • + *
  • Emit-only: consume the body and output only the signature (or a + * boolean verification result, when selected).
  • + *
+ *

+ * MAC builders mirror this pattern and can verify against an expected tag or a + * tag carried in the input trailer. + *

+ * + *

KEM envelopes

+ *

+ * {@link KemDataContentBuilder} first collects KEM parameters (algorithm id, + * recipient key, KDF configuration), runs encapsulation or decapsulation to + * derive a symmetric key, and then injects that key into an explicit symmetric + * payload builder such as {@link AesDataContentBuilder} or + * {@link ChaChaDataContentBuilder}. This avoids duplicating KEM configuration + * across symmetric builders and matches a natural envelope workflow. + *

+ * + *

Key management

+ *
    + *
  • Use a caller-supplied key object.
  • + *
  • Generate a new key or key pair using an algorithm-specific key-generation + * spec.
  • + *
  • Import a key from standard encodings (for example, X.509 public or PKCS#8 + * private) or other supported forms.
  • + *
+ *

+ * Builders typically provide convenience methods for common defaults (for + * example, AES-GCM or Ed25519). + *

+ * + *

Error handling and verification

+ *
    + *
  • Verification failures usually surface as {@code IOException} at + * end-of-stream in verify mode.
  • + *
  • Some builders offer explicit emit-boolean modes or callbacks to report + * verification outcomes without throwing.
  • + *
  • Configuration errors (incompatible keys/specs) are detected early by + * underlying core factories.
  • + *
+ * + *

Usage pattern

{@code
+ * // A builder in this package produces a DataContent stage:
+ * zeroecho.sdk.content.api.DataContent content = ...;
+ *
+ * // Wire the upstream and stream the result to an output sink:
+ * content.setInput(upstreamContent);
+ * try (java.io.InputStream in = content.getStream()) {
+ *     in.transferTo(out);
+ * }
+ * }
+ * + *

Thread safety

+ *
    + *
  • Builder instances are mutable and not thread-safe; configure and use each + * instance on a single thread.
  • + *
  • Produced content stages are single-use; create a fresh instance per + * pipeline run.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.sdk.builders.alg; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/builders/core/DataContentBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/core/DataContentBuilder.java new file mode 100644 index 0000000..b601b7e --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/core/DataContentBuilder.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.builders.core; + +import zeroecho.sdk.content.api.DataContent; + +/** + * A builder interface for constructing instances of {@link DataContent}. + *

+ * This interface enables a uniform and extensible way to create various + * {@code DataContent} implementations in a fluent, builder-based style. It is + * intended to be used with builder classes that encapsulate construction + * parameters and logic specific to each {@code DataContent} subtype. + *

+ * The {@link #build(boolean)} method configures the resulting + * {@code DataContent} instance for either encryption or decryption mode. When + * encryption is selected, additional metadata (such as headers) may be added to + * the output stream; this metadata must be preserved and reused during + * decryption. + *

+ * Typical usage example: + * + *

{@code
+ * DataContent content = SomeDataContentBuilder.builder()
+ *     .parameterX(...)
+ *     .parameterY(...)
+ *     .build(true); // true for encryption mode
+ * }
+ * + * @param the type of {@link DataContent} produced by this builder + * + * @author Leo Galambos + */ +@FunctionalInterface +public interface DataContentBuilder { + /** + * Constructs and returns a new {@link DataContent} instance based on the + * builder's current configuration. + * + * @param encrypt whether the resulting {@code DataContent} should be configured + * for encryption ({@code true}) or decryption ({@code false}). + * In encryption mode, additional headers or metadata may be + * inserted into the stream which must be retained for successful + * decryption. + * @return the constructed {@code DataContent} instance + */ + T build(boolean encrypt); +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/core/DataContentChainBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/core/DataContentChainBuilder.java new file mode 100644 index 0000000..4dfbcb4 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/core/DataContentChainBuilder.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.builders.core; + +import zeroecho.sdk.content.api.DataContent; + +/** + * A builder interface for constructing a chain of {@link DataContent} + * components, where each content unit passes its output as the input to the + * next one. + * + *

+ * This builder simplifies the composition of processing pipelines such as: + *

    + *
  • Encryption with multiple layers (e.g., compression → encryption → + * signing)
  • + *
  • Decryption chains (e.g., verification → decryption → decompression)
  • + *
+ * + *

+ * The builder supports both encryption and decryption modes, which are applied + * consistently across all {@link DataContentBuilder} instances in the chain. + *

+ * + *

+ * Usage example:

{@code
+ * DataContent chain = DataContentChainBuilder.encrypt()
+ *     .add(PlainStringBuilder.builder().value("hello"))
+ *     .add(DerivedAesParametersBuilder.builder().password("secret"))
+ *     .build();
+ * }
+ */ +public interface DataContentChainBuilder { + /** + * Adds a {@link DataContentBuilder} to the chain and links its output to the + * input of the previously added {@code DataContent}, if any. + * + *

+ * The {@link DataContent} produced by this builder becomes the new tail of the + * chain. + *

+ * + * @param builder the builder producing the next {@code DataContent}; must not + * be null + * @return this chain builder instance, allowing for fluent chaining + * @throws NullPointerException if {@code builder} is null + */ + DataContentChainBuilder add(DataContentBuilder builder); + + /** + * Finalizes the chain and returns the tail {@link DataContent} instance. + * + *

+ * The returned content may internally reference earlier content via + * {@link DataContent#setInput(DataContent)} chaining. + *

+ * + * @return the last {@code DataContent} in the chain, or {@code null} if no + * builders were added + */ + DataContent build(); + + /** + * Creates a new {@code DataContentChainBuilder} configured for encryption mode. + * + *

+ * All added {@link DataContentBuilder} instances will receive {@code true} for + * their {@code build(boolean encrypt)} method, indicating encryption behavior. + *

+ * + * @return a new chain builder in encryption mode + */ + static DataContentChainBuilder encrypt() { + return new DefaultDataContentChainBuilder(true); + } + + /** + * Creates a new {@code DataContentChainBuilder} configured for decryption mode. + * + *

+ * All added {@link DataContentBuilder} instances will receive {@code false} for + * their {@code build(boolean encrypt)} method, indicating decryption behavior. + *

+ * + * @return a new chain builder in decryption mode + */ + static DataContentChainBuilder decrypt() { + return new DefaultDataContentChainBuilder(false); + } + + /** + * Default implementation of {@link DataContentChainBuilder}. + * + *

+ * Maintains a reference to the tail of the content chain. Each added builder is + * built in the specified encryption/decryption mode and connected via + * {@link DataContent#setInput(DataContent)} to the previously built content. + *

+ */ + final class DefaultDataContentChainBuilder implements DataContentChainBuilder { + private DataContent tail; + private final boolean encrypt; + + private DefaultDataContentChainBuilder(final boolean encrypt) { + this.encrypt = encrypt; + } + + @Override + public DataContentChainBuilder add(final DataContentBuilder builder) { + final DataContent previous = tail; + + tail = builder.build(encrypt); + + if (previous != null) { + tail.setInput(previous); + } + + return this; + } + + @Override + public DataContent build() { + return tail; + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/core/PlainBytesBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/core/PlainBytesBuilder.java new file mode 100644 index 0000000..0f373dc --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/core/PlainBytesBuilder.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.builders.core; + +import java.util.Objects; + +import zeroecho.sdk.content.api.PlainContent; +import zeroecho.sdk.content.builtin.PlainBytes; + +/** + * Builder interface for constructing {@link PlainBytes} instances that + * encapsulate unencrypted byte array content. + *

+ * This builder allows specifying a raw byte array to be wrapped as + * {@code PlainContent}, which can be used as input for cryptographic operations + * or as standalone plaintext data. + *

+ * The {@link #build(boolean)} method accepts an {@code encrypt} flag for API + * consistency, but the flag has no effect for plain byte content. + * + *

+ * Usage example: + * + *

{@code
+ * PlainContent content = PlainBytesBuilder.builder()
+ *     .bytes(new byte[] { 1, 2, 3, 4 })
+ *     .build(false); // encrypt flag is ignored for PlainBytes
+ * }
+ * + * @see PlainBytes + */ +public interface PlainBytesBuilder extends DataContentBuilder { + + /** + * Sets the byte array content to be wrapped by {@link PlainBytes}. + * + * @param bytes the byte array; must not be null + * @return this builder instance for method chaining + * @throws NullPointerException if {@code bytes} is null + */ + PlainBytesBuilder bytes(byte[] bytes); + + /** + * Creates a new instance of the default builder implementation. + * + * @return a new {@code PlainBytesBuilder} + */ + static PlainBytesBuilder builder() { + return new DefaultPlainBytesBuilder(); + } + + /** + * Default implementation of the {@link PlainBytesBuilder} interface. + *

+ * Builds a {@link PlainBytes} instance wrapping the specified byte array. + *

+ */ + final class DefaultPlainBytesBuilder implements PlainBytesBuilder { + private byte[] bytesField; + + private DefaultPlainBytesBuilder() { + } + + @Override + public PlainBytesBuilder bytes(final byte[] bytes) { + this.bytesField = Objects.requireNonNull(bytes, "bytes must not be null"); + return this; + } + + @Override + public PlainContent build(final boolean encrypt) { + if (bytesField == null) { + throw new IllegalStateException("bytes must be set before building"); + } + return new PlainBytes(bytesField); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/core/PlainFileBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/core/PlainFileBuilder.java new file mode 100644 index 0000000..99720f5 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/core/PlainFileBuilder.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.builders.core; + +import java.net.URL; +import java.util.Objects; + +import zeroecho.sdk.content.api.PlainContent; +import zeroecho.sdk.content.builtin.PlainFile; + +/** + * Builder interface for constructing {@link PlainContent} instances that + * represent unencrypted file-based content sourced from a {@link URL}. + *

+ * This builder allows specifying the source URL of a file to be used as plain + * (non-encrypted) content. It is typically used as input for cryptographic + * operations or for representing raw file data. + *

+ * The {@link #build(boolean)} method accepts an {@code encrypt} flag to comply + * with the {@link DataContentBuilder} interface; however, this flag is ignored + * for plain content types. + *

+ * Usage example: + * + *

{@code
+ * PlainContent plainFile = PlainFileBuilder.builder()
+ *     .url(new URL("file:///path/to/file.txt"))
+ *     .build(true); // encrypt flag is ignored for PlainFile
+ * }
+ * + * @see PlainFile + */ +public interface PlainFileBuilder extends DataContentBuilder { + /** + * Sets the URL of the file to be used as the plain content source. + * + * @param url the URL of the file; must not be {@code null} + * @return this builder instance + */ + PlainFileBuilder url(URL url); + + /** + * Creates a new instance of the default builder implementation. + * + * @return a new {@code PlainFileBuilder} + */ + static PlainFileBuilder builder() { + return new DefaultPlainFileBuilder(); + } + + /** + * Default implementation of the {@link PlainFileBuilder} interface. + *

+ * Builds a {@link PlainFile} instance using the specified URL. + */ + final class DefaultPlainFileBuilder implements PlainFileBuilder { + private URL urlField; + + private DefaultPlainFileBuilder() { + } + + @Override + public PlainFileBuilder url(final URL url) { + this.urlField = Objects.requireNonNull(url); + return this; + } + + @Override + public PlainContent build(final boolean encrypt) { + return new PlainFile(urlField); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/core/PlainStringBuilder.java b/lib/src/main/java/zeroecho/sdk/builders/core/PlainStringBuilder.java new file mode 100644 index 0000000..0a52efe --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/core/PlainStringBuilder.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.builders.core; + +import zeroecho.sdk.content.builtin.PlainString; + +/** + * Builder interface for constructing {@link PlainString} instances, which + * represent unencrypted textual content. + *

+ * This builder supports specifying a string value to be encapsulated and + * optionally used in cryptographic processing pipelines, although the value + * itself remains unencrypted. + *

+ * The {@link #build(boolean)} method accepts an {@code encrypt} flag to comply + * with the {@link DataContentBuilder} interface. However, this flag is ignored + * because {@code PlainString} represents unencrypted data. + *

+ * Usage example: + * + *

{@code
+ * PlainString plainText = PlainStringBuilder.builder()
+ *     .value("Hello, World!")
+ *     .build(true); // encrypt flag is ignored for PlainString
+ * }
+ */ +public interface PlainStringBuilder extends DataContentBuilder { + + /** + * Creates a new instance of the default builder implementation. + * + * @return a new {@code PlainStringBuilder} + */ + static PlainStringBuilder builder() { + return new DefaultPlainStringBuilder(); + } + + /** + * Sets the string value that the {@link PlainString} instance will contain. + * + * @param value the string value; may be {@code null} or empty depending on + * usage + * @return this builder instance + */ + PlainStringBuilder value(String value); + + /** + * Default implementation of the {@link PlainStringBuilder} interface. + *

+ * Builds a {@link PlainString} instance using the specified string value. + */ + final class DefaultPlainStringBuilder implements PlainStringBuilder { + private String valueField; + + private DefaultPlainStringBuilder() { + } + + @Override + public PlainStringBuilder value(final String value) { + this.valueField = value; + return this; + } + + @Override + public PlainString build(final boolean encrypt) { + return new PlainString(valueField); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/builders/core/package-info.java b/lib/src/main/java/zeroecho/sdk/builders/core/package-info.java new file mode 100644 index 0000000..be1523b --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/core/package-info.java @@ -0,0 +1,94 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Core builders for composing streaming {@code DataContent} pipelines and plain + * sources. + * + *

+ * This package provides a small set of generic builder interfaces plus + * plain-content builders for bytes, strings, and files. Builders create + * {@code DataContent} stages that can be chained so that each stage reads from + * the previous one while applying transformation or sourcing bytes. + *

+ * + *

What is here

+ *
    + *
  • {@link DataContentBuilder} - functional interface that builds a + * {@code DataContent} configured for encryption or decryption.
  • + *
  • {@link DataContentChainBuilder} - composes multiple + * {@code DataContentBuilder} instances into one tail stage via + * {@code setInput(...)} chaining.
  • + *
  • {@link PlainBytesBuilder} - wraps a caller-provided byte array as plain + * content.
  • + *
  • {@link PlainStringBuilder} - wraps a {@code String} as plain + * content.
  • + *
  • {@link PlainFileBuilder} - wraps a {@link java.net.URL}-addressed file as + * plain content.
  • + *
+ * + *

Chaining model

+ *

+ * A chain builder holds the current tail stage. Each {@code add(...)} call + * builds the next stage (in a consistent encrypt/decrypt mode) and links the + * previous tail as its input. The final {@code build()} returns the tail, which + * internally references the rest of the chain via {@code setInput(...)}. + *

+ * + *

Typical usage

{@code
+ * // Encrypt: plain text -> (for example) AES-GCM stage defined elsewhere.
+ * zeroecho.sdk.content.api.DataContent chain =
+ *     zeroecho.sdk.builders.core.DataContentChainBuilder.encrypt()
+ *         .add(zeroecho.sdk.builders.core.PlainStringBuilder.builder().value("hello"))
+ *         // .add(zeroecho.sdk.builders.alg.AesDataContentBuilder.builder().modeGcm(128).generateKey(256))
+ *         .build();
+ *
+ * // Decrypt: build the inverse chain with the decrypt() factory.
+ * zeroecho.sdk.content.api.DataContent decChain =
+ *     zeroecho.sdk.builders.core.DataContentChainBuilder.decrypt()
+ *         // .add(zeroecho.sdk.builders.alg.AesDataContentBuilder.builder().modeGcm(128).withHeader())
+ *         .build();
+ * }
+ * + *

Notes

+ *
    + *
  • Plain-content builders ignore the encrypt/decrypt flag; they simply + * provide bytes.
  • + *
  • Builders and produced stages are not thread-safe; configure and use per + * pipeline.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.sdk.builders.core; diff --git a/lib/src/main/java/zeroecho/sdk/builders/package-info.java b/lib/src/main/java/zeroecho/sdk/builders/package-info.java new file mode 100644 index 0000000..8ccb9fb --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/builders/package-info.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Builders for composing streaming data-content pipelines across core sources + * and algorithm-specific stages. + * + *

+ * This package groups the top-level builder APIs used by the SDK to create + * streaming {@code DataContent} pipelines. It ties together the generic chain + * and plain-source builders from {@link zeroecho.sdk.builders.core} with the + * algorithm-focused builders from {@link zeroecho.sdk.builders.alg}, and + * provides cross-cutting utilities such as + * {@link TagTrailerDataContentBuilder}. + *

+ * + *

What lives where

+ *
    + *
  • Core builders - generic composition and plain sources in + * {@link zeroecho.sdk.builders.core}: + *
      + *
    • {@link zeroecho.sdk.builders.core.DataContentBuilder} - minimal contract + * implemented by all builders.
    • + *
    • {@link zeroecho.sdk.builders.core.DataContentChainBuilder} - chains + * multiple stages in encrypt or decrypt mode.
    • + *
    • {@link zeroecho.sdk.builders.core.PlainBytesBuilder}, + * {@link zeroecho.sdk.builders.core.PlainStringBuilder}, + * {@link zeroecho.sdk.builders.core.PlainFileBuilder} - plain, + * non-cryptographic sources.
    • + *
    + *
  • + *
  • Algorithm builders - streaming crypto stages in + * {@link zeroecho.sdk.builders.alg}: + *
      + *
    • Symmetric encryption: + * {@link zeroecho.sdk.builders.alg.AesDataContentBuilder}, + * {@link zeroecho.sdk.builders.alg.ChaChaDataContentBuilder}.
    • + *
    • Asymmetric encryption: + * {@link zeroecho.sdk.builders.alg.RsaEncDataContentBuilder}, + * {@link zeroecho.sdk.builders.alg.ElgamalEncDataContentBuilder}.
    • + *
    • Signatures: {@link zeroecho.sdk.builders.alg.RsaSigDataContentBuilder}, + * {@link zeroecho.sdk.builders.alg.EcdsaDataContentBuilder}, + * {@link zeroecho.sdk.builders.alg.Ed25519DataContentBuilder}, + * {@link zeroecho.sdk.builders.alg.Ed448DataContentBuilder}, + * {@link zeroecho.sdk.builders.alg.SphincsPlusDataContentBuilder}.
    • + *
    • MAC and digest: {@link zeroecho.sdk.builders.alg.HmacDataContentBuilder}, + * {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.
    • + *
    • KEM envelopes: {@link zeroecho.sdk.builders.alg.KemDataContentBuilder} + * (derives a symmetric key and injects it into a chosen symmetric payload + * builder).
    • + *
    + *
  • + *
  • Cross-cutting: + *
      + *
    • {@link TagTrailerDataContentBuilder} - appends or verifies an + * authentication tag carried as an input trailer using a + * {@link zeroecho.core.tag.TagEngine}.
    • + *
    + *
  • + *
+ * + *

Chaining model

+ *

+ * Pipelines are assembled by creating a + * {@link zeroecho.sdk.builders.core.DataContentChainBuilder} in encrypt or + * decrypt mode and adding builder stages in order. Each stage is built in the + * selected mode, linked to the previous tail via {@code setInput(...)} and + * exposed as a single tail {@link zeroecho.sdk.content.api.DataContent}. + *

+ * + *

Headers, trailers, and verification

+ *
    + *
  • Symmetric builders can emit compact headers containing non-secret + * parameters (for example, IV or nonce) and parse them on decrypt.
  • + *
  • Signature and MAC builders support pass-through or emit-only forms for + * signatures or tags.
  • + *
  • {@link TagTrailerDataContentBuilder} focuses on trailer-style tags with + * explicit verify policies.
  • + *
+ * + *

Typical usage

{@code
+ * // Build a simple HMAC trailer pipeline over a plain string and append the tag.
+ * zeroecho.sdk.content.api.DataContent macEnc =
+ *     zeroecho.sdk.builders.core.DataContentChainBuilder.encrypt()
+ *         .add(zeroecho.sdk.builders.core.PlainStringBuilder.builder().value("hello"))
+ *         .add(new zeroecho.sdk.builders.TagTrailerDataContentBuilder(
+ *                 zeroecho.core.tag.TagEngineBuilder.hmac(secretKey, zeroecho.core.alg.hmac.HmacSpec.sha256())))
+ *         .build();
+ *
+ * // Build the corresponding verification pipeline that strips the trailer and throws on mismatch.
+ * zeroecho.sdk.content.api.DataContent macDec =
+ *     zeroecho.sdk.builders.core.DataContentChainBuilder.decrypt()
+ *         .add(new zeroecho.sdk.builders.TagTrailerDataContentBuilder(
+ *                 zeroecho.core.tag.TagEngineBuilder.hmac(secretKey, zeroecho.core.alg.hmac.HmacSpec.sha256())))
+ *         .build();
+ * }
+ * + *

Thread safety

+ *
    + *
  • Builder instances are mutable and not thread-safe; configure and use each + * instance on a single thread.
  • + *
  • Produced {@code DataContent} stages are single-use and should not be + * shared across pipelines.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.sdk.builders; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/content/api/AbstractExportableDataContent.java b/lib/src/main/java/zeroecho/sdk/content/api/AbstractExportableDataContent.java new file mode 100644 index 0000000..7f81f2a --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/api/AbstractExportableDataContent.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.content.api; + +import java.util.Objects; + +/** + * An abstract base class for exportable data content, providing default + * implementations for managing input data and export mode. + * + *

+ * This class is intended to be extended by concrete implementations that + * generate various forms of export output, such as raw binary streams, shell + * scripts, or platform-specific wrappers. + *

+ * + *

+ * It manages: + *

    + *
  • The {@link DataContent} input that is to be exported
  • + *
  • The {@link ExportMode} that controls how the export is formatted
  • + *
+ * + *

+ * Implementing classes must override {@link ExportableDataContent#getStream()} + * to provide the actual export logic based on the configured mode. + *

+ */ +public abstract class AbstractExportableDataContent implements ExportableDataContent { + /** + * The export mode (RAW, BASH_SCRIPT, CMD_SCRIPT, etc.). Defaults to RAW. + */ + protected ExportMode mode = ExportMode.RAW; + /** + * The input data to be exported. Must be non-null before use. + */ + protected DataContent input; + + /** + * Sets the input {@link DataContent} for export. + * + * @param input the input data to be exported + * @throws NullPointerException if {@code input} is {@code null} + */ + @Override + public void setInput(DataContent input) { + this.input = Objects.requireNonNull(input, "Input content cannot be null."); + } + + /** + * Returns the currently selected export mode. + * + * @return the {@link ExportMode} in use + */ + @Override + public ExportMode getExportMode() { + return mode; + } + + /** + * Sets the export mode to control how the data is output. + * + * @param mode the desired {@link ExportMode} + */ + @Override + public void setExportMode(ExportMode mode) { + this.mode = mode; + } +} diff --git a/lib/src/main/java/zeroecho/sdk/content/api/DataContent.java b/lib/src/main/java/zeroecho/sdk/content/api/DataContent.java new file mode 100644 index 0000000..b042bc6 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/api/DataContent.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * 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.sdk.content.api; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; + +/** + * Represents a generic unit of data content. This may include plain, secret, or + * encrypted forms of data. + */ +public interface DataContent { // NOPMD + /** + * Default maximum number of bytes that {@link #toBytes()} will read. This + * prevents accidental memory overload when reading large content into RAM. + */ + int DEFAULT_MAX_READ_SIZE = 32 * 1024 * 1024; // 32 MB + + /** + * Returns an {@link InputStream} representing the content's data. + *

+ * The stream may be raw or transformed depending on the content type. + * + * @return input stream of the content; never {@code null} + * @throws IOException if an I/O error occurs opening the stream + */ + InputStream getStream() throws IOException; + + /** + * Returns the content's data as a byte array, with a size limit of 16MB by + * default. + *

+ * This method is intended for small to medium-sized content. If the content + * exceeds {@link #DEFAULT_MAX_READ_SIZE}, a {@link IllegalStateException} is + * thrown. + * + * @return the content data as {@code byte[]} + * @throws RuntimeException if reading from the stream fails + * @throws IllegalStateException if content exceeds allowed memory limit + */ + default byte[] toBytes() { + try (InputStream in = getStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + in.transferTo(out); + return out.toByteArray(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read content stream", e); + } + } + + /** + * Returns the content's data as a UTF-8 encoded string. + *

+ * The default implementation converts {@code toBytes()} using UTF-8. + * Warning: This method is only safe if the content represents + * valid UTF-8 text. For arbitrary binary data (e.g., encrypted content), this + * may produce invalid or garbled output. + * + * @return the content data as {@code String} + * @throws RuntimeException if reading from the stream fails + * @throws IllegalArgumentException if the bytes are not valid UTF-8 (optional) + */ + default String toText() { + return new String(toBytes(), StandardCharsets.UTF_8); + } + + /** + * Connects this content to the output of a previous stage in the pipeline. + *

+ * This method is used to supply the input data that this content will process + * (e.g., a {@code PlainContent} being encrypted, or an encrypted content being + * decrypted). + *

+ * + * @param input the {@code DataContent} instance providing upstream data; may be + * {@code null} if this is the start of the pipeline + * @throws UnsupportedOperationException if not overridden by a subclass + * @throws IllegalArgumentException if the input is not allowed by the + * implementation + * + * @implSpec The default implementation of this method throws + * {@link UnsupportedOperationException}. Subclasses that support + * chaining must override this method. + */ + default void setInput(DataContent input) { + throw new UnsupportedOperationException("This content type does not accept input chaining."); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/content/api/EncryptedContent.java b/lib/src/main/java/zeroecho/sdk/content/api/EncryptedContent.java new file mode 100644 index 0000000..5ea6111 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/api/EncryptedContent.java @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.content.api; + +/** + * Represents encrypted content, which is the result of an encryption process + * and can be safely deployed to a public space without security concerns. + * Deployment methods may include saving to a file, writing to standard output, + * or using steganography for enhanced secrecy. + */ +public interface EncryptedContent extends DataContent { // NOPMD + +} diff --git a/lib/src/main/java/zeroecho/sdk/content/api/ExportableDataContent.java b/lib/src/main/java/zeroecho/sdk/content/api/ExportableDataContent.java new file mode 100644 index 0000000..d4e5cd7 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/api/ExportableDataContent.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.content.api; + +/** + * An extension of {@link DataContent} that provides export capabilities in + * different modes. + *

+ * This interface is intended for data sources that support not only raw binary + * streaming but also platform-specific script export, such as Bash or Windows + * CMD. The export mode determines how the underlying content is transformed or + * wrapped. + * + *

+ * Typical use cases include: + *

    + *
  • Uploading images or data directly to a remote server (RAW mode)
  • + *
  • Generating self-contained Bash scripts that embed the encoded data + * (BASH_SCRIPT mode)
  • + *
  • Generating CMD scripts for use on Windows systems (CMD_SCRIPT mode)
  • + *
+ * + *

+ * Implementations are responsible for adapting the content to the selected + * {@code ExportMode}. + * + * @see ExportableDataContent.ExportMode + * @see DataContent + */ +public interface ExportableDataContent extends DataContent { + /** + * Enumeration of supported export modes. + */ + enum ExportMode { + /** + * Raw mode returns the content as a direct InputStream, without any + * transformation or encoding. + */ + RAW, + /** + * Bash script mode returns a shell script with embedded Base64-encoded content, + * suitable for execution on Unix-like systems. + */ + BASH_SCRIPT, + /** + * CMD script mode returns a Windows batch file containing encoded content that + * can be decoded and used locally. + */ + CMD_SCRIPT + } + + /** + * Returns the current export mode. + * + * @return the export mode that determines how the content will be formatted or + * transformed + */ + ExportMode getExportMode(); + + /** + * Sets the desired export mode. + * + * @param mode the export mode to use; must not be {@code null} + * @throws NullPointerException if the mode is {@code null} + */ + void setExportMode(ExportMode mode); +} diff --git a/lib/src/main/java/zeroecho/sdk/content/api/PlainContent.java b/lib/src/main/java/zeroecho/sdk/content/api/PlainContent.java new file mode 100644 index 0000000..3da26c7 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/api/PlainContent.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.content.api; + +/** + * Represents a plain content that includes some original content, such as data + * from interactive input, a file, or the result of decrypting encrypted + * content. + */ +public interface PlainContent extends DataContent { // NOPMD + +} diff --git a/lib/src/main/java/zeroecho/sdk/content/api/SecretContent.java b/lib/src/main/java/zeroecho/sdk/content/api/SecretContent.java new file mode 100644 index 0000000..86a4fbb --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/api/SecretContent.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.content.api; + +/** + * Represents secret content, which is a specific type of plain content that + * stores a secret phrase. This phrase may be obtained from input, read from a + * file, or generated dynamically. + */ +public interface SecretContent extends PlainContent { // NOPMD + +} diff --git a/lib/src/main/java/zeroecho/sdk/content/api/package-info.java b/lib/src/main/java/zeroecho/sdk/content/api/package-info.java new file mode 100644 index 0000000..718f6a2 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/api/package-info.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Content abstractions for streaming data in the SDK. + * + *

+ * This package defines the common {@link DataContent} contract used by SDK + * pipelines, plus marker interfaces that classify content as plain or + * encrypted, and an extended interface for exporting content in alternate + * formats. Implementations expose data via {@link DataContent#getStream()} and + * may participate in pipelines by accepting upstream input with + * {@link DataContent#setInput(DataContent)}. + *

+ * + *

Key elements

+ *
    + *
  • {@link DataContent} - base interface representing a unit of content with + * stream access and convenience helpers for bytes and text.
  • + *
  • {@link PlainContent} - marker for content representing cleartext or + * decrypted data.
  • + *
  • {@link EncryptedContent} - marker for content that is the result of + * encryption and can be stored or transported in untrusted locations.
  • + *
  • {@link SecretContent} - specialization of plain content intended to carry + * secret phrases.
  • + *
  • {@link ExportableDataContent} - extension that can render content in + * different export modes via {@link ExportableDataContent.ExportMode} (for + * example, RAW, BASH_SCRIPT, CMD_SCRIPT). {@link AbstractExportableDataContent} + * provides a small base with input plumbing and mode management.
  • + *
+ * + *

Usage

+ *

+ * Content is consumed as a stream and must be closed by the caller. For small + * payloads, helpers like {@link DataContent#toBytes()} and + * {@link DataContent#toText()} can be convenient, but streaming should be + * preferred for large data. + *

+ *
{@code
+ * // Consume a DataContent instance and write it to an OutputStream.
+ * zeroecho.sdk.content.api.DataContent content = ...obtained from a builder or pipeline ...;
+ * try (java.io.InputStream in = content.getStream()) {
+ *     in.transferTo(out);
+ * }
+ * }
+ * + *

Notes

+ *
    + *
  • Implementations may or may not support + * {@link DataContent#setInput(DataContent)}; the default method throws + * {@link UnsupportedOperationException} to signal non-participation in + * chaining.
  • + *
  • Exportable content chooses its formatting according to + * {@link ExportableDataContent#getExportMode()}.
  • + *
  • Thread-safety is not guaranteed; consult concrete implementations.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.sdk.content.api; diff --git a/lib/src/main/java/zeroecho/sdk/content/builtin/PlainBytes.java b/lib/src/main/java/zeroecho/sdk/content/builtin/PlainBytes.java new file mode 100644 index 0000000..8428ec5 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/builtin/PlainBytes.java @@ -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.sdk.content.builtin; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * An implementation of {@link PlainContent} that encapsulates a byte array. + * + * Provides read-only access to the content through an {@link InputStream} + * backed by the internal byte buffer. + * + * This class represents the start of a {@link DataContent} processing chain, + * thus it does not accept input from preceding content. + * + * @author Leo Galambos + */ +public class PlainBytes implements PlainContent { + /** + * Logger instance for the {@code PlainBytes} class used to log runtime + * information, warnings, or errors. It is statically initialized with the class + * name to ensure consistent logging context throughout the class. + */ + private static final Logger LOG = Logger.getLogger(PlainBytes.class.getName()); + /** + * Byte array buffer that holds the raw data managed by this instance. + */ + protected final byte[] buffer; + + /** + * Constructs a new {@code PlainBytes} instance by copying the provided byte + * array. + * + * @param buffer the byte array to wrap + */ + public PlainBytes(final byte[] buffer) { + Objects.requireNonNull(buffer, "PlainBytes cannot operate with null buffer"); + + this.buffer = Arrays.copyOf(buffer, buffer.length); + } + + /** + * Constructs a new {@code PlainBytes} instance with a buffer of the specified + * length. + * + * @param length the size of the internal byte buffer to allocate + * @throws NegativeArraySizeException if {@code length} is negative + */ + protected PlainBytes(final int length) { + this.buffer = new byte[length]; + } + + /** + * {@inheritDoc} + * + * {@code PlainBytes} represents the start of a {@link DataContent} chain and + * therefore must not have any input. Calling this method with a + * non-{@code null} argument will result in an exception. + * + * + * @param input the preceding {@link DataContent}, which must be {@code null} + * @throws IllegalArgumentException if {@code input} is not {@code null} + */ + @Override + public void setInput(final DataContent input) { + if (input != null) { + throw new IllegalArgumentException( + getClass().getName() + " must be the first element in a DataContent chain; it cannot accept input"); + } + } + + /** + * Returns an {@link InputStream} for reading the byte array. + * + * @return a new {@link ByteArrayInputStream} + * @throws IOException if an I/O error occurs + */ + @Override + public InputStream getStream() throws IOException { + LOG.log(Level.INFO, "opening byte array for read, length={0}", buffer.length); + + return new ByteArrayInputStream(buffer); + } + + /** + * Returns a copy of the internal byte buffer. + * + * @return a new byte array containing the data from the internal buffer + */ + @Override + public byte[] toBytes() { + return Arrays.copyOf(buffer, buffer.length); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/content/builtin/PlainFile.java b/lib/src/main/java/zeroecho/sdk/content/builtin/PlainFile.java new file mode 100644 index 0000000..5a93300 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/builtin/PlainFile.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.content.builtin; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * A {@link PlainContent} implementation that reads content from a file-like + * URL. + */ +public final class PlainFile implements PlainContent { + /** + * Logger instance for the {@code PlainFile} class used for logging debug, + * informational, and error messages. Initialized with the class name to provide + * context-specific logging output. + */ + private static final Logger LOG = Logger.getLogger(PlainFile.class.getName()); + + /** + * URL representing the location of the file associated with this instance. + * + * This URL may point to a file on the local file system, a remote resource, or + * any other location accessible via the {@link java.net.URL} protocol. It is + * final and set during construction. + */ + private final URL location; + + /** + * Constructs a PlainFile from the specified URL. + * + * @param location the file URL; must not be null + */ + public PlainFile(final URL location) { + Objects.requireNonNull(location, "URL must not be null"); + + this.location = location; + } + + /** + * {@inheritDoc} + * + * {@code PlainFile} represents the start of a {@link DataContent} chain and + * therefore must not have any input. Calling this method with a + * non-{@code null} argument will result in an exception. + * + * @param input the preceding {@link DataContent}, which must be {@code null} + * @throws IllegalArgumentException if {@code input} is not {@code null} + */ + @Override + public void setInput(final DataContent input) { + if (input != null) { + throw new IllegalArgumentException( + getClass().getName() + " must be the first element in a DataContent chain; it cannot accept input"); + } + } + + /** + * Returns an {@link InputStream} for reading from the file. + * + * @return an {@link InputStream} from the URL + * @throws IOException if an I/O error occurs opening the stream + */ + @Override + public InputStream getStream() throws IOException { + LOG.log(Level.INFO, "opening {0}", location); + + return location.openStream(); + } + +} diff --git a/lib/src/main/java/zeroecho/sdk/content/builtin/PlainString.java b/lib/src/main/java/zeroecho/sdk/content/builtin/PlainString.java new file mode 100644 index 0000000..d15207b --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/builtin/PlainString.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.content.builtin; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * A {@link PlainContent} implementation that wraps a UTF-8 string. + * + * @author Leo Galambos + */ +public class PlainString implements PlainContent { + /** + * Logger instance for the {@code PlainString} class used to log informational, + * debug, or error messages related to the class’s operations. + * + * The logger is initialized with the name of the {@code PlainString} class, + * enabling targeted and organized logging output. + */ + private static final Logger LOG = Logger.getLogger(PlainString.class.getName()); + + /** + * The plain text content represented by this instance. + */ + protected String str; + + /** + * Constructs a PlainString with the given string. + * + * @param str the plain text content; must not be null + */ + public PlainString(final String str) { + Objects.requireNonNull(str, "plain string must not be null"); + + this.str = str; + } + + /** + * Returns the original string content. + * + * @return the string + */ + @Override + public String toText() { + return str; + } + + /** + * {@inheritDoc} + * + * {@code PlainString} represents the start of a {@link DataContent} chain and + * therefore must not have any input. Calling this method with a + * non-{@code null} argument will result in an exception. + * + * @param input the preceding {@link DataContent}, which must be {@code null} + * @throws IllegalArgumentException if {@code input} is not {@code null} + */ + @Override + public void setInput(final DataContent input) { + if (input != null) { + throw new IllegalArgumentException( + getClass().getName() + " must be the first element in a DataContent chain; it cannot accept input"); + } + } + + /** + * Returns an {@link InputStream} of the UTF-8 encoded string. + * + * @return a {@link ByteArrayInputStream} + */ + @Override + public InputStream getStream() { + LOG.log(Level.FINE, "opening \"{0}\"", str); + return new ByteArrayInputStream(toBytes()); + } + + /** + * Returns the UTF-8 encoded byte array of the string. + * + * @return a byte array representation + */ + @Override + public byte[] toBytes() { + return str.getBytes(StandardCharsets.UTF_8); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/content/builtin/SecretPassword.java b/lib/src/main/java/zeroecho/sdk/content/builtin/SecretPassword.java new file mode 100644 index 0000000..b765b45 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/builtin/SecretPassword.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.content.builtin; + +import conflux.Ctx; +import conflux.Key; +import zeroecho.sdk.content.api.SecretContent; +import zeroecho.sdk.util.Password; + +/** + * A {@link SecretContent} implementation that encapsulates a passwordKey + * string. This class extends {@link PlainString} and enforces immutability of + * the passwordKey after construction. + *

+ * Passwords can be generated randomly or provided explicitly. Once set, + * attempts to change the passwordKey via parameters will cause an exception. + *

+ * This class supports applying parameters from a map and collecting its state + * back into a map, using the key {@link #PASSWORD}. + * + * @author Leo Galambos + */ +public class SecretPassword extends PlainString implements SecretContent { + private final Key PASSWORD = Key.of("secret.password", String.class); + + /** + * Constructs a {@code SecretPassword} with a randomly generated printable + * passwordKey of the specified length. + * + * @param length the length of the generated passwordKey + * @throws IllegalArgumentException if {@code length} is less than or equal to + * zero + */ + public SecretPassword(final int length) { + super(Password.generatePrintablePassword(length)); + Ctx.INSTANCE.put(PASSWORD, str); + } + + /** + * Constructs a {@code SecretPassword} wrapping the specified passwordKey + * string. The passwordKey may be {@code null}. + * + * @param password the passwordKey string, or {@code null} + */ + public SecretPassword(final String password) { + super(password); + Ctx.INSTANCE.put(PASSWORD, str); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/content/builtin/package-info.java b/lib/src/main/java/zeroecho/sdk/content/builtin/package-info.java new file mode 100644 index 0000000..10864dc --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/builtin/package-info.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Built-in plain content sources: bytes, strings, files, and passwords. + * + *

+ * This package provides simple {@link zeroecho.sdk.content.api.DataContent} + * implementations that act as the first stage of a pipeline. They expose bytes + * via {@link zeroecho.sdk.content.api.DataContent#getStream()} and deliberately + * reject upstream input because they are sources, not transforms. + *

+ * + *

Components

+ *
    + *
  • {@link PlainBytes} - wraps an in-memory byte array and exposes it as a + * read-only stream. The provided array is defensively copied so later caller + * mutations do not affect the content.
  • + *
  • {@link PlainString} - wraps a UTF-8 string and exposes it as bytes on + * demand.
  • + *
  • {@link PlainFile} - reads content from a {@link java.net.URL} such as + * {@code file:} or {@code https:}.
  • + *
  • {@link SecretPassword} - specialization for secret phrases; extends + * {@link PlainString} and publishes the password into a process context for + * downstream consumers when constructed.
  • + *
+ * + *

Behavior

+ *
    + *
  • These classes start a data pipeline and therefore throw + * {@link IllegalArgumentException} if + * {@link zeroecho.sdk.content.api.DataContent#setInput(zeroecho.sdk.content.api.DataContent)} + * is called with a non-null argument.
  • + *
  • Callers must close the {@link java.io.InputStream} returned by + * {@link zeroecho.sdk.content.api.DataContent#getStream()}.
  • + *
  • {@link PlainBytes} returns a defensive copy from + * {@link zeroecho.sdk.content.api.DataContent#toBytes()} and + * {@link PlainString} encodes to UTF-8 when bytes are requested.
  • + *
+ * + *

Typical usage

{@code
+ * // Wrap a byte[] as a DataContent source and stream it to an OutputStream.
+ * zeroecho.sdk.content.api.DataContent src = new zeroecho.sdk.content.builtin.PlainBytes(data);
+ * try (java.io.InputStream in = src.getStream()) {
+ *     in.transferTo(out);
+ * }
+ *
+ * // Read from a file URL.
+ * java.net.URL url = java.nio.file.Path.of("input.bin").toUri().toURL();
+ * zeroecho.sdk.content.api.DataContent fileSrc = new zeroecho.sdk.content.builtin.PlainFile(url);
+ * try (java.io.InputStream in = fileSrc.getStream()) {
+ *     in.transferTo(out);
+ * }
+ *
+ * // Use a password source (also published to a shared context by the constructor).
+ * zeroecho.sdk.content.api.DataContent secret = new zeroecho.sdk.content.builtin.SecretPassword(24);
+ * }
+ * + * @since 1.0 + */ +package zeroecho.sdk.content.builtin; diff --git a/lib/src/main/java/zeroecho/sdk/content/export/Base64Stream.java b/lib/src/main/java/zeroecho/sdk/content/export/Base64Stream.java new file mode 100644 index 0000000..386efb0 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/export/Base64Stream.java @@ -0,0 +1,198 @@ +/******************************************************************************* + * 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.sdk.content.export; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Base64; +import java.util.Base64.Encoder; + +/** + * A streaming {@link InputStream} that encodes binary input data into Base64 + * format with optional line prefixes and suffixes for each encoded line. + * + *

+ * This class is designed for script-friendly output formatting, such as + * generating platform-specific batch or shell script lines where each line + * might begin with a command (e.g., {@code echo }) and end with a + * platform-specific line terminator (e.g., {@code \n} for UNIX-like systems or + * {@code \r\n} for Windows). + *

+ * + *

+ * It supports chunked streaming and avoids holding the entire Base64-encoded + * content in memory, making it suitable for large binary inputs. Lines are + * split according to the specified maximum line length and are composed as + * follows: + *

+ * + *
+ *     [prefix][base64-encoded data][suffix]
+ * 
+ * + *

+ * If a suffix is defined, the actual Base64 data per line will be truncated to + * {@code lineLength - suffix.length} characters to ensure total line length + * does not exceed {@code lineLength}. + *

+ * + *

+ * This stream does not automatically insert newlines between lines unless + * explicitly provided via the {@code suffix} argument (e.g., + * {@code "\n".getBytes()}). + *

+ * + *

+ * Usage example: + *

+ * + *
{@code
+ * InputStream source = new FileInputStream("input.bin");
+ * InputStream encoded = new Base64Stream(
+ *     source,
+ *     "echo ".getBytes(StandardCharsets.UTF_8),
+ *     76,
+ *     "\n".getBytes(StandardCharsets.UTF_8)
+ * );
+ * encoded.transferTo(System.out);
+ * }
+ * + * @author Leo Galambos + */ +public class Base64Stream extends InputStream { + + private final InputStream source; + private InputStream in; + private InputStream prefix; + private InputStream suffix; + private final Encoder base64; + private int pos; + private final int lineLength; + private int lineBreak; + + private InputStream is = nullInputStream(); // NOPMD + private int breakPos; // NOPMD + private boolean closed; + + private final static int TRIPLES_INPUT = 1000; + + /** + * Constructs a new {@code Base64Stream}. + * + * @param source the raw binary input stream to be Base64 encoded + * @param linePrefix optional prefix to prepend at the beginning of each line + * (e.g., {@code "echo "}); can be {@code null} for no prefix + * @param lineLength the total maximum length of each output line, including any + * prefix and suffix + * @param lineSuffix optional suffix to append at the end of each line (e.g., + * newline); can be {@code null} + */ + public Base64Stream(InputStream source, byte[] linePrefix, int lineLength, byte[] lineSuffix) { + super(); + + this.source = source; + this.lineLength = lineLength; + in = nullInputStream(); + base64 = Base64.getEncoder(); + if (linePrefix != null) { + prefix = new ByteArrayInputStream(linePrefix); + } + if (lineSuffix != null) { + suffix = new ByteArrayInputStream(lineSuffix); + } + lineBreak = lineLength - ((lineSuffix == null) ? 0 : lineSuffix.length); + } + + @Override + public int read() throws IOException { + byte[] result = { 0 }; + int count = read(result, 0, 1); + return (count == 0) ? -1 : result[0] & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (pos == lineLength) { + pos = 0; + } + + if (pos == 0 && prefix != null) { + prefix.reset(); + breakPos = Integer.MAX_VALUE; + is = prefix; + } else { + if (pos == lineBreak && suffix != null && !closed) { + suffix.reset(); + breakPos = Integer.MAX_VALUE; + is = suffix; + } else { + if (in.available() == 0) { + ensureData(); + } + breakPos = lineBreak; + is = in; + } + } + + int l = Math.min(Math.min(is.available(), len), breakPos - pos); + pos += l; + return is.read(b, off, l); + } + + /** + * Reads the next block of raw bytes from the source and encodes them into + * Base64. Handles stream exhaustion and final line suffix if applicable. + */ + private void ensureData() throws IOException { + byte[] buf = source.readNBytes(3 * TRIPLES_INPUT); + + if (buf.length == 0 && suffix != null && !closed) { + System.out.println("suffix"); + + closed = true; + suffix.reset(); + breakPos = lineBreak = Integer.MAX_VALUE; + in = suffix; + return; + } + + if (buf.length == 3 * TRIPLES_INPUT) { + in = new ByteArrayInputStream(base64.withoutPadding().encode(buf)); + } else { + in = new ByteArrayInputStream(base64.encode(buf)); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/content/export/PiwigoExportDataContent.java b/lib/src/main/java/zeroecho/sdk/content/export/PiwigoExportDataContent.java new file mode 100644 index 0000000..08f3d13 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/export/PiwigoExportDataContent.java @@ -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.sdk.content.export; + +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.SequenceInputStream; +import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +import zeroecho.sdk.content.api.AbstractExportableDataContent; + +/** + * {@code PiwigoExportDataContent} is a specialized exportable content class + * that supports uploading an image file to a Piwigo photo gallery server, + * either directly via HTTP POST or by generating platform-specific scripts for + * deferred uploading. + * + *

+ * Depending on the export mode (RAW, BASH_SCRIPT, CMD_SCRIPT), it can: + *

    + *
  • Upload the image to a Piwigo server directly using HTTP + * multipart/form-data
  • + *
  • Generate a Bash script that decodes the base64-encoded image and uploads + * it using curl
  • + *
  • Generate a CMD (Windows batch) script that reconstructs the image using + * certutil and uploads it
  • + *
+ * + *

+ * This class integrates with {@code AbstractExportableDataContent} and expects + * its {@code input} field to be set before invoking {@link #getStream()}. + *

+ * + *

+ * The image is uploaded using the {@code pwg.images.add} method of the Piwigo + * API. + *

+ */ +class PiwigoExportDataContent extends AbstractExportableDataContent { + private final String imageFileName; + private final String piwigoUrl; + private final String username; + private final String password; + private final String albumId; + + /** + * Constructs a new exportable Piwigo upload object. + * + * @param imageFileName the name of the image file to assign during upload or + * script output + * @param piwigoUrl the URL of the Piwigo API endpoint + * @param username the Piwigo username for authentication + * @param password the Piwigo password + * @param albumId the ID of the Piwigo album to which the image will be + * uploaded + */ + public PiwigoExportDataContent(String imageFileName, String piwigoUrl, String username, String password, + String albumId) { + super(); + + this.imageFileName = imageFileName; + this.piwigoUrl = piwigoUrl; + this.username = username; + this.password = password; + this.albumId = albumId; + } + + /** + * Returns an {@code InputStream} that provides either the raw upload stream, or + * a platform-specific script depending on the export mode. + * + * @return the resulting {@code InputStream} + * @throws IOException if reading the input or creating the stream fails + */ + @Override + public InputStream getStream() throws IOException { + if (input == null) { + throw new IllegalStateException("Input not set."); + } + + return switch (mode) { + case RAW -> performDirectUpload(input.getStream()); + case BASH_SCRIPT -> generateBashScript(input.getStream()); + case CMD_SCRIPT -> generateCmdScript(input.getStream()); + }; + } + + /** + * Performs a direct upload of the image data to the Piwigo server using a + * multipart/form-data HTTP POST request. + * + * @param dataStream the input stream of the binary image data + * @return the server's response stream + * @throws IOException if the upload fails + */ + private InputStream performDirectUpload(InputStream dataStream) throws IOException { + String boundary = "----Boundary" + System.currentTimeMillis(); + + URL url = URI.create(piwigoUrl).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + + try (OutputStream out = conn.getOutputStream(); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) { + + writeFormField(writer, boundary, "method", "pwg.images.add"); + writeFormField(writer, boundary, "username", username); + writeFormField(writer, boundary, "password", password); + writeFormField(writer, boundary, "category", albumId); + + writer.write("--" + boundary + "\r\n"); + writer.write("Content-Disposition: form-data; name=\"image\"; filename=\"" + imageFileName + "\"\r\n"); + writer.write("Content-Type: image/jpeg\r\n\r\n"); + writer.flush(); + + dataStream.transferTo(out); + out.flush(); + writer.write("\r\n--" + boundary + "--\r\n"); + writer.flush(); + } + + InputStream responseStream; + try { + responseStream = conn.getInputStream(); + } catch (IOException e) { + InputStream errorStream = conn.getErrorStream(); + if (errorStream != null) { + return errorStream; + } + return new ByteArrayInputStream(("Error: " + e.getMessage()).getBytes(StandardCharsets.UTF_8)); + } + + return responseStream; + } + + /** + * Writes a single form field as part of a multipart/form-data HTTP request. + * + * @param writer the writer to output the field to + * @param boundary the multipart boundary + * @param name the name of the form field + * @param value the value of the form field + * @throws IOException if writing fails + */ + private void writeFormField(Writer writer, String boundary, String name, String value) throws IOException { + writer.write("--" + boundary + "\r\n"); + writer.write("Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n"); + writer.write(value + "\r\n"); + } + + /** + * Generates a Bash script that reconstructs the image using a Base64 heredoc + * block and uploads it using {@code curl}. + * + * @param originalStream the original binary stream of the image + * @return a stream containing the complete shell script + */ + private InputStream generateBashScript(InputStream originalStream) { + InputStream header = new ByteArrayInputStream(("#!/bin/bash\nset -e\n\ncurl -X POST \"" + piwigoUrl + "\" \\\n" + + " -F method=\"pwg.images.add\" \\\n" + " -F username=\"" + username + "\" \\\n" + " -F password=\"" + + password + "\" \\\n" + " -F category=\"" + albumId + "\" \\\n" + " -F image=@<(base64 -d <<'EOF'\n") + .getBytes(StandardCharsets.UTF_8)); + + @SuppressWarnings("resource") + InputStream body = new Base64Stream(originalStream, null, 76, new byte[] { 10 }); + InputStream footer = new ByteArrayInputStream("EOF\n)\n".getBytes(StandardCharsets.UTF_8)); + + return new SequenceInputStream(new SequenceInputStream(header, body), footer); + } + + /** + * Generates a CMD batch script that reconstructs the image using certutil and + * uploads it using {@code curl}. + * + * @param originalStream the original binary stream of the image + * @return a stream containing the complete Windows batch script + */ + private InputStream generateCmdScript(InputStream originalStream) { + InputStream header = new ByteArrayInputStream( + "@echo off\nsetlocal\necho -----BEGIN BASE64----- > tmp.b64\n".getBytes(StandardCharsets.UTF_8)); + + @SuppressWarnings("resource") + InputStream body = new Base64Stream(originalStream, "echo ".getBytes(), 76, " >> tmp.b64\r\n".getBytes()); + InputStream footer = new ByteArrayInputStream( + ("echo -----END BASE64----- >> tmp.b64\n" + "certutil -decode tmp.b64 \"" + imageFileName + "\" >nul\n" + + "del tmp.b64\n" + "curl -X POST \"" + piwigoUrl + "\" ^\n" + " -F method=pwg.images.add ^\n" + + " -F username=" + username + " ^\n" + " -F password=" + password + " ^\n" + " -F category=" + + albumId + " ^\n" + " -F image=@" + imageFileName + "\n" + "del \"" + imageFileName + "\"\n") + .getBytes(StandardCharsets.UTF_8)); + + return new SequenceInputStream(new SequenceInputStream(header, body), footer); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/content/export/package-info.java b/lib/src/main/java/zeroecho/sdk/content/export/package-info.java new file mode 100644 index 0000000..b6edfcb --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/export/package-info.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Export helpers and platform deployers for SDK content. + * + *

+ * This package provides streaming utilities and exportable content + * implementations that render {@link zeroecho.sdk.content.api.DataContent} for + * deployment to external platforms or for script-based transport. Exports can + * be produced as raw bytes or as platform-specific scripts according to + * {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}. + *

+ * + *

Scope

+ *
    + *
  • Streaming utilities that reformat content for export-friendly forms (for + * example, Base64 with line control).
  • + *
  • Exportable content that uploads directly to a remote service or generates + * scripts for deferred execution.
  • + *
  • Foundations for additional deployers, including steganographic carriers + * and other public platforms beyond Piwigo.
  • + *
+ * + *

Key elements

+ *
    + *
  • {@link Base64Stream} - an {@link java.io.InputStream} that encodes a + * binary stream to Base64 with optional per-line prefix/suffix and configurable + * line length (useful for script emitters).
  • + *
  • Piwigo uploader - an exportable content implementation + * (package-private) that can either upload an image directly to a Piwigo server + * or generate Bash/CMD scripts that reconstruct and upload the image. It is + * built on {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and + * honors + * {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}.
  • + *
+ * + *

Typical usage

+ *

Format a stream as Base64 command lines

{@code
+ * java.io.InputStream source = ... raw bytes ...;
+ * java.io.InputStream encoded = new zeroecho.sdk.content.export.Base64Stream(
+ *     source,
+ *     "echo ".getBytes(java.nio.charset.StandardCharsets.UTF_8),
+ *     76,
+ *     "\n".getBytes(java.nio.charset.StandardCharsets.UTF_8)
+ * );
+ * encoded.transferTo(System.out);
+ * }
+ * + *

Render an exportable content in a chosen mode

{@code
+ * zeroecho.sdk.content.api.ExportableDataContent content = ... some exportable content ...;
+ * content.setExportMode(zeroecho.sdk.content.api.ExportableDataContent.ExportMode.BASH_SCRIPT);
+ * try (java.io.InputStream script = content.getStream()) {
+ *     script.transferTo(out);
+ * }
+ * }
+ * + *

Security notes

+ *
    + *
  • Prefer exporting {@link zeroecho.sdk.content.api.EncryptedContent} when + * targeting untrusted destinations.
  • + *
  • Avoid embedding secrets in scripts; pass credentials via environment + * variables or secure stores when possible.
  • + *
  • When adding steganographic exporters, document cover formats and + * concealment limits clearly.
  • + *
+ * + *

Extensibility

+ *
    + *
  • New deployers should extend + * {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and select a + * default {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode} + * appropriate for the platform.
  • + *
  • Utilities like {@link Base64Stream} can be reused to generate + * platform-friendly payloads without buffering whole files.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.sdk.content.export; diff --git a/lib/src/main/java/zeroecho/sdk/content/package-info.java b/lib/src/main/java/zeroecho/sdk/content/package-info.java new file mode 100644 index 0000000..aa10d0c --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/content/package-info.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Content abstractions and streaming payloads for the SDK. + * + *

+ * This package defines the common content model used by the SDK and organizes + * concrete sources and exporters under dedicated subpackages. Content objects + * expose streaming access to bytes and can be chained into pipelines produced + * by builder APIs. + *

+ * + *

Subpackages

+ *
    + *
  • {@link zeroecho.sdk.content.api} - core interfaces and bases, including + * {@link zeroecho.sdk.content.api.DataContent}, marker types such as + * {@link zeroecho.sdk.content.api.PlainContent} and + * {@link zeroecho.sdk.content.api.EncryptedContent}, and + * {@link zeroecho.sdk.content.api.ExportableDataContent} for alternate + * renderings.
  • + *
  • {@link zeroecho.sdk.content.builtin} - built-in plain sources: + * {@link zeroecho.sdk.content.builtin.PlainBytes}, + * {@link zeroecho.sdk.content.builtin.PlainString}, + * {@link zeroecho.sdk.content.builtin.PlainFile}, and + * {@link zeroecho.sdk.content.builtin.SecretPassword}.
  • + *
  • {@link zeroecho.sdk.content.export} - export helpers and platform + * deployers, such as {@link zeroecho.sdk.content.export.Base64Stream} and + * exportable content that targets external destinations (for example, gallery + * platforms) or script-based transports.
  • + *
+ * + *

Responsibilities

+ *
    + *
  • Provide a uniform, streaming surface through + * {@link zeroecho.sdk.content.api.DataContent#getStream()} for large-payload + * processing.
  • + *
  • Classify content as plain, secret, or encrypted using marker interfaces + * so pipelines and UIs can apply appropriate handling.
  • + *
  • Support exportable content that can render itself as raw bytes or + * platform-specific scripts via + * {@link zeroecho.sdk.content.api.ExportableDataContent}.
  • + *
+ * + *

Typical usage

{@code
+ * // Stream a DataContent instance to an OutputStream.
+ * zeroecho.sdk.content.api.DataContent content = ... obtained from a builder chain ...;
+ * try (java.io.InputStream in = content.getStream()) {
+ *     in.transferTo(out);
+ * }
+ *
+ * // If exportable, render in an alternate mode (for example, a shell script).
+ * if (content instanceof zeroecho.sdk.content.api.ExportableDataContent exportable) {
+ *     exportable.setExportMode(
+ *         zeroecho.sdk.content.api.ExportableDataContent.ExportMode.BASH_SCRIPT);
+ *     try (java.io.InputStream script = exportable.getStream()) {
+ *         script.transferTo(out);
+ *     }
+ * }
+ * }
+ * + *

Extensibility

+ *
    + *
  • New sources should implement {@link zeroecho.sdk.content.api.DataContent} + * and follow the streaming contract.
  • + *
  • New exporters should extend + * {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and honor the + * selected + * {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}.
  • + *
  • The export area is intended to host deployers for additional public + * platforms and, where appropriate, steganographic carriers for concealed + * delivery of encrypted streams.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.sdk.content; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/guard/Decryptor.java b/lib/src/main/java/zeroecho/sdk/guard/Decryptor.java new file mode 100644 index 0000000..256db80 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/Decryptor.java @@ -0,0 +1,180 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.guard; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.util.List; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.AEADBadTagException; +import javax.crypto.spec.SecretKeySpec; + +import zeroecho.core.io.Util; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * Decrypting stage that scans recipient entries to recover the CEK and then + * delegates to the symmetric builder. + */ +final class Decryptor implements PlainContent { + private static final Logger LOG = Logger.getLogger(Decryptor.class.getName()); + + private final List openers; + private final UnlockMaterial material; + private final AesDataContentBuilder aesBuilder; + private final ChaChaDataContentBuilder chachaBuilder; + private final int keyBytes; + private final int maxRecipients; + private final int maxEntryLen; + private DataContent upstream; + + /* package */ Decryptor(List openers, UnlockMaterial material, AesDataContentBuilder aesBuilder, + ChaChaDataContentBuilder chachaBuilder, int keyBytes, int maxRecipients, int maxEntryLen) { + this.openers = openers; + this.material = material; + this.aesBuilder = aesBuilder; + this.chachaBuilder = chachaBuilder; + this.keyBytes = keyBytes; + this.maxRecipients = maxRecipients; + this.maxEntryLen = maxEntryLen; + } + + /** + * Sets the upstream encrypted input to be decrypted. + * + * @param input the upstream encrypted content + * @throws NullPointerException if {@code input} is null + */ + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input); + } + + /** + * Returns a stream that yields the plaintext by first recovering the CEK from + * recipient entries and then delegating to the symmetric builder to process its + * own header and ciphertext. + * + * @return an input stream over the plaintext + * @throws IOException if header parsing fails, if CEK cannot be recovered, or + * if symmetric stage fails + */ + @Override + public InputStream getStream() throws IOException { // NOPMD + Objects.requireNonNull(upstream, "decrypt: missing input"); + InputStream in = upstream.getStream(); // NOPMD + + // 1) Read recipients + int count = Util.readPack7I(in); + if (count < 0 || count > maxRecipients) { + in.close(); + throw new IOException("invalid recipient count: " + count); + } + + String matchedId = null; + byte[] cek = null; + + for (int i = 0; i < count; i++) { + String id = Util.readUTF8(in, 256); // reasonable max id length + byte[] blob = Util.read(in, maxEntryLen); + + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "round {0} id {1} material {2}", new Object[] { i, id, material.description() }); // NOPMD + } + + for (RecipientOpener op : openers) { + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "testing {0}", op.getClass().getName()); + } + try { + byte[] maybe = op.tryOpen(id, blob, material); + if (maybe != null) { + if (maybe.length > keyBytes) { + if (LOG.isLoggable(Level.WARNING)) { + LOG.log(Level.WARNING, + "Suspicious material in field {0}: {1}/{2} finds the secret of length {3}, while {4} is a limit. Ignoring.", + new Object[] { i, id, op.toString(), maybe.length, keyBytes }); // NOPMD + } + } else { + cek = maybe; + matchedId = id; + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "*** match with {0} ***", op.getClass().getName()); + } + break; + } + } + } catch (AEADBadTagException ex) { + // wrong key/password for that entry, continue scanning + LOG.log(Level.FINE, "failed AEAD {0}", ex); + } catch (GeneralSecurityException | IOException | IllegalArgumentException ex) { + // entry not applicable to this opener/material; ignore and continue + LOG.log(Level.FINE, "failed {0}", ex); + } + } + if (cek != null) { + // Skip remaining recipient entries if any + for (int j = i + 1; j < count; j++) { + Util.readUTF8(in, 256); + Util.read(in, maxEntryLen); + } + break; + } + } + + if (cek == null) { + throw new IOException("unable to unlock CEK with provided material"); + } + + LOG.log(Level.INFO, "found={0}", matchedId); + + // 2) Build symmetric decrypt stage and feed the remaining stream + final DataContent symmetric; + if (aesBuilder != null) { + symmetric = aesBuilder.withKey(new SecretKeySpec(cek, "AES")).build(false); + } else { + symmetric = chachaBuilder.withKey(new SecretKeySpec(cek, "ChaCha20")).build(false); + } + symmetric.setInput(new TailDataContent(in)); + return symmetric.getStream(); + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/guard/EncCtxOpener.java b/lib/src/main/java/zeroecho/sdk/guard/EncCtxOpener.java new file mode 100644 index 0000000..459d780 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/EncCtxOpener.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * 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.sdk.guard; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.util.Objects; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.EncryptionContext; + +/** + * Opener that recognizes "CTX-ENC:<algId>" entries and attempts CEK + * recovery using an {@link EncryptionContext} in DECRYPT role. If constructed + * without a context, it derives one from the entryId and the supplied private + * key. + */ +public final class EncCtxOpener implements RecipientOpener { + private final EncryptionContext provided; // may be null + + /** Derive the context on demand from entryId and private key. */ + public EncCtxOpener() { + this.provided = null; + } + + /** + * Use the provided context once for the matching entry. The opener will consume + * and close this context when used. + */ + public EncCtxOpener(EncryptionContext ctx) { + this.provided = Objects.requireNonNull(ctx); + } + + @Override + public byte[] tryOpen(String entryId, byte[] blob, UnlockMaterial material) + throws IOException, GeneralSecurityException { + if (entryId == null || !entryId.startsWith("CTX-ENC:")) { + return null; // NOPMD + } + + final EncryptionContext dec; + if (provided != null) { + dec = provided; + } else { + if (!(material instanceof UnlockMaterial.Private)) { + return null; // NOPMD + } + final String algId = entryId.substring("CTX-ENC:".length()); + final java.security.PrivateKey priv = ((UnlockMaterial.Private) material).key(); + dec = CryptoAlgorithms.create(algId, KeyUsage.DECRYPT, priv); + } + + try (dec; + InputStream s = dec.attach(new ByteArrayInputStream(blob)); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + s.transferTo(out); + return out.toByteArray(); + } + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/guard/EncCtxRecipient.java b/lib/src/main/java/zeroecho/sdk/guard/EncCtxRecipient.java new file mode 100644 index 0000000..d3adbce --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/EncCtxRecipient.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * 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.sdk.guard; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import zeroecho.core.context.EncryptionContext; + +/** + * Recipient implementation that delegates content-encryption key (CEK) wrapping + * to a provided {@link EncryptionContext}. + * + *

+ * Unlike {@link KemCtxRecipient}, which derives a key-encryption key (KEK) via + * key encapsulation, this class directly uses an existing + * {@code EncryptionContext} to transform the CEK into a wrapped form. The CEK + * is streamed through the context, and the transformed output is returned as a + * recipient entry. + *

+ * + *

Thread-safety

This class is not thread-safe. Each instance binds to + * a single {@code EncryptionContext} and must not be reused concurrently. + * + * @since 1.0 + */ +public final class EncCtxRecipient implements Recipient { + private final EncryptionContext enc; + private final boolean decoy; + + /** + * Constructs a recipient that uses the given {@link EncryptionContext}. + * + * @param enc the encryption context used to wrap CEKs + */ + public EncCtxRecipient(EncryptionContext enc) { + this(enc, false); + } + + /** + * Constructs a recipient backed by the specified {@link EncryptionContext}. + * + *

+ * The provided encryption context is used to wrap content-encryption keys + * (CEKs) for this recipient. A recipient may also be marked as a decoy to + * obscure the number of real recipients in a multi-recipient envelope. + *

+ * + * @param enc the encryption context used to perform CEK wrapping; must not be + * {@code null} + * @param decoy {@code true} if this recipient is a decoy entry (fake recipient + * that cannot unwrap a CEK), {@code false} if it is a real + * recipient + */ + public EncCtxRecipient(EncryptionContext enc, boolean decoy) { + this.enc = enc; + this.decoy = decoy; + } + + /** + * Returns the identifier string for this recipient mechanism. + * + *

+ * The format is {@code "CTX-ENC:"} where {@code } is the + * canonical identifier of the underlying {@link EncryptionContext} algorithm. + *

+ * + * @return identifier string for this recipient type + */ + @Override + public String id() { + return "CTX-ENC:" + enc.algorithm().id(); + } + + /** + * Builds a recipient entry for the given content-encryption key (CEK). + * + *

+ * The CEK bytes are streamed through the attached {@link EncryptionContext}, + * and the encrypted result is returned. The resulting byte array represents the + * recipient entry that can later be processed for decryption with the + * corresponding context. + *

+ * + * @param cek the content-encryption key to wrap + * @return the wrapped CEK encoded as a recipient entry + * @throws IOException if streaming through the encryption context fails + */ + @Override + public byte[] buildRecipientEntry(byte[] cek) throws IOException { + ByteArrayInputStream in = new ByteArrayInputStream(cek); + try (enc; InputStream s = enc.attach(in); ByteArrayOutputStream out = new ByteArrayOutputStream()) { + s.transferTo(out); + return out.toByteArray(); + } + } + + /** + * Returns whether this recipient entry is marked as a decoy. + * + *

+ * A decoy recipient is syntactically valid but intentionally unusable for + * recovering the content-encryption key (CEK). Such entries are included to + * conceal the number and identity of real recipients within a multi-recipient + * envelope. + *

+ * + * @return {@code true} if this recipient is a decoy (fake recipient that cannot + * unwrap a CEK); {@code false} if it is a real recipient + */ + @Override + public boolean decoy() { + return decoy; + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/guard/Encryptor.java b/lib/src/main/java/zeroecho/sdk/guard/Encryptor.java new file mode 100644 index 0000000..ecbbe0d --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/Encryptor.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.guard; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.security.GeneralSecurityException; +import java.util.List; +import java.util.Objects; + +import javax.crypto.spec.SecretKeySpec; + +import zeroecho.core.io.Util; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.EncryptedContent; +import zeroecho.sdk.util.RandomSupport; + +/** + * Encrypting stage that emits the recipient table followed by the symmetric + * builder output. + */ +final class Encryptor implements EncryptedContent { + private final List recipients; + private final AesDataContentBuilder aesBuilder; + private final ChaChaDataContentBuilder chachaBuilder; + private final int keyBytes; + private final int maxRecipients; + private final int maxEntryLen; + private DataContent upstream; + + /* package */ Encryptor(List recipients, AesDataContentBuilder aesBuilder, + ChaChaDataContentBuilder chachaBuilder, int keyBytes, int maxRecipients, int maxEntryLen) { + this.recipients = recipients; + this.aesBuilder = aesBuilder; + this.chachaBuilder = chachaBuilder; + this.keyBytes = keyBytes; + this.maxRecipients = maxRecipients; + this.maxEntryLen = maxEntryLen; + } + + /** + * Sets the upstream input to be encrypted. + * + * @param input the upstream content + * @throws NullPointerException if {@code input} is null + */ + @Override + public void setInput(DataContent input) { + this.upstream = Objects.requireNonNull(input); + } + + /** + * Returns a stream that yields the recipient header followed by the symmetric + * payload stream. + * + * @return an input stream over the envelope and ciphertext + * @throws IOException if building recipient entries fails or if upstream is + * missing + */ + @Override + public InputStream getStream() throws IOException { + Objects.requireNonNull(upstream, "encrypt: missing input"); + + // 1) Generate CEK for payload + byte[] cek = RandomSupport.generateRandom(keyBytes); + byte[] cekDecoy = RandomSupport.generateRandom(keyBytes); + + // 2) Build recipient entries + if (recipients.size() > maxRecipients) { + throw new IOException("too many recipients: " + recipients.size()); + } + ByteArrayOutputStream header = new ByteArrayOutputStream(1024); + Util.writePack7I(header, recipients.size()); + for (Recipient r : recipients) { + String id = r.id(); + Util.writeUTF8(header, id); + try { + byte[] blob = r.buildRecipientEntry(r.decoy() ? cekDecoy : cek); + if (blob.length > maxEntryLen) { + throw new IOException("recipient entry too large: " + blob.length); + } + Util.write(header, blob); + } catch (GeneralSecurityException e) { + throw new IOException("recipient build failed for " + id, e); + } + } + + // 3) Configure symmetric stage (it owns its own header) + final DataContent symmetric; + if (aesBuilder != null) { + symmetric = aesBuilder.withKey(new SecretKeySpec(cek, "AES")).build(true); + } else { + symmetric = chachaBuilder.withKey(new SecretKeySpec(cek, "ChaCha20")).build(true); + } + symmetric.setInput(upstream); + + // 4) Return [recipient header] + [symmetric payload stream] + InputStream headerStream = new ByteArrayInputStream(header.toByteArray()); + InputStream payloadStream = symmetric.getStream(); + return new SequenceInputStream(headerStream, payloadStream); + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/guard/KemCtxOpener.java b/lib/src/main/java/zeroecho/sdk/guard/KemCtxOpener.java new file mode 100644 index 0000000..73d13d3 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/KemCtxOpener.java @@ -0,0 +1,115 @@ +/******************************************************************************* + * 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.sdk.guard; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Objects; + +import javax.crypto.AEADBadTagException; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.KemContext; +import zeroecho.core.io.Util; +import zeroecho.sdk.util.Kdf; + +/** + * Opener for "KEM:<algId>:GCM-WRAP" entries. If a context is provided, it + * is used directly; otherwise a context is derived from entryId and the + * supplied private key. + */ +public final class KemCtxOpener implements RecipientOpener { + private final KemContext provided; // may be null + + /** Derive the context on demand from entryId and private key. */ + public KemCtxOpener() { + this.provided = null; + } + + /** + * Use the provided context once for the matching entry. The opener will consume + * and close this context when used. + */ + public KemCtxOpener(KemContext kem) { + this.provided = Objects.requireNonNull(kem); + } + + @Override + public byte[] tryOpen(String entryId, byte[] entryBlob, UnlockMaterial material) + throws GeneralSecurityException, IOException { + if (entryId == null || !entryId.startsWith("KEM:")) { + return null; // NOPMD + } + + final int next = entryId.indexOf(':', 4); + if (next <= 4) { // NOPMD + return null; // NOPMD malformed id + } + final String kemId = entryId.substring(4, next); + + final KemContext kem; + if (provided != null) { + // Caller-supplied context (material not required) + kem = provided; + } else { + if (!(material instanceof UnlockMaterial.Private)) { + return null; // NOPMD + } + final java.security.PrivateKey prv = ((UnlockMaterial.Private) material).key(); + kem = CryptoAlgorithms.create(kemId, KeyUsage.DECAPSULATE, prv); + } + + try (KemContext c = kem; ByteArrayInputStream in = new ByteArrayInputStream(entryBlob)) { + byte[] kemCt = Util.read(in, 1 << 20); + byte[] salt = Util.read(in, 64); + byte[] wrapIv = Util.read(in, 64); + byte[] wrapped = Util.read(in, 1 << 16); + + byte[] ss = c.decapsulate(kemCt); + byte[] info = ("KDF:" + kem.algorithm().id()).getBytes(StandardCharsets.US_ASCII); + byte[] kek = Kdf.hkdfSha256(ss, salt, info, 32); + try { + return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); + } catch (AEADBadTagException e) { + // Sender could have used 128-bit KEK + kek = Kdf.hkdfSha256(ss, salt, "KEM:KEK".getBytes(StandardCharsets.UTF_8), 16); + return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); + } + } + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/guard/KemCtxRecipient.java b/lib/src/main/java/zeroecho/sdk/guard/KemCtxRecipient.java new file mode 100644 index 0000000..2a652f4 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/KemCtxRecipient.java @@ -0,0 +1,185 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.guard; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; + +import zeroecho.core.context.KemContext; +import zeroecho.core.context.KemContext.KemResult; +import zeroecho.core.io.Util; +import zeroecho.sdk.util.Kdf; +import zeroecho.sdk.util.RandomSupport; + +/** + * Recipient implementation that derives a key-encryption key (KEK) via a + * {@link KemContext} encapsulation and uses it to wrap a content-encryption key + * (CEK). + * + *

+ * The workflow performed by this class is: + *

+ *
    + *
  1. Encapsulate with the given {@link KemContext} to obtain a ciphertext and + * a shared secret.
  2. + *
  3. Derive a KEK from the shared secret using HKDF-SHA256 with a random salt + * and an {@code info} string bound to the algorithm identifier.
  4. + *
  5. Generate a fresh IV and wrap the CEK under AES-GCM with the derived + * KEK.
  6. + *
  7. Serialize the components (KEM ciphertext, salt, IV, wrapped CEK) into a + * single byte array suitable for inclusion in a recipient entry.
  8. + *
+ * + *

Thread-safety

This class is not thread-safe. Each instance binds to + * a single {@code KemContext} and must not be reused concurrently. + * + * @since 1.0 + */ +public final class KemCtxRecipient implements Recipient { + private final KemContext ctx; + private final int kekBytes; + private final int saltLen; + private final boolean decoy; + + /** + * Constructs a recipient that uses the given KEM context and parameters. + * + * @param ctx the KEM context providing encapsulation + * @param kekBytes number of bytes of KEK material to derive via HKDF + * @param saltLen length of the random salt to apply during HKDF + */ + public KemCtxRecipient(KemContext ctx, int kekBytes, int saltLen) { + this(ctx, kekBytes, saltLen, false); + } + + /** + * Constructs a recipient that wraps content-encryption keys (CEKs) using the + * given KEM context and HKDF-based key derivation parameters. + * + *

+ * The KEM context performs encapsulation to derive a shared secret, which is + * expanded with HKDF into key-encryption key (KEK) material. The KEK is then + * used to wrap the CEK for this recipient. A recipient may also be flagged as a + * decoy to obscure the actual number of usable recipients. + *

+ * + * @param ctx the KEM context responsible for performing encapsulation; + * must not be {@code null} + * @param kekBytes number of bytes of KEK material to derive via HKDF + * @param saltLen length in bytes of the random salt applied during HKDF + * @param decoy {@code true} if this recipient is a decoy (fake entry that + * cannot unwrap a CEK); {@code false} if it is a real recipient + */ + public KemCtxRecipient(KemContext ctx, int kekBytes, int saltLen, boolean decoy) { + this.ctx = ctx; + this.kekBytes = kekBytes; + this.saltLen = saltLen; + this.decoy = decoy; + } + + /** + * Returns the identifier string for this recipient mechanism. + * + *

+ * The format is {@code "KEM::GCM-WRAP"} where {@code } is taken + * from the underlying {@link KemContext} algorithm. + *

+ * + * @return identifier string for this recipient type + */ + @Override + public String id() { + return "KEM:" + ctx.algorithm().id() + ":GCM-WRAP"; + } + + /** + * Builds a recipient entry for the given content-encryption key (CEK). + * + *

+ * Encapsulates with the KEM context, derives a KEK, wraps the CEK, and + * serializes the structure into a binary entry. The returned value contains all + * material required for the recipient to later decapsulate and unwrap. + *

+ * + * @param cek the content-encryption key to be wrapped + * @return encoded recipient entry containing encapsulation, salt, IV, and + * wrapped CEK + * @throws GeneralSecurityException if encapsulation, KDF, or wrapping fails + * @throws IOException if an I/O error occurs during encoding + */ + @Override + public byte[] buildRecipientEntry(byte[] cek) throws GeneralSecurityException, IOException { + try (ctx) { + KemResult kr = ctx.encapsulate(); + byte[] kemCt = kr.ciphertext(); + byte[] ss = kr.sharedSecret(); + + byte[] salt = RandomSupport.generateRandom(saltLen); + byte[] info = ("KDF:" + ctx.algorithm().id()).getBytes(StandardCharsets.US_ASCII); + byte[] kek = Kdf.hkdfSha256(ss, salt, info, kekBytes); + + byte[] iv = RandomSupport.generateRandom(12); + byte[] wrapped = MultiRecipientDataSourceBuilder.aesGcmWrap(kek, iv, null, cek); + + ByteArrayOutputStream out = new ByteArrayOutputStream( + kemCt.length + salt.length + iv.length + wrapped.length + 8); + Util.write(out, kemCt); + Util.write(out, salt); + Util.write(out, iv); + Util.write(out, wrapped); + return out.toByteArray(); + } + } + + /** + * Returns whether this recipient entry is marked as a decoy. + * + *

+ * A decoy recipient is syntactically valid but intentionally unusable for + * recovering the content-encryption key (CEK). Such entries are included to + * conceal the number and identity of real recipients within a multi-recipient + * envelope. + *

+ * + * @return {@code true} if this recipient is a decoy (fake recipient that cannot + * unwrap a CEK); {@code false} if it is a real recipient + */ + @Override + public boolean decoy() { + return decoy; + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/guard/MultiRecipientDataSourceBuilder.java b/lib/src/main/java/zeroecho/sdk/guard/MultiRecipientDataSourceBuilder.java new file mode 100644 index 0000000..e5b10ec --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/MultiRecipientDataSourceBuilder.java @@ -0,0 +1,531 @@ +/******************************************************************************* + * 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.sdk.guard; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Provider; +import java.security.Security; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.crypto.Cipher; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.KemContext; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.content.api.DataContent; + +/** + * Builds a data content pipeline that supports multiple recipients and + * delegates payload encryption to AES or ChaCha builders. + * + *

+ * This builder is responsible only for the multi-recipient envelope: generating + * a content-encryption key (CEK), constructing recipient entries that can + * recover the same CEK, and scanning those entries on decrypt to recover it. + * Once the CEK is ready, the builder delegates the actual payload encryption or + * decryption to an injected symmetric data-content builder (AES or ChaCha), + * which is responsible for its own header and ciphertext framing. + *

+ * + *

+ * Envelope layout. The stream begins with a recipient table owned by + * this builder: + *

    + *
  • pack7 recipientCount
  • + *
  • for each recipient: UTF-8 entryId, then length-prefixed entryBlob
  • + *
  • then follows the symmetric builder's header and payload stream
  • + *
+ * The symmetric stage parses its own header just as in the KEM-oriented + * pipeline. + */ +public final class MultiRecipientDataSourceBuilder implements DataContentBuilder { + // ---- symmetric selection (mirrors KemDataContentBuilder style) ---- + private AesDataContentBuilder aesBuilder; + private ChaChaDataContentBuilder chachaBuilder; + + // ---- recipient scanning and construction ---- + private final List recipients = new ArrayList<>(); + private final List openers = new ArrayList<>(); + private UnlockMaterial unlockMaterial; + + // ---- limits and CEK sizing ---- + private int payloadKeyBytes = 32; // e.g., 32 for AES-256 or ChaCha20 + private int maxRecipients = 64; + private int maxEntryLen = 1 << 20; // up to 1 MiB per recipient entry + + /** + * Creates a new builder for constructing a multi-recipient data source. + * + *

+ * The returned builder is initialized with safe defaults: + *

+ *
    + *
  • Payload key length: 32 bytes (suitable for AES-256).
  • + *
  • Maximum recipients: 64.
  • + *
+ * + *

+ * Callers can customize these parameters before building the final data source. + *

+ * + * @return a fresh {@code MultiRecipientDataSourceBuilder} instance with default + * settings + */ + public static MultiRecipientDataSourceBuilder builder() { + return new MultiRecipientDataSourceBuilder(); + } + + /** + * Selects AES payload processing by installing an AES data content builder. + * + *

+ * The AES builder owns its parameters and header. This method clears any + * previously installed ChaCha builder. + *

+ * + * @param builder configured AES data content builder + * @return this builder + * @throws NullPointerException if {@code builder} is {@code null} + */ + public MultiRecipientDataSourceBuilder withAes(AesDataContentBuilder builder) { + this.aesBuilder = Objects.requireNonNull(builder, "aesBuilder"); + this.chachaBuilder = null; + return this; + } + + /** + * Randomly permutes the list of recipients using a default source of + * randomness. All permutations occur with approximately equal likelihood. + */ + public void shuffle() { + Collections.shuffle(recipients); + } + + /** + * Selects ChaCha20-Poly1305 payload processing by installing a ChaCha data + * content builder. + * + *

+ * The ChaCha builder owns its parameters and header. This method clears any + * previously installed AES builder. + *

+ * + * @param builder configured ChaCha data content builder + * @return this builder + * @throws NullPointerException if {@code builder} is {@code null} + */ + public MultiRecipientDataSourceBuilder withChaCha(ChaChaDataContentBuilder builder) { + this.chachaBuilder = Objects.requireNonNull(builder, "chachaBuilder"); + this.aesBuilder = null; + return this; + } + + /** + * Sets the CEK length in bytes that will be generated on encrypt and expected + * on decrypt. + * + *

+ * Typical values: 16 or 32 for AES, 32 for ChaCha20-Poly1305. + *

+ * + * @param keyBytes CEK length in bytes + * @return this builder + * @throws IllegalArgumentException if {@code keyBytes} is not positive + */ + public MultiRecipientDataSourceBuilder payloadKeyBytes(int keyBytes) { + if (keyBytes <= 0) { + throw new IllegalArgumentException("payloadKeyBytes must be > 0"); + } + this.payloadKeyBytes = keyBytes; + return this; + } + + /** + * Adds a password-based recipient that derives a KEK via PBKDF2(HMAC-SHA-256) + * and AES-GCM-wraps the CEK. + * + * @param password the password; the caller should clear the array after use + * @param iterations PBKDF2 iteration count + * @param saltLen salt length in bytes + * @param kekBytes derived KEK length in bytes (for example, 16 or 32) + * @return this builder + * @throws NullPointerException if {@code password} is {@code null} + */ + public MultiRecipientDataSourceBuilder addPasswordRecipient(char[] password, int iterations, int saltLen, + int kekBytes) { + this.recipients.add(new PasswordRecipient(password, iterations, saltLen, kekBytes)); + return this; + } + + /** + * Adds a universal recipient backed by a caller-supplied {@link KemContext}. + * + *

+ * The context is used once to {@link KemContext#encapsulate()} and then closed. + * The entry format and key wrapping are: + * {@code kemCt || hkdfSalt || wrapIv || gcmWrappedCEK}. The entry id is + * {@code "KEM::GCM-WRAP"} so that default openers still match. + *

+ * + * @param kem configured context for + * {@link zeroecho.core.KeyUsage#ENCAPSULATE} + * @param kekBytes derived KEK length in bytes (e.g., 16 or 32) + * @param saltLen HKDF salt length in bytes + * @return this builder + * @throws NullPointerException if {@code kem} is {@code null} + */ + public MultiRecipientDataSourceBuilder addRecipient(KemContext kem, int kekBytes, int saltLen) { + this.recipients.add(new KemCtxRecipient(Objects.requireNonNull(kem), kekBytes, saltLen)); + return this; + } + + /** + * Adds a universal recipient backed by a caller-supplied + * {@link EncryptionContext}. + * + *

+ * The context must be configured for {@link zeroecho.core.KeyUsage#ENCRYPT} + * with the appropriate asymmetric algorithm (e.g., RSA-OAEP, ElGamal, ECIES). + * The CEK is encrypted by streaming it through + * {@link EncryptionContext#attach(java.io.InputStream)}. The entry id is + * {@code "ENC:" + ctx.algorithm().id()}. + *

+ * + *

+ * The context is consumed once and then closed by the builder. + *

+ * + * @param ctx the encryption context configured for ENCRYPT + * @return this builder + * @throws NullPointerException if {@code ctx} is {@code null} + */ + public MultiRecipientDataSourceBuilder addRecipient(EncryptionContext ctx) { + this.recipients.add(new EncCtxRecipient(Objects.requireNonNull(ctx))); + return this; + } + + /** + * Adds a password-based decoy recipient that derives a KEK via + * PBKDF2(HMAC-SHA-256) and AES-GCM-wraps the CEK. + * + * @param password the password; the caller should clear the array after use + * @param iterations PBKDF2 iteration count + * @param saltLen salt length in bytes + * @param kekBytes derived KEK length in bytes (for example, 16 or 32) + * @return this builder + * @throws NullPointerException if {@code password} is {@code null} + */ + public MultiRecipientDataSourceBuilder addPasswordRecipientDecoy(char[] password, int iterations, int saltLen, + int kekBytes) { + this.recipients.add(new PasswordRecipient(password, iterations, saltLen, kekBytes, true)); + return this; + } + + /** + * Adds a universal decoy recipient backed by a caller-supplied + * {@link KemContext}. + * + *

+ * The context is used once to {@link KemContext#encapsulate()} and then closed. + * The entry format and key wrapping are: + * {@code kemCt || hkdfSalt || wrapIv || gcmWrappedCEK}. The entry id is + * {@code "KEM::GCM-WRAP"} so that default openers still match. + *

+ * + * @param kem configured context for + * {@link zeroecho.core.KeyUsage#ENCAPSULATE} + * @param kekBytes derived KEK length in bytes (e.g., 16 or 32) + * @param saltLen HKDF salt length in bytes + * @return this builder + * @throws NullPointerException if {@code kem} is {@code null} + */ + public MultiRecipientDataSourceBuilder addRecipientDecoy(KemContext kem, int kekBytes, int saltLen) { + this.recipients.add(new KemCtxRecipient(Objects.requireNonNull(kem), kekBytes, saltLen, true)); + return this; + } + + /** + * Adds a universal decoy recipient backed by a caller-supplied + * {@link EncryptionContext}. + * + *

+ * The context must be configured for {@link zeroecho.core.KeyUsage#ENCRYPT} + * with the appropriate asymmetric algorithm (e.g., RSA-OAEP, ElGamal, ECIES). + * The CEK is encrypted by streaming it through + * {@link EncryptionContext#attach(java.io.InputStream)}. The entry id is + * {@code "ENC:" + ctx.algorithm().id()}. + *

+ * + *

+ * The context is consumed once and then closed by the builder. + *

+ * + * @param ctx the encryption context configured for ENCRYPT + * @return this builder + * @throws NullPointerException if {@code ctx} is {@code null} + */ + public MultiRecipientDataSourceBuilder addRecipientDecoy(EncryptionContext ctx) { + this.recipients.add(new EncCtxRecipient(Objects.requireNonNull(ctx), true)); + return this; + } + + /** + * Supplies the unlocking material for decryption, either a private key or a + * password. + * + *

+ * Only a single unlocking material is used. During decryption each opener + * attempts to recover the CEK from the recipient entries using this material. + *

+ * + * @param material the unlocking material (private key or password) + * @return this builder + * @throws NullPointerException if {@code material} is {@code null} + */ + public MultiRecipientDataSourceBuilder unlockWith(UnlockMaterial material) { + this.unlockMaterial = Objects.requireNonNull(material); + return this; + } + + /** + * Adds a custom recipient opener used during decryption when scanning recipient + * entries. + * + *

+ * If no openers are added explicitly, the default set is installed during + * {@link #build(boolean)} and covers KEM, RSA-OAEP, ElGamal, and PBKDF2 + * password recipients. + *

+ * + * @param opener the opener to add + * @return this builder + * @throws NullPointerException if {@code opener} is {@code null} + */ + public MultiRecipientDataSourceBuilder addOpener(RecipientOpener opener) { + this.openers.add(Objects.requireNonNull(opener)); + return this; + } + + /** + * Adds a universal opener backed by a caller-supplied {@link KemContext}. + * + *

+ * The opener matches entries whose id starts with {@code "KEM:"} and whose + * algorithm id equals {@code kem.algorithm().id()}. When a match is attempted, + * the context is used to {@link KemContext#decapsulate(byte[])} and then + * closed. + *

+ * + * @param kem context configured for {@link zeroecho.core.KeyUsage#DECAPSULATE} + * @return this builder + * @throws NullPointerException if {@code kem} is {@code null} + */ + public MultiRecipientDataSourceBuilder addOpener(KemContext kem) { + this.openers.add(new KemCtxOpener(Objects.requireNonNull(kem))); + return this; + } + + /** + * Adds a universal opener backed by a caller-supplied + * {@link EncryptionContext}. + * + *

+ * The opener matches entries whose id equals + * {@code "ENC:" + ctx.algorithm().id()}. When a match is attempted, the CEK + * ciphertext blob is streamed through + * {@link EncryptionContext#attach(java.io.InputStream)} to obtain the plaintext + * CEK. The context is then closed. + *

+ * + * @param ctx context configured for {@link zeroecho.core.KeyUsage#DECRYPT} + * @return this builder + * @throws NullPointerException if {@code ctx} is {@code null} + */ + public MultiRecipientDataSourceBuilder addOpener(EncryptionContext ctx) { + this.openers.add(new EncCtxOpener(Objects.requireNonNull(ctx))); + return this; + } + + /** + * Applies defensive limits for parsing the recipient table. + * + *

+ * All values are clamped to at least 1. These limits guard against resource + * exhaustion from malformed input. + *

+ * + * @param maxRecipients maximum number of recipient entries allowed + * @param maxEntryLen maximum size in bytes for a single recipient entry blob + * @return this builder + */ + public MultiRecipientDataSourceBuilder headerLimits(int maxRecipients, int maxEntryLen) { + this.maxRecipients = Math.max(1, maxRecipients); + this.maxEntryLen = Math.max(1, maxEntryLen); + return this; + } + + /** + * Builds either an encrypting or a decrypting content wrapper depending on the + * {@code encrypt} flag. + * + *

+ * When {@code encrypt} is {@code true}, the wrapper emits the recipient table + * followed by the symmetric builder's header and ciphertext. When + * {@code encrypt} is {@code false}, the wrapper unlocks the CEK from the first + * matching recipient entry and streams the symmetric plaintext. + *

+ * + *

+ * Default openers are installed automatically if none were added explicitly. + * They support KEM, RSA-OAEP, ElGamal with PKCS1 padding, and PBKDF2 password + * recipients. + *

+ * + * @param encrypt {@code true} to build an encrypting content source, + * {@code false} to build a decrypting one + * @return an encrypting or decrypting {@link DataContent} wrapper + * @throws IllegalStateException if no symmetric builder is selected, if + * encryption is requested with no recipients, or + * if decryption is requested without unlock + * material + */ + @Override + public DataContent build(boolean encrypt) { + if (aesBuilder == null && chachaBuilder == null) { + throw new IllegalStateException("No symmetric builder selected. Call withAes(...) or withChaCha(...)."); + } + if (encrypt && recipients.isEmpty()) { + throw new IllegalStateException("No recipients configured for encryption"); + } + if (!encrypt && unlockMaterial == null) { + throw new IllegalStateException("Missing unlock material for decryption"); + } + if (openers.isEmpty()) { + openers.add(new EncCtxOpener()); + openers.add(new KemCtxOpener()); + openers.add(new PasswordOpener()); + } + return encrypt + ? new Encryptor(recipients, aesBuilder, chachaBuilder, payloadKeyBytes, maxRecipients, maxEntryLen) + : new Decryptor(openers, unlockMaterial, aesBuilder, chachaBuilder, payloadKeyBytes, maxRecipients, + maxEntryLen); + } + + // ======================================================================== + // Helpers + // ======================================================================== + + private static Cipher cipherPreferJdk(String transformation) throws GeneralSecurityException { + Provider jdk = Security.getProvider("SunJCE"); + if (jdk != null) { + try { + return Cipher.getInstance(transformation, jdk); + } catch (GeneralSecurityException ignore) { // NOPMD + /* fall through */ + } + } + return Cipher.getInstance(transformation); + } + + /** + * PBKDF2 with HMAC-SHA-256 key derivation. + * + * @param password password characters + * @param salt salt bytes + * @param iterations iteration count + * @param outLen output length in bytes + * @return derived key material + * @throws GeneralSecurityException if a provider cannot compute PBKDF2 + */ + /* default */ static byte[] pbkdf2HmacSha256(char[] password, byte[] salt, int iterations, int outLen) + throws GeneralSecurityException { + PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, outLen * 8); + return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded(); + } + + /** + * AES-GCM key wrap that returns {@code ciphertext||tag}; IV and optional AAD + * are provided as parameters. + * + * @param kek key-encryption key + * @param iv 12-byte IV + * @param aad optional additional authenticated data or null + * @param plaintext CEK to wrap + * @return wrapped bytes of CEK + * @throws GeneralSecurityException if AES-GCM fails + * @throws IOException never in the current implementation + */ + /* default */ static byte[] aesGcmWrap(byte[] kek, byte[] iv, byte[] aad, byte[] plaintext) + throws GeneralSecurityException, IOException { + Cipher c = cipherPreferJdk("AES/GCM/NOPADDING"); + c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(kek, "AES"), new GCMParameterSpec(128, iv)); + if (aad != null && aad.length > 0) { + c.updateAAD(aad); + } + return c.doFinal(plaintext); + } + + /** + * AES-GCM key unwrap that authenticates and returns the plaintext CEK. + * + * @param kek key-encryption key + * @param iv 12-byte IV + * @param aad optional additional authenticated data or null + * @param ciphertext wrapped bytes of CEK (ciphertext||tag) + * @return unwrapped CEK + * @throws GeneralSecurityException if AES-GCM authentication or decryption + * fails + * @throws IOException never in the current implementation + */ + /* default */ static byte[] aesGcmUnwrap(byte[] kek, byte[] iv, byte[] aad, byte[] ciphertext) + throws GeneralSecurityException, IOException { + Cipher c = cipherPreferJdk("AES/GCM/NOPADDING"); + c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(kek, "AES"), new GCMParameterSpec(128, iv)); + if (aad != null && aad.length > 0) { + c.updateAAD(aad); + } + return c.doFinal(ciphertext); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/guard/PasswordOpener.java b/lib/src/main/java/zeroecho/sdk/guard/PasswordOpener.java new file mode 100644 index 0000000..a796b9f --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/PasswordOpener.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.guard; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; + +import javax.crypto.AEADBadTagException; + +import zeroecho.core.io.Util; + +/** + * Recipient opener for password-based entries that derives a KEK via PBKDF2 and + * unwraps the CEK with AES-GCM. + */ +public final class PasswordOpener implements RecipientOpener { + /** + * Attempts to open a password-based recipient entry using a password unlock + * material. + * + * @param entryId the recipient entry identifier + * @param entryBlob the recipient entry blob encoding + * {@code iter || salt || iv || wrappedCEK} + * @param material the unlocking material; must contain a password + * @return the recovered CEK or {@code null} if {@code entryId} does not match + * the password scheme or material is not applicable + * @throws GeneralSecurityException if PBKDF2 or AES-GCM fails or if + * authentication fails after a match + * @throws IOException if entry decoding fails + */ + @Override + public byte[] tryOpen(String entryId, byte[] entryBlob, UnlockMaterial material) + throws GeneralSecurityException, IOException { + if (!"PWD:PBKDF2-SHA256:GCM-WRAP".equals(entryId)) { + return null; // NOPMD + } + if (!(material instanceof UnlockMaterial.Password)) { + return null; // NOPMD + } + + ByteArrayInputStream in = new ByteArrayInputStream(entryBlob); + int iterations = Util.readPack7I(in); + byte[] salt = Util.read(in, 64); + byte[] wrapIv = Util.read(in, 64); + byte[] wrapped = Util.read(in, 1 << 16); + + char[] password = ((UnlockMaterial.Password) material).password(); + byte[] kek = MultiRecipientDataSourceBuilder.pbkdf2HmacSha256(password, salt, iterations, 32); // try 256-bit + try { + return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); + } catch (AEADBadTagException e256) { + kek = MultiRecipientDataSourceBuilder.pbkdf2HmacSha256(password, salt, iterations, 16); // fall back to 128-bit + return MultiRecipientDataSourceBuilder.aesGcmUnwrap(kek, wrapIv, null, wrapped); + } + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/guard/PasswordRecipient.java b/lib/src/main/java/zeroecho/sdk/guard/PasswordRecipient.java new file mode 100644 index 0000000..b29b154 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/PasswordRecipient.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * 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.sdk.guard; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Objects; + +import zeroecho.core.io.Util; +import zeroecho.sdk.util.RandomSupport; + +/** + * Password recipient that derives a KEK via PBKDF2(HMAC-SHA-256) and wraps the + * CEK with AES-GCM. + */ +public final class PasswordRecipient implements Recipient { + private final char[] password; + private final int iterations; + private final int saltLen; + private final int kekBytes; + private final boolean decoy; + + /** + * Creates a password-based recipient. + * + * @param password the password; the caller should clear the array after use + * @param iterations PBKDF2 iteration count + * @param saltLen salt length in bytes + * @param kekBytes derived KEK length in bytes + * @throws NullPointerException if {@code password} is null + */ + public PasswordRecipient(char[] password, int iterations, int saltLen, int kekBytes) { + this(password, iterations, saltLen, kekBytes, false); + } + + /** + * Creates a password-based recipient that derives a key-encryption key (KEK) + * using PBKDF2. + * + *

+ * The supplied password is expanded with PBKDF2 using the provided iteration + * count and a randomly generated salt of the specified length. The result is a + * KEK of the requested size in bytes, which is then used to wrap the + * content-encryption key (CEK) for this recipient. A recipient may also be + * marked as a decoy to obscure the actual number of usable recipients. + *

+ * + *

Security considerations

+ *
    + *
  • The caller should clear the {@code password} array after constructing the + * recipient to minimize exposure in memory.
  • + *
  • Choose an iteration count appropriate to the target platform to resist + * brute-force attacks.
  • + *
  • Decoy recipients increase confidentiality by hiding the number of real + * recipients but cannot successfully unwrap the CEK.
  • + *
+ * + * @param password the password used as input to PBKDF2; must not be + * {@code null} + * @param iterations the PBKDF2 iteration count + * @param saltLen length of the random salt (in bytes) + * @param kekBytes desired length of the derived KEK (in bytes) + * @param decoy {@code true} if this is a decoy recipient (fake entry that + * cannot unwrap a CEK); {@code false} if it is a real + * recipient + * @throws NullPointerException if {@code password} is {@code null} + */ + public PasswordRecipient(char[] password, int iterations, int saltLen, int kekBytes, boolean decoy) { + this.password = Objects.requireNonNull(password); + this.iterations = iterations; + this.saltLen = saltLen; + this.kekBytes = kekBytes; + this.decoy = decoy; + } + + /** + * Returns the recipient entry identifier used in the header. + * + * @return {@code "PWD:PBKDF2-SHA256:GCM-WRAP"} + */ + @Override + public String id() { + return "PWD:PBKDF2-SHA256:GCM-WRAP"; + } + + /** + * Builds the recipient entry blob for this password recipient. + * + * @param cek the CEK to wrap + * @return a blob containing {@code pack7(iter) || salt || wrapIv || wrappedCEK} + * @throws GeneralSecurityException if PBKDF2 or AES-GCM fails + * @throws IOException if blob assembly fails + */ + @Override + public byte[] buildRecipientEntry(byte[] cek) throws GeneralSecurityException, IOException { + byte[] salt = RandomSupport.generateRandom(saltLen); + byte[] kek = MultiRecipientDataSourceBuilder.pbkdf2HmacSha256(password, salt, iterations, kekBytes); + byte[] wrapIv = RandomSupport.generateRandom(12); + byte[] wrapped = MultiRecipientDataSourceBuilder.aesGcmWrap(kek, wrapIv, null, cek); + + ByteArrayOutputStream out = new ByteArrayOutputStream(64 + wrapped.length); + Util.writePack7I(out, iterations); + Util.write(out, salt); + Util.write(out, wrapIv); + Util.write(out, wrapped); + return out.toByteArray(); + } + + /** + * Returns whether this recipient entry is marked as a decoy. + * + *

+ * A decoy recipient is syntactically valid but intentionally unusable for + * recovering the content-encryption key (CEK). Such entries are included to + * conceal the number and identity of real recipients within a multi-recipient + * envelope. + *

+ * + * @return {@code true} if this recipient is a decoy (fake recipient that cannot + * unwrap a CEK); {@code false} if it is a real recipient + */ + @Override + public boolean decoy() { + return decoy; + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/guard/Recipient.java b/lib/src/main/java/zeroecho/sdk/guard/Recipient.java new file mode 100644 index 0000000..1838057 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/Recipient.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * 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.sdk.guard; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +/** + * Recipient describes how to encode a single recipient entry that can recover a + * content-encryption key (CEK) during decryption. + * + *

Responsibilities

Implementations provide a stable identifier and + * produce an opaque binary blob that contains everything the matching + * {@code RecipientOpener} needs to recover the CEK (for example, KEM + * ciphertext, salts, nonces, or wrapped CEK bytes). The multi-recipient + * envelope writes the identifier and a length-prefixed copy of the blob into + * its header. + * + *

+ * Typical usage: + *

+ *
{@code
+ * // RSA-OAEP recipient; the header will contain the id and the RSA-wrapped CEK
+ * Recipient r = new MultiRecipientDataSourceBuilder.RsaOaepRecipient(rsaPublicKey);
+ * byte[] entryBlob = r.buildRecipientEntry(cek);
+ * }
+ */ +public interface Recipient { + /** + * Indicates whether this recipient entry is a decoy (fake recipient). + * + *

+ * A decoy recipient is included in the envelope to obscure the actual number + * and identity of real recipients. Its entry is syntactically valid but + * intentionally unusable for recovering the content-encryption key (CEK). + *

+ * + *

Security considerations

+ *
    + *
  • Decoys prevent traffic analysis from revealing how many legitimate + * recipients are present.
  • + *
  • They are indistinguishable from real recipients to outsiders until a + * matching {@code RecipientOpener} attempts decryption and fails.
  • + *
  • Applications should balance the number of decoys against performance and + * bandwidth overhead.
  • + *
+ * + * @return {@code true} if this entry is a decoy and cannot be used to recover + * the CEK; {@code false} if it is a real recipient entry + */ + boolean decoy(); + + /** + * Returns a stable, self-describing identifier for this recipient entry. + * + *

+ * The identifier is written as UTF-8 into the envelope header and allows fast + * routing to a suitable {@code RecipientOpener} (for example, + * {@code "RSA:OAEP-SHA256"}, {@code "KEM:ML-KEM:GCM-WRAP"}). When the envelope + * is configured to suppress labels for hardening, it may write an empty string + * in place of this value; implementations should still return their normal + * identifier here. + *

+ * + * @return non-null identifier string used in the envelope header; may be empty + * only when the caller chooses to suppress labels + */ + String id(); + + /** + * Builds the binary recipient entry for this recipient and the provided CEK. + * + *

+ * The returned blob is an opaque byte array that the matching + * {@code RecipientOpener} can parse and use to recover the CEK. Implementations + * must not modify or retain the {@code cek} array; copy it if needed. The + * multi-recipient envelope is responsible for framing and length-prefixing, so + * the blob itself must not include additional length headers. + *

+ * + * @param cek the content-encryption key to protect for this recipient; never + * null + * @return an opaque blob encoding the data required to recover {@code cek} for + * this recipient + * @throws GeneralSecurityException if a cryptographic operation fails (for + * example, KEM, RSA-OAEP, or AEAD wrap) + * @throws IOException if serialization of the entry blob fails + */ + byte[] buildRecipientEntry(byte[] cek) throws GeneralSecurityException, IOException; +} diff --git a/lib/src/main/java/zeroecho/sdk/guard/RecipientOpener.java b/lib/src/main/java/zeroecho/sdk/guard/RecipientOpener.java new file mode 100644 index 0000000..35c328c --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/RecipientOpener.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * 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.sdk.guard; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +/** + * RecipientOpener attempts to recover a content-encryption key (CEK) from a + * single recipient entry during decryption. + * + *

Behavior

Implementations examine the envelope header fields for one + * recipient, optionally use the {@code entryId} as a hint, and try to derive or + * unwrap the CEK from the opaque {@code entryBlob} using the provided + * {@code UnlockMaterial}. In hardened configurations the {@code entryId} may be + * an empty string; openers should still attempt a best-effort decode based on + * the blob format and the supplied material. + * + *

+ * Return and failure semantics: + *

+ *
    + *
  • Return a non-null byte array containing the CEK on success.
  • + *
  • Return {@code null} if this opener is clearly not applicable to the entry + * (for example, mismatched id or unsupported blob format).
  • + *
  • Throw {@link GeneralSecurityException} when the opener considers the + * entry applicable but the cryptographic attempt fails (for example, + * authentication failure, padding error, malformed ciphertext, or key + * mismatch).
  • + *
  • Throw {@link IOException} for structural or I/O errors while parsing the + * blob (for example, truncated data).
  • + *
+ * + *

+ * Implementation notes: + *

+ *
    + *
  • Openers are expected to be stateless and reusable; they must not retain + * references to {@code UnlockMaterial} or the returned CEK.
  • + *
  • Do not log sensitive values, and avoid timing-sensitive distinctions + * beyond what the caller needs to continue scanning other entries.
  • + *
+ * + *

+ * Typical scan loop: + *

+ *
{@code
+ * for (RecipientOpener opener : openers) {
+ *     try {
+ *         byte[] cek = opener.tryOpen(entryId, entryBlob, material);
+ *         if (cek != null) {
+ *             // success; use CEK and stop scanning
+ *             break;
+ *         }
+ *     } catch (javax.crypto.AEADBadTagException wrongKeyOrPassword) {
+ *         // applicable but authentication failed; continue scanning others
+ *     } catch (GeneralSecurityException ignore) {
+ *         // not applicable or other crypto failure; continue scanning
+ *     }
+ * }
+ * }
+ */ +interface RecipientOpener { // NOPMD + /** + * Tries to recover the content-encryption key from the supplied recipient + * entry. + * + * @param entryId identifier string written by the sender; may be empty when + * labels are suppressed; used only as a routing hint + * @param entryBlob opaque bytes of the recipient entry produced by the matching + * {@code Recipient}; never null + * @param material unlocking material to use (for example, a private key or a + * password); never null + * @return a newly allocated byte array containing the recovered CEK, or + * {@code null} if this opener is not applicable + * @throws GeneralSecurityException if the entry appears applicable but + * cryptographic verification or unwrapping + * fails + * @throws IOException if parsing the entry blob fails due to + * structural errors or truncated data + */ + byte[] tryOpen(String entryId, byte[] entryBlob, UnlockMaterial material) + throws GeneralSecurityException, IOException; +} diff --git a/lib/src/main/java/zeroecho/sdk/guard/TailDataContent.java b/lib/src/main/java/zeroecho/sdk/guard/TailDataContent.java new file mode 100644 index 0000000..7640741 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/TailDataContent.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.guard; + +import java.io.IOException; +import java.io.InputStream; + +import zeroecho.sdk.content.api.DataContent; + +/** + * Simple passthrough data content for feeding a partially consumed stream into + * the symmetric stage. + */ +final class TailDataContent implements DataContent { + private final InputStream stream; + + /* package */ TailDataContent(InputStream stream) { + this.stream = stream; + } + + @Override + public InputStream getStream() throws IOException { + return stream; + } +} \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/guard/UnlockMaterial.java b/lib/src/main/java/zeroecho/sdk/guard/UnlockMaterial.java new file mode 100644 index 0000000..40a090f --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/UnlockMaterial.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * 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.sdk.guard; + +import zeroecho.core.annotation.Describable; + +/** + * UnlockMaterial represents the unlocking data supplied to a recipient opener + * to recover a content-encryption key (CEK). + * + *

Overview

The multi-recipient envelope scans recipient entries and + * delegates each attempt to a {@code RecipientOpener}. An opener may require + * either a private key or a password to unwrap or derive the CEK. This sealed + * interface defines the two supported kinds of unlocking material and + * centralizes their lifetime and handling. + * + *

Usage

{@code
+ * // Decrypt with a private key
+ * DataContent decRsa = new MultiRecipientDataSourceBuilder()
+ *     .withAes(AesDataContentBuilder.builder().modeGcm(128).withHeader())
+ *     .payloadKeyBytes(32)
+ *     .unlockWith(new UnlockMaterial.Private(rsaPrivateKey))
+ *     .build(false);
+ *
+ * // Decrypt with a password
+ * char[] pwd = "correct horse battery staple".toCharArray();
+ * DataContent decPwd = new MultiRecipientDataSourceBuilder()
+ *     .withAes(AesDataContentBuilder.builder().modeCbcPkcs7().withHeader())
+ *     .payloadKeyBytes(32)
+ *     .unlockWith(new UnlockMaterial.Password(pwd))
+ *     .build(false);
+ * // Clear the password when no longer needed
+ * java.util.Arrays.fill(pwd, '\0');
+ * }
+ * + *

Security notes

+ *
    + *
  • {@link Password} stores a reference to the caller-provided {@code char[]} + * for performance and zeroization. The caller is responsible for clearing the + * array after use.
  • + *
  • {@link Private} holds a {@link java.security.PrivateKey}. Manage the + * key's lifetime outside the opener and avoid logging or copying it + * unnecessarily.
  • + *
+ */ +sealed public interface UnlockMaterial extends Describable { + /** + * Private holds a private key used to decrypt or decapsulate a recipient entry. + * + *

+ * Typical uses include RSA-OAEP decryption, ElGamal decryption, or KEM + * decapsulation with a private KEM key. + *

+ * + * @param key the private key used by a matching {@code RecipientOpener}; must + * not be null + */ + record Private(java.security.PrivateKey key) implements UnlockMaterial, Describable { + + @Override + public String description() { + return "Unlock via key of " + key.getAlgorithm(); + } + } + + /** + * Password holds characters used to derive a key-encryption key (KEK) for + * unwrapping the CEK. + * + *

+ * The array reference is stored as provided to allow the caller to clear it + * after use. If defensive copying is desired, the caller should provide a copy + * and clear both copies after decryption. + *

+ * + * @param password the password characters; the caller should clear the array + * when no longer needed + */ + record Password(char[] password) implements UnlockMaterial, Describable { + + @Override + public String description() { + return "Unlock via password"; + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/guard/package-info.java b/lib/src/main/java/zeroecho/sdk/guard/package-info.java new file mode 100644 index 0000000..42d74f2 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/guard/package-info.java @@ -0,0 +1,162 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Multi-recipient envelope for symmetric payloads with pluggable recipients, + * stateless openers, and a compact header. + * + *

Overview

This package implements a streaming envelope that protects + * one content-encryption key (CEK) for multiple recipients and delegates the + * actual payload protection to a symmetric data-content builder (for example, + * AES-GCM or AES-CBC). At encrypt time the envelope generates a fresh CEK, + * creates a table of recipient entries that can recover that CEK, and then + * streams the symmetric stage header and ciphertext. At decrypt time openers + * scan the recipient table using a single piece of unlocking material until one + * opener recovers the CEK, after which the symmetric stage processes the + * remainder of the stream. + * + *

Header format

The envelope header is self-delimiting and precedes + * the symmetric stage bytes. All lengths are encoded as unsigned pack7 integers + * and all byte sequences are length-prefixed by pack7. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Header format
FieldTypeEncodingDescription
recipientCountintpack7Number of recipient entries that follow.
Repeated for i = 1..recipientCount
entryIdStringpack7 length + UTF-8 bytesHuman-readable identifier that helps route to a matching opener, for + * example "RSA:OAEP-SHA256" or "KEM:ML-KEM:GCM-WRAP".
entryBlobbyte[]pack7 length + bytesOpaque per-recipient data sufficient for a matching opener to recover the + * CEK. Examples include RSA-wrapped CEK, KEM ciphertext plus salts and IVs, or + * AEAD-wrapped CEK.
End repeated section
symmetricHeaderbyte[]builder-definedOptional header owned and emitted by the symmetric stage (for example, + * IV, tag size, or AAD hash), if the chosen symmetric builder is configured to + * write one.
ciphertextbyte streambuilder-definedPayload bytes produced by the symmetric stage.
+ * + *

Key ideas

+ *
    + *
  • Separation of concerns: the envelope manages CEK + * generation and recipient entries; the symmetric builder manages algorithm + * parameters and payload framing.
  • + *
  • Stateless strategy objects: recipients encode entries; + * openers attempt recovery of the CEK; neither carries long-lived secret + * state.
  • + *
  • Defensive parsing: the builder applies limits to the + * number of recipients and the size of each entry blob; the symmetric stage + * applies its own limits to its header and payload.
  • + *
+ * + *

Design decision: separate unlocking material from openers

This + * package intentionally keeps {@link zeroecho.sdk.guard.UnlockMaterial} + * separate from {@link zeroecho.sdk.guard.RecipientOpener}. Openers encode how + * to interpret and attempt a specific entry type and are reusable and + * stateless; unlocking material centralizes ownership of secrets (private keys + * or passwords), which simplifies secret lifecycle management and allows the + * envelope to try one piece of material across heterogeneous entries in a + * uniform scan. If desired, an adapter can bind material to an opener instance + * in calling code without changing these interfaces. + * + *

Hardening options

The following options are design targets and not + * yet implemented in this package: + *
    + *
  • Label suppression: omit human-readable {@code entryId} + * strings and write empty identifiers, forcing openers to attempt blind parses + * of entry blobs.
  • + *
  • Entry padding or bucketing: normalize entry blob sizes + * to reduce size-based fingerprinting.
  • + *
+ * Both options trade additional compute and bandwidth for reduced metadata + * leakage and can be added without changing the basic header framing shown + * above. + * + *

Typical usage

{@code
+ * // Encrypt for multiple recipients; AES-256/GCM carries its own header.
+ * DataContent enc = new MultiRecipientDataSourceBuilder()
+ *     .withAes(AesDataContentBuilder.builder().modeGcm(128).withHeader())
+ *     .payloadKeyBytes(32)
+ *     .addRsaOaepRecipient(rsaPub)
+ *     .addKemRecipient("ML-KEM", kemPub, 32, 16)
+ *     .addPasswordRecipient(pass, 120_000, 16, 32)
+ *     .build(true);
+ * enc.setInput(plainSource);
+ *
+ * // Decrypt with any one unlocking material; openers are installed by default.
+ * DataContent dec = new MultiRecipientDataSourceBuilder()
+ *     .withAes(AesDataContentBuilder.builder().modeGcm(128).withHeader())
+ *     .payloadKeyBytes(32)
+ *     .unlockWith(new UnlockMaterial.Private(rsaPrv))
+ *     .build(false);
+ * dec.setInput(cipherSource);
+ * }
+ */ +package zeroecho.sdk.guard; diff --git a/lib/src/main/java/zeroecho/sdk/integrations/covert/TextualCodec.java b/lib/src/main/java/zeroecho/sdk/integrations/covert/TextualCodec.java new file mode 100644 index 0000000..8665dbc --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/integrations/covert/TextualCodec.java @@ -0,0 +1,178 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.integrations.covert; + +import java.util.ArrayDeque; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Queue; +import java.util.TreeMap; + +import zeroecho.sdk.util.RandomSupport; + +/** + * A utility class for generating pseudo-random textual content based on + * predefined character frequency distributions. + *

+ * The {@code TextualCodec} class contains a nested {@link Generator} class that + * can be configured with a set of character frequencies (e.g., English letter + * frequencies) to produce text that mimics the statistical distribution of the + * given language or character set. + */ +public final class TextualCodec { // NOPMD + /** + * Private constructor to prevent instantiation of {@code TextualCodec}. + *

+ * This class is intended to be used as a utility container for static members + * and should not be instantiated. + */ + private TextualCodec() { + + } + + /** + * Generates characters or strings using a frequency-based distribution. + *

+ * This generator uses a cumulative frequency table internally to map random + * numbers to characters, enabling the creation of realistic-looking text that + * follows the given character frequency distribution. The generator also avoids + * consecutive duplicate characters by employing a simple queuing mechanism. + */ + public static class Generator { + /** + * Internal map representing the cumulative frequency ranges mapped to + * characters. + */ + private final NavigableMap ranges = new TreeMap<>(); + /** + * The maximum value of the cumulative frequency range. + */ + private final double maxRange; + /** + * A predefined English character frequency distribution including the space + * character, based on typical usage in English text. + */ + public final static Map ENGLISH = Map.ofEntries(Map.entry('a', 8.2), Map.entry('b', 1.5), + Map.entry('c', 2.8), Map.entry('d', 4.3), Map.entry('e', 12.7), Map.entry('f', 2.2), + Map.entry('g', 2.0), Map.entry('h', 6.1), Map.entry('i', 7.0), Map.entry('j', 0.15), + Map.entry('k', 0.77), Map.entry('l', 4.0), Map.entry('m', 2.4), Map.entry('n', 6.7), + Map.entry('o', 7.5), Map.entry('p', 1.9), Map.entry('q', 0.095), Map.entry('r', 6.0), + Map.entry('s', 6.3), Map.entry('t', 9.1), Map.entry('u', 2.8), Map.entry('v', 0.98), + Map.entry('w', 2.4), Map.entry('x', 0.15), Map.entry('y', 2.0), Map.entry('z', 0.074), + Map.entry(' ', 25.4)); + /** + * A default generator using the {@link #ENGLISH} frequency distribution. + */ + public final static Generator EN = new Generator(ENGLISH); + + private Character lastChar = '~'; + private final Queue backlog = new ArrayDeque<>(); + + /** + * Constructs a new {@code Generator} with the specified character frequency + * distribution. + * + * @param frequencies a map of characters to their relative frequencies (must be + * non-negative) + */ + public Generator(Map frequencies) { + double cumulative = 0.0; + for (Map.Entry entry : frequencies.entrySet()) { + double freq = entry.getValue(); + if (freq <= 0) { + continue; + } + ranges.put(cumulative, entry.getKey()); + cumulative = cumulative + freq; + } + + maxRange = cumulative; + } + + /** + * Generates a string of the specified length using the configured character + * frequency distribution. Consecutive duplicate characters are avoided when + * possible. + * + * @param length the number of characters to generate + * @return a randomly generated string + */ + public String getText(int length) { + StringBuffer sb = new StringBuffer(); + + while (length-- > 0) { // NOPMD + sb.append(getChar()); + } + + return sb.toString(); + } + + /** + * Returns the next randomly generated character, avoiding consecutive + * duplicates when possible. + * + * @return the next character in the generated sequence + */ + public char getChar() { + if (backlog.isEmpty() || lastChar.equals(backlog.peek())) { + Character next = getChar(RandomSupport.getRandom().nextDouble(maxRange)); + while (lastChar.equals(next)) { + backlog.add(next); + next = getChar(RandomSupport.getRandom().nextDouble(maxRange)); + } + lastChar = next; + return lastChar; + } + + return lastChar = backlog.poll(); + } + + /** + * Returns a character based on the provided value in the frequency range. + * + * @param value a value between 0 (inclusive) and {@code maxRange} (exclusive) + * @return the corresponding character for the specified value + * @throws IllegalArgumentException if the value is out of range + */ + public char getChar(double value) { + if (value < 0.0 || value >= maxRange) { + throw new IllegalArgumentException("Value must be in [0.0, " + maxRange + ")"); + } + + Map.Entry entry = ranges.floorEntry(value); + return (entry == null) ? ranges.firstEntry().getValue() : entry.getValue(); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/integrations/covert/jpeg/JpegExifEmbedder.java b/lib/src/main/java/zeroecho/sdk/integrations/covert/jpeg/JpegExifEmbedder.java new file mode 100644 index 0000000..06cdde5 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/integrations/covert/jpeg/JpegExifEmbedder.java @@ -0,0 +1,228 @@ +/******************************************************************************* + * 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.sdk.integrations.covert.jpeg; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.imaging.Imaging; +import org.apache.commons.imaging.ImagingException; +import org.apache.commons.imaging.common.ImageMetadata; +import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata; +import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter; +import org.apache.commons.imaging.formats.tiff.TiffField; +import org.apache.commons.imaging.formats.tiff.TiffImageMetadata; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputField; +import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet; + +import zeroecho.core.io.Util; +import zeroecho.sdk.util.Pack7LStreamWriter; + +/** + * Utility class for embedding and extracting binary payloads across multiple + * EXIF slots in JPEG files. + *

+ * The configured slot list defines both the location and chunking order of the + * embedded payload. Slot types are respected — for example, ASCII fields + * automatically use Base64 encoding to ensure compatibility with textual EXIF + * constraints. + *

+ *

+ * The original JPEG EXIF metadata is preserved, except for any overwritten + * fields defined via slot configuration. + *

+ */ +public final class JpegExifEmbedder { + private static final Logger LOG = Logger.getLogger(JpegExifEmbedder.class.getName()); + + private final List slots = new ArrayList<>(); + + /** + * Configures the list of EXIF slots to be used for payload embedding and + * extraction. The order determines the splitting/joining order of the payload. + * + * @param slots List of slot configurations + */ + public void setSlots(List slots) { + this.slots.clear(); + this.slots.addAll(slots); + } + + /** + * Embeds a binary payload into a JPEG file by spreading the data across a + * series of pre-configured EXIF slots. + *

+ * The payload is read from the provided {@code InputStream}, prefixed using + * 7-bit variable-length encoding, and split across EXIF fields based on their + * capacity and type. Fields that accept only text data will store the payload + * encoded as Base64. The modified JPEG is then written to the given output + * stream using a lossless EXIF update. + *

+ * + * @param jpegPath the path to the input JPEG file; must not be {@code null} + * @param payloadInput the input stream containing the binary payload to embed; + * it must be prefixed using 7-bit length encoding (e.g. + * {@code writePack7L}) + * @param jpegOutput the output stream where the modified JPEG will be written + * @return the total number of bytes read from {@code payloadInput}, including + * the prefix + * @throws IOException if an I/O error occurs while reading the payload, + * processing EXIF metadata, or writing the output JPEG + */ + public int embed(Path jpegPath, InputStream payloadInput, OutputStream jpegOutput) throws IOException { + try { + ImageMetadata metadata = Imaging.getMetadata(jpegPath.toFile()); + TiffImageMetadata exif = (metadata instanceof JpegImageMetadata jmeta) ? jmeta.getExif() : null; + + TiffOutputSet outputSet = (exif != null) ? exif.getOutputSet() : new TiffOutputSet(ByteOrder.BIG_ENDIAN); + + byte[] allBytes = Util.readWithPackedLengthPrefix(payloadInput, 1024 * 1024); + int offset = 0; + + for (Slot slot : slots) { + boolean useBase64 = slot.tagInfo.isText(); + int chunkLimit = slot.defaultCapacity; + + byte[] encoded; + if (useBase64) { + // base64 expands ~33%, so reduce raw chunk size + int safeRawSize = chunkLimit * 3 / 4; + int size = Math.min(safeRawSize, allBytes.length - offset); + byte[] raw = new byte[size]; // NOPMD + System.arraycopy(allBytes, offset, raw, 0, size); + offset += size; + + byte[] base64Bytes = Base64.getEncoder().encode(raw); + String base64String = new String(base64Bytes, StandardCharsets.US_ASCII); // NOPMD + + encoded = slot.tagInfo.encodeValue(slot.tagInfo.dataTypes.get(0), base64String, + outputSet.byteOrder); + } else { + int size = Math.min(chunkLimit, allBytes.length - offset); + byte[] chunk = new byte[size]; // NOPMD + System.arraycopy(allBytes, offset, chunk, 0, size); + offset += size; + + encoded = slot.tagInfo.encodeValue(slot.tagInfo.dataTypes.get(0), chunk, outputSet.byteOrder); + } + + int directoryType = slot.tagInfo.directoryType.directoryType; + TiffOutputDirectory directory = outputSet.findDirectory(directoryType); + if (directory == null) { + directory = new TiffOutputDirectory(directoryType, outputSet.byteOrder); // NOPMD + outputSet.addDirectory(directory); + } + + TiffOutputField field = new TiffOutputField(slot.tagInfo, slot.tagInfo.dataTypes.get(0), encoded.length, // NOPMD + encoded); + + directory.removeField(slot.tagInfo); // Remove only if collides + directory.add(field); + + if (offset >= allBytes.length) { + break; + } + } + + new ExifRewriter().updateExifMetadataLossless(jpegPath.toFile(), jpegOutput, outputSet); + + return allBytes.length; + } catch (ImagingException e) { + throw new IOException("Failed to process EXIF data", e); + } + } + + /** + * Extracts a binary payload that was previously embedded into the JPEG via EXIF + * slots. + * + * @param jpegPath input JPEG path + * @param payloadOutput stream to write the reconstructed binary payload + * @throws IOException if the extraction fails + */ + public void extract(Path jpegPath, OutputStream payloadOutput) throws IOException { + try { + ImageMetadata metadata = Imaging.getMetadata(jpegPath.toFile()); + TiffImageMetadata exif = (metadata instanceof JpegImageMetadata jmeta) ? jmeta.getExif() : null; + + if (exif == null) { + LOG.warning("EXIF metadata not found in image."); + return; + } + + Pack7LStreamWriter output = new Pack7LStreamWriter(payloadOutput); + + for (Slot slot : slots) { + TiffField field = exif.findField(slot.tagInfo); + if (field == null) { + continue; + } + + Object value = slot.tagInfo.getValue(field); + byte[] chunk; + + if (value instanceof byte[] binary) { + chunk = slot.tagInfo.isText() ? Base64.getDecoder().decode(binary) : binary; + + } else if (value instanceof String str) { + chunk = slot.tagInfo.isText() ? Base64.getDecoder().decode(str.getBytes(StandardCharsets.US_ASCII)) + : str.getBytes(StandardCharsets.UTF_8); + + } else { + LOG.log(Level.WARNING, "Unsupported EXIF field value type for tag: {0}", slot.tagInfo.name); + continue; + } + + if (chunk.length > output.write(chunk)) { + // all data has been read + break; + } + } + + } catch (ImagingException e) { + throw new IOException("Failed to read EXIF from JPEG", e); + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/integrations/covert/jpeg/Slot.java b/lib/src/main/java/zeroecho/sdk/integrations/covert/jpeg/Slot.java new file mode 100644 index 0000000..f9059e5 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/integrations/covert/jpeg/Slot.java @@ -0,0 +1,303 @@ +/******************************************************************************* + * 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.sdk.integrations.covert.jpeg; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants; +import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType; +import org.apache.commons.imaging.formats.tiff.fieldtypes.AbstractFieldType; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii; +import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoUndefined; + +/** + * Represents a target slot in an EXIF/TIFF metadata directory for covert data + * embedding. + *

+ * A {@code Slot} defines where and how binary payloads may be hidden within the + * structure of JPEG metadata. It supports both predefined slots (registered by + * name) and fully custom slots specified via tag metadata. + *

+ * + *

+ * Slot specifications can be written as strings using the following syntax: + *

+ * + *
{@code
+ * [group.]name[/tag=tagId,type,count,dir][:capacity]
+ * }
+ * + *
    + *
  • group: Optional slot group, one of EXIF, GPS, INTEROP, TIFF, or + * THUMBNAIL. Default is EXIF.
  • + *
  • name: Slot name (must match a registered name or be part of a tag + * definition).
  • + *
  • tag=...: Optional explicit tag definition for custom slots.
  • + *
  • capacity: Optional maximum capacity in bytes (default is + * 1024).
  • + *
+ * + *

+ * Predefined slots are stored in a registry and provide convenient aliases for + * common EXIF tags. Custom slots can be created dynamically using tag + * information and are parsed using the {@link #parse(String)} method. + *

+ * + *

+ * This class is immutable and not intended to be extended. + *

+ * + * @author Leo Galambos + */ +public final class Slot { // NOPMD + private static final Map REGISTRY = new HashMap<>(); + + static { + register("usercomment", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_USER_COMMENT, 4096); + register("makernote", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_MAKER_NOTE, 4096); + register("exifversion", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_EXIF_VERSION, 1024); + register("software", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_SOFTWARE, 2048); + register("interoptag", SlotGroup.INTEROP, + new TagInfoUndefined("CustomInterop", 0xC4A6, TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD), 2048); + register("thumbcomment", SlotGroup.THUMBNAIL, + new TagInfoAscii("ThumbComment", 0xC4A7, 64, TiffDirectoryType.TIFF_DIRECTORY_IFD1), 1024); + } + + private static final Map DIRECTORY_MAP = Map.ofEntries( + Map.entry("ifd0", TiffDirectoryType.TIFF_DIRECTORY_IFD0), + Map.entry("ifd1", TiffDirectoryType.TIFF_DIRECTORY_IFD1), + Map.entry("ifd2", TiffDirectoryType.TIFF_DIRECTORY_IFD2), + Map.entry("ifd3", TiffDirectoryType.TIFF_DIRECTORY_IFD3), + Map.entry("exif", TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD), + Map.entry("gps", TiffDirectoryType.EXIF_DIRECTORY_GPS), + Map.entry("interop", TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD), + Map.entry("maker", TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTES), + Map.entry("root", TiffDirectoryType.TIFF_DIRECTORY_ROOT), + Map.entry("sub_ifd", TiffDirectoryType.EXIF_DIRECTORY_SUB_IFD)); + + private static final Map FIELD_TYPE_MAP = new HashMap<>(); + + static { + for (AbstractFieldType t : AbstractFieldType.ANY) { + FIELD_TYPE_MAP.put(t.getName().toLowerCase(Locale.ROOT), t); + } + } + + /** + * Logical groupings of EXIF/TIFF directories used to categorize slots. + *

+ * Each group corresponds to a specific metadata section within an image file, + * typically used in image metadata standards such as EXIF and TIFF. + */ + public enum SlotGroup { + /** + * EXIF metadata group, containing tags related to camera settings, image + * capture information, and other extended image metadata defined by the EXIF + * standard. + */ + EXIF, + /** + * TIFF metadata group, which includes baseline TIFF tags such as image + * dimensions, compression type, and color format. + */ + TIFF, + /** + * GPS metadata group, storing geolocation information like latitude, longitude, + * altitude, and GPS timestamp. + */ + GPS, + /** + * Interoperability metadata group, used to ensure compatibility across + * different file formats or devices. Typically contains a few specific tags. + */ + INTEROP, + /** + * Thumbnail metadata group, storing metadata and image data for embedded + * preview thumbnails. + */ + THUMBNAIL + } + + /** + * The logical group (e.g., EXIF, GPS, TIFF) this slot belongs to. + */ + public final SlotGroup group; + /** + * Metadata tag info describing the structure of this slot. + */ + public final TagInfo tagInfo; + /** + * The default capacity, in bytes, of this slot for data embedding. + */ + public final int defaultCapacity; + + private Slot(SlotGroup group, TagInfo tagInfo, int defaultCapacity) { + this.group = group; + this.tagInfo = tagInfo; + this.defaultCapacity = defaultCapacity; + } + + /** + * Registers a named predefined slot in the internal registry. This allows slots + * to be referenced later by name in shorthand form. + * + * @param name the lowercase name used for lookup + * @param group the logical slot group + * @param tagInfo the tag metadata for the slot + * @param defaultCapacity the default maximum capacity for this slot in bytes + * @return the registered {@code Slot} instance + */ + public static Slot register(String name, SlotGroup group, TagInfo tagInfo, int defaultCapacity) { + Slot slot = new Slot(group, tagInfo, defaultCapacity); + REGISTRY.put(name.toLowerCase(Locale.ROOT), slot); + return slot; + } + + /** + * Parses a slot specification string into a {@code Slot} object. + *

+ * The input may refer to a predefined slot by name, or a custom slot defined + * with full tag details. See class-level documentation for the full + * specification syntax. + *

+ * + * @param spec the slot specification string + * @return a {@code Slot} representing the parsed definition + * @throws IllegalArgumentException if the input format is invalid or references + * unknown components + */ + public static Slot parse(String spec) { + Pattern pattern = Pattern.compile( + "(?:(\\w+)\\.)?(\\w+)(?:/tag=([\\d]+),([a-z0-9_]+),(\\d+),(\\w+))?(?::(\\d+))?", + Pattern.CASE_INSENSITIVE); + Matcher m = pattern.matcher(spec); + if (!m.matches()) { + throw new IllegalArgumentException("Invalid slot spec: " + spec); + } + + String groupStr = m.group(1); + String name = m.group(2); + String tagIdStr = m.group(3); + String typeStr = m.group(4); + String countStr = m.group(5); + String dirStr = m.group(6); + String sizeStr = m.group(7); + + SlotGroup group = groupStr != null ? SlotGroup.valueOf(groupStr.toUpperCase(Locale.ROOT)) : SlotGroup.EXIF; + int size = sizeStr != null ? Integer.parseInt(sizeStr) : 1024; + + if (tagIdStr == null) { + Slot predefined = REGISTRY.get(name.toLowerCase(Locale.ROOT)); + if (predefined == null) { + throw new IllegalArgumentException("No predefined slot found: " + name); + } + return predefined; + } + + int tagId = Integer.parseInt(tagIdStr); + int count = Integer.parseInt(countStr); + + TiffDirectoryType dir = Optional.ofNullable(DIRECTORY_MAP.get(dirStr.toLowerCase(Locale.ROOT))) + .orElseThrow(() -> new IllegalArgumentException("Unknown directory: " + dirStr)); + + AbstractFieldType fieldType = Optional.ofNullable(FIELD_TYPE_MAP.get(typeStr.toLowerCase(Locale.ROOT))) + .orElseThrow(() -> new IllegalArgumentException("Unsupported type: " + typeStr)); + + TagInfo tagInfo = new TagInfo(name, tagId, fieldType, count, dir); + return new Slot(group, tagInfo, size); + } + + /** + * Returns a human-readable representation of the slot, including its tag name + * and capacity. + * + * @return string representation of the slot + */ + @Override + public String toString() { + return tagInfo.name + ":" + defaultCapacity; + } + + /** + * Compares this slot to another for equality based on tag ID and directory + * type. + * + * @param o the object to compare + * @return true if the other object is a slot with the same tag and directory + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Slot slot = (Slot) o; + return tagInfo.tag == slot.tagInfo.tag && tagInfo.directoryType == slot.tagInfo.directoryType; + } + + /** + * Computes a hash code based on the slot's tag and directory. + * + * @return the hash code + */ + @Override + public int hashCode() { + return Objects.hash(tagInfo.tag, tagInfo.directoryType); + } + + /** + * Returns a list of default slot configurations commonly available in standard + * EXIF metadata. These slots are selected for compatibility and relatively high + * storage capacity. + * + * @return list of default {@code Slot} instances + */ + public static List defaults() { + return List.of(new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_USER_COMMENT, 4096), + new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_MAKER_NOTE, 4096), + new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_EXIF_VERSION, 1024), + new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_SOFTWARE, 2048)); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/integrations/covert/jpeg/package-info.java b/lib/src/main/java/zeroecho/sdk/integrations/covert/jpeg/package-info.java new file mode 100644 index 0000000..e012f25 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/integrations/covert/jpeg/package-info.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Covert data embedding and extraction using JPEG EXIF metadata. + * + *

+ * This package provides utilities for hiding and recovering binary payloads + * inside EXIF/TIFF fields of JPEG images. The embedder preserves existing + * metadata where possible and distributes the payload across a caller-defined + * sequence of slots, transparently applying Base64 for text-only tags. The + * implementation performs lossless EXIF updates using a JPEG metadata library. + *

+ * + *

Key elements

+ *
    + *
  • {@link JpegExifEmbedder} - embeds a length-prefixed binary payload into a + * JPEG by splitting it across configured slots and writes the result using a + * lossless EXIF rewrite; also reconstructs the payload by reading the same + * slots in order.
  • + *
  • {@link Slot} - an immutable description of one EXIF/TIFF field with a + * default capacity and grouping; supports a compact specification syntax and a + * small registry of predefined, high-capacity tags.
  • + *
+ * + *

Slot configuration

+ *

+ * Slots identify where and in what order payload chunks are written or read. + * They can be referenced by predefined name (for example, {@code usercomment}, + * {@code makernote}) or specified explicitly using tag metadata. The textual + * format is: + *

+ *
{@code
+ * [group.]name[/tag=tagId,type,count,dir][:capacity]
+ * }
+ *
    + *
  • group: EXIF, GPS, INTEROP, TIFF, or THUMBNAIL (default EXIF).
  • + *
  • name: registry key or custom tag name.
  • + *
  • tag=...: explicit tag definition for a custom slot.
  • + *
  • capacity: maximum bytes for this slot (default 1024).
  • + *
+ *

+ * Use {@link Slot#parse(String)} for custom definitions, + * {@link Slot#register(String, Slot.SlotGroup, org.apache.commons.imaging.formats.tiff.taginfos.TagInfo, int)} + * to extend the registry, and {@link Slot#defaults()} to obtain a conservative + * default set. + *

+ * + *

Payload format and processing

+ *
    + *
  • Length prefix: Embedding reads the payload using a 7-bit + * variable-length size prefix, then splits the bytes across slots in order; + * extraction writes the same prefix back before streaming the reconstructed + * bytes.
  • + *
  • Text vs binary slots: When a slot is text-only, the embedder + * Base64-encodes the chunk before storing; binary-capable slots store raw + * bytes.
  • + *
  • Metadata preservation: Existing EXIF is retained except for fields + * explicitly overwritten by the chosen slots; updates are written + * losslessly.
  • + *
+ * + *

Thread-safety

+ *
    + *
  • {@link JpegExifEmbedder} maintains a mutable, ordered slot list and is + * not intended for concurrent reconfiguration.
  • + *
  • {@link Slot} instances are immutable and safe to share across + * threads.
  • + *
+ * + *

Use cases and extensions

+ *
    + *
  • Transport encrypted streams disguised within benign JPEG metadata.
  • + *
  • Covert carriers for small secrets with compatibility across common image + * tools.
  • + *
  • Future extension: combine with exporters or steganographic pipelines in + * other packages to automate deployment to public platforms while keeping + * payloads concealed.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.sdk.integrations.covert.jpeg; diff --git a/lib/src/main/java/zeroecho/sdk/integrations/covert/package-info.java b/lib/src/main/java/zeroecho/sdk/integrations/covert/package-info.java new file mode 100644 index 0000000..1b73d27 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/integrations/covert/package-info.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Helpers for generating pseudo-random cover text using character frequency + * tables. + * + *

+ * This package provides utilities for producing benign-looking textual fillers + * that mimic the statistical properties of a target language. The primary use + * is to generate cover text for covert or steganographic workflows where + * ciphertext or opaque data needs a plausible textual carrier. These helpers do + * not offer confidentiality by themselves; combine them with encryption when + * handling secrets. + *

+ * + *

Key elements

+ *
    + *
  • {@link TextualCodec} - utility container for text-oriented helpers.
  • + *
  • {@link TextualCodec.Generator} - frequency-driven character generator + * with a default English distribution and APIs to produce single characters or + * fixed-length strings.
  • + *
+ * + *

Typical usage

{@code
+ * // Use the built-in English distribution to create a short cover text.
+ * zeroecho.sdk.integrations.covert.TextualCodec.Generator gen =
+ *     zeroecho.sdk.integrations.covert.TextualCodec.Generator.EN;
+ * String sample = gen.getText(256);
+ *
+ * // Or construct a custom distribution.
+ * java.util.Map freq = java.util.Map.ofEntries(
+ *     java.util.Map.entry('e', 12.7), java.util.Map.entry('t', 9.1),
+ *     java.util.Map.entry('a', 8.2),  java.util.Map.entry(' ', 25.4)
+ * );
+ * zeroecho.sdk.integrations.covert.TextualCodec.Generator custom =
+ *     new zeroecho.sdk.integrations.covert.TextualCodec.Generator(freq);
+ * String cover = custom.getText(128);
+ * }
+ * + *

Notes

+ *
    + *
  • Generated text is intended as camouflage, not as a security control.
  • + *
  • Distributions should be non-negative and representative of the intended + * cover domain.
  • + *
  • Instances are not thread-safe; use a separate generator per thread.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.sdk.integrations.covert; diff --git a/lib/src/main/java/zeroecho/sdk/integrations/stegano/ImageFormat.java b/lib/src/main/java/zeroecho/sdk/integrations/stegano/ImageFormat.java new file mode 100644 index 0000000..6eb21ee --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/integrations/stegano/ImageFormat.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * 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.sdk.integrations.stegano; + +/** + * Enumeration of image formats supported for steganographic processing. + * + *

+ * These formats are chosen because they provide predictable binary layouts and + * are commonly used in practical steganographic embedding and extraction. + *

+ */ +public enum ImageFormat { + /** + * Portable Network Graphics format. + *

+ * PNG uses lossless compression, which makes it suitable for precise bit-level + * embedding without introducing distortion into the carrier image. + *

+ */ + PNG, + /** + * Bitmap format. + *

+ * BMP is an uncompressed raster format. Its simple and direct structure makes + * it reliable for low-level manipulation during steganographic processing. + *

+ */ + BMP, + /** + * Joint Photographic Experts Group format. + *

+ * JPEG is a lossy format based on DCT (Discrete Cosine Transform) blocks. + * Steganographic methods for JPEG must work safely within the compression + * domain to preserve image integrity while embedding data. + *

+ */ + JPEG +} diff --git a/lib/src/main/java/zeroecho/sdk/integrations/stegano/LSBSteganographyMethod.java b/lib/src/main/java/zeroecho/sdk/integrations/stegano/LSBSteganographyMethod.java new file mode 100644 index 0000000..5045b1d --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/integrations/stegano/LSBSteganographyMethod.java @@ -0,0 +1,254 @@ +/******************************************************************************* + * 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.sdk.integrations.stegano; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.imageio.ImageIO; + +/** + * Least Significant Bit steganography implementation operating in the spatial + * domain. + * + *

+ * This class embeds and extracts message bits from image pixels by manipulating + * the least significant bit of each grayscale value. The implementation uses + * stream-based I/O to support large inputs without loading everything into + * memory at once. + *

+ * + *

Algorithm and characteristics

+ *

+ * Embedding proceeds left-to-right, top-to-bottom across pixels. The message is + * prefixed with a 4-byte length header (big-endian) and then written bit-by-bit + * into the LSB of each pixel's grayscale value. During embedding the image + * content is converted to grayscale and written back to all color channels, + * which discards original chroma. + *

+ *

+ * Extraction reads the first 32 bits to recover the message length and then + * continues to read that many message bytes from subsequent pixel LSBs. + *

+ * + *

Capacity

+ *

+ * Effective capacity is {@code width * height - 32} bits, where 32 bits are + * reserved for the length header. The caller must ensure that the message + * length in bits does not exceed the available capacity; otherwise the embedded + * payload will be incomplete. + *

+ * + *

Format support

+ *

+ * Output must be a lossless format such as PNG or BMP. JPEG is not supported + * because its lossy compression would destroy LSB-embedded payloads. + *

+ * + *

Usage

{@code
+ * SteganographyMethod method = new LSBSteganographyMethod();
+ * try (InputStream carrier = Files.newInputStream(Path.of("cover.png"));
+ *      InputStream secret  = new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8));
+ *      InputStream stego   = method.embed(carrier, ImageFormat.PNG, secret)) {
+ *     Files.write(Path.of("stego.png"), stego.readAllBytes());
+ * }
+ *
+ * try (InputStream stego = Files.newInputStream(Path.of("stego.png"));
+ *      InputStream recovered = method.extract(stego)) {
+ *     String text = new String(recovered.readAllBytes(), StandardCharsets.UTF_8);
+ * }
+ * }
+ */ +public class LSBSteganographyMethod implements SteganographyMethod { + private static final Logger LOG = Logger.getLogger(LSBSteganographyMethod.class.getName()); + + /** + * Embeds the provided message into the least significant bits of the image and + * returns a new image stream in the requested lossless format. + * + *

+ * The output image is grayscale and may differ visually from the input because + * the algorithm writes identical gray values to all RGB channels. The message + * is stored as a 4-byte big-endian length prefix followed by the raw message + * bytes. + *

+ * + * @param inputImage the source image stream to use as the cover image + * @param outputFormat the desired output format; must be lossless (for example, + * {@code PNG} or {@code BMP}) + * @param message the message stream to embed + * @return an {@code InputStream} containing the stego image encoded in + * {@code outputFormat} + * @throws IOException if an I/O error occurs while reading the + * image or message, or while writing the + * output image + * @throws IllegalArgumentException if {@code outputFormat} is unsupported for + * this method (for example, {@code JPEG}) + */ + @Override + public InputStream embed(InputStream inputImage, ImageFormat outputFormat, InputStream message) throws IOException { + if (outputFormat == ImageFormat.JPEG) { + throw new IllegalArgumentException("LSB does not support lossy formats like JPEG."); + } + + BufferedImage img = ImageIO.read(inputImage); + if (img == null) { + throw new IOException("Failed to read input image."); + } + + byte[] messageBytes = message.readAllBytes(); + ByteArrayOutputStream fullData = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(fullData); + dos.writeInt(messageBytes.length); + dos.write(messageBytes); + byte[] fullMessage = fullData.toByteArray(); + + final int width = img.getWidth(); + final int height = img.getHeight(); + int msgBitIndex = 0; + + if (LOG.isLoggable(Level.INFO)) { + LOG.log(Level.INFO, "embed to {2} picture w={0} x h={1}", new Object[] { width, height, outputFormat }); + } + + outer: for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (msgBitIndex >= fullMessage.length * 8) { + break outer; + } + + int rgb = img.getRGB(x, y); + int gray = rgb & 0xFF; + int bit = (fullMessage[msgBitIndex / 8] >> (7 - (msgBitIndex % 8))) & 1; + gray = (gray & 0xFE) | bit; + int newRgb = (gray << 16) | (gray << 8) | gray; + img.setRGB(x, y, newRgb); + msgBitIndex++; + } + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageIO.write(img, outputFormat.name().toLowerCase(Locale.ROOT), out); + return new ByteArrayInputStream(out.toByteArray()); + } + + /** + * Extracts the embedded message from the least significant bits of the provided + * image. + * + *

+ * This method expects the message to be stored as a 4-byte big-endian length + * prefix followed by exactly that many message bytes. The input should be a + * lossless image produced by + * {@link #embed(InputStream, ImageFormat, InputStream)} or an equivalent LSB + * spatial-domain encoder. + *

+ * + * @param inputImage the stego image stream that contains an embedded message + * @return an {@code InputStream} of the extracted message bytes + * @throws IOException if an I/O error occurs while reading the image or if the + * image cannot be decoded + */ + @Override + public InputStream extract(InputStream inputImage) throws IOException { + BufferedImage img = ImageIO.read(inputImage); + if (img == null) { + throw new IOException("Failed to read input image."); + } + + final int width = img.getWidth(); + final int height = img.getHeight(); + ByteArrayOutputStream messageBytes = new ByteArrayOutputStream(); + + if (LOG.isLoggable(Level.INFO)) { + LOG.log(Level.INFO, "extract from picture w={0} x h={1}", new Object[] { width, height }); + } + + int byteVal = 0; + int messageLength = -1; + int bitsCollected = 0; + + outer: for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int gray = img.getRGB(x, y) & 0xFF; + int bit = gray & 1; + byteVal = (byteVal << 1) | bit; + bitsCollected++; + + if (bitsCollected == 8) { // NOPMD + messageBytes.write(byteVal); + byteVal = 0; + bitsCollected = 0; + + if (messageLength == -1 && messageBytes.size() == 4) { + byte[] lenBytes = messageBytes.toByteArray(); + messageBytes.reset(); + messageLength = ByteBuffer.wrap(lenBytes).getInt(); + } + + if (messageLength != -1 && messageBytes.size() == messageLength) { + break outer; + } + } + } + } + + return new ByteArrayInputStream(messageBytes.toByteArray()); + } + + /** + * Returns metadata describing this LSB spatial-domain steganography method. + * + *

+ * The metadata includes a short identifier, a descriptive long name, and a + * concise description of the embedding approach. + *

+ * + * @return method metadata for LSB spatial-domain steganography + */ + @Override + public StegoMetadata getMetadata() { + return new StegoMetadata("LSB", "Least Significant Bit Spatial Domain Image Steganography", + "Embeds message bits into the least significant bits of grayscale pixel values."); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/integrations/stegano/SteganographyMethod.java b/lib/src/main/java/zeroecho/sdk/integrations/stegano/SteganographyMethod.java new file mode 100644 index 0000000..b87e349 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/integrations/stegano/SteganographyMethod.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.integrations.stegano; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Contract for stream-based steganographic methods. + * + *

+ * Implementations of this interface provide algorithms to hide and recover + * messages in digital images. Streams are used for both image data and message + * payloads to support large files and efficient, pipeline-friendly processing. + *

+ * + *

+ * A typical implementation will consume an input image stream, apply an + * embedding or extraction algorithm, and return a new stream without requiring + * the entire image to reside in memory. + *

+ */ +public interface SteganographyMethod { + + /** + * Embeds a message into a source image and produces a new image in the + * requested format. + * + *

+ * The caller provides an input image, a message stream, and specifies the + * desired output format. The implementation applies its embedding algorithm and + * returns an {@link InputStream} representing the new image. + *

+ * + * @param inputImage the source image stream; format may vary depending on + * implementation capabilities + * @param outputFormat the desired format for the resulting image; must be + * supported and generally lossless (for example, + * {@link ImageFormat#PNG}) + * @param message the stream containing the message to be embedded in the + * image + * @return an input stream of the new image containing the embedded message, + * encoded in the requested format + * @throws IOException if an I/O error occurs while reading or + * writing streams + * @throws IllegalArgumentException if the specified output format is not + * supported by this method + */ + InputStream embed(InputStream inputImage, ImageFormat outputFormat, InputStream message) throws IOException; + + /** + * Extracts a hidden message from an image. + * + *

+ * The caller supplies an image stream that contains an embedded message. The + * implementation applies its extraction algorithm and returns the recovered + * message as a new stream. + *

+ * + * @param inputImage the image stream containing an embedded message; must be in + * a format compatible with this method + * @return an input stream of the extracted message data + * @throws IOException if an I/O error occurs while reading or writing streams + */ + InputStream extract(InputStream inputImage) throws IOException; + + /** + * Returns metadata describing this steganographic method. + * + *

+ * The metadata includes a short identifier, a descriptive name, and a textual + * explanation of the embedding technique. + *

+ * + * @return the metadata object associated with this steganographic method + */ + StegoMetadata getMetadata(); +} diff --git a/lib/src/main/java/zeroecho/sdk/integrations/stegano/StegoMetadata.java b/lib/src/main/java/zeroecho/sdk/integrations/stegano/StegoMetadata.java new file mode 100644 index 0000000..e25de45 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/integrations/stegano/StegoMetadata.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.integrations.stegano; + +/** + * Metadata that describes a steganographic method. + * + *

+ * Each instance carries both a short identifier and a more descriptive + * technical name, together with a human-readable explanation of the embedding + * approach. This record is typically used for discovery, logging, or user + * interfaces that need to present details about available steganographic + * techniques. + *

+ * + *

Examples

{@code
+ * StegoMetadata meta = new StegoMetadata(
+ *     "LSB",
+ *     "Least Significant Bit Embedding",
+ *     "Encodes hidden data into the least significant bits of pixel values"
+ * );
+ * }
+ * + * @param name method identifier, for example {@code "LSB"} or + * {@code "DCT"} + * @param fullName full technical name of the steganographic method + * @param description short explanation of how the method works + */ +public record StegoMetadata(String name, String fullName, String description) { +} diff --git a/lib/src/main/java/zeroecho/sdk/integrations/stegano/package-info.java b/lib/src/main/java/zeroecho/sdk/integrations/stegano/package-info.java new file mode 100644 index 0000000..d705aa5 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/integrations/stegano/package-info.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * 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. + ******************************************************************************/ +/** + * Steganographic integrations for ZeroEcho SDK. + * + *

+ * This package provides abstractions and implementations for hiding and + * recovering messages in digital images. Steganography allows information to be + * concealed in carrier media so that its presence is not apparent to a casual + * observer. + *

+ * + *

Design principles

+ *
    + *
  • All algorithms implement the + * {@link zeroecho.sdk.integrations.stegano.SteganographyMethod} interface, + * which defines stream-oriented {@code embed} and {@code extract} operations + * and supplies a metadata descriptor.
  • + *
  • Supported carrier formats are represented by + * {@link zeroecho.sdk.integrations.stegano.ImageFormat}. Only lossless formats + * are suitable for direct bit-level embedding, but some algorithms may also + * provide support for lossy domains such as JPEG with dedicated + * techniques.
  • + *
  • Methods are designed to operate on {@link java.io.InputStream} and + * {@link java.io.OutputStream} pipelines, making them compatible with large + * files and streaming workflows.
  • + *
+ * + *

Extensibility

+ *

+ * The package is not limited to one specific algorithm. In addition to classic + * spatial-domain approaches, implementors can provide methods that operate in + * the frequency domain (for example, DCT coefficients for JPEG) or more + * advanced hybrid techniques. New methods can be registered by implementing the + * {@code SteganographyMethod} interface and returning appropriate + * {@link zeroecho.sdk.integrations.stegano.StegoMetadata}. + *

+ * + *

Typical workflow

{@code
+ * SteganographyMethod method = ...; // e.g. resolved from a registry
+ * try (InputStream carrier = Files.newInputStream(Path.of("cover.png"));
+ *      InputStream secret  = new ByteArrayInputStream("hello".getBytes(UTF_8));
+ *      InputStream stego   = method.embed(carrier, ImageFormat.PNG, secret)) {
+ *     Files.write(Path.of("stego.png"), stego.readAllBytes());
+ * }
+ *
+ * try (InputStream stego = Files.newInputStream(Path.of("stego.png"));
+ *      InputStream recovered = method.extract(stego)) {
+ *     String text = new String(recovered.readAllBytes(), UTF_8);
+ * }
+ * }
+ */ +package zeroecho.sdk.integrations.stegano; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/io/SignatureTrailerInputStream.java b/lib/src/main/java/zeroecho/sdk/io/SignatureTrailerInputStream.java new file mode 100644 index 0000000..ed8de55 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/io/SignatureTrailerInputStream.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * 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.sdk.io; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import java.util.function.Consumer; + +import zeroecho.core.context.SignatureContext; +import zeroecho.core.io.TailStrippingInputStream; + +/** + * SignatureTrailerInputStream reads an upstream stream that has a trailing + * signature tag and exposes only the original body while capturing the detached + * signature. + * + *

Overview

This class is a specialized + * {@code TailStrippingInputStream} that wraps a {@link SignatureContext} + * produced stream. The wrapped context emits {@code [body][signature]}; this + * stream transparently strips the trailing signature bytes from the output and + * optionally delivers a defensive copy of the signature to a user-supplied + * callback. + * + *

+ * The signature length is obtained from {@link SignatureContext#tagLength()} + * and the upstream stream is obtained via + * {@link SignatureContext#wrap(java.io.InputStream)}. Resource ownership is + * explicit: {@link #close()} closes both the underlying stream chain and the + * {@code SignatureContext}. + *

+ * + *

Usage

{@code
+ * SignatureContext sc = CryptoAlgorithms.create("Ed25519", KeyUsage.SIGN, privateKey, spec);
+ * try (InputStream in = new SignatureTrailerInputStream(
+ *         sc,
+ *         originalContent.getStream(),
+ *         8192,
+ *         sig -> ctx.put(SIG_KEY, sig))) {
+ *     // read 'in' to obtain only the body; signature is delivered to the callback
+ *     in.transferTo(out);
+ * }
+ * }
+ * + *

Thread-safety

Instances are not thread-safe. Use from a single + * thread and do not share across readers. + */ +public final class SignatureTrailerInputStream extends TailStrippingInputStream { + + private final SignatureContext sc; + private final Consumer onSignature; + + /** + * Creates a new signature-stripping stream that passes through only the body + * while capturing the trailing signature tag. + * + *

+ * This constructor immediately calls + * {@link SignatureContext#wrap(java.io.InputStream)} to obtain the producing + * stream and configures the tail length to + * {@link SignatureContext#tagLength()}. + *

+ * + * @param sc the signature context that wraps the upstream stream and + * provides tag metadata; must not be null + * @param upstream the upstream stream that yields {@code [body][signature]}; + * must not be null + * @param bufferSize internal buffer size used by the tail stripper; must be + * positive + * @param onSignature optional callback invoked exactly once with a defensive + * copy of the finalized signature bytes; may be null + * @throws IOException if initializing the underlying wrapped stream + * fails + * @throws NullPointerException if {@code sc} or {@code upstream} is null + */ + public SignatureTrailerInputStream(SignatureContext sc, InputStream upstream, int bufferSize, + Consumer onSignature) throws IOException { + super(Objects.requireNonNull(sc, "sc").wrap(Objects.requireNonNull(upstream, "upstream")), sc.tagLength(), + bufferSize); + this.sc = sc; + this.onSignature = onSignature; + } + + /** + * Receives the stripped tail bytes and forwards a defensive copy to the + * registered callback. + * + *

+ * If no callback is set or the tail is {@code null}, this method returns + * without action. Any runtime exception thrown by the callback is caught and + * ignored so that stream semantics remain unaffected. + *

+ * + * @param tail the stripped signature bytes, or {@code null} if no tail was + * detected + * @throws IOException never thrown by this implementation, preserved for the + * template method contract + */ + @Override + protected void processTail(byte[] tail) throws IOException { + if (tail == null || onSignature == null) { + return; + } + try { + onSignature.accept(tail.clone()); + } catch (RuntimeException ignored) { // NOPMD + // user callback must not break stream semantics + } + } + + /** + * Closes the stream and the associated {@link SignatureContext}. + * + *

+ * This method first closes the underlying stream chain (draining to EOF if + * required by the base class), then closes the signature context. If closing + * the underlying stream throws an {@link IOException}, that exception is + * rethrown after attempting to close the context. + *

+ * + * @throws IOException if closing the underlying stream fails + */ + @Override + public void close() throws IOException { + try (sc) { + super.close(); // drains to EOF if needed and closes the wrapped stream + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/io/package-info.java b/lib/src/main/java/zeroecho/sdk/io/package-info.java new file mode 100644 index 0000000..c83cbac --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/io/package-info.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Stream utilities for SDK pipelines. + * + *

+ * This package contains small helpers that adapt or post-process streams + * produced by SDK builders and core contexts. Utilities focus on trailer + * handling and other pipeline-friendly behaviors without buffering entire + * payloads. + *

+ * + *

Key elements

+ *
    + *
  • {@link SignatureTrailerInputStream} - a specialized tail-stripping stream + * for signature workflows. It wraps a + * {@link zeroecho.core.context.SignatureContext}, exposes only the original + * body, and captures the trailing fixed-length signature. Built atop + * {@link zeroecho.core.io.TailStrippingInputStream}.
  • + *
+ * + *

Typical usage

{@code
+ * // Create a signature context (for example, Ed25519) in SIGN mode using your algorithm descriptor.
+ * zeroecho.core.context.SignatureContext sc = ... created by an algorithm for SIGN ...;
+ *
+ * // Wrap the producing stream; only the body is exposed and the signature is delivered to a callback.
+ * try (java.io.InputStream in = new zeroecho.sdk.io.SignatureTrailerInputStream(
+ *          sc,
+ *          upstream,                  // original message bytes
+ *          8192,                      // buffer size for the stripper
+ *          sig -> { ... store or transmit 'sig' ... })) {
+ *     in.transferTo(out);             // writes only the body to 'out'
+ * }
+ * }
+ * + *

Design notes

+ *
    + *
  • Utilities are single-purpose and not thread-safe; create a new instance + * per pipeline.
  • + *
  • Resource ownership is explicit: where a stream is created from a core + * context, closing the utility also closes the underlying context.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.sdk.io; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/package-info.java b/lib/src/main/java/zeroecho/sdk/package-info.java new file mode 100644 index 0000000..3ebc2ae --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/package-info.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * + */ +package zeroecho.sdk; \ No newline at end of file diff --git a/lib/src/main/java/zeroecho/sdk/util/BouncyCastleActivator.java b/lib/src/main/java/zeroecho/sdk/util/BouncyCastleActivator.java new file mode 100644 index 0000000..2cfc133 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/util/BouncyCastleActivator.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.util; + +import java.security.Security; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; + +/** + * Provides initialization support for the Bouncy Castle cryptographic provider. + *

+ * This utility class automatically adds the Bouncy Castle provider to the Java + * Security framework upon class loading. It also offers an explicit + * initialization method for optional use. + *

+ * The class is declared as {@code final} and has a private constructor to + * prevent instantiation, emphasizing its utility nature. + * + *

+ * Example usage: + * + *

{@code
+ * BouncyCastleActivator.init();
+ * }
+ * + * @author Leo Galambos + */ +public final class BouncyCastleActivator { + /** + * Logger instance for the {@code BouncyCastleActivator} class, used to log + * messages related to the initialization and management of the Bouncy Castle + * security provider. + *

+ * Initialized with the class name to ensure logs are specific and traceable to + * this component. Useful for debugging issues during cryptographic provider + * setup. + *

+ */ + private static final Logger LOG = Logger.getLogger(BouncyCastleActivator.class.getName()); + + /** + * Static initializer that registers the Bouncy Castle provider with the Java + * Security framework. Logs the initialization process. + */ + static { + LOG.log(Level.INFO, "BouncyCastle provider initialization"); + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + + LOG.log(Level.INFO, "BouncyCastle PQC provider initialization"); + if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastlePQCProvider()); + } + } + + /** + * Explicitly logs the activation of the Bouncy Castle provider. This method can + * be called to confirm provider activation. + */ + static public void init() { + LOG.log(Level.INFO, "BrouncyCastle activated"); + } + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private BouncyCastleActivator() { + // this is a utility class + } +} diff --git a/lib/src/main/java/zeroecho/sdk/util/Kdf.java b/lib/src/main/java/zeroecho/sdk/util/Kdf.java new file mode 100644 index 0000000..560d3a5 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/util/Kdf.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.util; + +import java.security.GeneralSecurityException; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +/** + * HKDF utilities for deriving symmetric keys from a KEM shared secret. This + * class provides an RFC 5869 compliant HKDF-SHA256 implementation. + * + *

Notes

The function implements HKDF-Extract followed by HKDF-Expand + * with SHA-256. If {@code salt} is {@code null} or empty, a zero-filled salt of + * length 32 is used as recommended by RFC 5869. If {@code info} is + * {@code null}, it is treated as an empty byte array. + * + *
{@code
+ * // Example: derive 32 bytes for AES-256 or ChaCha20-Poly1305
+ * byte[] ikm   = kemSharedSecret;          // input keying material from KEM
+ * byte[] salt  = null;                     // or a KEM-specific salt
+ * byte[] info  = "ZeroEcho-KEM".getBytes(StandardCharsets.US_ASCII);
+ * int    len   = 32;
+ * byte[] key   = Kdf.hkdfSha256(ikm, salt, info, len);
+ * }
+ */ +public final class Kdf { // NOPMD + + private static final String HMAC = "HmacSHA256"; + private static final int HASH_LEN = 32; // bytes + + private Kdf() { + // utility + } + + /** + * Derives {@code length} bytes of key material using HKDF-SHA256 as specified + * in RFC 5869. + * + *

+ * HKDF-Extract: {@code PRK = HMAC(salt, IKM)} where {@code salt} defaults to + * {@code HashLen} zeros if {@code salt} is {@code null} or empty. + *

+ * + *

+ * HKDF-Expand: {@code T(0)=empty}, {@code T(i)=HMAC(PRK, T(i-1) || info || i)}, + * and the output key material is the first {@code length} bytes of + * {@code T(1)||...||T(N)}, where {@code N = ceil(length / HashLen)} and + * {@code i} is a single octet counter. + *

+ * + * @param ikm input keying material; must not be {@code null} or empty + * @param salt optional salt; if {@code null} or empty, a zero salt of 32 + * bytes is used + * @param info optional context/application info; if {@code null}, treated as + * empty + * @param length number of output bytes to produce; must be in range 1..(255*32) + * @return derived keying material of exactly {@code length} bytes + * @throws GeneralSecurityException if the MAC operation fails + * @throws IllegalArgumentException if {@code ikm} is empty or {@code length} is + * out of range + */ + public static byte[] hkdfSha256(byte[] ikm, byte[] salt, byte[] info, int length) throws GeneralSecurityException { + + if (ikm == null || ikm.length == 0) { + throw new IllegalArgumentException("ikm must not be null or empty"); + } + if (length < 1 || length > 255 * HASH_LEN) { + throw new IllegalArgumentException("length must be in range 1.." + (255 * HASH_LEN)); + } + final byte[] saltUse; + if (salt == null || salt.length == 0) { + saltUse = new byte[HASH_LEN]; // all zeros + } else { + saltUse = salt.clone(); + } + final byte[] infoUse = (info == null) ? new byte[0] : info.clone(); + + // ----- Extract ----- + Mac mac = Mac.getInstance(HMAC); + mac.init(new SecretKeySpec(saltUse, HMAC)); + final byte[] prk = mac.doFinal(ikm); + + // ----- Expand ----- + final int n = (length + HASH_LEN - 1) / HASH_LEN; // ceil + final byte[] okm = new byte[length]; + byte[] t = new byte[0]; + int pos = 0; + + for (int i = 1; i <= n; i++) { + mac.init(new SecretKeySpec(prk, HMAC)); // NOPMD + mac.update(t, 0, t.length); + mac.update(infoUse, 0, infoUse.length); + mac.update((byte) i); + t = mac.doFinal(); + + final int toCopy = Math.min(HASH_LEN, length - pos); + System.arraycopy(t, 0, okm, pos, toCopy); + pos += toCopy; + } + return okm; + } +} diff --git a/lib/src/main/java/zeroecho/sdk/util/OutputToInputStreamAdapter.java b/lib/src/main/java/zeroecho/sdk/util/OutputToInputStreamAdapter.java new file mode 100644 index 0000000..8185d80 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/util/OutputToInputStreamAdapter.java @@ -0,0 +1,181 @@ +/******************************************************************************* + * 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.sdk.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Objects; + +/** + * An abstract adapter that converts an {@link OutputStream}-based + * transformation (such as encryption or compression) into an + * {@link InputStream}-based one. This class reads data from a given + * {@link InputStream} (the {@code previousInput}), processes it through a + * transformation {@link OutputStream} (e.g., encryption, compression), and + * exposes the transformed data via the standard {@link InputStream} API. + *

+ * Subclasses must initialize the {@code transformationOut} field with a proper + * {@link OutputStream} that writes transformed data into the internal buffer + * ({@code baos}). This initialization must be done after construction + * and before any read operation, via a subclass- specific method such + * as an initialize(...) method. + *

+ * + *

Subclass Contract

+ *
    + *
  • Set {@code transformationOut} to a valid {@link OutputStream} that writes + * to {@code baos}.
  • + *
  • Do not call virtual methods like initialization from + * constructors.
  • + *
  • Call the initialization method before performing any reads.
  • + *
+ * + * @author Leo Galambos + */ +public abstract class OutputToInputStreamAdapter extends InputStream { + /** + * Default buffer size in bytes. Typically set to 8 KB (8 * 1024 bytes). + */ + protected static final int DEFAULT_BUF_SIZE = 8 * 1024; + /** + * Buffer size in bytes used by this instance. Initialized during object + * construction and remains constant. + */ + protected final int BUF_SIZE; + + /** + * The original input stream containing untransformed data. + */ + protected final InputStream previousInput; + + /** + * A buffer holding transformed data. + */ + protected ByteArrayOutputStream baos; + + /** + * The output stream performing transformation and writing to {@code baos}. Must + * be initialized by subclasses before use. + */ + protected OutputStream transformationOut; + /** + * Input stream initialized to a no-op {@code InputStream} that contains no + * data. + *

+ * This stream is set to {@link InputStream#nullInputStream()}, which is a + * convenient way to avoid {@code null} values while providing a valid, + * non-functional stream. Useful as a default placeholder until a real stream is + * assigned. + *

+ */ + private InputStream bais = nullInputStream(); + + /** + * Constructs a new adapter wrapping the specified input stream with a given + * buffer size for transformation output. + * + * @param previousInput The input stream to read untransformed data from. + * @param bufSize The buffer size for the transformation output buffer. + */ + public OutputToInputStreamAdapter(final InputStream previousInput, final int bufSize) { + super(); + + Objects.requireNonNull(previousInput, "input stream must not be null"); + + this.previousInput = previousInput; + this.BUF_SIZE = bufSize; + baos = new ByteArrayOutputStream(BUF_SIZE); + } + + /** + * Constructs a new adapter wrapping the specified input stream using the + * default buffer size. + * + * @param previousInput The input stream to read untransformed data from. + */ + public OutputToInputStreamAdapter(final InputStream previousInput) { + this(previousInput, DEFAULT_BUF_SIZE); + } + + @Override + public int read() throws IOException { + ensureData(); + return bais.read(); + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + ensureData(); + return bais.read(b, off, len); + } + + @Override + public void close() throws IOException { + previousInput.close(); + } + + /** + * Reads untransformed data from {@code previousInput}, writes it to + * {@code transformationOut} for processing, and buffers the transformed data in + * {@code baos} for reading. + * + * @throws IOException If an I/O error occurs during reading or transformation. + */ + private void ensureData() throws IOException { + if (bais.available() > 0 || baos == null) { + return; + } + + bais = null; // NOPMD + + final byte[] chunk = previousInput.readNBytes(BUF_SIZE); + if (chunk.length == 0) { + transformationOut.close(); + final byte[] finalBytes = baos.toByteArray(); + bais = new ByteArrayInputStream(finalBytes); + baos = null; + return; + } else { + transformationOut.write(chunk); + } + + final byte[] chunkBytes = baos.toByteArray(); + bais = new ByteArrayInputStream(chunkBytes); + baos.reset(); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/util/Pack7LStreamWriter.java b/lib/src/main/java/zeroecho/sdk/util/Pack7LStreamWriter.java new file mode 100644 index 0000000..a0dc1c6 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/util/Pack7LStreamWriter.java @@ -0,0 +1,188 @@ +/******************************************************************************* + * 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.sdk.util; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A streaming writer that reads a variable-length 7-bit encoded length prefix + * from the input data and writes the subsequent payload to a target + * {@link OutputStream}. + *

+ * The {@link #write(byte[])} method can be called multiple times, even with + * partial data. It will return how many bytes from the input array were + * consumed. + */ +public class Pack7LStreamWriter { + private final OutputStream out; + private boolean lengthDecoded; + private long expectedLength = -1; + private long written; + + // Buffer for partially-read length prefix (max 10 bytes) + private final byte[] lengthBuf = new byte[10]; + private int lengthBufSize; + + /** + * Constructs a new {@code Pack7LStreamWriter}. + * + * @param out the {@link OutputStream} where the payload will be written after + * decoding the length + */ + public Pack7LStreamWriter(OutputStream out) { + if (out == null) { + throw new IllegalArgumentException("OutputStream cannot be null"); + } + this.out = out; + } + + /** + * Writes the given data to the internal buffer or to the output stream once the + * length prefix has been fully decoded. Returns how many bytes from + * {@code data} were consumed. + * + * @param data the input byte array, possibly containing a length prefix and/or + * payload + * @return the number of bytes consumed from {@code data} + * @throws IOException if an I/O error occurs while writing to the output stream + */ + public int write(byte[] data) throws IOException { + return write(data, 0, data.length); + } + + /** + * Processes a chunk of input data starting at the specified offset and up to + * the specified length. + *

+ * On the first call(s), this method reads a variable-length 7-bit encoded + * prefix from the input data, which specifies how many total bytes are expected + * to follow. Once the prefix is fully read, the method writes up to the + * remaining number of payload bytes to the underlying {@link OutputStream}. + *

+ * + *

+ * This method is designed to be called repeatedly with partial or complete + * input buffers. It maintains internal state and will continue consuming input + * until the expected number of payload bytes are written. + *

+ * + * @param data the input byte array containing part of the length prefix + * and/or payload; must not be {@code null} + * @param offset the starting position in the array to read from + * @param length the number of bytes to read from the array + * @return the number of bytes consumed from the input buffer + * @throws IOException if an I/O error occurs during writing or if + * the length prefix is malformed + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} are + * invalid for the given array + */ + public int write(byte[] data, int offset, int length) throws IOException { + int consumed = 0; + + // Step 1: Decode the length prefix + if (!lengthDecoded) { + while (consumed < length) { + byte b = data[offset + consumed]; + lengthBuf[lengthBufSize++] = b; + consumed++; + + if ((b & 0x80) != 0 || lengthBufSize >= 10) { + expectedLength = decodePack7L(lengthBuf, 0, lengthBufSize); + lengthDecoded = true; + break; + } + } + + // If length still not fully read, return now + if (!lengthDecoded) { + return consumed; + } + } + + // Step 2: Write payload + long remaining = expectedLength - written; + int writable = (int) Math.min(remaining, length - consumed); + + if (writable > 0) { + out.write(data, offset + consumed, writable); + consumed += writable; + written += writable; + } + + return consumed; + } + + /** + * Checks whether the payload has been fully written. + * + * @return {@code true} if all expected payload bytes have been written and the + * length has been decoded; {@code false} otherwise. + */ + public boolean isComplete() { + return lengthDecoded && written == expectedLength; + } + + /** + * Decodes a packed 7-bit encoded long from the given byte array. + * + * @param buf the byte array containing the encoded value + * @param offset the offset in the array + * @param length the number of bytes to read + * @return the decoded long value + * @throws IOException if the encoding is malformed + */ + private static long decodePack7L(byte[] buf, int offset, int length) throws IOException { + if (length == 0) { + throw new IOException("Empty length buffer"); + } + + long result = buf[offset] & 0xFF; + if ((buf[offset] & 0x80) != 0) { + return result & 0x7F; + } + + for (int i = 1; i < length; i++) { + int b = buf[offset + i] & 0xFF; + if (b >= 0x80) { // NOPMD + result = (result << 7) | (b & 0x7F); + return result; + } + result = (result << 7) | b; + } + + throw new IOException("Incomplete packed length encoding"); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/util/Password.java b/lib/src/main/java/zeroecho/sdk/util/Password.java new file mode 100644 index 0000000..8995998 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/util/Password.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Utility class for generating random passwords and secure random byte arrays. + *

+ * This class provides methods to: + *

    + *
  • Generate cryptographically secure random byte arrays of specified + * length.
  • + *
  • Generate random passwords composed of characters derived from random + * bytes.
  • + *
+ *

+ * A single instance of {@link SecureRandom} is used unless + * {@code UNSAFE_SINGLE_SECURE} is set to {@code false}, in which case a new + * {@link SecureRandom} instance is created for each operation. + *

+ * This class is thread-safe through use of a {@link ReentrantLock}. + * + * @author Leo Galambos + */ +public final class Password { + /** + * Private constructor to prevent instantiation of this utility class. + */ + private Password() { + // this is a utility class + } + + /** + * Generates a random password by filling the provided byte array with + * cryptographically strong random bytes. The randomness is influenced by a + * combination of a user-supplied seed string and a randomly generated salt, + * ensuring that the result is both secure and non-deterministic across multiple + * invocations with the same input. + *

+ * Internally, the method uses the SHA-256 digest of the concatenation of the + * seed and a 16-byte random salt to derive a seed for a {@link SecureRandom} + * instance. This seed is mixed into the internal state of the + * {@code SecureRandom} generator to produce a high-entropy, unpredictable byte + * sequence. As a result, the output differs each time the method is called, + * even with the same seed and password buffer. + *

+ * Note: The generated salt is not returned or stored. If you require + * reproducible output or need to verify the result later, you must persist the + * salt separately. + * + * @param password the byte array to be filled with random password bytes; must + * not be {@code null} + * @param seed a user-supplied string used to influence the randomness + * generation; must not be {@code null} + * @return the same {@code password} byte array, now filled with + * cryptographically strong random data + * @throws NoSuchAlgorithmException if the SHA-256 digest or strong + * {@code SecureRandom} implementation is not + * available + * @throws NullPointerException if {@code password} or {@code seed} is + * {@code null} + */ + public static byte[] generateRandom(final byte[] password, final String seed) throws NoSuchAlgorithmException { + final byte[] salt = new byte[16]; + RandomSupport.getRandom().nextBytes(salt); + + // Combine input + salt + final byte[] seedBytes = seed.getBytes(StandardCharsets.UTF_8); + final byte[] combined = new byte[seedBytes.length + salt.length]; + System.arraycopy(seedBytes, 0, combined, 0, seedBytes.length); + System.arraycopy(salt, 0, combined, seedBytes.length, salt.length); + + // Derive seed using SHA-256 + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + final byte[] rndSeed = digest.digest(combined); + + // Seed SecureRandom (deterministic per run, but uses a fresh salt) + final SecureRandom seededRandom = SecureRandom.getInstanceStrong(); + seededRandom.setSeed(rndSeed); // mixes with internal state + + // Generate random output + seededRandom.nextBytes(password); + + return password; + } + + /** + * Generates a cryptographically secure random password consisting of printable + * ASCII characters. + * + * @param length the desired length of the password; must be positive + * @return a random password string using printable characters in the ASCII + * range 33–126 + * @throws IllegalArgumentException if {@code length} is less than or equal to + * zero + */ + public static String generatePrintablePassword(final int length) { + if (length <= 0) { + throw new IllegalArgumentException("Password length must be greater than zero"); + } + + final StringBuilder password = new StringBuilder(length); + for (int i = 0; i < length; i++) { + final int ascii = RandomSupport.getRandom().nextInt('~' - '!' + 1) + '!'; + password.append((char) ascii); + } + + return password.toString(); + } +} diff --git a/lib/src/main/java/zeroecho/sdk/util/RandomSupport.java b/lib/src/main/java/zeroecho/sdk/util/RandomSupport.java new file mode 100644 index 0000000..592742e --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/util/RandomSupport.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.util; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utility class providing support for secure random number generation. + *

+ * This class encapsulates logic for generating cryptographically secure random + * data using Java's {@link SecureRandom}. It optionally supports a singleton + * instance pattern for the {@code SecureRandom} generator, controlled by the + * {@code UNSAFE_SINGLE_SECURE} flag. + *

+ * + *

+ * Note: Reusing a single {@code SecureRandom} instance can + * improve performance, but may reduce randomness guarantees in some + * multi-threaded or long-lived contexts. Always assess your threat model and + * performance needs when toggling {@code UNSAFE_SINGLE_SECURE}. + *

+ * + * @author Leo Galambos + */ +public final class RandomSupport { + private static final Logger LOG = Logger.getLogger(RandomSupport.class.getName()); + + /** Flag indicating whether a single {@link SecureRandom} instance is reused. */ + private final static boolean UNSAFE_SINGLE_SECURE = true; + /** Lock for thread-safe access to the {@link SecureRandom} instance. */ + private static Lock instanceLock = new ReentrantLock(); + /** Shared {@link SecureRandom} instance. */ + private static SecureRandom RANDOM; + + static { + try { + RANDOM = SecureRandom.getInstanceStrong(); + } catch (NoSuchAlgorithmException e) { + LOG.logp(Level.WARNING, "Password", "", "NoSuchAlgorithmException", e); + RANDOM = new SecureRandom(); + } + } + + private RandomSupport() { + // this is a utility class + } + + /** + * Retrieves a {@link SecureRandom} instance. + *

+ * If {@code UNSAFE_SINGLE_SECURE} is true, returns a shared instance; + * otherwise, creates a new {@link SecureRandom} instance. + * + * @return A {@link SecureRandom} instance for random number generation. + */ + public static SecureRandom getRandom() { + instanceLock.lock(); + try { + if (UNSAFE_SINGLE_SECURE) { + return RANDOM; + } else { + LOG.log(Level.INFO, "creating a new SecureRandom"); + return new SecureRandom(); + } + } finally { + instanceLock.unlock(); + } + } + + /** + * Generates a secure random byte array of the specified size. + * + * @param size The size of the byte array to generate. + * @return A byte array filled with cryptographically secure random bytes. + */ + public static byte[] generateRandom(final int size) { + return generateRandom(new byte[size]); + } + + /** + * Fills the given byte array with random bytes and returns it. + *

+ * This method uses a thread-safe {@code SecureRandom} instance to generate + * cryptographically strong random values. + * + * @param buffer the byte array to fill with random bytes + * @return the same byte array, now containing random data + * @throws NullPointerException if {@code buffer} is {@code null} + */ + public static byte[] generateRandom(final byte[] buffer) { + getRandom().nextBytes(buffer); + return buffer; + } +} diff --git a/lib/src/main/java/zeroecho/sdk/util/X509Support.java b/lib/src/main/java/zeroecho/sdk/util/X509Support.java new file mode 100644 index 0000000..68ee094 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/util/X509Support.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * 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.sdk.util; + +import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; + +/** + * Utility class providing support for working with X.509 certificates, private + * keys, and certification requests using PEM-encoded files and the Bouncy + * Castle library. + *

+ * This class includes static helper methods to: + *

    + *
  • Load an X.509 certificate from a PEM file
  • + *
  • Load a PKCS#10 certification request (CSR) from a PEM file
  • + *
  • Print a {@link java.security.PrivateKey} in PEM format
  • + *
  • Print a {@link java.security.cert.Certificate}, including its PEM-encoded + * form
  • + *
+ *

+ * All methods are static and the class cannot be instantiated. + * + *

+ * Note: This class relies on the Bouncy Castle library and assumes the + * Bouncy Castle provider is correctly configured. + * + * @author Leo Galambos + */ +public final class X509Support { + private static final Logger LOG = Logger.getLogger(X509Support.class.getName()); + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private X509Support() { + // this is a utility class + } + + /** + * Loads an X509Certificate from a PEM file. + * + * @param path the file path to the certificate PEM file + * @return the loaded {@link X509Certificate} or null if invalid + * @throws IOException if an I/O error occurs reading the file + */ + public static Certificate loadCertificate(final String path) throws IOException { + try (PEMParser pemParser = new PEMParser(Files.newBufferedReader(Paths.get(path), StandardCharsets.UTF_8))) { + final Object obj = pemParser.readObject(); + if (obj instanceof org.bouncycastle.cert.X509CertificateHolder) { + final org.bouncycastle.cert.X509CertificateHolder holder = (org.bouncycastle.cert.X509CertificateHolder) obj; + return new org.bouncycastle.cert.jcajce.JcaX509CertificateConverter() + .setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(holder); + } else { + System.err.println("File does not contain a valid X509 certificate."); + return null; + } + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error reading certificate file: {0}", path); + throw e; + } catch (CertificateException e) { + LOG.log(Level.SEVERE, "Error reading certificate file: {0}", path); + return null; + } + } + + /** + * Utility method to print a {@link PrivateKey} in PEM format to standard + * output. + *

+ * This method writes the private key using a {@link JcaPEMWriter} and prints it + * to the console in PEM (Base64-encoded) format. If an error occurs during the + * writing process, it logs a warning. + * + * @param privKey the {@link PrivateKey} to print + */ + public static void printPrivateKey(final PrivateKey privKey) { + try (StringWriter sw = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(sw)) { + pemWriter.writeObject(privKey); + pemWriter.flush(); + System.out.println("Private Key (PEM):\n" + sw.toString()); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to print private key", e); + } + } + + /** + * Prints the certificate details to standard output, including its PEM encoded + * representation. + * + * @param cert the {@link Certificate} to print + */ + public static void printCertificate(final Certificate cert) { + try { + System.out.println("Certificate:"); + System.out.println(cert.toString()); + + // Print PEM encoded certificate + try (StringWriter sw = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(sw)) { + pemWriter.writeObject(cert); + pemWriter.flush(); + System.out.println(sw.toString()); + } + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to print certificate", e); + } + } + + /** + * Loads a PKCS#10 Certification Request (CSR) from a PEM file. + * + * @param path the file path to the CSR PEM file + * @return the loaded {@link PKCS10CertificationRequest} or null if invalid + * @throws IOException if an I/O error occurs reading the file + */ + public static PKCS10CertificationRequest loadCSR(final String path) throws IOException { + try (PEMParser pemParser = new PEMParser(Files.newBufferedReader(Paths.get(path), StandardCharsets.UTF_8))) { + final Object obj = pemParser.readObject(); + if (obj instanceof PKCS10CertificationRequest) { + return (PKCS10CertificationRequest) obj; + } else { + System.err.println("File does not contain a valid PKCS#10 CSR."); + return null; + } + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error reading CSR file: {0}", path); + throw e; + } + } +} diff --git a/lib/src/main/java/zeroecho/sdk/util/package-info.java b/lib/src/main/java/zeroecho/sdk/util/package-info.java new file mode 100644 index 0000000..39841d1 --- /dev/null +++ b/lib/src/main/java/zeroecho/sdk/util/package-info.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +/** + * Utility helpers for provider setup, key derivation, randomness, encoding, and + * X.509 handling. + * + *

+ * This package gathers small, focused utilities used across the SDK: activation + * of Bouncy Castle providers, RFC 5869 HKDF helpers, adapters that expose + * output-style transforms as readable streams, streaming decoders for + * length-prefixed payloads, password helpers, random-byte support, and simple + * X.509 PEM utilities. + *

+ * + *

Key elements

+ *
    + *
  • {@link BouncyCastleActivator} - one-time registration of Bouncy Castle + * providers. A static initializer adds providers; + * {@link BouncyCastleActivator#init()} can be called explicitly to confirm + * activation.
  • + *
  • {@link Kdf} - HKDF-SHA-256 derivation utilities for producing symmetric + * keys from KEM or agreement secrets.
  • + *
  • {@link OutputToInputStreamAdapter} - base class that adapts an + * {@link java.io.OutputStream} driven transformation into an + * {@link java.io.InputStream}; subclasses provide the transforming output and + * the adapter exposes transformed bytes as they become available.
  • + *
  • {@link Pack7LStreamWriter} - incremental writer that consumes a 7-bit + * packed length prefix, then copies exactly that many payload bytes to a target + * stream.
  • + *
  • {@link Password} - helpers for generating random bytes and printable + * passwords.
  • + *
  • {@link RandomSupport} - shared or per-call + * {@link java.security.SecureRandom} access with thread-safe helpers for + * filling arrays.
  • + *
  • {@link X509Support} - minimal PEM-based load and print helpers for + * certificates, private keys, and certificate signing requests.
  • + *
+ * + *

Typical usage

{@code
+ * // Ensure Bouncy Castle providers are present.
+ * zeroecho.sdk.util.BouncyCastleActivator.init();
+ *
+ * // Derive 32 bytes with HKDF-SHA-256 for a symmetric key.
+ * byte[] key = zeroecho.sdk.util.Kdf.hkdfSha256(ikm, salt, info, 32);
+ * }
+ * + *

Design notes

+ *
    + *
  • Utilities avoid retaining sensitive material and prefer cloning caller + * arrays where appropriate.
  • + *
  • Streaming adapters do not buffer whole payloads except where transform + * semantics require it.
  • + *
  • Provider setup is explicit and emits concise diagnostics to aid + * development.
  • + *
+ * + * @since 1.0 + */ +package zeroecho.sdk.util; diff --git a/lib/src/main/javadoc/css/overview.css b/lib/src/main/javadoc/css/overview.css new file mode 100644 index 0000000..08561b6 --- /dev/null +++ b/lib/src/main/javadoc/css/overview.css @@ -0,0 +1,92 @@ +/* ZeroEcho overview styles - loaded via --add-stylesheet */ + +/* Group header cells */ +table.zecat { + border-collapse: collapse; +} + +table.zecat caption { + background: #000000; + color: #ffffff; +} + +/* Group header cells */ +table.zecat th { + text-align: center; + background: #dfdfdf; + font-weight: 700; +} + +table.zecat thead { + border-bottom: 3px solid #555; +} + +/* Make grouped subcolumns narrow and centered */ +table.zecat th.vcol, +table.zecat td.narrow { + text-align: center; + background: #efefef; + vertical-align: middle; + width: 28px; /* adjust as needed */ + min-width: 24px; + padding: 2px 4px; +} + +/* Vertical label inside subheader cells */ +table.zecat th.vcol .vlabel { + display: inline-block; + padding: 4px 4px; + writing-mode: vertical-rl; /* preferred where supported */ + transform: rotate(180deg); /* keep upright when vertical-rl flips glyphs */ + white-space: nowrap; + line-height: 1; +} + +/* Fallback for engines that ignore writing-mode */ +@supports not (writing-mode: vertical-rl) { + table.zecat th.vcol .vlabel { + transform: rotate(-90deg); + transform-origin: center center; + display: inline-block; + } +} + +/* Keep checkmark cells tidy */ +table.zecat td.zecenter { + text-align: center; +} + +/* Thin separator under each tbody row */ +table.zecat tbody td { + border-bottom: 1px solid #e0e0e0; +} + +/* + table.zecat tbody tr:last-child td { border-bottom: 0; } +*/ + +/* Thick vertical separators between major columns only */ +table.zecat th.display-col, +table.zecat td.display-td { + border-left: 1px solid #555; +} + +table.zecat th.group { + border-left: 1px solid #555; +} + +table.zecat th.fam-first, +table.zecat td.fam-first-td { + border-left: 1px solid #555; /* between Display and Family group */ +} + +table.zecat th.usage-first, +table.zecat td.usage-first-td { + border-left: 1px solid #555; /* between Family and Key usage group */ +} + +table.zecat th.default-col, +table.zecat td.default-td { + border-left: 1px solid #555; /* between Key usage group and Default spec */ +} + diff --git a/lib/src/main/javadoc/overview.html b/lib/src/main/javadoc/overview.html new file mode 100644 index 0000000..0785478 --- /dev/null +++ b/lib/src/main/javadoc/overview.html @@ -0,0 +1,68 @@ + + + + + ZeroEcho Library Overview + + +

ZeroEcho Library

+ +

+ ZeroEcho is a layered cryptography library delivered as the lib module. It exposes a minimal + core of cryptographic primitives and a higher-level SDK of composition tools. The design favors clear + separation of responsibilities, safe defaults, and extensibility for future algorithms and pipelines. +

+ + + +

Layering

+
    +
  • Core (zeroecho.core): low-level cryptographic engine. + This includes algorithm definitions, stateful contexts, registry and metadata, specification and SPI + contracts, and selected helpers required by algorithms (e.g., I/O, marshalling, tagging, auditing, policies). +
  • +
  • SDK (zeroecho.sdk): developer-facing composition layer built on the core. + It provides content abstractions, fluent builders (both generic and per-algorithm), utilities for + multi-recipient or composed flows, and supporting helpers used by those builders. +
  • +
+ +

Package map (lib)

+
    +
  • zeroecho.core +
      +
    • alg: concrete algorithms and small shared helpers used by algorithms.
    • +
    • context: stateful operation interfaces (encryption, signatures, digests, MAC, agreement, KEM, and related variants).
    • +
    • spec / spi: specification objects and construction/factory contracts.
    • +
    • policy / audit / err / annotation: cross-cutting concerns within the core.
    • +
    • io / marshal / tag: helpers used by algorithms and contexts at runtime.
    • +
    • (root): provider/registry surface and core metadata.
    • +
    +
  • +
  • zeroecho.sdk +
      +
    • content: content abstractions, basic implementations, and export facilities.
    • +
    • builders: composition APIs, including generic builders and per-algorithm builders.
    • +
    • guard: utilities for multi-recipient and other composed workflows.
    • +
    • io / logging: supporting helpers used by the SDK layer and applications.
    • +
    +
  • +
+ +

Design principles

+
    +
  • Stratification: the core remains focused on algorithms and correctness; the SDK focuses on developer ergonomics and composition.
  • +
  • Composability: data flows are constructed through builders and content abstractions with predictable, chainable behavior.
  • +
  • Extensibility: new algorithms, formats, and flows can be added with minimal impact on existing code.
  • +
  • Safety: role-based binding and policy checks promote safe defaults and clear intent.
  • +
+ +

Intended use

+

+ Third-party applications depend on the lib module. Most integrations work at the SDK layer to compose + data pipelines, while the core layer provides the cryptographic foundation and guarantees. The project’s + structure and documentation aim to make entry points, responsibilities, and extension points explicit. +

+ + + \ No newline at end of file diff --git a/lib/src/main/resources/META-INF/services/zeroecho.core.CryptoAlgorithm b/lib/src/main/resources/META-INF/services/zeroecho.core.CryptoAlgorithm new file mode 100644 index 0000000..05527b9 --- /dev/null +++ b/lib/src/main/resources/META-INF/services/zeroecho.core.CryptoAlgorithm @@ -0,0 +1,23 @@ +zeroecho.core.alg.aes.AesAlgorithm +zeroecho.core.alg.bike.BikeAlgorithm +zeroecho.core.alg.chacha.ChaCha20Poly1305Algorithm +zeroecho.core.alg.chacha.ChaChaAlgorithm +zeroecho.core.alg.cmce.CmceAlgorithm +zeroecho.core.alg.dh.DhAlgorithm +zeroecho.core.alg.digest.Sha2Sha3Algorithm +zeroecho.core.alg.ecdh.EcdhAlgorithm +zeroecho.core.alg.ecdsa.EcdsaAlgorithm +zeroecho.core.alg.ed448.Ed448Algorithm +zeroecho.core.alg.ed25519.Ed25519Algorithm +zeroecho.core.alg.elgamal.ElgamalAlgorithm +zeroecho.core.alg.frodo.FrodoAlgorithm +zeroecho.core.alg.hmac.HmacAlgorithm +zeroecho.core.alg.hqc.HqcAlgorithm +zeroecho.core.alg.kyber.KyberAlgorithm +zeroecho.core.alg.ntru.NtruAlgorithm +zeroecho.core.alg.ntruprime.NtrulPrimeAlgorithm +zeroecho.core.alg.ntruprime.SntruPrimeAlgorithm +zeroecho.core.alg.rsa.RsaAlgorithm +zeroecho.core.alg.saber.SaberAlgorithm +zeroecho.core.alg.sphincsplus.SphincsPlusAlgorithm +zeroecho.core.alg.xdh.XdhAlgorithm diff --git a/lib/src/main/resources/jul.properties b/lib/src/main/resources/jul.properties new file mode 100644 index 0000000..79d78cb --- /dev/null +++ b/lib/src/main/resources/jul.properties @@ -0,0 +1,13 @@ +handlers = java.util.logging.ConsoleHandler +.level = INFO + +zeroecho.core.tag.ByteVerificationStrategy.level = FINE +zeroecho.core.tag.SignatureVerificationStrategy.level = FINE + +# Console handler uses our one-line formatter +java.util.logging.ConsoleHandler.level = ALL +java.util.logging.ConsoleHandler.formatter = zeroecho.logging.CompactOneLineFormatter + +# Optional: quiet noisy default loggers +sun.util.logging.level = WARNING +jdk.event.security.level = WARNING diff --git a/lib/src/test/java/zeroecho/core/CatalogContractTest.java b/lib/src/test/java/zeroecho/core/CatalogContractTest.java new file mode 100644 index 0000000..2c84d71 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/CatalogContractTest.java @@ -0,0 +1,491 @@ +/******************************************************************************* + * 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.core; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.SecretKey; +import javax.xml.XMLConstants; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import zeroecho.core.audit.JulAuditListenerStd; +import zeroecho.core.context.DigestContext; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.KemContext; +import zeroecho.core.context.MacContext; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.io.TailStrippingInputStream; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.sdk.util.BouncyCastleActivator; + +public class CatalogContractTest { + + static Level effectiveLevel(Logger lg) { + for (Logger x = lg; x != null; x = x.getParent()) { + if (x.getLevel() != null) { + return x.getLevel(); + } + } + return null; + } + + static void dump(String name) { + Logger lg = Logger.getLogger(name); + System.out.println("[" + name + "] level=" + lg.getLevel() + " effective=" + effectiveLevel(lg) + " useParent=" + + lg.getUseParentHandlers()); + for (Handler h : lg.getHandlers()) { + System.out.println(" handler=" + h.getClass().getSimpleName() + " level=" + h.getLevel()); + } + } + + @BeforeAll + public static void setupProvider() { + BouncyCastleActivator.init(); + + Logger jul = Logger.getLogger("zeroecho.audit"); + jul.setLevel(Level.FINE); // see PROGRESS at FINE + + CryptoAlgorithms.setAuditListener(JulAuditListenerStd.builder().logger(jul).infoLevel(Level.INFO) + .warnLevel(Level.WARNING).progressLevel(Level.FINE).includeStackTraces(true).build()); + CryptoAlgorithms.setAuditMode(CryptoAlgorithms.AuditMode.WRAP); + + dump(""); + dump("zeroecho.core.audit"); + } + + private static void logBegin(Object... params) { + String thisClass = CatalogContractTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = CatalogContractTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static String prettyPrintXml(String xml) throws TransformerException { + TransformerFactory tf = TransformerFactory.newInstance(); + tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + try { + tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + } catch (IllegalArgumentException ignored) { + } + try { + tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + } catch (IllegalArgumentException ignored) { + } + + Transformer t = tf.newTransformer(); + t.setOutputProperty(OutputKeys.METHOD, "xml"); + t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); + t.setOutputProperty(OutputKeys.INDENT, "yes"); + t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + StringWriter out = new StringWriter(); + t.transform(new StreamSource(new StringReader(xml)), new StreamResult(out)); + return out.toString(); + } + + // ---------- Tests ---------- + + @Test + void catalogSane() throws TransformerException { + logBegin(); + CryptoCatalog cat = CryptoCatalog.load(); + System.out.println(prettyPrintXml(cat.toXml())); + assertDoesNotThrow(cat::validate); + assertTrue(CryptoAlgorithms.available().size() > 0); + logEnd(); + } + + @Test + void genericRoundTrips() throws Exception { + logBegin(); + byte[] msg = "roundtrip".getBytes(); + CryptoAlgorithms.setAuditMode(CryptoAlgorithms.AuditMode.WRAP); + + for (String id : CryptoAlgorithms.available()) { + CryptoAlgorithm alg = CryptoAlgorithms.require(id); + + // SIGN/VERIFY (asymmetric) + if (alg.roles().contains(KeyUsage.SIGN) && alg.roles().contains(KeyUsage.VERIFY) + && !alg.asymmetricBuildersInfo().isEmpty()) { + trySignVerify(id, msg); + System.out.println(); + } + + // ENCRYPT/DECRYPT + boolean hasSym = !alg.symmetricBuildersInfo().isEmpty(); + boolean hasAsym = !alg.asymmetricBuildersInfo().isEmpty(); + if (alg.roles().contains(KeyUsage.ENCRYPT) && alg.roles().contains(KeyUsage.DECRYPT)) { + if (hasSym || hasAsym) { + tryEncryptDecrypt(id, msg, hasSym, hasAsym); + System.out.println(); + } + } + + // KEM + if (alg.roles().contains(KeyUsage.ENCAPSULATE) && alg.roles().contains(KeyUsage.DECAPSULATE) + && !alg.asymmetricBuildersInfo().isEmpty()) { + tryKem(id, msg); + System.out.println(); + } + + // DIGEST + if (alg.roles().contains(KeyUsage.DIGEST)) { + tryDigest(id, msg); + System.out.println(); + } + + // MAC (single role now; verification via setExpectedTag) + if (alg.roles().contains(KeyUsage.MAC) && !alg.symmetricBuildersInfo().isEmpty()) { + tryMac(id, msg); + System.out.println(); + } + } + logEnd(); + } + + // ---------- Helpers: SIGN/VERIFY ---------- + + private void trySignVerify(String id, byte[] msg) throws Exception { + logBegin(id, Integer.valueOf(msg.length)); + KeyPair kp = tryKeyPairWithDefaultSpec(CryptoAlgorithms.require(id)); + if (kp == null) { + logEnd(); + return; + } + + // SIGN: produce [body][signature] and capture trailer + SignatureContext signer = CryptoAlgorithms.create(id, KeyUsage.SIGN, kp.getPrivate(), null); + final byte[][] sigHolder = new byte[1][]; + final int sigLen = signer.tagLength(); + try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen, 8192) { + @Override + protected void processTail(byte[] tail) throws IOException { + sigHolder[0] = (tail == null ? null : tail.clone()); + } + }) { + assertArrayEquals(msg, readAll(in)); + } finally { + signer.close(); + } + byte[] sig = sigHolder[0]; + assertNotNull(sig, "signature missing"); + assertTrue(sig.length > 0, "signature empty"); + + // VERIFY: supply signature via setExpectedTag and drain (throws on mismatch) + SignatureContext verifier = CryptoAlgorithms.create(id, KeyUsage.VERIFY, kp.getPublic(), null); + verifier.setExpectedTag(sig); + try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) { + readAll(verIn); + } finally { + verifier.close(); + } + logEnd(); + } + + // ---------- Helpers: ENCRYPT/DECRYPT ---------- + + private void tryEncryptDecrypt(String id, byte[] msg, boolean hasSym, boolean hasAsym) throws Exception { + logBegin(id, Integer.valueOf(msg.length)); + CryptoAlgorithm alg = CryptoAlgorithms.require(id); + + if (hasSym) { + SecretKey sk = tryGenerateSecretWithDefaultSpec(alg); + if (sk != null) { + conflux.CtxInterface session = conflux.Ctx.INSTANCE.getContext("encdec-" + System.nanoTime()); + + EncryptionContext enc = CryptoAlgorithms.create(id, KeyUsage.ENCRYPT, sk, null); + if (enc instanceof zeroecho.core.spi.ContextAware ca) { + ca.setContext(session); + } + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + EncryptionContext dec = CryptoAlgorithms.create(id, KeyUsage.DECRYPT, sk, null); + if (dec instanceof zeroecho.core.spi.ContextAware ca2) { + ca2.setContext(session); + } + byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); + dec.close(); + + assertArrayEquals(msg, pt, "decrypt mismatch (symmetric) for " + id); + logEnd(); + return; + } + } + + if (hasAsym) { + KeyPair kp = tryKeyPairWithDefaultSpec(alg); + if (kp != null) { + EncryptionContext enc = CryptoAlgorithms.create(id, KeyUsage.ENCRYPT, kp.getPublic(), null); + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + EncryptionContext dec = CryptoAlgorithms.create(id, KeyUsage.DECRYPT, kp.getPrivate(), null); + byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); + dec.close(); + + assertArrayEquals(msg, pt, "decrypt mismatch (asymmetric) for " + id); + } + } + logEnd(); + } + + // ---------- Helpers: KEM ---------- + + private void tryKem(String id, byte[] msg) throws Exception { // msg unused; kept for symmetry/logging + logBegin(id); + KeyPair kp = tryKeyPairWithDefaultSpec(CryptoAlgorithms.require(id)); + if (kp == null) { + logEnd(); + return; + } + + KemContext pub = CryptoAlgorithms.create(id, KeyUsage.ENCAPSULATE, kp.getPublic(), null); + KemContext prv = CryptoAlgorithms.create(id, KeyUsage.DECAPSULATE, kp.getPrivate(), null); + + KemContext.KemResult res = pub.encapsulate(); + byte[] ss = prv.decapsulate(res.ciphertext()); + + assertArrayEquals(res.sharedSecret(), ss, "KEM shared secret mismatch"); + + pub.close(); + prv.close(); + logEnd(); + } + + // ---------- Helpers: DIGEST (streaming via trailer) ---------- + + private void tryDigest(String id, byte[] msg) throws Exception { + logBegin(id, Integer.valueOf(msg.length)); + + DigestContext dctx = CryptoAlgorithms.create(id, KeyUsage.DIGEST, NullKey.INSTANCE, null); + final byte[][] digestHolder = new byte[1][]; + final int tagLen = dctx.tagLength(); + + // Produce digest as trailer and capture + try (InputStream in = new TailStrippingInputStream(dctx.wrap(new ByteArrayInputStream(msg)), tagLen, 8192) { + @Override + protected void processTail(byte[] tail) throws IOException { + digestHolder[0] = (tail == null ? null : tail.clone()); + } + }) { + assertArrayEquals(msg, readAll(in)); // passthrough body unchanged + } finally { + dctx.close(); + } + + byte[] digest = digestHolder[0]; + assertNotNull(digest); + assertTrue(digest.length > 0); + logEnd(); + } + + // ---------- Helpers: MAC (produce + verify) ---------- + + private void tryMac(String id, byte[] msg) throws Exception { + logBegin(id, Integer.valueOf(msg.length)); + + SecretKey sk = tryGenerateSecretWithDefaultSpec(CryptoAlgorithms.require(id)); + if (sk == null) { + logEnd(); + return; + } + + // Produce tag: [body][tag], capture trailer + MacContext mac = CryptoAlgorithms.create(id, KeyUsage.MAC, sk, null); + final byte[][] tagHolder = new byte[1][]; + final int tagLen = mac.tagLength(); + try (InputStream in = new TailStrippingInputStream(mac.wrap(new ByteArrayInputStream(msg)), tagLen, 8192) { + @Override + protected void processTail(byte[] tail) throws IOException { + tagHolder[0] = (tail == null ? null : tail.clone()); + } + }) { + assertArrayEquals(msg, readAll(in)); + } finally { + mac.close(); + } + byte[] tag = tagHolder[0]; + assertNotNull(tag); + assertTrue(tag.length > 0); + + // Verify: provide expected tag and drain (throws on mismatch) + MacContext ver = CryptoAlgorithms.create(id, KeyUsage.MAC, sk, null); + ver.setExpectedTag(tag); + try (InputStream in = ver.wrap(new ByteArrayInputStream(msg))) { + readAll(in); + } finally { + ver.close(); + } + + logEnd(); + } + + // ---------- Utility: readAll ---------- + + private static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + // ---------- Utility: discover default key material ---------- + + private KeyPair tryKeyPairWithDefaultSpec(CryptoAlgorithm alg) { + logBegin(alg.id()); + try { + List infos = alg.asymmetricBuildersInfo(); + if (infos.isEmpty()) { + System.out.println("no asymmetric builder info"); + logEnd(); + return null; + } + + for (CryptoAlgorithm.AsymBuilderInfo bi : infos) { + if (bi.defaultKeySpec == null) { + continue; + } + try { + @SuppressWarnings("unchecked") + Class specType = (Class) bi.specType; + AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec; + + AsymmetricKeyBuilder builder = alg.asymmetricKeyBuilder(specType); + System.out.println("...building with " + specType.getName()); + KeyPair kp = builder.generateKeyPair(spec); + if (kp != null) { + logEnd(); + return kp; + } + } catch (UnsupportedOperationException e) { + // import-only + } catch (Throwable t) { + System.out.println("builder " + bi.specType.getSimpleName() + " failed to generate keypair: " + + t.getClass().getSimpleName() + ": " + t.getMessage()); + } + } + + System.out.println("no builder with working default keygen"); + logEnd(); + return null; + } catch (Throwable t) { + System.out.println("...*** SKIP *** keyPair cannot be generated: " + t); + return null; + } + } + + private SecretKey tryGenerateSecretWithDefaultSpec(CryptoAlgorithm alg) { + logBegin(alg.id()); + try { + List infos = alg.symmetricBuildersInfo(); + if (infos.isEmpty()) { + System.out.println("no symmetric builder info"); + logEnd(); + return null; + } + + for (CryptoAlgorithm.SymBuilderInfo bi : infos) { + if (bi.defaultKeySpec() == null) { + continue; + } + try { + @SuppressWarnings("unchecked") + Class specType = (Class) bi.specType(); + AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec(); + + SymmetricKeyBuilder builder = alg.symmetricKeyBuilder(specType); + SecretKey sk = builder.generateSecret(spec); + if (sk != null) { + logEnd(); + return sk; + } + } catch (UnsupportedOperationException e) { + // import-only + } catch (Throwable t) { + System.out.println("symmetric builder " + bi.specType().getSimpleName() + + " failed to generate secret: " + t.getClass().getSimpleName() + ": " + t.getMessage()); + } + } + + System.out.println("no builder with working default secret generation"); + logEnd(); + return null; + } catch (Throwable t) { + System.out.println("...*** SKIP *** secret cannot be generated: " + t); + return null; + } + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java b/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java new file mode 100644 index 0000000..e125a99 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/aes/AesGcmCrossCheckTest.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * 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.core.alg.aes; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import conflux.Ctx; +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spi.ContextAware; + +public class AesGcmCrossCheckTest { + + // ---- logging helpers (same pattern you use elsewhere) ---- + + private static void logBegin(Object... params) { + String thisClass = AesGcmCrossCheckTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = AesGcmCrossCheckTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + @BeforeAll + static void setup() { + // add providers if needed + } + + private static byte[] rand(int n) { + byte[] a = new byte[n]; + new SecureRandom().nextBytes(a); + return a; + } + + private static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + private static byte[] jcaGcmEncrypt(SecretKey key, byte[] iv, int tagBits, byte[] aad, byte[] msg) + throws Exception { + Cipher c = Cipher.getInstance("AES/GCM/NOPADDING"); + c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(tagBits, iv)); + if (aad != null && aad.length > 0) { + c.updateAAD(aad); + } + return c.doFinal(msg); + } + + private static byte[] jcaGcmDecrypt(SecretKey key, byte[] iv, int tagBits, byte[] aad, byte[] ct) throws Exception { + Cipher c = Cipher.getInstance("AES/GCM/NOPADDING"); + c.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(tagBits, iv)); + if (aad != null && aad.length > 0) { + c.updateAAD(aad); + } + return c.doFinal(ct); + } + + @Test + void aesGcm_stream_vs_jca_ctxOnly_crosscheck() throws Exception { + final int SIZE = 32 * 1024 + 777; + final int TAG_BITS = 128; + logBegin(SIZE, "AES/GCM ctx-only cross-check"); + + // --- test vectors --- + byte[] msg = rand(SIZE); + byte[] iv = rand(12); // 12-byte IV for GCM + byte[] aad = "test-aad-123".getBytes(); + + // --- key (either via your builder or direct JCA; both fine) --- + CryptoAlgorithm aesAlg = CryptoAlgorithms.require("AES"); + SecretKey key = aesAlg.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); + // or: + // KeyGenerator kg = KeyGenerator.getInstance("AES"); + // kg.init(256); + // SecretKey key = kg.generateKey(); + + // --- per-test context; store IV/AAD under the names AesCipherContext expects + // --- + CtxInterface session = Ctx.INSTANCE.getContext("aes-gcm-xchk-" + System.nanoTime()); + session.put(ConfluxKeys.iv("AES"), iv); + session.put(ConfluxKeys.aad("AES"), aad); + + AesSpec spec = AesSpec.gcm128(null); + + // === STREAM ENCRYPT === + EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec); + ((ContextAware) enc).setContext(session); + byte[] ct_stream = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + // === JCA ENCRYPT (reference) === + byte[] ct_jca = jcaGcmEncrypt(key, iv, TAG_BITS, aad, msg); + + System.out.printf("ct_stream: %d bytes, ct_jca: %d bytes%n", ct_stream.length, ct_jca.length); + assertArrayEquals(ct_jca, ct_stream, "STREAM ciphertext != JCA ciphertext (IV/AAD/msg must match)"); + + // === STREAM DECRYPT of JCA ciphertext === + EncryptionContext dec1 = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec); + ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx + byte[] pt1 = readAll(dec1.attach(new ByteArrayInputStream(ct_jca))); + dec1.close(); + assertArrayEquals(msg, pt1, "STREAM decrypt(JCA ct) mismatch"); + + // === JCA DECRYPT of STREAM ciphertext === + byte[] pt2 = jcaGcmDecrypt(key, iv, TAG_BITS, aad, ct_stream); + assertArrayEquals(msg, pt2, "JCA decrypt(STREAM ct) mismatch"); + + logEnd(); + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/aes/AesLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/aes/AesLargeDataTest.java new file mode 100644 index 0000000..399fc37 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/aes/AesLargeDataTest.java @@ -0,0 +1,192 @@ +/******************************************************************************* + * 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.core.alg.aes; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import conflux.Ctx; +import conflux.CtxInterface; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spi.ContextAware; + +public class AesLargeDataTest { + + @BeforeAll + static void setup() { + // Providers if needed, e.g.: + // Security.addProvider(new BouncyCastleProvider()); + } + + private static void logBegin(Object... params) { + String thisClass = AesLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = AesLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + private static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + /** + * AES-GCM round-trip using Ctx-only parameters (default; IV is shared in the + * session Ctx). + */ + @Test + void aesGcmLargeData_ctxOnly() throws Exception { + final int SIZE = 32 * 1024 + 777; + logBegin(SIZE, "AES/GCM/NOPADDING (Ctx-only)"); + + byte[] msg = randomBytes(SIZE); + CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); + + SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); + + CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); + + AesSpec spec = AesSpec.gcm128(null); + + EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec); + ((ContextAware) enc).setContext(session); + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + EncryptionContext dec = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec); + ((ContextAware) dec).setContext(session); + byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); + dec.close(); + + assertArrayEquals(msg, pt, "AES-GCM (Ctx-only) mismatch"); + logEnd(); + } + + /** AES-GCM round-trip using an in-band header (codec in Ctx). */ + @Test + void aesGcmLargeData_headerCodec() throws Exception { + final int SIZE = 32 * 1024 + 777; + logBegin(SIZE, "AES/GCM/NOPADDING (HeaderCodec)"); + + byte[] msg = randomBytes(SIZE); + System.out.printf("...input: %d bytes%n", msg.length); + CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); + + SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); + + CtxInterface session = Ctx.INSTANCE.getContext("aes-hdr-" + System.nanoTime()); + AesSpec spec = AesSpec.gcm128(new AesHeaderCodec()); + + EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec); + ((ContextAware) enc).setContext(session); + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + System.out.printf("...encrypted: %d bytes%n", ct.length); + + EncryptionContext dec = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec); + ((ContextAware) dec).setContext(session); + byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); + dec.close(); + System.out.printf("...decrypted: %d bytes%n", pt.length); + + assertArrayEquals(msg, pt, "AES-GCM (HeaderCodec) mismatch"); + logEnd(); + } + + /** AES-CBC/PKCS7Padding round-trip (Ctx-only IV). */ + @Test + void aesCbcPkcs5LargeData_ctxOnly() throws Exception { + final int SIZE = 32 * 1024 + 777; + logBegin(SIZE, "AES/CBC/PKCS7Padding (Ctx-only)"); + + byte[] msg = randomBytes(SIZE); + CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); + + SecretKey key = aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.aes256()); + + CtxInterface session = Ctx.INSTANCE.getContext("aes-cbc-" + System.nanoTime()); + + AesSpec spec = AesSpec.cbcPkcs7(null); + + EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec); + ((ContextAware) enc).setContext(session); + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + EncryptionContext dec = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, key, spec); + ((ContextAware) dec).setContext(session); + byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); + dec.close(); + + assertArrayEquals(msg, pt, "AES-CBC decrypt mismatch"); + logEnd(); + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/chacha/ChaChaLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/chacha/ChaChaLargeDataTest.java new file mode 100644 index 0000000..16ec8c8 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/chacha/ChaChaLargeDataTest.java @@ -0,0 +1,329 @@ +/******************************************************************************* + * 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.core.alg.chacha; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import conflux.Ctx; +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spi.ContextAware; + +/** + * Large-data round-trip tests for ChaCha20 and ChaCha20-Poly1305, mirroring the + * style of AesLargeDataTest. + */ +public class ChaChaLargeDataTest { + + @BeforeAll + static void setup() { + // If you need external providers, add them here, e.g.: + // Security.addProvider(new BouncyCastleProvider()); + } + + private static void logBegin(Object... params) { + String thisClass = ChaChaLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = ChaChaLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + private static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + // ------------------------------------------------------------------------ + // ChaCha20 (stream) + // ------------------------------------------------------------------------ + + /** ChaCha20 round-trip using Ctx-only parameters (nonce shared via Ctx). */ + @Test + void chacha20LargeData_ctxOnly() throws Exception { + final int SIZE = 32 * 1024 + 777; + logBegin(SIZE, "ChaCha20 (Ctx-only)"); + + byte[] msg = randomBytes(SIZE); + CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20"); + SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + + CtxInterface session = Ctx.INSTANCE.getContext("chacha-ctx-" + System.nanoTime()); + ChaChaSpec spec = ChaChaSpec.builder().initialCounter(1).header(null).build(); + + EncryptionContext enc = CryptoAlgorithms.create("CHACHA20", KeyUsage.ENCRYPT, key, spec); + ((ContextAware) enc).setContext(session); + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + EncryptionContext dec = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec); + ((ContextAware) dec).setContext(session); + byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); + dec.close(); + + assertArrayEquals(msg, pt, "ChaCha20 (Ctx-only) mismatch"); + logEnd(); + } + + /** ChaCha20 round-trip using an in-band header (nonce + counter in header). */ + @Test + void chacha20LargeData_headerCodec() throws Exception { + final int SIZE = 32 * 1024 + 777; + logBegin(SIZE, "ChaCha20 (HeaderCodec)"); + + byte[] msg = randomBytes(SIZE); + CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20"); + SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + + CtxInterface session = Ctx.INSTANCE.getContext("chacha-hdr-" + System.nanoTime()); + ChaChaSpec spec = ChaChaSpec.builder().initialCounter(1).header(new ChaChaHeaderCodec()).build(); + + EncryptionContext enc = CryptoAlgorithms.create("CHACHA20", KeyUsage.ENCRYPT, key, spec); + ((ContextAware) enc).setContext(session); + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + EncryptionContext dec = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec); + ((ContextAware) dec).setContext(session); + byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); + dec.close(); + + assertArrayEquals(msg, pt, "ChaCha20 (HeaderCodec) mismatch"); + logEnd(); + } + + /** + * ChaCha20: counter affects the keystream. - Encrypt with counter=7. - Decrypt + * with the same counter → success. - Decrypt with a different counter → + * plaintext differs. + */ + @Test + void chacha20_counterMatters() throws Exception { + final int SIZE = 16 * 1024 + 3; + logBegin(SIZE, "ChaCha20 counter matters"); + + byte[] msg = randomBytes(SIZE); + CryptoAlgorithm chacha = CryptoAlgorithms.require("CHACHA20"); + SecretKey key = chacha.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + + // Encrypt with explicit counter=7 (in ctx), headerless (ctx-only). + CtxInterface encCtx = Ctx.INSTANCE.getContext("chacha-ctr-enc-" + System.nanoTime()); + encCtx.put(ConfluxKeys.tagBits("CHACHA20"), 7); + ChaChaSpec spec = ChaChaSpec.builder().initialCounter(1).header(null).build(); + + EncryptionContext enc = CryptoAlgorithms.create("CHACHA20", KeyUsage.ENCRYPT, key, spec); + ((ContextAware) enc).setContext(encCtx); + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + // Grab the generated nonce from the enc ctx to feed decrypt ctxs + byte[] nonce = encCtx.get(ConfluxKeys.iv("CHACHA20")); + + // Correct decryption (counter=7) + CtxInterface decCtxOk = Ctx.INSTANCE.getContext("chacha-ctr-dec-ok-" + System.nanoTime()); + decCtxOk.put(ConfluxKeys.iv("CHACHA20"), nonce); + decCtxOk.put(ConfluxKeys.tagBits("CHACHA20"), 7); + + EncryptionContext decOk = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec); + ((ContextAware) decOk).setContext(decCtxOk); + byte[] ptOk = readAll(decOk.attach(new ByteArrayInputStream(ct))); + decOk.close(); + assertArrayEquals(msg, ptOk, "ChaCha20 decrypt with correct counter failed"); + + // Wrong decryption (counter=8) → plaintext must differ + CtxInterface decCtxBad = Ctx.INSTANCE.getContext("chacha-ctr-dec-bad-" + System.nanoTime()); + decCtxBad.put(ConfluxKeys.iv("CHACHA20"), nonce); + decCtxBad.put(ConfluxKeys.tagBits("CHACHA20"), 8); + + EncryptionContext decBad = CryptoAlgorithms.create("CHACHA20", KeyUsage.DECRYPT, key, spec); + ((ContextAware) decBad).setContext(decCtxBad); + byte[] ptBad = readAll(decBad.attach(new ByteArrayInputStream(ct))); + decBad.close(); + + assertFalse(Arrays.equals(msg, ptBad), "ChaCha20 decrypt with wrong counter should not match"); + logEnd(); + } + + // ------------------------------------------------------------------------ + // ChaCha20-Poly1305 (AEAD) + // ------------------------------------------------------------------------ + + /** AEAD round-trip using Ctx-only parameters (nonce + AAD in Ctx). */ + @Test + void chacha20Poly1305LargeData_ctxOnly_withAad() throws Exception { + final int SIZE = 32 * 1024 + 777; + logBegin(SIZE, "ChaCha20-Poly1305 (Ctx-only + AAD)"); + + byte[] msg = randomBytes(SIZE); + byte[] aad = "associated-data-ctx-only".getBytes(); + + CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305"); + SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + + CtxInterface session = Ctx.INSTANCE.getContext("chacha-aead-ctx-" + System.nanoTime()); + session.put(ConfluxKeys.aad("CHACHA20-POLY1305"), aad); + + ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build(); + + EncryptionContext enc = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec); + ((ContextAware) enc).setContext(session); + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + EncryptionContext dec = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec); + ((ContextAware) dec).setContext(session); + byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); + dec.close(); + + assertArrayEquals(msg, pt, "ChaCha20-Poly1305 (Ctx-only + AAD) mismatch"); + logEnd(); + } + + /** AEAD round-trip with in-band header; header validates AAD hash. */ + @Test + void chacha20Poly1305LargeData_headerCodec_withAad() throws Exception { + final int SIZE = 32 * 1024 + 777; + logBegin(SIZE, "ChaCha20-Poly1305 (HeaderCodec + AAD)"); + + byte[] msg = randomBytes(SIZE); + byte[] aad = "associated-data-header".getBytes(); + + CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305"); + SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + + CtxInterface session = Ctx.INSTANCE.getContext("chacha-aead-hdr-" + System.nanoTime()); + session.put(ConfluxKeys.aad("CHACHA20-POLY1305"), aad); + + ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(new ChaCha20Poly1305HeaderCodec()).build(); + + EncryptionContext enc = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec); + ((ContextAware) enc).setContext(session); + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + EncryptionContext dec = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec); + ((ContextAware) dec).setContext(session); + byte[] pt = readAll(dec.attach(new ByteArrayInputStream(ct))); + dec.close(); + + assertArrayEquals(msg, pt, "ChaCha20-Poly1305 (HeaderCodec + AAD) mismatch"); + logEnd(); + } + + /** + * AEAD: AAD mismatch must fail verification (expect an IOException from + * doFinal). + */ + @Test + void chacha20Poly1305_aadMismatchFails() throws Exception { + final int SIZE = 8 * 1024 + 9; + logBegin(SIZE, "ChaCha20-Poly1305 AAD mismatch"); + + byte[] msg = randomBytes(SIZE); + byte[] aadEnc = "aad-enc".getBytes(); + byte[] aadDec = "aad-dec-different".getBytes(); + + CryptoAlgorithm aead = CryptoAlgorithms.require("CHACHA20-POLY1305"); + SecretKey key = aead.symmetricKeyBuilder(ChaChaKeyGenSpec.class).generateSecret(ChaChaKeyGenSpec.chacha256()); + + // Encrypt with AAD = aadEnc (ctx-only, no header) + CtxInterface encCtx = Ctx.INSTANCE.getContext("chacha-aead-enc-" + System.nanoTime()); + encCtx.put(ConfluxKeys.aad("CHACHA20-POLY1305"), aadEnc); + + ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder().header(null).build(); + + EncryptionContext enc = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.ENCRYPT, key, spec); + ((ContextAware) enc).setContext(encCtx); + byte[] ct = readAll(enc.attach(new ByteArrayInputStream(msg))); + enc.close(); + + // Decrypt with different AAD → should fail during doFinal() + CtxInterface decCtx = Ctx.INSTANCE.getContext("chacha-aead-dec-" + System.nanoTime()); + decCtx.put(ConfluxKeys.aad("CHACHA20-POLY1305"), aadDec); + // copy the nonce from encCtx + byte[] nonce = encCtx.get(ConfluxKeys.iv("CHACHA20-POLY1305")); + decCtx.put(ConfluxKeys.iv("CHACHA20-POLY1305"), nonce); + + EncryptionContext dec = CryptoAlgorithms.create("CHACHA20-POLY1305", KeyUsage.DECRYPT, key, spec); + ((ContextAware) dec).setContext(decCtx); + + assertThrows(IOException.class, () -> { + @SuppressWarnings("unused") + byte[] ignored = readAll(dec.attach(new ByteArrayInputStream(ct))); + dec.close(); + }, "ChaCha20-Poly1305 should fail with AAD mismatch"); + + logEnd(); + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java b/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java new file mode 100644 index 0000000..12f1a7a --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/common/agreement/AgreementAlgorithmsRoundTripTest.java @@ -0,0 +1,358 @@ +/******************************************************************************* + * 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.core.alg.common.agreement; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.List; + +import javax.crypto.KeyAgreement; +import javax.crypto.interfaces.DHPrivateKey; +import javax.crypto.interfaces.DHPublicKey; +import javax.crypto.spec.DHParameterSpec; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import zeroecho.core.Capability; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.AgreementContext; +import zeroecho.core.context.MessageAgreementContext; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spec.ContextSpec; +import zeroecho.core.spec.VoidSpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.sdk.util.BouncyCastleActivator; + +public class AgreementAlgorithmsRoundTripTest { + + // ----- logging helpers ----- + private static void logBegin(Object... params) { + String thisClass = AgreementAlgorithmsRoundTripTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = AgreementAlgorithmsRoundTripTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static String hex(byte[] b) { + if (b == null) { + return "null"; + } + StringBuilder sb = new StringBuilder(b.length * 2); + for (byte v : b) { + if (sb.length() == 80) { + sb.append("..."); + break; + } + if ((v & 0xFF) < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(v & 0xFF)); + } + return sb.toString(); + } + + private static String keyInfo(String who, Key key) { + if (key == null) { + return who + "=(null)"; + } + String fmt = key.getFormat(); + byte[] enc = key.getEncoded(); + return who + "=(" + key.getAlgorithm() + ", format=" + fmt + ", encodedLen=" + (enc == null ? -1 : enc.length) + + ")"; + } + + @BeforeAll + static void setup() { + // If needed, install/activate providers (e.g., BouncyCastlePQCProvider) here. + BouncyCastleActivator.init(); + } + + @Test + void runAllAgreementAlgos() throws Exception { + logBegin("AGREEMENT sweep"); + + for (String id : CryptoAlgorithms.available()) { + final CryptoAlgorithm alg; + try { + alg = CryptoAlgorithms.require(id); + } catch (Throwable t) { + continue; + } + + final List caps = alg.listCapabilities(); + + for (Capability cap : caps) { + if (cap.role() != KeyUsage.AGREEMENT) { + continue; + } + + System.out.printf(" ...%s - AGREEMENT capability found", id); + + final Class ctxType = cap.contextType(); + final Class keyType = cap.keyType(); + final Class specType = cap.specType(); + @SuppressWarnings("unused") + final ContextSpec defVal = cap.defaultSpec().get(); + + // System.out.printf(" ......type=[%s] key=[%s] spec=[%s] default=[%s]%n", + // ctxType, keyType, specType, defVal); + + // AGREEMENT (KEM-style) via MessageAgreementContext adapter + if (MessageAgreementContext.class.isAssignableFrom(ctxType)) { + + KeyPair bob = generateKeyPair(alg); + if (bob == null) { + System.out.println(" ...bob=null"); + continue; + } + + System.out.println("runAgreement(" + alg.id() + ", VoidSpec)"); + System.out.println(" Bob.public " + keyInfo("key", bob.getPublic())); + System.out.println(" Bob.private " + keyInfo("key", bob.getPrivate())); + + // Alice (initiator): has Bob's public key + MessageAgreementContext aliceCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, + bob.getPublic(), VoidSpec.INSTANCE); + + // Bob (responder): has his private key + MessageAgreementContext bobCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, + bob.getPrivate(), VoidSpec.INSTANCE); + + // Initiator produces encapsulation message (ciphertext) to send + byte[] enc = aliceCtx.getPeerMessage(); + // Responder consumes it + bobCtx.setPeerMessage(enc); + + // Both derive the same shared secret + byte[] kA = aliceCtx.deriveSecret(); + byte[] kB = bobCtx.deriveSecret(); + + System.out.println(" KEM.ciphertext=" + hex(enc)); + System.out.println(" Alice.secret =" + hex(kA)); + System.out.println(" Bob.secret =" + hex(kB)); + + assertArrayEquals(kA, kB, alg.id() + ": agreement secrets mismatch"); + System.out.println("...ok"); + + try { + aliceCtx.close(); + } catch (Exception ignored) { + } + try { + bobCtx.close(); + } catch (Exception ignored) { + } + + break; + } + + // -------- Classic DH/XDH: AgreementContext + real ContextSpec -------- + else if (AgreementContext.class.isAssignableFrom(ctxType) + && ContextSpec.class.isAssignableFrom(specType) && keyType == PrivateKey.class) { + + KeyPair alice; + KeyPair bob; + + alice = generateKeyPair(alg); + bob = generateKeyPair(alg); + + if (alice == null || bob == null) { + continue; + } + + // Prefer the capability's default ContextSpec if provided + ContextSpec spec = null; + try { + ContextSpec def = cap.defaultSpec().get(); + spec = def; + } catch (Throwable ignore) { + spec = tryExtractContextSpec(alg); + } + if (spec == null) { + continue; + } + + System.out.println("runAgreement(" + alg.id() + ", " + spec.getClass().getSimpleName() + ")"); + System.out.println(" Alice.public " + keyInfo("key", alice.getPublic())); + System.out.println(" Alice.private " + keyInfo("key", alice.getPrivate())); + System.out.println(" Bob.public " + keyInfo("key", bob.getPublic())); + System.out.println(" Bob.private " + keyInfo("key", bob.getPrivate())); + + // assertDhCompatible(alice.getPrivate(), bob.getPublic()); + // assertDhCompatible(bob.getPrivate(), alice.getPublic()); + + AgreementContext aCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, alice.getPrivate(), + spec); + AgreementContext bCtx = CryptoAlgorithms.create(alg.id(), KeyUsage.AGREEMENT, bob.getPrivate(), + spec); + aCtx.setPeerPublic(bob.getPublic()); + bCtx.setPeerPublic(alice.getPublic()); + + byte[] zA = aCtx.deriveSecret(); + byte[] zB = bCtx.deriveSecret(); + + System.out.println(" Alice.secret =" + hex(zA)); + System.out.println(" Bob.secret =" + hex(zB)); + + assertArrayEquals(zA, zB, alg.id() + ": DH/XDH secrets mismatch"); + System.out.println("...ok"); + + try { + aCtx.close(); + } catch (IOException ignore) { + } + try { + bCtx.close(); + } catch (IOException ignore) { + } + } + } + } + + logEnd(); + } + + // ----- helpers ----- + private static KeyPair generateKeyPair(CryptoAlgorithm alg) { + try { + for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) { + // System.out.println(" ...keyPair " + bi.getClass().getName()); + if (bi.defaultKeySpec == null) { + // System.out.println(" ......skip default = null --> it was for keyImport"); + continue; + } + @SuppressWarnings("unchecked") + Class specType = (Class) bi.specType; + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(specType); + AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec; + // System.out.println(" ......generated from " + spec); + return b.generateKeyPair(spec); + } + } catch (Throwable ignore) { + } + return null; + } + + private static ContextSpec tryExtractContextSpec(CryptoAlgorithm alg) { + try { + for (Capability c : alg.listCapabilities()) { + if (c.role() == KeyUsage.AGREEMENT && ContextSpec.class.isAssignableFrom(c.specType())) { + try { + return c.defaultSpec().get(); + } catch (Throwable ignore) { + // continue searching + } + } + } + } catch (Throwable ignore) { + } + return null; + } + + public static void assertDhCompatible(PrivateKey priv, PublicKey pub) throws GeneralSecurityException { + if (!(priv instanceof DHPrivateKey)) { + throw new InvalidKeyException("Private key is not DH: " + (priv == null ? "null" : priv.getAlgorithm())); + } + if (!(pub instanceof DHPublicKey)) { + throw new InvalidKeyException("Public key is not DH: " + (pub == null ? "null" : pub.getAlgorithm())); + } + + DHPrivateKey dhPriv = (DHPrivateKey) priv; + DHPublicKey dhPub = (DHPublicKey) pub; + + // 1) Parameter compatibility: same p,g and l-compatible (0 means "unspecified") + DHParameterSpec a = dhPriv.getParams(); + DHParameterSpec b = dhPub.getParams(); + if (a == null || b == null) { + throw new InvalidKeyException("Missing DH parameters on one of the keys"); + } + if (!a.getP().equals(b.getP()) || !a.getG().equals(b.getG())) { + System.out.printf(" ...a.p=%s%n ...b.p=%s%n ...a.g=%s%n ...b.g=%s%n", a.getP(), b.getP(), a.getG(), + b.getG()); + throw new InvalidKeyException("Incompatible DH parameters: (p,g) differ"); + } + int la = a.getL(); + int lb = b.getL(); + if (la != 0 && lb != 0 && la != lb) { + throw new InvalidKeyException( + "Incompatible DH parameters: private value length (l) differs: " + la + " vs " + lb); + } + + // 2) Public value Y sanity check: 2 <= Y <= p-2 (reject trivial/small subgroup + // values) + BigInteger p = a.getP(); + BigInteger y = dhPub.getY(); + if (y == null) { + throw new InvalidKeyException("DH public key has null Y"); + } + BigInteger TWO = BigInteger.valueOf(2); + if (y.compareTo(TWO) < 0 || y.compareTo(p.subtract(TWO)) > 0) { + throw new InvalidKeyException("DH public value Y out of range"); + } + + // 3) Trial doPhase to prove the pair actually works with the provider + KeyAgreement ka = KeyAgreement.getInstance("DiffieHellman"); + try { + ka.init(priv); + ka.doPhase(pub, true); // if this throws, they are not operationally compatible + // (we don't need the secret here; just proving doPhase succeeds) + } catch (InvalidKeyException e) { + throw new InvalidKeyException("KeyAgreement.doPhase failed: " + e.getMessage(), e); + } + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/ecdsa/EcdsaLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/ecdsa/EcdsaLargeDataTest.java new file mode 100644 index 0000000..de7bf34 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/ecdsa/EcdsaLargeDataTest.java @@ -0,0 +1,215 @@ +/******************************************************************************* + * 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.core.alg.ecdsa; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.io.TailStrippingInputStream; + +public class EcdsaLargeDataTest { + + @BeforeAll + static void setup() { + // No special provider needed for JDK 21+ SunEC. + } + + private static void logBegin(Object... params) { + String thisClass = EcdsaLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + java.util.Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = EcdsaLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + private static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + private static byte[] jcaEcdsaSign(String jcaName, PrivateKey priv, byte[] msg) throws Exception { + Signature s = Signature.getInstance(jcaName); // e.g. SHA256withECDSAinP1363Format + s.initSign(priv); // randomized ECDSA (fresh k) + int off = 0; + while (off < msg.length) { + int len = Math.min(8192, msg.length - off); + s.update(msg, off, len); + off += len; + } + return s.sign(); + } + + private static boolean jcaEcdsaVerify(String jcaName, PublicKey pub, byte[] msg, byte[] sig) throws Exception { + Signature s = Signature.getInstance(jcaName); + s.initVerify(pub); + int off = 0; + while (off < msg.length) { + int len = Math.min(8192, msg.length - off); + s.update(msg, off, len); + off += len; + } + return s.verify(sig); + } + + private static void runCase(EcdsaCurveSpec spec, int size) throws Exception { + logBegin(spec.name(), Integer.valueOf(size), "ECDSA cross-check"); + + if (!CryptoAlgorithms.available().contains("ECDSA")) { + System.out.println("...*** SKIP *** ECDSA not registered"); + return; + } + + byte[] msg = randomBytes(size); + System.out.println("...msg=" + msg.length + " bytes"); + + // Key pair via your unified ECDSA algorithm and enum spec + KeyPair kp = CryptoAlgorithms.keyPair("ECDSA", spec); + + // SIGN (streaming): emits [body][signature]; capture trailer + SignatureContext signer = CryptoAlgorithms.create("ECDSA", KeyUsage.SIGN, kp.getPrivate(), spec); + final byte[][] sigHolder = new byte[1][]; + final int sigLen = signer.tagLength(); // should equal spec.signFixedLength() + + byte[] sink1; + try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen, 8192) { + @Override + protected void processTail(byte[] tail) throws IOException { + sigHolder[0] = (tail == null ? null : tail.clone()); + } + }) { + sink1 = readAll(in); + } finally { + try { + signer.close(); + } catch (Exception ignore) { + } + } + + assertArrayEquals(msg, sink1, "sign passthrough mismatch"); + byte[] ourSig = sigHolder[0]; + assertNotNull(ourSig, "signature missing"); + System.out.println("...signature size: " + ourSig.length + " (expected " + spec.signFixedLength() + ")"); + + // VERIFY with our streaming verifier + SignatureContext verifier = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), spec); + verifier.setExpectedTag(ourSig); + byte[] sink2; + try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) { + sink2 = readAll(verIn); + } finally { + try { + verifier.close(); + } catch (Exception ignore) { + } + } + assertArrayEquals(msg, sink2, "verify passthrough mismatch"); + System.out.println("...verified (our verifier on our signature): true"); + + // Cross-verify with JCA (P1363 format): JCA must accept our signature + assertTrue(jcaEcdsaVerify(spec.jcaFactory(), kp.getPublic(), msg, ourSig), + "JCA verify failed on our signature"); + + // Extra symmetry check (optional): our verifier must accept a JCA signature + // (different bytes) + byte[] jcaSig = jcaEcdsaSign(spec.jcaFactory(), kp.getPrivate(), msg); + SignatureContext verifier2 = CryptoAlgorithms.create("ECDSA", KeyUsage.VERIFY, kp.getPublic(), spec); + verifier2.setExpectedTag(jcaSig); + try (InputStream verIn2 = verifier2.wrap(new ByteArrayInputStream(msg))) { + byte[] passthrough = readAll(verIn2); + assertArrayEquals(msg, passthrough, "our verifier failed on JCA signature"); + } finally { + try { + verifier2.close(); + } catch (Exception ignore) { + } + } + System.out.println("...verified (our verifier on JCA signature): true"); + + logEnd(); + } + + @Test + void ecdsa_p256_streaming_sign_verify_and_crosscheck_with_jca() throws Exception { + runCase(EcdsaCurveSpec.P256, 48 * 1024 + 123); + } + + @Test + void ecdsa_p384_streaming_sign_verify_and_crosscheck_with_jca() throws Exception { + runCase(EcdsaCurveSpec.P384, 48 * 1024 + 127); + } + + @Test + void ecdsa_p521_streaming_sign_verify_and_crosscheck_with_jca() throws Exception { + runCase(EcdsaCurveSpec.P512, 48 * 1024 + 131); + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/ed25519/Ed25519LargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/ed25519/Ed25519LargeDataTest.java new file mode 100644 index 0000000..18056e1 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/ed25519/Ed25519LargeDataTest.java @@ -0,0 +1,191 @@ +/******************************************************************************* + * 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.core.alg.ed25519; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.io.TailStrippingInputStream; + +public class Ed25519LargeDataTest { + + @BeforeAll + static void setup() { + // Provider setup if needed + } + + private static void logBegin(Object... params) { + String thisClass = Ed25519LargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + java.util.Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = Ed25519LargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + private static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + private static byte[] jcaEd25519Sign(PrivateKey priv, byte[] msg) throws Exception { + Signature s = Signature.getInstance("Ed25519"); + s.initSign(priv); + int off = 0; + while (off < msg.length) { + int len = Math.min(8192, msg.length - off); + s.update(msg, off, len); + off += len; + } + return s.sign(); + } + + private static boolean jcaEd25519Verify(PublicKey pub, byte[] msg, byte[] sig) throws Exception { + Signature s = Signature.getInstance("Ed25519"); + s.initVerify(pub); + int off = 0; + while (off < msg.length) { + int len = Math.min(8192, msg.length - off); + s.update(msg, off, len); + off += len; + } + return s.verify(sig); + } + + @Test + void ed25519_streaming_sign_verify_and_crosscheck_with_jca() throws Exception { + final int SIZE = 48 * 1024 + 123; + logBegin(Integer.valueOf(SIZE), "Ed25519 cross-check"); + + byte[] msg = randomBytes(SIZE); + + if (!CryptoAlgorithms.available().contains("Ed25519")) { + System.out.println("...*** SKIP *** Ed25519 not registered"); + return; + } + + KeyPair kp = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); + + // SIGN: context emits [body][signature] — capture trailer via + // TailStrippingInputStream + SignatureContext signer = CryptoAlgorithms.create("Ed25519", KeyUsage.SIGN, kp.getPrivate(), null); + final byte[][] sigHolder = new byte[1][]; + final int sigLen = signer.tagLength(); + + byte[] sink1; + try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen, 8192) { + @Override + protected void processTail(byte[] tail) throws IOException { + sigHolder[0] = (tail == null ? null : tail.clone()); + } + }) { + sink1 = readAll(in); + } finally { + try { + signer.close(); + } catch (Exception ignore) { + } + } + + assertArrayEquals(msg, sink1, "sign passthrough mismatch"); + byte[] ourSig = sigHolder[0]; + assertNotNull(ourSig, "signature missing"); + System.out.println("...input size: " + msg.length); + System.out.println("...signature size: " + ourSig.length); + + // Cross-check with JCA + byte[] refSig = jcaEd25519Sign(kp.getPrivate(), msg); + assertArrayEquals(refSig, ourSig, "signature mismatch vs JCA reference"); + + // VERIFY: supply expected tag and drain (throws on mismatch) + SignatureContext verifier = CryptoAlgorithms.create("Ed25519", KeyUsage.VERIFY, kp.getPublic(), null); + verifier.setExpectedTag(ourSig); + + byte[] sink2; + try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) { + sink2 = readAll(verIn); + } finally { + try { + verifier.close(); + } catch (Exception ignore) { + } + } + + assertArrayEquals(msg, sink2, "verify passthrough mismatch"); + System.out.println("...verified: true"); + + // JCA verify on produced signature + assertTrue(jcaEd25519Verify(kp.getPublic(), msg, ourSig), "JCA verify failed on our signature"); + + logEnd(); + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/ed448/Ed448LargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/ed448/Ed448LargeDataTest.java new file mode 100644 index 0000000..03398af --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/ed448/Ed448LargeDataTest.java @@ -0,0 +1,191 @@ +/******************************************************************************* + * 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.core.alg.ed448; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.SignatureContext; +import zeroecho.core.io.TailStrippingInputStream; + +public class Ed448LargeDataTest { + + @BeforeAll + static void setup() { + // Provider setup if needed + } + + private static void logBegin(Object... params) { + String thisClass = Ed448LargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + java.util.Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = Ed448LargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + private static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + private static byte[] jcaEd448Sign(PrivateKey priv, byte[] msg) throws Exception { + Signature s = Signature.getInstance("Ed448"); + s.initSign(priv); + int off = 0; + while (off < msg.length) { + int len = Math.min(8192, msg.length - off); + s.update(msg, off, len); + off += len; + } + return s.sign(); + } + + private static boolean jcaEd448Verify(PublicKey pub, byte[] msg, byte[] sig) throws Exception { + Signature s = Signature.getInstance("Ed448"); + s.initVerify(pub); + int off = 0; + while (off < msg.length) { + int len = Math.min(8192, msg.length - off); + s.update(msg, off, len); + off += len; + } + return s.verify(sig); + } + + @Test + void ed448_streaming_sign_verify_and_crosscheck_with_jca() throws Exception { + final int SIZE = 48 * 1024 + 123; + logBegin(Integer.valueOf(SIZE), "Ed448 cross-check"); + + byte[] msg = randomBytes(SIZE); + + if (!CryptoAlgorithms.available().contains("Ed448")) { + System.out.println("...*** SKIP *** Ed448 not registered"); + return; + } + + KeyPair kp = CryptoAlgorithms.keyPair("Ed448", Ed448KeyGenSpec.defaultSpec()); + + // SIGN: context emits [body][signature] — capture trailer via + // TailStrippingInputStream + SignatureContext signer = CryptoAlgorithms.create("Ed448", KeyUsage.SIGN, kp.getPrivate(), null); + final byte[][] sigHolder = new byte[1][]; + final int sigLen = signer.tagLength(); + + byte[] sink1; + try (InputStream in = new TailStrippingInputStream(signer.wrap(new ByteArrayInputStream(msg)), sigLen, 8192) { + @Override + protected void processTail(byte[] tail) throws IOException { + sigHolder[0] = (tail == null ? null : tail.clone()); + } + }) { + sink1 = readAll(in); + } finally { + try { + signer.close(); + } catch (Exception ignore) { + } + } + + assertArrayEquals(msg, sink1, "sign passthrough mismatch"); + byte[] ourSig = sigHolder[0]; + assertNotNull(ourSig, "signature missing"); + System.out.println("...input size: " + msg.length); + System.out.println("...signature size: " + ourSig.length); + + // Cross-check with JCA + byte[] refSig = jcaEd448Sign(kp.getPrivate(), msg); + assertArrayEquals(refSig, ourSig, "signature mismatch vs JCA reference"); + + // VERIFY: supply expected tag and drain (throws on mismatch) + SignatureContext verifier = CryptoAlgorithms.create("Ed448", KeyUsage.VERIFY, kp.getPublic(), null); + verifier.setExpectedTag(ourSig); + + byte[] sink2; + try (InputStream verIn = verifier.wrap(new ByteArrayInputStream(msg))) { + sink2 = readAll(verIn); + } finally { + try { + verifier.close(); + } catch (Exception ignore) { + } + } + + assertArrayEquals(msg, sink2, "verify passthrough mismatch"); + System.out.println("...verified: true"); + + // JCA verify on produced signature + assertTrue(jcaEd448Verify(kp.getPublic(), msg, ourSig), "JCA verify failed on our signature"); + + logEnd(); + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/elgamal/ElgamalLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/elgamal/ElgamalLargeDataTest.java new file mode 100644 index 0000000..5ce0006 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/elgamal/ElgamalLargeDataTest.java @@ -0,0 +1,163 @@ +/******************************************************************************* + * 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.core.alg.elgamal; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.SecureRandom; +import java.security.Security; +import java.util.Arrays; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.EncryptionContext; + +public class ElgamalLargeDataTest { + + @BeforeAll + static void setup() { + Security.addProvider(new BouncyCastleProvider()); + } + + private static void logBegin(Object... params) { + String thisClass = ElgamalLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = ElgamalLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + /** + * PKCS1-style padding round-trip on >16KB payload (non-multiple of block size). + */ + @Test + void elgamalPkcs1LargeData() throws Exception { + final int SIZE = 32 * 1024 + 777; // >16KB, odd length to cross block boundaries + logBegin(SIZE, "PKCS1"); + + byte[] msg = randomBytes(SIZE); + System.out.printf("...input: %d bytes%n", msg.length); + + KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + ElgamalEncSpec spec = ElgamalEncSpec.pkcs1(); + + EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec); + InputStream ctIn = enc.attach(new ByteArrayInputStream(msg)); + byte[] ct = ctIn.readAllBytes(); + enc.close(); + System.out.printf("...encrypted: %d bytes%n", ct.length); + + EncryptionContext dec = CryptoAlgorithms.create("ElGamal", KeyUsage.DECRYPT, kp.getPrivate(), spec); + InputStream ptIn = dec.attach(new ByteArrayInputStream(ct)); + byte[] pt = ptIn.readAllBytes(); + dec.close(); + System.out.printf("...decrypted: %d bytes%n", pt.length); + + assertArrayEquals(msg, pt, "ElGamal PKCS1 decrypt mismatch"); + logEnd(); + } + + /** NOPADDING round-trip on >16KB payload (non-multiple of block size). */ + @Test + void elgamalNoPaddingLargeData() throws Exception { + final int SIZE = 32 * 255 * 4; + logBegin(SIZE, "NOPADDING"); + + byte[] msg = randomBytes(SIZE); + System.out.printf("...input: %d bytes%n", msg.length); + + KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + ElgamalEncSpec spec = ElgamalEncSpec.noPadding(); + + EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec); + InputStream ctIn = enc.attach(new ByteArrayInputStream(msg)); + byte[] ct = ctIn.readAllBytes(); + enc.close(); + System.out.printf("...encrypted: %d bytes%n", ct.length); + + EncryptionContext dec = CryptoAlgorithms.create("ElGamal", KeyUsage.DECRYPT, kp.getPrivate(), spec); + InputStream ptIn = dec.attach(new ByteArrayInputStream(ct)); + byte[] pt = ptIn.readAllBytes(); + dec.close(); + System.out.printf("...decrypted: %d bytes%n", pt.length); + + assertArrayEquals(msg, pt, "ElGamal NOPADDING decrypt mismatch"); + logEnd(); + } + + /** NOPADDING round-trip on >16KB payload (non-multiple of block size). */ + @Test + void elgamalNoPaddingFailLargeData() throws Exception { + final int SIZE = 32 * 255 * 4 + 3 /* + 777 */; + logBegin(SIZE, "NOPADDING"); + + byte[] msg = randomBytes(SIZE); + System.out.printf("...input: %d bytes%n", msg.length); + + KeyPair kp = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + ElgamalEncSpec spec = ElgamalEncSpec.noPadding(); + + EncryptionContext enc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, kp.getPublic(), spec); + InputStream ctIn = enc.attach(new ByteArrayInputStream(msg)); + assertThrowsExactly(IllegalStateException.class, () -> ctIn.readAllBytes(), + "No-padding cipher streams cannot processes incomplete blocks: 3 instead of 255"); + + logEnd(); + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/hmac/HmacLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/hmac/HmacLargeDataTest.java new file mode 100644 index 0000000..a885f3f --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/hmac/HmacLargeDataTest.java @@ -0,0 +1,181 @@ +/******************************************************************************* + * 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.core.alg.hmac; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.MacContext; +import zeroecho.core.io.TailStrippingInputStream; + +public class HmacLargeDataTest { + + @BeforeAll + static void setup() { + // register providers if needed + } + + private static void logBegin(Object... params) { + String thisClass = HmacLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = HmacLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + private static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return out.toByteArray(); + } + + private static byte[] jcaMac(String alg, SecretKey key, byte[] msg) throws Exception { + Mac mac = Mac.getInstance(alg); + mac.init(key); + int off = 0; + while (off < msg.length) { + int len = Math.min(8192, msg.length - off); + mac.update(msg, off, len); + off += len; + } + return mac.doFinal(); + } + + @Test + void hmac_sha256_streaming_mac_and_verify_with_jca_crosscheck() throws Exception { + final int SIZE = 64 * 1024 + 321; + final String ALG_ID = "HMAC"; + final String JCA_MAC = "HmacSHA256"; + logBegin(Integer.valueOf(SIZE), JCA_MAC); + + if (!CryptoAlgorithms.available().contains(ALG_ID)) { + System.out.println("...*** SKIP *** " + ALG_ID + " not registered"); + return; + } + + byte[] msg = randomBytes(SIZE); + System.out.println("...input size: " + msg.length); + + CryptoAlgorithm algo = CryptoAlgorithms.require(ALG_ID); + + // Generate a key (macName must match) + SecretKey key = algo.symmetricKeyBuilder(HmacKeyGenSpec.class).generateSecret(new HmacKeyGenSpec(JCA_MAC, 256)); + + // --- MAC (produce): engine emits [body][tag]; capture trailer --- + HmacSpec spec = HmacSpec.sha256(); + MacContext mac = CryptoAlgorithms.create(ALG_ID, KeyUsage.MAC, key, spec); + final byte[][] tagHolder = new byte[1][]; + final int tagLen = mac.tagLength(); + + byte[] pass1; + try (InputStream in = new TailStrippingInputStream(mac.wrap(new ByteArrayInputStream(msg)), tagLen, 8192) { + @Override + protected void processTail(byte[] tail) throws IOException { + tagHolder[0] = (tail == null ? null : tail.clone()); + } + }) { + pass1 = readAll(in); // body only (trailer stripped) + } finally { + try { + mac.close(); + } catch (Exception ignore) { + } + } + + assertArrayEquals(msg, pass1, "MAC passthrough mismatch"); + byte[] tag = tagHolder[0]; + assertNotNull(tag, "streaming HMAC tag null"); + assertTrue(tag.length > 0, "streaming HMAC tag empty"); + System.out.println("...tag size: " + tag.length); + + // Cross-check vs JCA + byte[] ref = jcaMac(JCA_MAC, key, msg); + assertArrayEquals(ref, tag, "HMAC tag mismatch vs JCA reference"); + + // --- VERIFY (consume): provide expected tag and drain (throws on mismatch) --- + MacContext ver = CryptoAlgorithms.create(ALG_ID, KeyUsage.MAC, key, spec); + ver.setExpectedTag(tag); + byte[] pass2; + try (InputStream in = ver.wrap(new ByteArrayInputStream(msg))) { + pass2 = readAll(in); + } finally { + try { + ver.close(); + } catch (Exception ignore) { + } + } + assertArrayEquals(msg, pass2, "VERIFY passthrough mismatch"); + System.out.println("...verified: true"); + + logEnd(); + } +} diff --git a/lib/src/test/java/zeroecho/core/alg/rsa/RsaLargeDataTest.java b/lib/src/test/java/zeroecho/core/alg/rsa/RsaLargeDataTest.java new file mode 100644 index 0000000..75275fe --- /dev/null +++ b/lib/src/test/java/zeroecho/core/alg/rsa/RsaLargeDataTest.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.alg.rsa; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.SecureRandom; +import java.util.Arrays; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.context.EncryptionContext; + +public class RsaLargeDataTest { + + @BeforeAll + static void setup() { + // If you register providers elsewhere, this can be empty. + // e.g., Security.addProvider(new BouncyCastleProvider()); + } + + private static void logBegin(Object... params) { + String thisClass = RsaLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = RsaLargeDataTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + /** OAEP(SHA-256) round-trip on >16KB payload (non-multiple of block size). */ + @Test + void rsaOaepLargeData() throws Exception { + final int SIZE = 32 * 1024 + 777; // >16KB, odd length to cross block boundaries + logBegin(SIZE, "OAEP(SHA-256)"); + + byte[] msg = randomBytes(SIZE); + System.out.printf("...input: %d bytes%n", msg.length); + + KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + RsaEncSpec spec = RsaEncSpec.oaep(RsaEncSpec.Hash.SHA256); + + EncryptionContext enc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, kp.getPublic(), spec); + InputStream ctIn = enc.attach(new ByteArrayInputStream(msg)); + byte[] ct = ctIn.readAllBytes(); + enc.close(); + System.out.printf("...encrypted: %d bytes%n", ct.length); + + EncryptionContext dec = CryptoAlgorithms.create("RSA", KeyUsage.DECRYPT, kp.getPrivate(), spec); + InputStream ptIn = dec.attach(new ByteArrayInputStream(ct)); + byte[] pt = ptIn.readAllBytes(); + dec.close(); + System.out.printf("...decrypted: %d bytes%n", pt.length); + + assertArrayEquals(msg, pt, "RSA OAEP decrypt mismatch"); + logEnd(); + } + + /** PKCS#1 v1.5 round-trip on >16KB payload (non-multiple of block size). */ + @Test + void rsaPkcs1v15LargeData() throws Exception { + final int SIZE = 32 * 1024 + 777; // >16KB, odd length + logBegin(SIZE, "PKCS1v1.5"); + + byte[] msg = randomBytes(SIZE); + System.out.printf("...input: %d bytes%n", msg.length); + + KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + RsaEncSpec spec = RsaEncSpec.pkcs1v15(); + + EncryptionContext enc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, kp.getPublic(), spec); + InputStream ctIn = enc.attach(new ByteArrayInputStream(msg)); + byte[] ct = ctIn.readAllBytes(); + enc.close(); + System.out.printf("...encrypted: %d bytes%n", ct.length); + + EncryptionContext dec = CryptoAlgorithms.create("RSA", KeyUsage.DECRYPT, kp.getPrivate(), spec); + InputStream ptIn = dec.attach(new ByteArrayInputStream(ct)); + byte[] pt = ptIn.readAllBytes(); + dec.close(); + System.out.printf("...decrypted: %d bytes%n", pt.length); + + assertArrayEquals(msg, pt, "RSA PKCS1v1.5 decrypt mismatch"); + logEnd(); + } +} diff --git a/lib/src/test/java/zeroecho/core/io/TailStrippingInputStreamTest.java b/lib/src/test/java/zeroecho/core/io/TailStrippingInputStreamTest.java new file mode 100644 index 0000000..fdf5072 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/io/TailStrippingInputStreamTest.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.core.io; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.random.RandomGenerator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class TailStrippingInputStreamTest { + + private static final int TAIL_LEN = 64; + private static final int IO_BUF = 4096; + + @Test + @DisplayName("Short stream (<64 bytes) → body=0, tail=all") + void testShortStream() throws Exception { + final String method = "testShortStream(tailLen=" + TAIL_LEN + ", ioBuf=" + IO_BUF + ")"; + System.out.println(method); + + final byte[] input = randomBytes(37); // < 64 + System.out.println("... input size: " + input.length); + + final CapturingTailStream ts = new CapturingTailStream(new ByteArrayInputStream(input), TAIL_LEN, IO_BUF); + final byte[] body; + try (InputStream in = ts) { + body = in.readAllBytes(); + System.out.println("... body size: " + body.length); + } + final byte[] tail = ts.getTail(); // printed at processTail() time + + // Assertions + assertEquals(0, body.length, "Body must be empty when input < tailLen"); + assertArrayEquals(input, tail, "Tail must equal the entire short input"); + + System.out.println("...ok"); + } + + @Test + @DisplayName("Large stream (>64 KiB) → body=input[0..n-tailLen), tail=last tailLen") + void testLargeStream() throws Exception { + final int size = 128 * 1024 + 5; // > 64 KiB + final String method = "testLargeStream(size=" + size + ", tailLen=" + TAIL_LEN + ", ioBuf=" + IO_BUF + ")"; + System.out.println(method); + + final byte[] input = randomBytes(size); + System.out.println("... input size: " + input.length); + + final CapturingTailStream ts = new CapturingTailStream(new ByteArrayInputStream(input), TAIL_LEN, IO_BUF); + final byte[] body; + try (InputStream in = ts) { + body = in.readAllBytes(); + System.out.println("... body size: " + body.length); + } + final byte[] tail = ts.getTail(); // printed at processTail() time + + // Expected splits + final int bodyLen = input.length - TAIL_LEN; + final byte[] expectedBody = new byte[bodyLen]; + final byte[] expectedTail = new byte[TAIL_LEN]; + System.arraycopy(input, 0, expectedBody, 0, bodyLen); + System.arraycopy(input, bodyLen, expectedTail, 0, TAIL_LEN); + + // Assertions + assertEquals(bodyLen, body.length, "Body length mismatch"); + assertArrayEquals(expectedBody, body, "Body payload mismatch"); + assertEquals(TAIL_LEN, tail.length, "Tail length mismatch"); + assertArrayEquals(expectedTail, tail, "Tail payload mismatch"); + + System.out.println("...ok"); + } + + // ---------- Helpers ---------- + + private static byte[] randomBytes(int size) { + byte[] data = new byte[size]; + RandomGenerator rng = RandomGenerator.of("L64X256MixRandom"); + rng.nextBytes(data); + return data; + } + + /** + * Test adapter that captures and prints the tail immediately when available. + */ + private static final class CapturingTailStream extends TailStrippingInputStream { + private byte[] tail; + + CapturingTailStream(InputStream upstream, int tailLen, int ioBuffer) { + super(upstream, tailLen, ioBuffer); + } + + @Override + protected void processTail(byte[] tail) throws IOException { + this.tail = tail; + System.out.println("... tail size: " + tail.length); + } + + byte[] getTail() { + return tail == null ? new byte[0] : tail.clone(); + } + } +} diff --git a/lib/src/test/java/zeroecho/core/io/UtilTest.java b/lib/src/test/java/zeroecho/core/io/UtilTest.java new file mode 100644 index 0000000..c704514 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/io/UtilTest.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * 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.core.io; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +public class UtilTest { + + @Test + public void testWriteAndReadUUID() throws IOException { + System.out.println("testWriteAndReadUUID"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + UUID original = UUID.randomUUID(); + Util.write(baos, original); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + UUID result = Util.readUUID(bais); + assertEquals(original, result, "UUID should be preserved"); + System.out.println("...ok"); + } + + @Test + public void testWriteAndReadUTF8() throws IOException { + System.out.println("testWriteAndReadUTF8"); + String str = "Hello, world! こんにちは世界 🌍"; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.writeUTF8(baos, str); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + String result = Util.readUTF8(bais, 1000); + assertEquals(str, result, "UTF8 string should be preserved"); + System.out.println("...ok"); + } + + @Test + public void testWriteAndReadLong() throws IOException { + System.out.println("testWriteAndReadLong"); + long value = 1234567890123456789L; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.writeLong(baos, value); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + long result = Util.readLong(bais); + assertEquals(value, result, "Long value should be preserved"); + System.out.println("...ok"); + } + + @Test + public void testWriteAndReadByteArray() throws IOException { + System.out.println("testWriteAndReadByteArray"); + byte[] data = new byte[256]; + new Random().nextBytes(data); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.write(baos, data); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + byte[] result = Util.read(bais, 1000); + assertArrayEquals(data, result, "Byte array should be preserved"); + System.out.println("...ok"); + } + + @Test + public void testReadWithMaxLengthExceeded() throws IOException { + System.out.println("testReadWithMaxLengthExceeded"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + // Write a packed integer length that's too large (e.g., 10_000_000) + Util.writePack7I(baos, 10_000_000); + baos.write(new byte[10]); // Minimal dummy data + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + IOException e = assertThrows(IOException.class, () -> Util.read(bais, 1000)); + assertTrue(e.getMessage().contains("exceeds maximum allowed length"), "Expected length limit exception"); + System.out.println("...ok"); + } + + @Test + public void testReadUTF8WithMaxLengthExceeded() throws IOException { + System.out.println("testReadUTF8WithMaxLengthExceeded"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.writePack7I(baos, 10_000_000); + baos.write("dummy".getBytes(StandardCharsets.UTF_8)); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + IOException e = assertThrows(IOException.class, () -> Util.readUTF8(bais, 1000)); + assertTrue(e.getMessage().contains("exceeds maximum allowed length"), "Expected length limit exception"); + System.out.println("...ok"); + } + + @Test + public void testPackedIntegerEncodingDecoding() throws IOException { + System.out.println("testPackedIntegerEncodingDecoding"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int original = 987654; + Util.writePack7I(baos, original); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + int result = Util.readPack7I(bais); + assertEquals(original, result, "Packed 7-bit integer should be preserved"); + + baos.reset(); + long originalL = 9876543210L; + Util.writePack7L(baos, originalL); + bais = new ByteArrayInputStream(baos.toByteArray()); + long resultL = Util.readPack7L(bais); + assertEquals(originalL, resultL, "Packed 7-bit long should be preserved"); + System.out.println("...ok"); + } + + @Test + public void testReadEOFHandling() throws IOException { + System.out.println("testReadEOFHandling"); + ByteArrayInputStream bais = new ByteArrayInputStream(new byte[3]); + assertThrows(EOFException.class, () -> Util.readLong(bais), "Expected EOFException for readLong"); + System.out.println("...ok"); + } + + @Test + public void testWriteAndReadLargeData() throws IOException { + System.out.println("testWriteAndReadLargeData"); + byte[] data = new byte[1024 * 1024]; // 1 MB + new Random().nextBytes(data); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Util.write(baos, data); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + byte[] result = Util.read(bais, data.length); + assertArrayEquals(data, result, "Large byte array should be preserved"); + System.out.println("...ok"); + } +} diff --git a/lib/src/test/java/zeroecho/core/storage/KeyringStoreDynamicTest.java b/lib/src/test/java/zeroecho/core/storage/KeyringStoreDynamicTest.java new file mode 100644 index 0000000..ee25d25 --- /dev/null +++ b/lib/src/test/java/zeroecho/core/storage/KeyringStoreDynamicTest.java @@ -0,0 +1,399 @@ +/******************************************************************************* + * 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.core.storage; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.alg.rsa.RsaKeyGenSpec; +import zeroecho.core.alg.rsa.RsaPrivateKeySpec; +import zeroecho.core.alg.rsa.RsaPublicKeySpec; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.sdk.util.BouncyCastleActivator; + +public class KeyringStoreDynamicTest { + + @BeforeAll + static void setupProviders() { + BouncyCastleActivator.init(); + } + + private static void logBegin(Object... params) { + String thisClass = KeyringStoreDynamicTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = KeyringStoreDynamicTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + private static byte[] randomBytes(int len) { + byte[] b = new byte[len]; + new SecureRandom().nextBytes(b); + return b; + } + + private static String encLen(byte[] der) { + if (der == null) { + return "0"; + } + + String str = Base64.getEncoder().withoutPadding().encodeToString(der); + if (str.length() > 64) { + str = str.substring(0, 64) + "..."; + } + + return der.length + " / b64 " + str; + } + + private static AlgorithmKeySpec makeImportSpec(Class specType, byte[] material, String algId, Object defaultSpec) + throws Exception { + try { + Method m = specType.getMethod("fromRaw", byte[].class); + Object spec = m.invoke(null, material); + return (AlgorithmKeySpec) spec; + } catch (NoSuchMethodException ignored) { + } + try { + Method m = specType.getMethod("fromRaw", String.class, byte[].class); + String name = deriveVariantNameForImport(algId, defaultSpec); + Object spec = m.invoke(null, name, material); + return (AlgorithmKeySpec) spec; + } catch (NoSuchMethodException ignored) { + } + try { + Method m = specType.getMethod("of", byte[].class); + Object spec = m.invoke(null, material); + return (AlgorithmKeySpec) spec; + } catch (NoSuchMethodException ignored) { + } + try { + Constructor c = specType.getConstructor(byte[].class); + Object spec = c.newInstance(new Object[] { material }); + return (AlgorithmKeySpec) spec; + } catch (NoSuchMethodException ignored) { + } + try { + Constructor c = specType.getConstructor(String.class); + Object spec = c.newInstance(Base64.getEncoder().encodeToString(material)); + return (AlgorithmKeySpec) spec; + } catch (NoSuchMethodException ignored) { + } + throw new IllegalStateException("No usable import factory/ctor found for " + specType.getName()); + } + + private static String deriveVariantNameForImport(String algId, Object defaultSpec) { + if (defaultSpec != null) { + try { + Method m = defaultSpec.getClass().getMethod("macName"); + Object v = m.invoke(defaultSpec); + if (v instanceof String) { + return (String) v; + } + } catch (Exception ignored) { + } + } + if ("HMAC".equalsIgnoreCase(algId)) { + return "HmacSHA256"; + } + return algId; + } + + private static boolean looksLikeImportSpecForPublic(Class specType) { + String n = specType.getSimpleName(); + return n.contains("Public") || n.endsWith("PublicKeySpec"); + } + + private static boolean looksLikeImportSpecForPrivate(Class specType) { + String n = specType.getSimpleName(); + return n.contains("Private") || n.endsWith("PrivateKeySpec"); + } + + private static boolean looksLikeImportSpecForSecret(Class specType) { + String n = specType.getSimpleName(); + return n.contains("Import") || n.endsWith("KeyImportSpec") || n.endsWith("SecretSpec"); + } + + @Test + void testExport(@TempDir Path tempDir) throws Exception { + logBegin(); + + Path keyringPath = tempDir.resolve("keyring-" + System.nanoTime() + ".txt"); + KeyringStore store = new KeyringStore(); + + CryptoAlgorithm algo = CryptoAlgorithms.require("RSA"); + KeyPair kp = algo.generateKeyPair(RsaKeyGenSpec.rsa4096()); + store.putPrivate("alice.priv", "RSA", new RsaPrivateKeySpec(kp.getPrivate().getEncoded())); + store.putPublic("alice.pub", "RSA", new RsaPublicKeySpec(kp.getPublic().getEncoded())); + + kp = algo.generateKeyPair(RsaKeyGenSpec.rsa4096()); + store.putPrivate("bob.priv", "RSA", new RsaPrivateKeySpec(kp.getPrivate().getEncoded())); + store.putPublic("bob.pub", "RSA", new RsaPublicKeySpec(kp.getPublic().getEncoded())); + store.save(keyringPath); + + store = KeyringStore.load(keyringPath); + String s = store.exportText(Collections.singleton("alice.pub")); + + assertTrue(s.contains("# KeyringStore v1\n")); + assertTrue(s.contains("\n@entry\n")); + assertTrue(s.contains("\nalias=alice.pub\n")); + assertTrue(s.contains("\nalgorithm=RSA\n")); + assertTrue(s.contains("\nkind=PUBLIC_KEY\n")); + assertTrue(s.contains("\nspec=zeroecho.core.alg.rsa.RsaPublicKeySpec\n")); + assertTrue(s.contains("\ns.type=RSA-PUB\n")); + assertTrue(s.contains("\ns.x509.b64=")); + + logEnd(); + } + + @Test + void keyring_dynamic_population_roundtrip_and_dump(@TempDir Path tempDir) throws Exception { + logBegin(); + + KeyringStore store = new KeyringStore(); + + Set ids = CryptoAlgorithms.available(); + System.out.println("...algorithms discovered: " + ids); + + int totalAdded = 0; + + for (String id : ids) { + CryptoAlgorithm alg = CryptoAlgorithms.require(id); + System.out.println("\n-- " + id + " --"); + + if (!alg.asymmetricBuildersInfo().isEmpty()) { + int perAlg = 0; + for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) { + if (bi.defaultKeySpec == null) { + continue; + } + try { + @SuppressWarnings("unchecked") + Class genSpecType = (Class) bi.specType; + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(genSpecType); + + AlgorithmKeySpec genSpec = (AlgorithmKeySpec) bi.defaultKeySpec; + KeyPair kp = b.generateKeyPair(genSpec); + PublicKey pub = kp.getPublic(); + PrivateKey prv = kp.getPrivate(); + + Class pubImpType = null; + Class prvImpType = null; + for (CryptoAlgorithm.AsymBuilderInfo x : alg.asymmetricBuildersInfo()) { + if (looksLikeImportSpecForPublic(x.specType)) { + pubImpType = x.specType; + } else if (looksLikeImportSpecForPrivate(x.specType)) { + prvImpType = x.specType; + } + } + if (pubImpType != null) { + AlgorithmKeySpec pubSpec = makeImportSpec(pubImpType, pub.getEncoded(), id, + bi.defaultKeySpec); + String alias = id.toLowerCase() + "-pub-" + perAlg; + store.putPublic(alias, id, pubSpec); + System.out.println("..." + alias + " saved, len=" + encLen(pub.getEncoded())); + totalAdded++; + } else { + System.out.println("...*** SKIP *** no public import spec for " + id); + } + if (prvImpType != null) { + AlgorithmKeySpec prvSpec = makeImportSpec(prvImpType, prv.getEncoded(), id, + bi.defaultKeySpec); + String alias = id.toLowerCase() + "-prv-" + perAlg; + store.putPrivate(alias, id, prvSpec); + System.out.println("..." + alias + " saved, len=" + encLen(prv.getEncoded())); + totalAdded++; + } else { + System.out.println("...*** SKIP *** no private import spec for " + id); + } + + perAlg++; + if (perAlg >= 3) { + break; + } + } catch (Throwable t) { + System.out.println("...*** SKIP asym for " + id + " *** " + t.getClass().getSimpleName() + ": " + + t.getMessage()); + } + } + } + + if (!alg.symmetricBuildersInfo().isEmpty()) { + int perAlg = 0; + for (CryptoAlgorithm.SymBuilderInfo bi : alg.symmetricBuildersInfo()) { + if (bi.defaultKeySpec() == null) { + continue; + } + try { + @SuppressWarnings("unchecked") + Class genSpecType = (Class) bi.specType(); + SymmetricKeyBuilder b = alg.symmetricKeyBuilder(genSpecType); + + AlgorithmKeySpec genSpec = (AlgorithmKeySpec) bi.defaultKeySpec(); + SecretKey sk = b.generateSecret(genSpec); + + Class impType = null; + for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) { + if (looksLikeImportSpecForSecret(x.specType())) { + impType = x.specType(); + } + } + if (impType == null) { + for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) { + try { + x.specType().getConstructor(byte[].class); + impType = x.specType(); + break; + } catch (NoSuchMethodException ignored) { + } + } + } + if (impType != null) { + byte[] raw = sk.getEncoded(); + if (raw == null) { + raw = randomBytes(32); + } + AlgorithmKeySpec imp = makeImportSpec(impType, raw, id, bi.defaultKeySpec()); + String alias = id.toLowerCase() + "-sec-" + perAlg; + store.putSecret(alias, id, imp); + System.out.println("..." + alias + " saved, len=" + (raw == null ? 0 : raw.length) + " " + + Base64.getEncoder().withoutPadding().encodeToString(raw)); + totalAdded++; + } else { + System.out.println("...*** SKIP *** no symmetric import spec for " + id); + } + perAlg++; + if (perAlg >= 3) { + break; + } + } catch (Throwable t) { + System.out.println("...*** SKIP sym for " + id + " *** " + t.getClass().getSimpleName() + ": " + + t.getMessage()); + } + } + } + } + + // Persist using JUnit-managed temp directory + Path keyringPath = tempDir.resolve("keyring-" + System.nanoTime() + ".txt"); + store.save(keyringPath); + System.out.println("\n...saved keyring: " + keyringPath.getFileName()); + System.out.println("...entries stored: " + totalAdded); + + KeyringStore loaded = KeyringStore.load(keyringPath); + assertTrue(loaded.aliases().size() >= Math.min(totalAdded, 1), "no entries reloaded"); + + int ok = 0; + for (String alias : loaded.aliases()) { + boolean success = false; + try { + PublicKey k = loaded.getPublic(alias); + if (k != null && k.getEncoded() != null) { + System.out.println("..." + alias + " OK public len=" + encLen(k.getEncoded())); + success = true; + } + } catch (Throwable ignore) { + } + if (!success) { + try { + PrivateKey k = loaded.getPrivate(alias); + if (k != null && k.getEncoded() != null) { + System.out.println("..." + alias + " OK private len=" + encLen(k.getEncoded())); + success = true; + } + } catch (Throwable ignore) { + } + } + if (!success) { + try { + SecretKey k = loaded.getSecret(alias); + if (k != null && k.getEncoded() != null) { + System.out.println("..." + alias + " OK secret len=" + encLen(k.getEncoded())); + success = true; + } + } catch (Throwable ignore) { + } + } + if (success) { + ok++; + } else { + System.out.println("...*** WARN *** could not reconstruct: " + alias); + } + } + assertTrue(ok > 0, "nothing reconstructed from keyring"); + + System.out.println("\n===== KEYRING DUMP BEGIN ====="); + List lines = Files.readAllLines(keyringPath, StandardCharsets.UTF_8); + for (String ln : lines) { + System.out.printf(ln.length() > 80 ? "%.77s...%n" : "%s%n", ln); + } + System.out.println("===== KEYRING DUMP END =====\n"); + + logEnd(); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/builders/TagTrailerDataContentBuilderTest.java b/lib/src/test/java/zeroecho/sdk/builders/TagTrailerDataContentBuilderTest.java new file mode 100644 index 0000000..7cf09d2 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/builders/TagTrailerDataContentBuilderTest.java @@ -0,0 +1,634 @@ +/******************************************************************************* + * 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.sdk.builders; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.Signature; +import java.util.Arrays; +import java.util.Random; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.aes.AesKeyGenSpec; +import zeroecho.core.alg.digest.DigestSpec; +import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec; +import zeroecho.core.alg.kyber.KyberKeyGenSpec; +import zeroecho.core.alg.rsa.RsaEncSpec; +import zeroecho.core.alg.rsa.RsaKeyGenSpec; +import zeroecho.core.alg.rsa.RsaSigSpec; +import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.KemContext; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.core.spi.AsymmetricKeyBuilder; +import zeroecho.core.spi.SymmetricKeyBuilder; +import zeroecho.core.tag.TagEngine; +import zeroecho.core.tag.TagEngineBuilder; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.alg.KemDataContentBuilder; +import zeroecho.sdk.builders.alg.RsaEncDataContentBuilder; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.builders.core.DataContentChainBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; +import zeroecho.sdk.guard.MultiRecipientDataSourceBuilder; +import zeroecho.sdk.guard.UnlockMaterial; +import zeroecho.sdk.util.BouncyCastleActivator; + +/** + * Round-trip tests for TagTrailerDataContentBuilder placed INSIDE AES, RSA, and + * KEM payloads using DataContentChainBuilder. + * + * Layout (ENCRYPT): Source -> TagTrailer(SHA-256) -> [AES|RSA|KEM payload] + * Layout (DECRYPT): Source -> [AES|RSA|KEM payload] -> TagTrailer(verify) + */ +public class TagTrailerDataContentBuilderTest { + + // ---------- boilerplate logging ---------- + private static void logBegin(Object... params) { + String thisClass = TagTrailerDataContentBuilderTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logBegin")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "(" + Arrays.deepToString(params) + ")"); + } + + private static void logEnd() { + String thisClass = TagTrailerDataContentBuilderTest.class.getName(); + String method = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk(frames -> frames + .dropWhile(f -> !f.getClassName().equals(thisClass) || f.getMethodName().equals("logEnd")) + .findFirst().map(StackWalker.StackFrame::getMethodName).orElse("")); + System.out.println(method + "...ok"); + } + + @BeforeAll + static void setup() { + // Optional: enable BC if you use BC-only modes in KEM payloads (EAX/OCB/CCM, + // etc.) + try { + BouncyCastleActivator.init(); + } catch (Throwable ignore) { + // keep tests runnable without BC if not present + } + } + + // ---------- helpers ---------- + private static byte[] random(int n) { + byte[] b = new byte[n]; + new Random().nextBytes(b); + return b; + } + + private static byte[] readAll(InputStream in) throws Exception { + try (in) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int r; + while ((r = in.read(buf)) != -1) { + out.write(buf, 0, r); + } + return out.toByteArray(); + } + } + + /** Minimal source builder for tests. */ + private static final class BytesSourceBuilder implements DataContentBuilder { + private final byte[] data; + + private BytesSourceBuilder(byte[] data) { + this.data = data.clone(); + } + + static BytesSourceBuilder of(byte[] data) { + return new BytesSourceBuilder(data); + } + + @Override + public PlainContent build(boolean encrypt) { + return new PlainContent() { + @Override + public void setInput(DataContent input) { + /* no upstream for sources */ } + + @Override + public InputStream getStream() { + return new ByteArrayInputStream(data); + } + }; + } + } + + // ---------- AES/GCM with TagTrailer (Ed25519 SIGNATURE) ---------- + @Test + void tag_inside_aes_gcm_with_ed25519_signature_roundtrip() throws Exception { + final int SIZE = 64 * 1024 + 11; + logBegin("AES/GCM + TagTrailer(Ed25519)", SIZE); + + byte[] msg = random(SIZE); + System.out.println("...msg=" + msg.length + " bytes"); + + // AES key + SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) + .generateSecret(AesKeyGenSpec.aes256()); + + // Ed25519 keys (JCA) + KeyPair ed = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); + + TagEngine tagEnc = TagEngineBuilder.ed25519Sign(ed.getPrivate()).get(); + TagEngine tagDec = TagEngineBuilder.ed25519Verify(ed.getPublic()).get(); + + // ENCRYPT: body -> [body||signature] -> AES-GCM + DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) + .add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192)) + .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build(); + + byte[] ct = readAll(enc.getStream()); + System.out.println("...ct=" + ct.length + " bytes"); + + // DECRYPT: AES-GCM -> strip trailer -> verify Ed25519 at EOF + DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct)) + .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) + .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build(); + + byte[] pt = readAll(dec.getStream()); + assertArrayEquals(msg, pt, "AES/GCM tag-in-payload (Ed25519) roundtrip mismatch"); + logEnd(); + } + + // ---------- AES/GCM with TagTrailer (SPHINCS+ SIGNATURE) ---------- + @Test + void tag_inside_aes_gcm_with_sphincsplus_signature_roundtrip() throws Exception { + final int SIZE = 64 * 1024 + 17; + logBegin("AES/GCM + TagTrailer(SPHINCS+)", SIZE); + + byte[] msg = random(SIZE); + System.out.println("...msg=" + msg.length + " bytes"); + + // AES key + SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) + .generateSecret(AesKeyGenSpec.aes256()); + + // SPHINCS+ key pair via registry (uses default param set from + // SphincsPlusKeyGenSpec) + KeyPair spx = CryptoAlgorithms.keyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec()); + + // Tag engines (SPHINCS+) + TagEngine tagEnc = TagEngineBuilder.sphincsPlusSign(spx.getPrivate()).get(); + TagEngine tagDec = TagEngineBuilder.sphincsPlusVerify(spx.getPublic()).get(); + + // ENCRYPT: body -> [body||spxSig] -> AES-GCM + DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) + .add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192)) + .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build(); + + byte[] ct = readAll(enc.getStream()); + System.out.println("...ct=" + ct.length + " bytes"); + + // DECRYPT: AES-GCM -> strip trailer -> verify SPHINCS+ at EOF + DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct)) + .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) + .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build(); + + byte[] pt = readAll(dec.getStream()); + assertArrayEquals(msg, pt, "AES/GCM tag-in-payload (SPHINCS+) roundtrip mismatch"); + logEnd(); + } + + // ---------- AES/GCM with TagTrailer (RSA-PSS SIGNATURE) ---------- + @Test + void tag_inside_aes_gcm_with_rsapss_signature_roundtrip() throws Exception { + final int SIZE = 96 * 1024 + 3; + logBegin("AES/GCM + TagTrailer(RSA-PSS)", SIZE); + + byte[] msg = random(SIZE); + System.out.println("...msg=" + msg.length + " bytes"); + + // AES key + SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) + .generateSecret(AesKeyGenSpec.aes256()); + + // RSA-2048 keys (use registry for convenience) + KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + + // Tag engines (SHA-256, saltLen=32) + RsaSigSpec pss = RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32); + TagEngine tagEnc = TagEngineBuilder.rsaSign(rsa.getPrivate(), pss).get(); + TagEngine tagDec = TagEngineBuilder.rsaVerify(rsa.getPublic(), pss).get(); + + // ENCRYPT: body -> [body||pssSig] -> AES-GCM + DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) + .add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192)) + .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build(); + + byte[] ct = readAll(enc.getStream()); + System.out.println("...ct=" + ct.length + " bytes"); + + // DECRYPT: AES-GCM -> strip trailer -> verify PSS at EOF + DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct)) + .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) + .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build(); + + byte[] pt = readAll(dec.getStream()); + assertArrayEquals(msg, pt, "AES/GCM tag-in-payload (RSA-PSS) roundtrip mismatch"); + logEnd(); + } + + // ---------- AES/GCM with TagTrailer (SHA-256) ---------- + + @Test + void tag_inside_aes_gcm_roundtrip() throws Exception { + final int SIZE = 48 * 1024 + 7; + logBegin("AES/GCM + TagTrailer", SIZE); + + byte[] msg = random(SIZE); + System.out.println("...msg=" + msg.length + " bytes"); + + // AES key up-front so decrypt can reuse it + SymmetricKeyBuilder gen = CryptoAlgorithms.require("AES") + .symmetricKeyBuilder(AesKeyGenSpec.class); + SecretKey aesKey = gen.generateSecret(AesKeyGenSpec.aes256()); + + // ENCRYPT: [source] -> [tag trailer] -> [aes gcm] + DataContent encChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)) + .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) // writes IV/AAD headers + // into stream + .build(); + + byte[] ciphertext = readAll(encChain.getStream()); + System.out.println("...ct=" + ciphertext.length + " bytes"); + + // DECRYPT: [source(ct)] -> [aes gcm] -> [tag trailer verify] + DataContent decChain = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) + .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) // reads IV/AAD headers + // back + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .throwOnMismatch()) + .build(); + + byte[] plain = readAll(decChain.getStream()); + assertArrayEquals(msg, plain, "AES/GCM tag-in-payload roundtrip mismatch"); + logEnd(); + } + + // ---------- RSA/OAEP with TagTrailer (keep body small; RSA is not a bulk + // stream cipher) ---------- + + @Test + void tag_inside_rsa_oaep_roundtrip() throws Exception { + final int SIZE = 96; // safe under RSA-2048 OAEP SHA-256 limit even with a 32-byte tag appended + logBegin("RSA/OAEP + TagTrailer", SIZE); + + byte[] msg = "hello-".getBytes(StandardCharsets.UTF_8); + msg = Arrays.copyOf(msg, SIZE); // pad deterministic length for the test + System.out.println("...msg=" + msg.length + " bytes"); + + KeyPair kp = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + + // ENCRYPT: [source] -> [tag trailer] -> [rsa/oaep] + DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)) + .add(RsaEncDataContentBuilder.builder().oaep(RsaEncSpec.Hash.SHA256).withPublicKey(kp.getPublic())) + .build(); + + byte[] ct = readAll(enc.getStream()); + System.out.println("...ct=" + ct.length + " bytes"); + + // DECRYPT: [source(ct)] -> [rsa/oaep] -> [tag verify] + DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct)) + .add(RsaEncDataContentBuilder.builder().oaep(RsaEncSpec.Hash.SHA256).withPrivateKey(kp.getPrivate())) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .throwOnMismatch()) + .build(); + + byte[] pt = readAll(dec.getStream()); + assertArrayEquals(msg, pt, "RSA/OAEP tag-in-payload roundtrip mismatch"); + logEnd(); + } + + // ---------- KEM → AES/GCM payload with TagTrailer (skip if no KEM registered) + // ---------- + + @Test + void tag_inside_kem_payload_roundtrip() throws Exception { + final int SIZE = 32 * 1024 + 3; + logBegin("KEM→AES/GCM + TagTrailer", SIZE); + + // Find a KEM we can use (e.g., "ML-KEM") with a working default spec. + String kemId = findKemIdOrNull(); + if (kemId == null) { + System.out.println("...*** SKIP *** no KEM algorithm registered"); + return; + } + + CryptoAlgorithm kemAlg = CryptoAlgorithms.require(kemId); + KeyPair kemKeys = tryKeyPairWithDefaultSpec(kemAlg); + if (kemKeys == null) { + System.out.println("...*** SKIP *** cannot generate KEM key pair"); + return; + } + + byte[] msg = random(SIZE); + System.out.println("...msg=" + msg.length + " bytes"); + + // ENCRYPT: [source] -> [tag trailer] -> [KEM envelope with AES/GCM payload] + AesDataContentBuilder aesEnc = AesDataContentBuilder.builder().modeGcm(128) // 128-bit tag + .withHeader(); // carry IV etc. + + DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192)) + .add(KemDataContentBuilder.builder().kem(kemId).recipientPublic(kemKeys.getPublic()).derivedKeyBytes(32) // AES-256 + // key + // derived + // from + // KEM + // secret + .hkdfSha256("KEM-tag-demo".getBytes(java.nio.charset.StandardCharsets.US_ASCII)) + .withAes(aesEnc)) + .build(); + + byte[] envelope = readAll(enc.getStream()); + System.out.println("...envelope=" + envelope.length + " bytes"); + + // DECRYPT: [source(envelope)] -> [KEM] -> [tag verify] + AesDataContentBuilder aesDec = AesDataContentBuilder.builder().modeGcm(128).withHeader(); + + DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(envelope)) + .add(KemDataContentBuilder.builder().kem(kemId).recipientPrivate(kemKeys.getPrivate()) + .derivedKeyBytes(32) + .hkdfSha256("KEM-tag-demo".getBytes(java.nio.charset.StandardCharsets.US_ASCII)) + .withAes(aesDec)) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .throwOnMismatch()) + .build(); + + byte[] pt = readAll(dec.getStream()); + assertArrayEquals(msg, pt, "KEM→GCM tag-in-payload roundtrip mismatch"); + logEnd(); + } + + // ------------------------------------------------------------------------- + // 1) AES-GCM payload, multi-recipient (RSA + Kyber + Password), tag trailer + // ------------------------------------------------------------------------- + @Test + void tag_trailer_inside_multi_recipient_aes_gcm_rsa_kem_password_roundtrip() throws Exception { + final int SIZE = 96 * 1024 + 7; + final char[] PASSWORD = "correct horse battery staple".toCharArray(); + + logBegin(Integer.valueOf(SIZE), "AES-256/GCM + RSA + ML-KEM + password"); + + // message + byte[] msg = random(SIZE); + System.out.println("...input=" + msg.length); + + // --- recipients --- + // RSA + KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + // ML-KEM (Kyber768 as a good mid-level) + KeyPair kem = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + + // --- symmetric payload (AES-256/GCM, tag 128) --- + // IV length is handled internally (12 bytes for GCM) and persisted via header. + AesDataContentBuilder aesEnc = AesDataContentBuilder.builder().modeGcm(128).withHeader(); // write + // IV/tagBits/AAD-hash + // header for decrypt + // side + + // --- tag trailer (SHA-256 digest as a trailer) --- + TagTrailerDataContentBuilder tagEnc = new TagTrailerDataContentBuilder<>( + TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192); + + EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kem.getPublic()); + + // --- envelope (ENCRYPT) with 3 recipients --- + MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesEnc) + // .addRsaOaepRecipient(rsa.getPublic()) // RSA-OAEP with SHA-256 MGF1 + // .addKemRecipient("ML-KEM", kem.getPublic(), 32 /* kekBytes */, 16 /* + // hkdfSaltLen */) + .addRecipient(rsaEnc) // new API call + .addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 32) // new API call + .addPasswordRecipient(PASSWORD, 120_000 /* pbkdf2 iters */, 16 /* saltLen */, 32 /* kekBytes */); + + // Pipeline (encrypt): Bytes → Tag(trailer) → Multi-Recipient + DataContent encTail = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)).add(tagEnc).add(envEnc) + .build(); + + byte[] encrypted = readAll(encTail.getStream()); + System.out.println("...encrypted=" + encrypted.length); + + // -------------- Decrypt three ways on the same ciphertext -------------- + + // a) by RSA private key + AesDataContentBuilder aesDecRsa = AesDataContentBuilder.builder().modeGcm(128).withHeader(); // read header to + // recover + // IV/tagBits + MultiRecipientDataSourceBuilder envDecRsa = new MultiRecipientDataSourceBuilder().withAes(aesDecRsa) + .unlockWith(new UnlockMaterial.Private(rsa.getPrivate())); + byte[] ptRsa = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted)).add(envDecRsa) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .throwOnMismatch()) + .build().getStream()); + System.out.println("...decrypted(RSA)=" + ptRsa.length); + assertArrayEquals(msg, ptRsa, "RSA path failed to recover the original"); + + // b) by KEM private key + AesDataContentBuilder aesDecKem = AesDataContentBuilder.builder().modeGcm(128).withHeader(); + MultiRecipientDataSourceBuilder envDecKem = new MultiRecipientDataSourceBuilder().withAes(aesDecKem) + .unlockWith(new UnlockMaterial.Private(kem.getPrivate())); + byte[] ptKem = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted)).add(envDecKem) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .throwOnMismatch()) + .build().getStream()); + System.out.println("...decrypted(KEM)=" + ptKem.length); + assertArrayEquals(msg, ptKem, "KEM path failed to recover the original"); + + // c) by password + AesDataContentBuilder aesDecPwd = AesDataContentBuilder.builder().modeGcm(128).withHeader(); + MultiRecipientDataSourceBuilder envDecPwd = new MultiRecipientDataSourceBuilder().withAes(aesDecPwd) + .unlockWith(new UnlockMaterial.Password(PASSWORD)); + byte[] ptPwd = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted)).add(envDecPwd) + .add(new TagTrailerDataContentBuilder<>(TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + .throwOnMismatch()) + .build().getStream()); + System.out.println("...decrypted(PASSWORD)=" + ptPwd.length); + assertArrayEquals(msg, ptPwd, "Password path failed to recover the original"); + + logEnd(); + } + + // ------------------------------------------------------------------------- + // 2) AES-CBC payload (PKCS#7), RSA recipient only, tag trailer for integrity + // ------------------------------------------------------------------------- + @Test + void tag_trailer_inside_multi_recipient_aes_cbc_rsa_roundtrip() throws Exception { + final int SIZE = 48 * 1024 + 321; + + logBegin(Integer.valueOf(SIZE), "AES-256/CBC + RSA"); + + byte[] msg = random(SIZE); + System.out.println("...input=" + msg.length); + + KeyPair rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + + // AES-256/CBC with header so IV/params are serialized by the AES stage + AesDataContentBuilder aesCbc = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + + TagTrailerDataContentBuilder tagEnc = new TagTrailerDataContentBuilder<>( + TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192); + + TagTrailerDataContentBuilder tagDec = new TagTrailerDataContentBuilder<>( + TagEngineBuilder.digest(DigestSpec.sha256())).bufferSize(8192) + // explicit for clarity + .throwOnMismatch(); + + EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + + // Envelope: recipient table (RSA-OAEP) + AES payload (CBC/PKCS7 with header) + MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesCbc) + // CEK length for AES-256 + .payloadKeyBytes(32) + // .addRsaOaepRecipient(rsa.getPublic()); old API + .addRecipient(rsaEnc); + + DataContent encTail = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) + // append digest trailer BEFORE symmetric encryption + .add(tagEnc).add(envEnc).build(); + + byte[] encrypted = readAll(encTail.getStream()); + System.out.println("...encrypted=" + encrypted.length); + + MultiRecipientDataSourceBuilder envDec = new MultiRecipientDataSourceBuilder() + .withAes(AesDataContentBuilder.builder().modeCbcPkcs5() + // must match encrypt side + .withHeader()) + .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate())); + + byte[] pt = readAll(DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(encrypted)) + // recover CEK via RSA-OAEP, then AES-CBC decrypt (reads header) + .add(envDec) + // strip & verify trailer + .add(tagDec).build().getStream()); + + System.out.println("...decrypted=" + pt.length); + + assertArrayEquals(msg, pt); + logEnd(); + } + + // ---------- AES/GCM with TagTrailer (ECDSA P-256 SIGNATURE) ---------- + @Test + void tag_inside_aes_gcm_with_ecdsa_p256_signature_roundtrip() throws Exception { + final int SIZE = 64 * 1024 + 5; + logBegin("AES/GCM + TagTrailer(ECDSA-P256)", SIZE); + + byte[] msg = random(SIZE); + System.out.println("...msg=" + msg.length + " bytes"); + + // AES key + SecretKey aesKey = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) + .generateSecret(AesKeyGenSpec.aes256()); + + // ECDSA P-256 keys (via your unified ECDSA algorithm) + KeyPair ecdsa = CryptoAlgorithms.keyPair("ECDSA", zeroecho.core.alg.ecdsa.EcdsaCurveSpec.P256); + + // Tag engines (ECDSA/P-256 using P1363 format, fixed 64-byte tag) + TagEngine tagEnc = TagEngineBuilder.ecdsaP256Sign(ecdsa.getPrivate()).get(); + TagEngine tagDec = TagEngineBuilder.ecdsaP256Verify(ecdsa.getPublic()).get(); + + // ENCRYPT: body -> [body||ecdsaSig] -> AES-GCM + DataContent enc = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)) + .add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192)) + .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()).build(); + + byte[] ct = readAll(enc.getStream()); + System.out.println("...ct=" + ct.length + " bytes"); + + // DECRYPT: AES-GCM -> strip trailer -> verify ECDSA at EOF + DataContent dec = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ct)) + .add(AesDataContentBuilder.builder().withKey(aesKey).modeGcm(128).withHeader()) + .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()).build(); + + byte[] pt = readAll(dec.getStream()); + assertArrayEquals(msg, pt, "AES/GCM tag-in-payload (ECDSA-P256) roundtrip mismatch"); + logEnd(); + } + + // ---------- small registry helpers ---------- + + private static String findKemIdOrNull() { + for (String id : CryptoAlgorithms.available()) { + if (id.equalsIgnoreCase("ML-KEM") || id.equalsIgnoreCase("RSA-KEM") || id.toUpperCase().contains("KEM")) { + return id; + } + } + return null; + } + + private static KeyPair tryKeyPairWithDefaultSpec(CryptoAlgorithm alg) { + try { + for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) { + if (bi.defaultKeySpec == null) { + continue; + } + @SuppressWarnings("unchecked") + Class specType = (Class) bi.specType; + AlgorithmKeySpec spec = (AlgorithmKeySpec) bi.defaultKeySpec; + AsymmetricKeyBuilder b = alg.asymmetricKeyBuilder(specType); + KeyPair kp = b.generateKeyPair(spec); + if (kp != null) { + return kp; + } + } + } catch (Throwable t) { + // fall through → return null + } + return null; + } +} diff --git a/lib/src/test/java/zeroecho/sdk/builders/alg/KemHybridRoundTripTest.java b/lib/src/test/java/zeroecho/sdk/builders/alg/KemHybridRoundTripTest.java new file mode 100644 index 0000000..93e13aa --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/builders/alg/KemHybridRoundTripTest.java @@ -0,0 +1,320 @@ +/******************************************************************************* + * 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.sdk.builders.alg; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.Security; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.stream.Stream; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.alg.bike.BikeKeyGenSpec; +import zeroecho.core.alg.cmce.CmceKeyGenSpec; +import zeroecho.core.alg.frodo.FrodoKeyGenSpec; +import zeroecho.core.alg.hqc.HqcKeyGenSpec; +import zeroecho.core.alg.kyber.KyberKeyGenSpec; +import zeroecho.core.alg.ntru.NtruKeyGenSpec; +import zeroecho.core.alg.ntruprime.NtrulPrimeKeyGenSpec; +import zeroecho.core.alg.ntruprime.SntruPrimeKeyGenSpec; +import zeroecho.core.alg.saber.SaberKeyGenSpec; +import zeroecho.core.annotation.Describable; +import zeroecho.core.spec.AlgorithmKeySpec; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; +import zeroecho.sdk.util.Kdf; + +/** + * Round-trip tests for KEM + symmetric envelope using the new + * {@link KemDataContentBuilder} that first configures the KEM and then + * delegates to an explicit AES/ChaCha builder. + * + *

+ * This replaces the older ad-hoc payload builders (GcmBuilder/CcmBuilder/etc.). + * Modes exercised here are those implemented by {@link AesDataContentBuilder} + * and {@link ChaChaDataContentBuilder}: AES-GCM, AES-CBC, AES-CTR, and + * ChaCha20-Poly1305. + *

+ */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.MethodName.class) +class KemHybridRoundTripTest { + + @BeforeAll + void setupProviders() { + // PQC providers (required by most KEM impls) + Security.addProvider(new BouncyCastlePQCProvider()); + Security.addProvider(new BouncyCastleProvider()); + } + + // Only modes supported by the concrete AES/ChaCha builders + private static final String[] MODES = new String[] { "GCM", "CBC", "CTR", "CHACHA20-POLY1305" }; + + // Non-empty AAD to force AEAD path for ChaCha20-Poly1305 and also exercise GCM + // AAD + private static final byte[] AAD = new byte[] { 0x01 }; + + // Nicely print a spec + private static String labelOf(AlgorithmKeySpec spec) { + if (spec instanceof Describable d) { + return d.description(); + } + return spec.getClass().getSimpleName(); + } + + // --- generic vectors over all KEMs we support (compile-time known) --- + static Stream vectors() { + List out = new ArrayList<>(); + + // 1) ML-KEM (Kyber) + var kybers = new KyberKeyGenSpec[] { KyberKeyGenSpec.kyber512(), KyberKeyGenSpec.kyber768(), + KyberKeyGenSpec.kyber1024() }; + for (var spec : kybers) { + for (var m : MODES) { + out.add(Arguments.of("ML-KEM", spec, m, "ML-KEM:" + labelOf(spec) + " · " + m)); + } + } + + // 2) NTRU (HPS/HRSS) + var ntrus = new NtruKeyGenSpec[] { NtruKeyGenSpec.hps2048_509(), NtruKeyGenSpec.hps2048_677(), + NtruKeyGenSpec.hps4096_821(), NtruKeyGenSpec.hps4096_1229(), NtruKeyGenSpec.hrss701(), + NtruKeyGenSpec.hrss1373() }; + for (var spec : ntrus) { + for (var m : MODES) { + out.add(Arguments.of("NTRU", spec, m, "NTRU:" + labelOf(spec) + " · " + m)); + } + } + + // 3) Classic McEliece (CMCE) + var cmces = new CmceKeyGenSpec[] { CmceKeyGenSpec.mceliece348864(), CmceKeyGenSpec.mceliece348864f(), + CmceKeyGenSpec.mceliece460896(), CmceKeyGenSpec.mceliece460896f(), CmceKeyGenSpec.mceliece6688128(), + CmceKeyGenSpec.mceliece6688128f(), CmceKeyGenSpec.mceliece6960119(), CmceKeyGenSpec.mceliece6960119f(), + CmceKeyGenSpec.mceliece8192128(), CmceKeyGenSpec.mceliece8192128f() }; + for (var spec : cmces) { + for (var m : MODES) { + out.add(Arguments.of("CMCE", spec, m, "CMCE:" + labelOf(spec) + " · " + m)); + } + } + + // 4) FrodoKEM + var frodos = new FrodoKeyGenSpec[] { FrodoKeyGenSpec.frodo640aes(), FrodoKeyGenSpec.frodo640shake(), + FrodoKeyGenSpec.frodo976aes(), FrodoKeyGenSpec.frodo976shake(), FrodoKeyGenSpec.frodo1344aes(), + FrodoKeyGenSpec.frodo1344shake() }; + for (var spec : frodos) { + for (var m : MODES) { + out.add(Arguments.of("Frodo", spec, m, "Frodo:" + labelOf(spec) + " · " + m)); + } + } + + // 5) SABER + var sabers = new SaberKeyGenSpec[] { SaberKeyGenSpec.lightsaberkem128r3(), SaberKeyGenSpec.saberkem128r3(), + SaberKeyGenSpec.firesaberkem128r3(), SaberKeyGenSpec.lightsaberkem192r3(), + SaberKeyGenSpec.saberkem192r3(), SaberKeyGenSpec.firesaberkem192r3(), + SaberKeyGenSpec.lightsaberkem256r3(), SaberKeyGenSpec.saberkem256r3(), + SaberKeyGenSpec.firesaberkem256r3() }; + for (var spec : sabers) { + for (var m : MODES) { + out.add(Arguments.of("SABER", spec, m, "SABER:" + labelOf(spec) + " · " + m)); + } + } + + // 6) BIKE + var bikes = new BikeKeyGenSpec[] { BikeKeyGenSpec.bike128(), BikeKeyGenSpec.bike192(), + BikeKeyGenSpec.bike256() }; + for (var spec : bikes) { + for (var m : MODES) { + out.add(Arguments.of("BIKE", spec, m, "BIKE:" + labelOf(spec) + " · " + m)); + } + } + + // 7) HQC + var hqcs = new HqcKeyGenSpec[] { HqcKeyGenSpec.hqc128(), HqcKeyGenSpec.hqc192(), HqcKeyGenSpec.hqc256() }; + for (var spec : hqcs) { + for (var m : MODES) { + out.add(Arguments.of("HQC", spec, m, "HQC:" + labelOf(spec) + " · " + m)); + } + } + + // 8) SNTRU Prime + var sntrus = new SntruPrimeKeyGenSpec[] { SntruPrimeKeyGenSpec.sntrup653(), SntruPrimeKeyGenSpec.sntrup761(), + SntruPrimeKeyGenSpec.sntrup857(), SntruPrimeKeyGenSpec.sntrup953(), SntruPrimeKeyGenSpec.sntrup1013(), + SntruPrimeKeyGenSpec.sntrup1277() }; + for (var spec : sntrus) { + for (var m : MODES) { + out.add(Arguments.of("SNTRUPrime", spec, m, "SNTRUPrime:" + labelOf(spec) + " · " + m)); + } + } + + // 9) NTRU LPRime + var ntrul = new NtrulPrimeKeyGenSpec[] { NtrulPrimeKeyGenSpec.ntrulpr653(), NtrulPrimeKeyGenSpec.ntrulpr761(), + NtrulPrimeKeyGenSpec.ntrulpr857(), NtrulPrimeKeyGenSpec.ntrulpr953(), + NtrulPrimeKeyGenSpec.ntrulpr1013(), NtrulPrimeKeyGenSpec.ntrulpr1277() }; + for (var spec : ntrul) { + for (var m : MODES) { + out.add(Arguments.of("NTRULPRime", spec, m, "NTRULPRime:" + labelOf(spec) + " · " + m)); + } + } + + return out.stream(); + } + + @ParameterizedTest(name = "{3}") + @MethodSource("vectors") + @zeroecho.core.annotation.DisplayName("KEM→symmetric (AES/ChaCha, single-thread) round-trip — generic") + void kemRoundTripGeneric(String kemId, AlgorithmKeySpec keyGenSpec, String mode, String pretty) throws Exception { + System.out.println("kemRoundTrip(" + pretty + ")"); + + // produce a pseudo-random payload + final int size = (128 * 1024) + 7; + final byte[] input = new byte[size]; + new Random(123456789L).nextBytes(input); + + // keypair via generic registry path + KeyPair kp = CryptoAlgorithms.keyPair(kemId, keyGenSpec); + + // encrypt + DataContent enc = encryptStage(kemId, kp, mode); + enc.setInput(new BytesContent(input)); + final byte[] encrypted; + try (InputStream is = enc.getStream()) { + encrypted = is.readAllBytes(); + } + + // decrypt + DataContent dec = decryptStage(kemId, kp, mode); + dec.setInput(new BytesContent(encrypted)); + final byte[] decrypted; + try (InputStream is = dec.getStream()) { + decrypted = is.readAllBytes(); + } + + System.out.println("... input size: " + input.length); + System.out.println("... encrypted size: " + encrypted.length); + System.out.println("... decrypted size: " + decrypted.length); + assertArrayEquals(input, decrypted, "round-trip mismatch"); + System.out.println("...ok"); + } + + private static DataContent encryptStage(String kemId, KeyPair kp, String mode) { + KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId).recipientPublic(kp.getPublic()) + .derivedKeyBytes(32); // AES-256 or ChaCha20 key + + switch (mode) { + case "GCM": { + AesDataContentBuilder aes = AesDataContentBuilder.builder().modeGcm(128).withHeader().withAad(AAD); + return kem.withAes(aes).build(true); + } + case "CBC": { + AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + return kem.withAes(aes).build(true); + } + case "CTR": { + AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCtr().withHeader(); + return kem.withAes(aes).build(true); + } + case "CHACHA20-POLY1305": { + ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder().withAad(AAD) // non-empty → AEAD + // variant + .withHeader(); // carry nonce + return kem.withChaCha(ch).build(true); + } + default: + throw new IllegalArgumentException("Unknown mode: " + mode); + } + } + + private static DataContent decryptStage(String kemId, KeyPair kp, String mode) { + KemDataContentBuilder kem = KemDataContentBuilder.builder().kem(kemId).recipientPrivate(kp.getPrivate()) + .derivedKeyBytes(32); + + switch (mode) { + case "GCM": { + AesDataContentBuilder aes = AesDataContentBuilder.builder().modeGcm(128).withHeader().withAad(AAD); + return kem.withAes(aes).build(false); + } + case "CBC": { + AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + return kem.withAes(aes).build(false); + } + case "CTR": { + AesDataContentBuilder aes = AesDataContentBuilder.builder().modeCtr().withHeader(); + return kem.withAes(aes).build(false); + } + case "CHACHA20-POLY1305": { + ChaChaDataContentBuilder ch = ChaChaDataContentBuilder.builder().withAad(AAD).withHeader(); + return kem.withChaCha(ch).build(false); + } + default: + throw new IllegalArgumentException("Unknown mode: " + mode); + } + } + + /** Simple byte-array source for the pipeline. */ + private static final class BytesContent implements PlainContent { + private final byte[] data; + + BytesContent(byte[] data) { + this.data = data; + } + + @Override + public InputStream getStream() throws IOException { + return new ByteArrayInputStream(data); + } + } + + // Sanity: HKDF provider is available (exercise earlier utility) + @org.junit.jupiter.api.Test + void hkdfAvailable() { + assertDoesNotThrow(() -> Kdf.hkdfSha256(new byte[] { 1, 2, 3 }, null, null, 32)); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/content/builtin/SecretPasswordTest.java b/lib/src/test/java/zeroecho/sdk/content/builtin/SecretPasswordTest.java new file mode 100644 index 0000000..5a768f1 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/content/builtin/SecretPasswordTest.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.content.builtin; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class SecretPasswordTest { + @Test + void testGetPlainText() { + + int len = 12; + + System.out.printf("generating password, %d characters...%n", len); + + SecretPassword sp = new SecretPassword(len); + + System.out.printf("...string %s (length %d)%n", sp.toText(), sp.toText().length()); + + assertEquals(len, sp.toText().length()); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/content/export/Base64StreamTest.java b/lib/src/test/java/zeroecho/sdk/content/export/Base64StreamTest.java new file mode 100644 index 0000000..3da1e68 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/content/export/Base64StreamTest.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.content.export; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; + +import org.junit.jupiter.api.Test; + +class Base64StreamTest { + + @Test + void testEncodedOutputFrom4KBInput() throws Exception { + System.out.println("testEncodedOutputFrom4KBInput"); + + byte[] inputBytes = new byte[4096]; + new SecureRandom().nextBytes(inputBytes); // random 4KB data + + byte[] inputEncoded = Base64.getEncoder().encode(inputBytes); + + System.out.println("...encoded length should be: " + inputEncoded.length); + + int lineLength = 76; + + String[] prefixes = { "echo ", null }; + byte[][] lineSeparators = { " >> file.tmp\r\n".getBytes(StandardCharsets.US_ASCII), + "\n".getBytes(StandardCharsets.US_ASCII), null }; + + for (String prefix : prefixes) { + for (byte[] lineSep : lineSeparators) { + System.out.printf("...***** testing with prefix=%s and lineSep=%s%n", + prefix == null ? "null" : "\"" + prefix + "\"", + lineSep == null ? "null" : "\"" + new String(lineSep, StandardCharsets.US_ASCII) + "\""); + + try (InputStream base64Stream = new Base64Stream(new ByteArrayInputStream(inputBytes), + prefix == null ? null : prefix.getBytes(StandardCharsets.US_ASCII), lineLength, lineSep)) { + + String encodedOutput = new String(base64Stream.readAllBytes(), StandardCharsets.US_ASCII); + System.out.println(encodedOutput); + + String[] lines = encodedOutput.split("\n"); + for (int i = 0; i < lines.length; i++) { + int space = lines[i].indexOf(' '); + if (space > 0) { + lines[i] = lines[i].split(" ")[prefix == null ? 0 : 1]; + } + } + + if (lineSep != null) { + for (String line : lines) { + assertTrue(line.length() <= lineLength, "Line exceeds max length: " + line.length()); + } + } + + String joined = String.join("", lines); + + System.out.println("...lines: " + lines.length); + System.out.println("......encoded length: " + joined.length()); + + if (lineSep == null && prefix != null) { + continue; + } + + byte[] decoded = Base64.getDecoder().decode(joined); + + System.out.println("...decoded length: " + decoded.length); + + assertArrayEquals(inputBytes, decoded, "Decoded content does not match original input"); + } + } + } + + System.out.println("...ok"); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/guard/MultiRecipientEnvelopeTest.java b/lib/src/test/java/zeroecho/sdk/guard/MultiRecipientEnvelopeTest.java new file mode 100644 index 0000000..27108cf --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/guard/MultiRecipientEnvelopeTest.java @@ -0,0 +1,738 @@ +/******************************************************************************* + * 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.sdk.guard; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.SecureRandom; +import java.security.Security; +import java.security.Signature; +import java.util.function.Supplier; +import java.util.random.RandomGenerator; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.ed25519.Ed25519KeyGenSpec; +import zeroecho.core.alg.elgamal.ElgamalParamSpec; +import zeroecho.core.alg.kyber.KyberKeyGenSpec; +import zeroecho.core.alg.rsa.RsaKeyGenSpec; +import zeroecho.core.alg.sphincsplus.SphincsPlusKeyGenSpec; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.context.KemContext; +import zeroecho.core.tag.TagEngineBuilder; +import zeroecho.sdk.builders.TagTrailerDataContentBuilder; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.core.DataContentBuilder; +import zeroecho.sdk.builders.core.DataContentChainBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.content.api.PlainContent; + +/** + * JDK21+ JUnit (no 'var') that prints sizes immediately after each stage: - + * Password guardian - Single RSA recipient - Multi-recipient (Password + RSA + + * ML-KEM) + optional ElGamal - AES-GCM and AES-CBC/PKCS7 Prints method name + * (with params if any) at start, "...ok" at the end. + */ +public class MultiRecipientEnvelopeTest { + + @BeforeAll + static void addProviders() { + Security.addProvider(new BouncyCastlePQCProvider()); // ML-KEM + Security.addProvider(new BouncyCastleProvider()); // extras + } + + // ------------------------------------------------------------------------------------ + // Password guardian + // ------------------------------------------------------------------------------------ + + @Test + @DisplayName("Password guardian + AES-256-GCM") + void testPasswordGuardian_Aes256Gcm() throws Exception { + final String method = "testPasswordGuardian_Aes256Gcm()"; + System.out.println(method); + + final byte[] input = randomInput(128 * 1024 + 7); + System.out.println("... input size: " + input.length); + + final char[] password = "CorrectHorseBatteryStaple".toCharArray(); + + // AES-256-GCM with header so IV/tag are persisted in-band + Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + + // Encrypt + MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + .payloadKeyBytes(32) + .addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32); + + DataContent encryptor = enc.build(true); + encryptor.setInput(new BytesContent(input)); + + byte[] encrypted; + try (InputStream es = encryptor.getStream()) { + encrypted = readAllBytesAndPrint(es, "... encrypted size"); + } + + // Decrypt + MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password)); + + DataContent decryptor = dec.build(false); + decryptor.setInput(new BytesContent(encrypted)); + + byte[] decrypted; + try (InputStream ds = decryptor.getStream()) { + decrypted = readAllBytesAndPrint(ds, "... decrypted size"); + } + + assertArrayEquals(input, decrypted); + System.out.println("...ok"); + } + + @Test + @DisplayName("Password guardian + AES-256-CBC/PKCS7") + void testPasswordGuardian_Aes256Cbc() throws Exception { + final String method = "testPasswordGuardian_Aes256Cbc()"; + System.out.println(method); + + final byte[] input = randomInput(128 * 1024 + 7); + System.out.println("... input size: " + input.length); + + final char[] password = "Tr0ub4dor&3".toCharArray(); + + // AES-256-CBC with PKCS7 padding, header persists IV + Supplier aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + + MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + .payloadKeyBytes(32) + .addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32); + + DataContent encryptor = enc.build(true); + encryptor.setInput(new BytesContent(input)); + + byte[] encrypted; + try (InputStream es = encryptor.getStream()) { + encrypted = readAllBytesAndPrint(es, "... encrypted size"); + } + + MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password)); + + DataContent decryptor = dec.build(false); + decryptor.setInput(new BytesContent(encrypted)); + + byte[] decrypted; + try (InputStream ds = decryptor.getStream()) { + decrypted = readAllBytesAndPrint(ds, "... decrypted size"); + } + + assertArrayEquals(input, decrypted); + System.out.println("...ok"); + } + + // ------------------------------------------------------------------------------------ + // Single ElGamal recipient (via EncryptionContext) + // ------------------------------------------------------------------------------------ + + @Test + @DisplayName("Single ElGamal(PKCS1 default) recipient + AES-256-GCM") + void testSingleElgamalGuardian_Aes256Gcm() throws Exception { + final String method = "testSingleElgamalGuardian_Aes256Gcm()"; + System.out.println(method); + + final byte[] input = randomInput(128 * 1024 + 7); + System.out.println("... input size: " + input.length); + + KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + + Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + + MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + .payloadKeyBytes(32).addRecipient(elgEnc); + + DataContent encryptor = enc.build(true); + encryptor.setInput(new BytesContent(input)); + + byte[] encrypted; + try (InputStream es = encryptor.getStream()) { + encrypted = readAllBytesAndPrint(es, "... encrypted size"); + } + + MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(elg.getPrivate())); + + DataContent decryptor = dec.build(false); + decryptor.setInput(new BytesContent(encrypted)); + + byte[] decrypted; + try (InputStream ds = decryptor.getStream()) { + decrypted = readAllBytesAndPrint(ds, "... decrypted size"); + } + + assertArrayEquals(input, decrypted); + System.out.println("...ok"); + } + + @Test + @DisplayName("Single ElGamal(PKCS1 default) recipient + AES-256-CBC") + void testSingleElgamalGuardian_Aes256Cbc() throws Exception { + final String method = "testSingleElgamalGuardian_Aes256Cbc()"; + System.out.println(method); + + final byte[] input = randomInput(128 * 1024 + 13); // cross blocks + System.out.println("... input size: " + input.length); + + KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + + Supplier aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + + MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + .payloadKeyBytes(32).addRecipient(elgEnc); + + DataContent encryptor = enc.build(true); + encryptor.setInput(new BytesContent(input)); + + byte[] encrypted; + try (InputStream es = encryptor.getStream()) { + encrypted = readAllBytesAndPrint(es, "... encrypted size"); + } + + MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(elg.getPrivate())); + + DataContent decryptor = dec.build(false); + decryptor.setInput(new BytesContent(encrypted)); + + byte[] pt; + try (InputStream ds = decryptor.getStream()) { + pt = readAllBytesAndPrint(ds, "... decrypted size"); + } + + assertArrayEquals(input, pt); + System.out.println("...ok"); + } + + // ------------------------------------------------------------------------------------ + // Single RSA recipient (via EncryptionContext) + // ------------------------------------------------------------------------------------ + + @Test + @DisplayName("Single RSA-OAEP(default) recipient + AES-256-GCM") + void testSingleRsaGuardian_Aes256Gcm() throws Exception { + final String method = "testSingleRsaGuardian_Aes256Gcm()"; + System.out.println(method); + + final byte[] input = randomInput(128 * 1024 + 7); + System.out.println("... input size: " + input.length); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(3072, new SecureRandom()); + KeyPair rsa = kpg.generateKeyPair(); + + EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + + Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + + MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + .payloadKeyBytes(32).addRecipient(rsaEnc); + + DataContent encryptor = enc.build(true); + encryptor.setInput(new BytesContent(input)); + + byte[] encrypted; + try (InputStream es = encryptor.getStream()) { + encrypted = readAllBytesAndPrint(es, "... encrypted size"); + } + + MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate())); + + DataContent decryptor = dec.build(false); + decryptor.setInput(new BytesContent(encrypted)); + + byte[] decrypted; + try (InputStream ds = decryptor.getStream()) { + decrypted = readAllBytesAndPrint(ds, "... decrypted size"); + } + + assertArrayEquals(input, decrypted); + System.out.println("...ok"); + } + + @Test + @DisplayName("Single RSA-OAEP(default) recipient + AES-256-CBC/PKCS7") + void testSingleRsaGuardian_Aes256Cbc() throws Exception { + final String method = "testSingleRsaGuardian_Aes256Cbc()"; + System.out.println(method); + + final byte[] input = randomInput(128 * 1024 + 7); + System.out.println("... input size: " + input.length); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(3072, new SecureRandom()); + KeyPair rsa = kpg.generateKeyPair(); + + EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + + Supplier aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + + MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + .payloadKeyBytes(32).addRecipient(rsaEnc); + + DataContent encryptor = enc.build(true); + encryptor.setInput(new BytesContent(input)); + + byte[] encrypted; + try (InputStream es = encryptor.getStream()) { + encrypted = readAllBytesAndPrint(es, "... encrypted size"); + } + + MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + .payloadKeyBytes(32).unlockWith(new UnlockMaterial.Private(rsa.getPrivate())); + + DataContent decryptor = dec.build(false); + decryptor.setInput(new BytesContent(encrypted)); + + byte[] decrypted; + try (InputStream ds = decryptor.getStream()) { + decrypted = readAllBytesAndPrint(ds, "... decrypted size"); + } + + assertArrayEquals(input, decrypted); + System.out.println("...ok"); + } + + // ------------------------------------------------------------------------------------ + // Multi-recipient (Password + RSA + KEM/ML-KEM + ElGamal) via contexts + // ------------------------------------------------------------------------------------ + + @Test + @DisplayName("Multi recipients (PWD+RSA+ML-KEM kyber512+ElGamal) + AES-256-GCM") + void testMultiRecipients_Kyber512_Aes256Gcm_AllUnlocks() throws Exception { + final String method = "testMultiRecipients_Kyber512_Aes256Gcm_AllUnlocks()"; + System.out.println(method); + + final byte[] input = randomInput(128 * 1024 + 7); + System.out.println("... input size: " + input.length); + + final char[] password = "group-secret".toCharArray(); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(3072, new SecureRandom()); + KeyPair rsa = kpg.generateKeyPair(); + + KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber512()); + KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + + EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); + + Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + + MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + .payloadKeyBytes(32) + .addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32) + .addRecipient(rsaEnc).addRecipient(elgEnc).addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 32); + + DataContent encryptor = enc.build(true); + encryptor.setInput(new BytesContent(input)); + + byte[] encrypted; + try (InputStream es = encryptor.getStream()) { + encrypted = readAllBytesAndPrint(es, "... encrypted size"); + } + + // Each unlock prints its decrypted size immediately + decryptAndAssert("... decrypt via password", aesGcm, new UnlockMaterial.Password(password), input, encrypted); + decryptAndAssert("... decrypt via RSA", aesGcm, new UnlockMaterial.Private(rsa.getPrivate()), input, encrypted); + decryptAndAssert("... decrypt via ML-KEM", aesGcm, new UnlockMaterial.Private(kyber.getPrivate()), input, + encrypted); + decryptAndAssert("... decrypt via ElGamal", aesGcm, new UnlockMaterial.Private(elg.getPrivate()), input, + encrypted); + + System.out.println("...ok"); + } + + @Test + @DisplayName("Multi recipients (PWD+RSA+ML-KEM kyber768+ElGamal) + AES-256-CBC/PKCS7") + void testMultiRecipients_Kyber768_Aes256Cbc_AllUnlocks() throws Exception { + final String method = "testMultiRecipients_Kyber768_Aes256Cbc_AllUnlocks()"; + System.out.println(method); + + final byte[] input = randomInput(128 * 1024 + 7); + System.out.println("... input size: " + input.length); + + final char[] password = "another-group-secret".toCharArray(); + + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(3072, new SecureRandom()); + KeyPair rsa = kpg.generateKeyPair(); + + rsa = CryptoAlgorithms.keyPair("RSA", RsaKeyGenSpec.rsa2048()); + KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + + EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); + + Supplier aesCbc = () -> AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + + MultiRecipientDataSourceBuilder enc = new MultiRecipientDataSourceBuilder().withAes(aesCbc.get()) + .payloadKeyBytes(32) + .addPasswordRecipient(password, /* iterations */ 10000, /* saltLen */ 16, /* kekBytes */ 32) + .addRecipient(rsaEnc).addRecipient(elgEnc).addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 32); + + DataContent encryptor = enc.build(true); + encryptor.setInput(new BytesContent(input)); + + byte[] encrypted; + try (InputStream es = encryptor.getStream()) { + encrypted = readAllBytesAndPrint(es, "... encrypted size"); + } + + decryptAndAssert("... decrypt via password", aesCbc, new UnlockMaterial.Password(password), input, encrypted); + decryptAndAssert("... decrypt via RSA", aesCbc, new UnlockMaterial.Private(rsa.getPrivate()), input, encrypted); + decryptAndAssert("... decrypt via ML-KEM", aesCbc, new UnlockMaterial.Private(kyber.getPrivate()), input, + encrypted); + decryptAndAssert("... decrypt via ElGamal", aesCbc, new UnlockMaterial.Private(elg.getPrivate()), input, + encrypted); + + System.out.println("...ok"); + } + + // ==================================================================================== + // Signed payload inside multi-recipient AES/GCM envelope (Ed25519 & SPHINCS+) + // ==================================================================================== + + @Test + @DisplayName("Signed payload (Ed25519) → Multi-recipient envelope (RSA + ML-KEM + PWD + ElGamal) → AES-256-GCM") + void testSignedPayload_Ed25519_MultiRecipients_AesGcm() throws Exception { + final String method = "testSignedPayload_Ed25519_MultiRecipients_AesGcm()"; + System.out.println(method); + + final byte[] msg = randomInput(96 * 1024 + 13); + System.out.println("... input size: " + msg.length); + + // Recipients + final char[] password = "signed-group-secret".toCharArray(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(3072, new SecureRandom()); + KeyPair rsa = kpg.generateKeyPair(); + KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber768()); + KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + + // Sender signature keys (Ed25519) + KeyPair ed = CryptoAlgorithms.keyPair("Ed25519", Ed25519KeyGenSpec.defaultSpec()); + + // AES-256-GCM payload builder + Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + + // Context recipients + EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); + + // Envelope (encrypt) + MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + .payloadKeyBytes(32).addRecipient(rsaEnc).addRecipient(elgEnc) + .addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 16) + .addPasswordRecipient(password, /* iterations */ 100_000, /* saltLen */ 16, /* kekBytes */ 32); + + // Tag trailer for SIGNING (Ed25519) + TagTrailerDataContentBuilder signTrailer = new TagTrailerDataContentBuilder<>( + TagEngineBuilder.ed25519Sign(ed.getPrivate())).bufferSize(8192); + + // Encrypt chain + DataContent encryptChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)).add(signTrailer) + .add(envEnc).build(); + + byte[] ciphertext; + try (InputStream es = encryptChain.getStream()) { + ciphertext = readAllBytesAndPrint(es, "... ciphertext size"); + } + + // Verify after each unlock + TagTrailerDataContentBuilder verifyTrailer; + + // via Password + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic())) + .bufferSize(8192).throwOnMismatch(); + DataContent decPwd = DataContentChainBuilder + .decrypt().add(BytesSourceBuilder.of(ciphertext)).add(new MultiRecipientDataSourceBuilder() + .withAes(aesGcm.get()).payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password))) + .add(verifyTrailer).build(); + byte[] ptPwd; + try (InputStream ds = decPwd.getStream()) { + ptPwd = readAllBytesAndPrint(ds, "... decrypted via password"); + } + assertArrayEquals(msg, ptPwd); + + // via RSA + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic())) + .bufferSize(8192).throwOnMismatch(); + DataContent decRsa = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) + .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .unlockWith(new UnlockMaterial.Private(rsa.getPrivate()))) + .add(verifyTrailer).build(); + byte[] ptRsa; + try (InputStream ds = decRsa.getStream()) { + ptRsa = readAllBytesAndPrint(ds, "... decrypted via RSA"); + } + assertArrayEquals(msg, ptRsa); + + // via ElGamal + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic())) + .bufferSize(8192).throwOnMismatch(); + DataContent decElgamal = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) + .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .unlockWith(new UnlockMaterial.Private(elg.getPrivate()))) + .add(verifyTrailer).build(); + byte[] ptElgamal; + try (InputStream ds = decElgamal.getStream()) { + ptElgamal = readAllBytesAndPrint(ds, "... decrypted via ElGamal"); + } + assertArrayEquals(msg, ptElgamal); + + // via ML-KEM + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.ed25519Verify(ed.getPublic())) + .bufferSize(8192).throwOnMismatch(); + DataContent decKem = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) + .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .unlockWith(new UnlockMaterial.Private(kyber.getPrivate()))) + .add(verifyTrailer).build(); + byte[] ptKem; + try (InputStream ds = decKem.getStream()) { + ptKem = readAllBytesAndPrint(ds, "... decrypted via ML-KEM"); + } + assertArrayEquals(msg, ptKem); + + System.out.println("...ok"); + } + + @Test + @DisplayName("Signed payload (SPHINCS+) → Multi-recipient envelope (RSA + ML-KEM + PWD + ElGamal) → AES-256-GCM") + void testSignedPayload_SphincsPlus_MultiRecipients_AesGcm() throws Exception { + final String method = "testSignedPayload_SphincsPlus_MultiRecipients_AesGcm()"; + System.out.println(method); + + final byte[] msg = randomInput(64 * 1024 + 19); + System.out.println("... input size: " + msg.length); + + // Recipients + final char[] password = "signed-group-secret-2".toCharArray(); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(3072, new SecureRandom()); + KeyPair rsa = kpg.generateKeyPair(); + KeyPair kyber = CryptoAlgorithms.keyPair("ML-KEM", KyberKeyGenSpec.kyber512()); + KeyPair elg = CryptoAlgorithms.keyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + + // Sender signature keys (SPHINCS+, default/best) + KeyPair spx = CryptoAlgorithms.keyPair("SPHINCS+", SphincsPlusKeyGenSpec.defaultSpec()); + + Supplier aesGcm = () -> AesDataContentBuilder.builder().modeGcm(128).withHeader(); + + // Context recipients + EncryptionContext rsaEnc = CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic()); + EncryptionContext elgEnc = CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic()); + KemContext kybKem = CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kyber.getPublic()); + + // Envelope recipients + MultiRecipientDataSourceBuilder envEnc = new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()) + .payloadKeyBytes(32).addRecipient(rsaEnc).addRecipient(elgEnc) + .addRecipient(kybKem, /* kekBytes */ 32, /* saltLen */ 16) + .addPasswordRecipient(password, /* iterations */ 120_000, /* saltLen */ 16, /* kekBytes */ 32); + + // Tag trailer for SIGNING (SPHINCS+) + TagTrailerDataContentBuilder signTrailer = new TagTrailerDataContentBuilder<>( + TagEngineBuilder.sphincsPlusSign(spx.getPrivate())).bufferSize(8192); + + // Encrypt chain + DataContent encryptChain = DataContentChainBuilder.encrypt().add(BytesSourceBuilder.of(msg)).add(signTrailer) + .add(envEnc).build(); + byte[] ciphertext; + try (InputStream es = encryptChain.getStream()) { + ciphertext = readAllBytesAndPrint(es, "... ciphertext size"); + } + + // Verify trailer with SPHINCS+ public key after each unlock + TagTrailerDataContentBuilder verifyTrailer; + + // via Password + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic())) + .bufferSize(8192).throwOnMismatch(); + DataContent decPwd = DataContentChainBuilder + .decrypt().add(BytesSourceBuilder.of(ciphertext)).add(new MultiRecipientDataSourceBuilder() + .withAes(aesGcm.get()).payloadKeyBytes(32).unlockWith(new UnlockMaterial.Password(password))) + .add(verifyTrailer).build(); + byte[] ptPwd; + try (InputStream ds = decPwd.getStream()) { + ptPwd = readAllBytesAndPrint(ds, "... decrypted via password"); + } + assertArrayEquals(msg, ptPwd); + + // via RSA + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic())) + .bufferSize(8192).throwOnMismatch(); + DataContent decRsa = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) + .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .unlockWith(new UnlockMaterial.Private(rsa.getPrivate()))) + .add(verifyTrailer).build(); + byte[] ptRsa; + try (InputStream ds = decRsa.getStream()) { + ptRsa = readAllBytesAndPrint(ds, "... decrypted via RSA"); + } + assertArrayEquals(msg, ptRsa); + + // via ElGamal + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic())) + .bufferSize(8192).throwOnMismatch(); + DataContent decElgamal = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) + .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .unlockWith(new UnlockMaterial.Private(elg.getPrivate()))) + .add(verifyTrailer).build(); + byte[] ptElgamal; + try (InputStream ds = decElgamal.getStream()) { + ptElgamal = readAllBytesAndPrint(ds, "... decrypted via ElGamal"); + } + assertArrayEquals(msg, ptElgamal); + + // via ML-KEM + verifyTrailer = new TagTrailerDataContentBuilder<>(TagEngineBuilder.sphincsPlusVerify(spx.getPublic())) + .bufferSize(8192).throwOnMismatch(); + DataContent decKem = DataContentChainBuilder.decrypt().add(BytesSourceBuilder.of(ciphertext)) + .add(new MultiRecipientDataSourceBuilder().withAes(aesGcm.get()).payloadKeyBytes(32) + .unlockWith(new UnlockMaterial.Private(kyber.getPrivate()))) + .add(verifyTrailer).build(); + byte[] ptKem; + try (InputStream ds = decKem.getStream()) { + ptKem = readAllBytesAndPrint(ds, "... decrypted via ML-KEM"); + } + assertArrayEquals(msg, ptKem); + + System.out.println("...ok"); + } + + // ------------------------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------------------------ + + /** Minimal source builder so we can compose pull-style chains. */ + private static final class BytesSourceBuilder implements DataContentBuilder { + private final byte[] data; + + private BytesSourceBuilder(byte[] data) { + this.data = data.clone(); + } + + static BytesSourceBuilder of(byte[] data) { + return new BytesSourceBuilder(data); + } + + @Override + public PlainContent build(boolean encrypt) { + return new PlainContent() { + @Override + public void setInput(DataContent input) { + /* source: no upstream */ } + + @Override + public InputStream getStream() { + return new ByteArrayInputStream(data); + } + }; + } + } + + private static void decryptAndAssert(String banner, Supplier aesFactory, + UnlockMaterial material, byte[] original, byte[] encrypted) throws IOException { + MultiRecipientDataSourceBuilder dec = new MultiRecipientDataSourceBuilder().withAes(aesFactory.get()) + .payloadKeyBytes(32).unlockWith(material); + + DataContent decryptor = dec.build(false); + decryptor.setInput(new BytesContent(encrypted)); + + byte[] decrypted; + try (InputStream ds = decryptor.getStream()) { + decrypted = readAllBytesAndPrint(ds, banner + " -> size"); + } + assertArrayEquals(original, decrypted); + } + + /** Reads all bytes and prints the size in a finally block. */ + private static byte[] readAllBytesAndPrint(InputStream in, String label) throws IOException { + byte[] result = new byte[0]; + try { + result = in.readAllBytes(); + return result; + } finally { + System.out.println(label + ": " + result.length); + } + } + + private static byte[] randomInput(int size) { + byte[] data = new byte[size]; + RandomGenerator rng = RandomGenerator.of("L64X256MixRandom"); + rng.nextBytes(data); + return data; + } + + /** Minimal PlainContent over a byte[]. */ + private static final class BytesContent implements PlainContent { + private final byte[] data; + + BytesContent(byte[] data) { + this.data = data; + } + + @Override + public InputStream getStream() throws IOException { + return new ByteArrayInputStream(data); + } + } +} diff --git a/lib/src/test/java/zeroecho/sdk/integrations/covert/TextualCodecTest.java b/lib/src/test/java/zeroecho/sdk/integrations/covert/TextualCodecTest.java new file mode 100644 index 0000000..cc656be --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/integrations/covert/TextualCodecTest.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.integrations.covert; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +public class TextualCodecTest { + + @Test + void testGenerate100CharacterText() { + System.out.println("testGenerate100CharacterText"); + + TextualCodec.Generator generator = TextualCodec.Generator.EN; + String result = generator.getText(100); + + System.out.println("...generated text: " + result); + + assertNotNull(result, "Generated text should not be null"); + assertEquals(100, result.length(), "Generated text should have 100 characters"); + + // Ensure all characters are from the expected alphabet + for (char c : result.toCharArray()) { + assertTrue(TextualCodec.Generator.ENGLISH.containsKey(c), + "Generated character '" + c + "' is not part of the English frequency table"); + } + + // Optional: Analyze distribution for debugging + Map histogram = new HashMap<>(); + for (char c : result.toCharArray()) { + histogram.merge(c, 1, Integer::sum); + } + + System.out.println("...character distribution: " + histogram); + System.out.println("...ok"); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/integrations/covert/jpeg/JpegExifIntegrationTest.java b/lib/src/test/java/zeroecho/sdk/integrations/covert/jpeg/JpegExifIntegrationTest.java new file mode 100644 index 0000000..1f8d3e2 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/integrations/covert/jpeg/JpegExifIntegrationTest.java @@ -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.sdk.integrations.covert.jpeg; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import conflux.Ctx; +import conflux.CtxInterface; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.alg.aes.AesKeyGenSpec; +import zeroecho.core.alg.aes.AesSpec; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.core.DataContentChainBuilder; +import zeroecho.sdk.builders.core.PlainBytesBuilder; +import zeroecho.sdk.content.api.DataContent; + +class JpegExifIntegrationTest { + + @TempDir + Path tempDir; + + private static byte[] readAll(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + in.transferTo(out); + return out.toByteArray(); + } + + @Test + void testEncryptEmbedExtractDecrypt() throws Exception { + System.out.println("testEncryptEmbedExtractDecrypt"); + + String inputText = "Hello, this is a secret message to embed in JPEG."; + + inputText = inputText + inputText; + inputText = inputText + inputText; + inputText = inputText + inputText; + + final byte[] inputBytes = inputText.getBytes(StandardCharsets.UTF_8); + final byte[] aad = { 1, 2, 3 }; + + System.out.println("...inputBytes.length=" + inputBytes.length); + + // AES encryption setup + /* + * CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); SecretKey key = + * aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec. + * aes256()); AesSpec spec = + * AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build(); + * EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, + * spec); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + + * System.nanoTime()); session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware) + * enc).setContext(session); + */ + SecretKey key = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class) + .generateSecret(AesKeyGenSpec.aes256()); + CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); + + byte[] encryptedBytes; + DataContent dccb = DataContentChainBuilder.encrypt() + // input + .add(PlainBytesBuilder.builder().bytes(inputBytes)) + // encryption + .add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()) + // using general AES/GCM/128 without specified header + .spec(AesSpec.gcm128(null)) + // but let the builder add the default header for storing AAD and IV + .withHeader().withAad(aad).context(session)) + // and create the pipeline + .build(); + try (InputStream in = dccb.getStream()) { + encryptedBytes = readAll(in); + } + + System.out.println("...encryptedBytes.length=" + encryptedBytes.length); + System.out.println("...-> " + Arrays.toString(encryptedBytes)); + + // Prepare JPEG test image + Path jpegOriginal = getResourcePath("test.jpg"); + Path jpegEmbedded = tempDir.resolve("stego.jpg"); + + // Embed payload + try (InputStream payloadInput = new ByteArrayInputStream(encryptedBytes); + OutputStream jpegOutput = Files.newOutputStream(jpegEmbedded)) { + + JpegExifEmbedder embedder = new JpegExifEmbedder(); + embedder.setSlots(Slot.defaults()); + int embed_size = embedder.embed(jpegOriginal, payloadInput, jpegOutput); + System.out.println("...embeddedStream.length=" + embed_size); + } + + // Extract payload + ByteArrayOutputStream extractedEncrypted = new ByteArrayOutputStream(); + JpegExifEmbedder embedder = new JpegExifEmbedder(); + embedder.setSlots(Slot.defaults()); + embedder.extract(jpegEmbedded, extractedEncrypted); + + byte[] extractedEncryptedBytes = extractedEncrypted.toByteArray(); + System.out.println("...extractedEncryptedBytes.length=" + extractedEncryptedBytes.length); + System.out.println("...-> " + Arrays.toString(extractedEncryptedBytes)); + + // Decrypt + dccb = DataContentChainBuilder.decrypt() + // input + .add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes)) + // encryption + .add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null)) + // let us use the default header for AAD and IV + .withHeader().withAad(aad).context(session)) + // and create the pipeline + .build(); + byte[] pt1; + try (InputStream in = dccb.getStream()) { + pt1 = readAll(in); + } + /* + * AesSpec spec = + * AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build(); + * EncryptionContext dec1 = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT, + * key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx + * byte[] pt1 = readAll(dec1.attach(new + * ByteArrayInputStream(extractedEncryptedBytes))); dec1.close(); + */ + String decrypted = new String(pt1, StandardCharsets.UTF_8); + + assertEquals(inputText, decrypted); + System.out.println("...ok"); + } + + private Path getResourcePath(String resource) throws URISyntaxException { + URL url = getClass().getClassLoader().getResource(resource); + if (url == null) { + throw new IllegalArgumentException("Missing resource: " + resource); + } + return Paths.get(url.toURI()); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/integrations/stegano/LSBSteganographyMethodTest.java b/lib/src/test/java/zeroecho/sdk/integrations/stegano/LSBSteganographyMethodTest.java new file mode 100644 index 0000000..ef58e16 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/integrations/stegano/LSBSteganographyMethodTest.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package zeroecho.sdk.integrations.stegano; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import org.junit.jupiter.api.Test; + +public class LSBSteganographyMethodTest { + + @Test + void testEmbedAndExtract() throws Exception { + SteganographyMethod method = new LSBSteganographyMethod(); + String message = "Hello from LSB!"; + InputStream imageIn = getClass().getResourceAsStream("/test.jpg"); + InputStream msgIn = new ByteArrayInputStream(message.getBytes()); + + // Embed + InputStream embedded = method.embed(imageIn, ImageFormat.PNG, msgIn); + + // Extract + InputStream extracted = method.extract(embedded); + String result = new String(extracted.readAllBytes()); + + assertEquals(message, result); + } + + @Test + void testMetadata() { + SteganographyMethod method = new LSBSteganographyMethod(); + StegoMetadata meta = method.getMetadata(); + + assertEquals("LSB", meta.name()); + assertTrue(meta.fullName().contains("Least Significant")); + } + + @Test + void createSample() throws Exception { + // for verification purposes + SteganographyMethod method = new LSBSteganographyMethod(); + String message = "Hello from LSB!"; + InputStream imageIn = getClass().getResourceAsStream("/test.jpg"); + InputStream msgIn = new ByteArrayInputStream(message.getBytes()); + + // Embed + InputStream embedded = method.embed(imageIn, ImageFormat.PNG, msgIn); + + embedded.transferTo(Files.newOutputStream(Paths.get("/tmp/lsb-test.png"), StandardOpenOption.CREATE)); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/util/KdfTest.java b/lib/src/test/java/zeroecho/sdk/util/KdfTest.java new file mode 100644 index 0000000..f918c0a --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/util/KdfTest.java @@ -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.sdk.util; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link Kdf#hkdfSha256(byte[], byte[], byte[], int)} using RFC + * 5869 test vectors. + * + *

What is covered

+ *
    + *
  • RFC 5869 Appendix A.1, A.2, A.3 known-answer tests for HKDF-SHA-256 (OKM + * only).
  • + *
  • Behavior with {@code salt == null} vs. {@code salt.length == 0} (both + * treated equivalently).
  • + *
  • Input immutability (arguments are not modified).
  • + *
  • Argument validation (empty IKM, invalid output length).
  • + *
+ */ +public class KdfTest { + + private static byte[] hex(String s) { + return HexFormat.of().parseHex(s.replaceAll("\\s+", "")); + } + + @Test + @DisplayName("RFC5869 A.1 - Basic test case with SHA-256") + void rfc5869_caseA1() throws Exception { + byte[] ikm = hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); // 22 times 0x0b + byte[] salt = hex("000102030405060708090a0b0c"); + byte[] info = hex("f0f1f2f3f4f5f6f7f8f9"); + int L = 42; + + byte[] okm = Kdf.hkdfSha256(ikm, salt, info, L); + + byte[] expectedOkm = hex( + "3cb25f25faacd57a90434f64d0362f2a" + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + "34007208d5b887185865"); + assertArrayEquals(expectedOkm, okm, "OKM must match RFC5869 A.1"); + } + + @Test + @DisplayName("RFC5869 A.2 - Test with longer inputs/outputs") + void rfc5869_caseA2() throws Exception { + byte[] ikm = hex("000102030405060708090a0b0c0d0e0f" + "101112131415161718191a1b1c1d1e1f" + + "202122232425262728292a2b2c2d2e2f" + "303132333435363738393a3b3c3d3e3f" + + "404142434445464748494a4b4c4d4e4f"); // 0x00..0x4f (80 bytes) + byte[] salt = hex("606162636465666768696a6b6c6d6e6f" + "707172737475767778797a7b7c7d7e7f" + + "808182838485868788898a8b8c8d8e8f" + "909192939495969798999a9b9c9d9e9f" + + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"); // 0x60..0xaf (80 bytes) + byte[] info = hex("b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" + "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" + + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" + "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" + + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); // 0xb0..0xff (80 bytes) + int L = 82; + + byte[] okm = Kdf.hkdfSha256(ikm, salt, info, L); + + byte[] expectedOkm = hex("b11e398dc80327a1c8e7f78c596a4934" + "4f012eda2d4efad8a050cc4c19afa97c" + + "59045a99cac7827271cb41c65e590e09" + "da3275600c2f09b8367793a9aca3db71" + + "cc30c58179ec3e87c14c01d5c1f3434f" + "1d87"); + assertArrayEquals(expectedOkm, okm, "OKM must match RFC5869 A.2"); + } + + @Test + @DisplayName("RFC5869 A.3 - Test with zero salt and empty info") + void rfc5869_caseA3() throws Exception { + byte[] ikm = hex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); // 22 times 0x0b + byte[] salt = new byte[0]; // empty salt + byte[] info = new byte[0]; // empty info + int L = 42; + + byte[] okm = Kdf.hkdfSha256(ikm, salt, info, L); + + byte[] expectedOkm = hex( + "8da4e775a563c18f715f802a063c5a31" + "b8a11f5c5ee1879ec3454e5f3c738d2d" + "9d201395faa4b61a96c8"); + assertArrayEquals(expectedOkm, okm, "OKM must match RFC5869 A.3"); + } + + @Test + @DisplayName("salt == null behaves like empty salt") + void nullSaltEqualsEmptySalt() throws Exception { + byte[] ikm = "ikm".getBytes(StandardCharsets.US_ASCII); + byte[] info = "info".getBytes(StandardCharsets.US_ASCII); + + byte[] a = Kdf.hkdfSha256(ikm, null, info, 32); + byte[] b = Kdf.hkdfSha256(ikm, new byte[0], info, 32); + assertArrayEquals(a, b, "null salt and empty salt must be equivalent"); + } + + @Test + @DisplayName("Inputs are not mutated") + void inputsAreNotMutated() throws Exception { + byte[] ikm = hex("00112233aa55"); + byte[] salt = hex("a0a1a2a3"); + byte[] info = hex("b0b1"); + + byte[] ikmCopy = ikm.clone(); + byte[] saltCopy = salt.clone(); + byte[] infoCopy = info.clone(); + + Kdf.hkdfSha256(ikm, salt, info, 16); + + assertArrayEquals(ikmCopy, ikm, "IKM must not be modified"); + assertArrayEquals(saltCopy, salt, "salt must not be modified"); + assertArrayEquals(infoCopy, info, "info must not be modified"); + } + + @Test + @DisplayName("Validation: empty IKM not allowed") + void emptyIkmDisallowed() { + assertThrows(IllegalArgumentException.class, () -> Kdf.hkdfSha256(new byte[0], null, null, 16)); + } + + @Test + @DisplayName("Validation: output length must be within 1..(255*HashLen)") + void invalidLengthsDisallowed() { + byte[] ikm = hex("01"); + assertThrows(IllegalArgumentException.class, () -> Kdf.hkdfSha256(ikm, null, null, 0)); + assertThrows(IllegalArgumentException.class, () -> Kdf.hkdfSha256(ikm, null, null, 255 * 32 + 1)); + } + + @Test + @DisplayName("Small sanity: different info yields different outputs") + void differentInfoProducesDifferentOkm() throws Exception { + byte[] ikm = "ikm".getBytes(StandardCharsets.US_ASCII); + byte[] salt = "salt".getBytes(StandardCharsets.US_ASCII); + byte[] info1 = "ctx1".getBytes(StandardCharsets.US_ASCII); + byte[] info2 = "ctx2".getBytes(StandardCharsets.US_ASCII); + + byte[] okm1 = Kdf.hkdfSha256(ikm, salt, info1, 32); + byte[] okm2 = Kdf.hkdfSha256(ikm, salt, info2, 32); + + assertNotEquals(HexFormat.of().formatHex(okm1), HexFormat.of().formatHex(okm2), + "Distinct info should produce distinct OKM"); + } + + @Test + @DisplayName("Idempotence for same inputs") + void repeatability() throws Exception { + byte[] ikm = "same-ikm".getBytes(StandardCharsets.US_ASCII); + byte[] salt = "same-salt".getBytes(StandardCharsets.US_ASCII); + byte[] info = "same-info".getBytes(StandardCharsets.US_ASCII); + + byte[] a = Kdf.hkdfSha256(ikm, salt, info, 48); + byte[] b = Kdf.hkdfSha256(ikm, salt, info, 48); + assertArrayEquals(a, b, "Same inputs must yield identical OKM"); + } + + @Test + @DisplayName("Produces exact requested length") + void exactLengthProduced() throws Exception { + byte[] ikm = "ikm".getBytes(StandardCharsets.US_ASCII); + for (int len : new int[] { 1, 16, 32, 33, 64, 100 }) { + byte[] okm = Kdf.hkdfSha256(ikm, null, null, len); + assertEquals(len, okm.length, "OKM length must equal requested length"); + } + } + + @Test + @DisplayName("Declared throws: GeneralSecurityException path is reachable") + void macAlgorithmAvailable() throws Exception { + // This test will fail only if HmacSHA256 is unavailable, which would throw + // GeneralSecurityException. + assertDoesNotThrow(() -> Kdf.hkdfSha256(new byte[] { 1, 2, 3 }, null, null, 32)); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/util/OutputToInputStreamAdapterTest.java b/lib/src/test/java/zeroecho/sdk/util/OutputToInputStreamAdapterTest.java new file mode 100644 index 0000000..3ed418e --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/util/OutputToInputStreamAdapterTest.java @@ -0,0 +1,119 @@ +/******************************************************************************* + * 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.sdk.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.junit.jupiter.api.Test; + +/** + * A simple concrete subclass of OutputToInputStreamAdapter that increments each + * byte of input by 1. + */ +class IncrementingAdapter extends OutputToInputStreamAdapter { + + public IncrementingAdapter(InputStream previousInput) { + super(previousInput); + } + + /** + * Initializes transformationOut as an OutputStream that increments bytes by 1. + */ + public void initialize() { + this.transformationOut = new OutputStream() { + @Override + public void write(int b) { + baos.write((b + 1) & 0xFF); + } + }; + } +} + +@Deprecated +public class OutputToInputStreamAdapterTest { + + private static final int BUF_SIZE = 8192; + + private InputStream generateLongInputStream() { + String phrase = "Hello World! "; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + while (baos.size() <= 3 * BUF_SIZE) { + try { + baos.write(phrase.getBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return new ByteArrayInputStream(baos.toByteArray()); + } + + @Test + public void testIncrementingAdapter() throws IOException { + System.out.println("testIncrementingAdapter"); + InputStream plainInput = generateLongInputStream(); + IncrementingAdapter adapter = new IncrementingAdapter(plainInput); + adapter.initialize(); + + ByteArrayOutputStream result = new ByteArrayOutputStream(); + + byte[] buffer = new byte[1024]; + int read; + while ((read = adapter.read(buffer)) != -1) { + result.write(buffer, 0, read); + } + adapter.close(); + + byte[] originalBytes = generateLongInputStream().readAllBytes(); + byte[] transformedBytes = result.toByteArray(); + + System.out.println("..." + transformedBytes.length + " bytes processed"); + + assertEquals(originalBytes.length, transformedBytes.length, "Lengths must be equal"); + + for (int i = 0; i < originalBytes.length; i++) { + int expected = ((originalBytes[i] & 0xFF) + 1) & 0xFF; + int actual = transformedBytes[i] & 0xFF; + assertEquals(expected, actual, "Byte at position " + i + " should be incremented by 1"); + } + System.out.println("...ok"); + } +} diff --git a/lib/src/test/java/zeroecho/sdk/util/Pack7LStreamWriterTest.java b/lib/src/test/java/zeroecho/sdk/util/Pack7LStreamWriterTest.java new file mode 100644 index 0000000..72fc7e4 --- /dev/null +++ b/lib/src/test/java/zeroecho/sdk/util/Pack7LStreamWriterTest.java @@ -0,0 +1,239 @@ +/******************************************************************************* + * 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.sdk.util; + +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.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Random; + +import org.junit.jupiter.api.Test; + +class Pack7LStreamWriterTest { + + @Test + void testSingleWriteCompletePayload() throws IOException { + System.out.println("testSingleWriteCompletePayload"); + + byte[] payload = new byte[50]; + new Random().nextBytes(payload); + + byte[] prefix = encodePack7L(payload.length); + byte[] full = new byte[prefix.length + payload.length]; + System.arraycopy(prefix, 0, full, 0, prefix.length); + System.arraycopy(payload, 0, full, prefix.length, payload.length); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Pack7LStreamWriter writer = new Pack7LStreamWriter(out); + + int consumed = writer.write(full); + + assertEquals(full.length, consumed); + assertArrayEquals(payload, out.toByteArray()); + assertTrue(writer.isComplete()); + + System.out.println("...ok"); + } + + @Test + void testPartialLengthPrefixThenPayload() throws IOException { + System.out.println("testPartialLengthPrefixThenPayload"); + + byte[] payload = new byte[123]; + new Random().nextBytes(payload); + + byte[] prefix = encodePack7L(payload.length); + byte[] all = new byte[prefix.length + payload.length]; + System.arraycopy(prefix, 0, all, 0, prefix.length); + System.arraycopy(payload, 0, all, prefix.length, payload.length); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Pack7LStreamWriter writer = new Pack7LStreamWriter(out); + + int partialPrefixLen = 2; + int consumed1 = writer.write(all, 0, partialPrefixLen); + assertEquals(partialPrefixLen, consumed1); + assertFalse(writer.isComplete()); + + int consumed2 = writer.write(all, consumed1, all.length - consumed1); + assertEquals(all.length - consumed1, consumed2); + assertTrue(writer.isComplete()); + assertArrayEquals(payload, out.toByteArray()); + + System.out.println("...ok"); + } + + @Test + void testExtraDataTruncated() throws IOException { + System.out.println("testExtraDataTruncated"); + + byte[] payload = new byte[100]; + new Random().nextBytes(payload); + byte[] extra = new byte[20]; + new Random().nextBytes(extra); + + byte[] prefix = encodePack7L(payload.length); + byte[] full = new byte[prefix.length + payload.length + extra.length]; + + System.arraycopy(prefix, 0, full, 0, prefix.length); + System.arraycopy(payload, 0, full, prefix.length, payload.length); + System.arraycopy(extra, 0, full, prefix.length + payload.length, extra.length); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Pack7LStreamWriter writer = new Pack7LStreamWriter(out); + + int consumed = writer.write(full); + + assertEquals(prefix.length + payload.length, consumed); + assertArrayEquals(payload, out.toByteArray()); + assertTrue(writer.isComplete()); + + System.out.println("...ok"); + } + + @Test + void testChunkedPayload() throws IOException { + System.out.println("testChunkedPayload"); + + byte[] payload = new byte[2048]; + new Random().nextBytes(payload); + byte[] prefix = encodePack7L(payload.length); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Pack7LStreamWriter writer = new Pack7LStreamWriter(out); + + writer.write(prefix); + + int totalConsumed = 0; + for (int i = 0; i < payload.length; i += 512) { + int chunkSize = Math.min(512, payload.length - i); + int consumed = writer.write(payload, i, chunkSize); + totalConsumed += consumed; + } + + assertEquals(payload.length, totalConsumed); + assertArrayEquals(payload, out.toByteArray()); + assertTrue(writer.isComplete()); + + System.out.println("...ok"); + } + + @Test + void testChunkedPayloadCycle() throws IOException { + System.out.println("testChunkedPayloadCycle"); + + byte[] payload = new byte[2048]; + new Random().nextBytes(payload); + byte[] prefix = encodePack7L(payload.length); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Pack7LStreamWriter writer = new Pack7LStreamWriter(out); + + int prefix_consumed; + + // prefix is also controlled with "consumed" counter + prefix_consumed = writer.write(prefix, 0, 1); + assertEquals(1, prefix_consumed); + + prefix_consumed = writer.write(prefix, 1, prefix.length - 1); + assertEquals(prefix.length - 1, prefix_consumed); + + int totalConsumed = 0; + for (int i = 0; i < payload.length * 10; i += 512) { + int chunkSize = Math.min(512, payload.length - i); + int consumed = writer.write(payload, i, chunkSize); + totalConsumed += consumed; + + if (consumed < chunkSize) { + // the full block was not accepted + break; + } + } + + assertEquals(payload.length, totalConsumed); + assertArrayEquals(payload, out.toByteArray()); + assertTrue(writer.isComplete()); + + System.out.println("...ok"); + } + + @Test + void testWriteReturnsPartialIfTooMuchProvided() throws IOException { + System.out.println("testWriteReturnsPartialIfTooMuchProvided"); + + int payloadLen = 64; + byte[] payload = new byte[payloadLen]; + new Random().nextBytes(payload); + byte[] extra = new byte[10]; + new Random().nextBytes(extra); + + byte[] prefix = encodePack7L(payloadLen); + byte[] full = new byte[prefix.length + payloadLen + extra.length]; + System.arraycopy(prefix, 0, full, 0, prefix.length); + System.arraycopy(payload, 0, full, prefix.length, payloadLen); + System.arraycopy(extra, 0, full, prefix.length + payloadLen, extra.length); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Pack7LStreamWriter writer = new Pack7LStreamWriter(out); + + int consumed = writer.write(full); + + assertEquals(prefix.length + payloadLen, consumed); + assertArrayEquals(payload, out.toByteArray()); + assertTrue(writer.isComplete()); + + System.out.println("...ok"); + } + + /** + * Helper method to encode a long using 7-bit packed encoding. + */ + private static byte[] encodePack7L(long val) { + byte[] buf = new byte[10]; + int idx = buf.length; + while ((val & ~0x7fL) != 0) { + buf[--idx] = (byte) (val & 0x7f); + val >>>= 7; + } + buf[--idx] = (byte) val; + buf[buf.length - 1] |= 0x80; + return Arrays.copyOfRange(buf, idx, buf.length); + } +} diff --git a/lib/src/test/resources/test.jpg b/lib/src/test/resources/test.jpg new file mode 100644 index 0000000..fa80f7b Binary files /dev/null and b/lib/src/test/resources/test.jpg differ diff --git a/samples/.classpath b/samples/.classpath new file mode 100644 index 0000000..f0d9c7c --- /dev/null +++ b/samples/.classpath @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/samples/.project b/samples/.project new file mode 100644 index 0000000..17fa20f --- /dev/null +++ b/samples/.project @@ -0,0 +1,23 @@ + + + samples + Project samples created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/samples/LICENSE b/samples/LICENSE new file mode 100644 index 0000000..208140a --- /dev/null +++ b/samples/LICENSE @@ -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. diff --git a/samples/build.gradle b/samples/build.gradle new file mode 100644 index 0000000..454e11f --- /dev/null +++ b/samples/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'java' +} + +dependencies { + testImplementation(project(":lib")) + testImplementation 'org.egothor:conflux:[1.0,2.0)' + testImplementation("org.bouncycastle:bcpkix-jdk18on:1.81") + testImplementation(platform("org.junit:junit-bom:5.10.2")) + testImplementation 'org.junit.jupiter:junit-jupiter' + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +repositories { + // for conflux lib + maven { + name = "GiteaMaven" + url = uri("https://gitea.egothor.org/api/packages/Egothor/maven") + } + + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +// Compile, but don't produce any artifacts +tasks.named("jar").configure { enabled = false } +tasks.named("javadoc").configure { enabled = false } +tasks.named("assemble").configure { enabled = false } + +// Make these tests opt-in so they don’t run in normal CI builds +tasks.named("test", Test) { + useJUnitPlatform { + includeTags("sample") // only run tests that are explicitly tagged as samples + } + // Only run if -PrunSamples is supplied + onlyIf { + project.hasProperty("runSamples") + } + + // Don't fail the build if zero tests match the tag + filter { + failOnNoMatchingTests = false + } +} diff --git a/samples/src/test/java/demo/AesTest.java b/samples/src/test/java/demo/AesTest.java new file mode 100644 index 0000000..b566fe2 --- /dev/null +++ b/samples/src/test/java/demo/AesTest.java @@ -0,0 +1,259 @@ +/******************************************************************************* + * 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 demo; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.Test; + +import conflux.Ctx; +import conflux.CtxInterface; +import zeroecho.core.ConfluxKeys; +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.aes.AesKeyGenSpec; +import zeroecho.core.alg.aes.AesSpec; +import zeroecho.core.context.EncryptionContext; +import zeroecho.core.spi.ContextAware; +import zeroecho.core.util.Strings; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.core.DataContentChainBuilder; +import zeroecho.sdk.builders.core.PlainBytesBuilder; +import zeroecho.sdk.content.api.DataContent; + +class AesTest { + private static final Logger LOG = Logger.getLogger(AesTest.class.getName()); + + SecretKey generateAesKey() throws GeneralSecurityException { + // Locate the AES algorithm in the catalog + CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); + SecretKey key = aes + // Retrieve the builder that works with AesKeyGenSpec - the specification for + // AES key generation + .symmetricKeyBuilder(AesKeyGenSpec.class) + // Generate a secret key according to the AES256 specification + .generateSecret(AesKeyGenSpec.aes256()); + // Log the generated key (truncated to short hex for readability) + LOG.log(Level.INFO, "AES256 key generated: {0}", Strings.toShortHexString(key.getEncoded())); + + // or just: CryptoAlgorithms.generateSecret("AES", AesKeyGenSpec.aes256()) + + return key; + } + + @Test + void aesEncryptCoreLevelAPI() throws GeneralSecurityException, IOException { + // Sample message to encrypt + byte[] msg = randomBytes(100); + + // AES-GCM specification with a 128-bit authentication tag + // The null parameter indicates that no header codec is used (more on this + // below) + AesSpec spec = AesSpec.gcm128(null); + // Encryption will generate a random IV for us. Normally it would be saved + // through the header codec, but since we do not use it here, the IV and other + // values are stored in a "context" instead. The "context" is a key-value + // in-memory store; we create a private context with a unique name for this + // session. + CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); + + SecretKey key = generateAesKey(); + byte[] encrypted; + // Request an encryption context using the key and AES-GCM-128 specification + try (EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key, spec)) { + // This context implements ContextAware, allowing us to associate our session + ((ContextAware) enc).setContext(session); + // Get an encrypted stream that processes the plaintext on-the-fly + try (InputStream encryptedStream = enc.attach(new ByteArrayInputStream(msg))) { + // In this sample we consume the encrypted stream fully into memory + encrypted = readAll(encryptedStream); + } + } + + LOG.log(Level.INFO, "Core: AES256/GCM128 IV={0} AAD={1} encrypted={2}", new Object[] { + // The IV was generated automatically (we could have provided one, but + // if omitted, it is created for us) + Strings.toShortHexString(session.get(ConfluxKeys.iv("AES"))), + // AAD is an additional parameter for GCM; again, automatically generated + // because we did not provide it + Strings.toShortHexString(session.get(ConfluxKeys.aad("AES"))), + // The final ciphertext + Strings.toShortHexString(encrypted) }); + } + + @Test + void aesEncryptSdkLevelAPI() throws GeneralSecurityException, IOException { + // Sample message to encrypt + byte[] msg = randomBytes(100); + + SecretKey key = generateAesKey(); + AesSpec spec = AesSpec.gcm128(null); + + // Separate context again to hold IV and AAD values so we can inspect them later + CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder() + // Use the generated AES key + .importKeyRaw(key.getEncoded()) + // Specify AES-GCM-128 mode + .spec(spec) + // Provide the context to capture IV and AAD + .context(session); + + DataContent dccb = DataContentChainBuilder + // Build an encryption pipeline + .encrypt() + // Input: the sample byte array + .add(PlainBytesBuilder.builder().bytes(msg)) + // Process through AES encryption + .add(aesBuilder) + // Finalize the pipeline + .build(); + + byte[] encrypted; + try (InputStream encryptedStream = dccb.getStream()) { + // Consume the encrypted data into memory + encrypted = readAll(encryptedStream); + } + + LOG.log(Level.INFO, "SDK: AES256/GCM128 IV={0} AAD={1} encrypted={2}", new Object[] { + // IV generated for us (we could provide one; if omitted, a random IV is + // created) + Strings.toShortHexString(session.get(ConfluxKeys.iv("AES"))), + // AAD generated automatically (not explicitly provided) + Strings.toShortHexString(session.get(ConfluxKeys.aad("AES"))), + // Final ciphertext + Strings.toShortHexString(encrypted) }); + } + + @Test + void aesEncryptSmarterSdkLevelAPI() throws GeneralSecurityException, IOException { + // Sample message to encrypt + byte[] msg = randomBytes(100); + + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder() + // Automatically generate a 256-bit AES key + .generateKey(256) + // Store ad-hoc generated parameters (IV, AAD) in the stream header + .withHeader() + // Use AES-GCM with a 128-bit authentication tag + .modeGcm(128); + + // The builder stores all generated attributes inside the stream header + DataContent dccb = DataContentChainBuilder + // Build an encryption pipeline + .encrypt() + // Input: the sample byte array + .add(PlainBytesBuilder.builder().bytes(msg)) + // Process through AES encryption + .add(aesBuilder) + // Finalize the pipeline + .build(); + + LOG.log(Level.INFO, "SDK-smart: AES256 key generated {0}", + // The key is generated within dccb's `build` method, not earlier + Strings.toShortHexString(aesBuilder.generatedKey().getEncoded())); + + byte[] encrypted; + try (InputStream encryptedStream = dccb.getStream()) { + // Consume the encrypted data into memory + encrypted = readAll(encryptedStream); + } + + LOG.log(Level.INFO, "SDK-smart: AES256/GCM128 IV=builtin AAD=builtin encrypted={0}", + // The ciphertext, with IV and AAD already embedded in the header + Strings.toShortHexString(encrypted)); + } + + @Test + void aesRoundSmarterSdkLevelAPI() throws GeneralSecurityException, IOException { + // Sample message to encrypt + byte[] msg = randomBytes(100); + + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader(); + + // The builder stores generated IV and AAD inside the stream header + DataContent dccb = DataContentChainBuilder.encrypt().add(PlainBytesBuilder.builder().bytes(msg)).add(aesBuilder) + .build(); + + SecretKey key = aesBuilder.generatedKey(); + LOG.log(Level.INFO, "SDK-smart: AES256 key generated {0}", Strings.toShortHexString(key.getEncoded())); + + byte[] encrypted; + try (InputStream encryptedStream = dccb.getStream()) { + // Consume the encrypted data into memory + encrypted = readAll(encryptedStream); + } + + dccb = DataContentChainBuilder.decrypt().add(PlainBytesBuilder.builder().bytes(encrypted)) + // Use the same AES key for decryption; IV and AAD are restored from the header + .add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).modeGcm(128).withHeader()) + // Build the pipeline + .build(); + byte[] decrypted; + try (InputStream decryptedStream = dccb.getStream()) { + // Consume the decrypted data into memory + decrypted = readAll(decryptedStream); + } + + LOG.log(Level.INFO, "original message={0} after AES roundtrip={1}", + new Object[] { Strings.toShortHexString(msg), Strings.toShortHexString(decrypted) }); + } + + // helpers + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + private static byte[] readAll(InputStream in) throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + in.transferTo(out); + out.flush(); + return out.toByteArray(); + } + } +} diff --git a/samples/src/test/java/demo/CombinedDeliveryTest.java b/samples/src/test/java/demo/CombinedDeliveryTest.java new file mode 100644 index 0000000..3b6b1a5 --- /dev/null +++ b/samples/src/test/java/demo/CombinedDeliveryTest.java @@ -0,0 +1,194 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package demo; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.SecureRandom; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.KeyUsage; +import zeroecho.core.alg.elgamal.ElgamalParamSpec; +import zeroecho.core.alg.kyber.KyberKeyGenSpec; +import zeroecho.core.alg.rsa.RsaKeyGenSpec; +import zeroecho.core.util.Strings; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.core.DataContentChainBuilder; +import zeroecho.sdk.builders.core.PlainBytesBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.guard.MultiRecipientDataSourceBuilder; +import zeroecho.sdk.guard.UnlockMaterial; +import zeroecho.sdk.util.BouncyCastleActivator; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CombinedDeliveryTest { + private static final Logger LOG = Logger.getLogger(CombinedDeliveryTest.class.getName()); + + @BeforeAll + void setupProviders() { + BouncyCastleActivator.init(); + } + + KeyPair generateKyberKeys() throws GeneralSecurityException { + KeyPair kp = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024()); + + LOG.log(Level.INFO, "ML-KEM key public={0} private={1}", + new Object[] { Strings.toShortHexString(kp.getPublic().getEncoded()), + Strings.toShortHexString(kp.getPrivate().getEncoded()) }); + + return kp; + } + + KeyPair generateRsaKeys() throws GeneralSecurityException { + KeyPair kp = CryptoAlgorithms.generateKeyPair("RSA", RsaKeyGenSpec.rsa4096()); + + LOG.log(Level.INFO, "RSA key public={0} private={1}", + new Object[] { Strings.toShortHexString(kp.getPublic().getEncoded()), + Strings.toShortHexString(kp.getPrivate().getEncoded()) }); + + return kp; + } + + KeyPair generateElGamalKeys() throws GeneralSecurityException { + KeyPair kp = CryptoAlgorithms.generateKeyPair("ElGamal", ElgamalParamSpec.ffdhe2048()); + + LOG.log(Level.INFO, "ElGamal key public={0} private={1}", + new Object[] { Strings.toShortHexString(kp.getPublic().getEncoded()), + Strings.toShortHexString(kp.getPrivate().getEncoded()) }); + + return kp; + } + + @Test + void combinedSdkLevelAPI() throws GeneralSecurityException, IOException { + // Sample message to encrypt + byte[] msg = randomBytes(100); + + KeyPair kem = generateKyberKeys(); + KeyPair rsa = generateRsaKeys(); + KeyPair elg = generateElGamalKeys(); + char[] password = "p@ssw07d".toCharArray(); + + KeyPair decoy1rsa = generateRsaKeys(); + KeyPair decoy2rsa = generateRsaKeys(); + + AesDataContentBuilder payload = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + + MultiRecipientDataSourceBuilder multi = MultiRecipientDataSourceBuilder.builder() + // AES-256 - 32 bytes of key material + .payloadKeyBytes(32).withAes(payload) + // add recipients - the context initialized with the public key + .addRecipient(CryptoAlgorithms.create("ElGamal", KeyUsage.ENCRYPT, elg.getPublic())) + .addRecipient(CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, rsa.getPublic())) + // ML-KEM PostQuantum uses AES for the inner payload + .addRecipient(CryptoAlgorithms.create("ML-KEM", KeyUsage.ENCAPSULATE, kem.getPublic()), + /* AES256 key size */ + 32, + /* salt size in hkdf */ + 32) + // Password (via KDF) + .addPasswordRecipient(password, /* iterations */ 200_000, /* saltLen */ 16, /* kekBytes */ 32) + // and some decoys + .addRecipient(CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, decoy1rsa.getPublic())) + .addRecipient(CryptoAlgorithms.create("RSA", KeyUsage.ENCRYPT, decoy2rsa.getPublic())); + + // shuffle all the recipients + multi.shuffle(); + + DataContent dccb = DataContentChainBuilder.encrypt().add(PlainBytesBuilder.builder().bytes(msg)) + // encrypt for multi recipients of various types + .add(multi).build(); + + byte[] encrypted; + try (InputStream encryptedStream = dccb.getStream()) { + // Consume the encrypted data into memory + encrypted = readAll(encryptedStream); + } + + recipientProcessing("ElGamal-ffdhe2048", msg, encrypted, new UnlockMaterial.Private(elg.getPrivate())); + recipientProcessing("RSA4096", msg, encrypted, new UnlockMaterial.Private(rsa.getPrivate())); + recipientProcessing("Kyber-1024", msg, encrypted, new UnlockMaterial.Private(kem.getPrivate())); + recipientProcessing("Password", msg, encrypted, new UnlockMaterial.Password(password)); + } + + private void recipientProcessing(String method, byte[] msg, byte[] encrypted, UnlockMaterial unlock) + throws IOException { + AesDataContentBuilder payload = AesDataContentBuilder.builder().modeCbcPkcs5().withHeader(); + MultiRecipientDataSourceBuilder multi = MultiRecipientDataSourceBuilder.builder() + // define our payload + .payloadKeyBytes(32).withAes(payload) + // one recipient + .unlockWith(unlock); + + // Decryption + DataContent dccb = DataContentChainBuilder.decrypt().add(PlainBytesBuilder.builder().bytes(encrypted)) + // decrypt via multi + .add(multi).build(); + + byte[] decrypted; + try (InputStream decryptedStream = dccb.getStream()) { + // Consume the decrypted data into memory + decrypted = readAll(decryptedStream); + } + + LOG.log(Level.INFO, "original message={0} after {2}/AES256/CBC/PKCS5 roundtrip={1}", + new Object[] { Strings.toShortHexString(msg), Strings.toShortHexString(decrypted), method }); + } + + // helpers + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + private static byte[] readAll(InputStream in) throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + in.transferTo(out); + out.flush(); + return out.toByteArray(); + } + } +} diff --git a/samples/src/test/java/demo/PostQuantumTest.java b/samples/src/test/java/demo/PostQuantumTest.java new file mode 100644 index 0000000..509f247 --- /dev/null +++ b/samples/src/test/java/demo/PostQuantumTest.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + ******************************************************************************/ +package demo; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.SecureRandom; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.alg.kyber.KyberKeyGenSpec; +import zeroecho.core.util.Strings; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.alg.KemDataContentBuilder; +import zeroecho.sdk.builders.core.DataContentChainBuilder; +import zeroecho.sdk.builders.core.PlainBytesBuilder; +import zeroecho.sdk.content.api.DataContent; +import zeroecho.sdk.util.BouncyCastleActivator; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PostQuantumTest { + private static final Logger LOG = Logger.getLogger(PostQuantumTest.class.getName()); + + @BeforeAll + void setupProviders() { + BouncyCastleActivator.init(); + } + + KeyPair generateKyberKeys() throws GeneralSecurityException { + KeyPair kp = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber1024()); + + LOG.log(Level.INFO, "ML-KEM key public={0} private={1}", + new Object[] { Strings.toShortHexString(kp.getPublic().getEncoded()), + Strings.toShortHexString(kp.getPrivate().getEncoded()) }); + + return kp; + } + + @Test + void pqcSdkLevelAPI() throws GeneralSecurityException, IOException { + // Sample message to encrypt + byte[] msg = randomBytes(100); + + KeyPair kp = generateKyberKeys(); + + DataContent dccb = DataContentChainBuilder.encrypt().add(PlainBytesBuilder.builder().bytes(msg)) + .add(KemDataContentBuilder.builder().kem("ML-KEM").recipientPublic(kp.getPublic()) + .hkdfSha256("KEM-demo".getBytes(StandardCharsets.US_ASCII)) + .withAes(AesDataContentBuilder.builder().modeGcm(128).withHeader())) + .build(); + + byte[] encrypted; + try (InputStream encryptedStream = dccb.getStream()) { + // Consume the encrypted data into memory + encrypted = readAll(encryptedStream); + } + + // Decryption + dccb = DataContentChainBuilder.decrypt().add(PlainBytesBuilder.builder().bytes(encrypted)) + .add(KemDataContentBuilder.builder().kem("ML-KEM").recipientPrivate(kp.getPrivate()) + .hkdfSha256("KEM-demo".getBytes(StandardCharsets.US_ASCII)) + .withAes(AesDataContentBuilder.builder().modeGcm(128).withHeader())) + .build(); + + byte[] decrypted; + try (InputStream decryptedStream = dccb.getStream()) { + // Consume the decrypted data into memory + decrypted = readAll(decryptedStream); + } + + LOG.log(Level.INFO, "original message={0} after ML-KEM(Kyber)+AES/GCM128 roundtrip={1}", + new Object[] { Strings.toShortHexString(msg), Strings.toShortHexString(decrypted) }); + } + + // helpers + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + private static byte[] readAll(InputStream in) throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + in.transferTo(out); + out.flush(); + return out.toByteArray(); + } + } +} diff --git a/samples/src/test/java/demo/SigningAesTest.java b/samples/src/test/java/demo/SigningAesTest.java new file mode 100644 index 0000000..b194211 --- /dev/null +++ b/samples/src/test/java/demo/SigningAesTest.java @@ -0,0 +1,212 @@ +/******************************************************************************* + * 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 demo; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.SecureRandom; +import java.security.Signature; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.crypto.SecretKey; + +import org.junit.jupiter.api.Test; + +import zeroecho.core.CryptoAlgorithm; +import zeroecho.core.CryptoAlgorithms; +import zeroecho.core.alg.aes.AesKeyGenSpec; +import zeroecho.core.alg.rsa.RsaKeyGenSpec; +import zeroecho.core.alg.rsa.RsaSigSpec; +import zeroecho.core.tag.TagEngine; +import zeroecho.core.tag.TagEngineBuilder; +import zeroecho.core.util.Strings; +import zeroecho.sdk.builders.TagTrailerDataContentBuilder; +import zeroecho.sdk.builders.alg.AesDataContentBuilder; +import zeroecho.sdk.builders.core.DataContentChainBuilder; +import zeroecho.sdk.builders.core.PlainBytesBuilder; +import zeroecho.sdk.content.api.DataContent; + +class SigningAesTest { + private static final Logger LOG = Logger.getLogger(SigningAesTest.class.getName()); + + SecretKey generateAesKey() throws GeneralSecurityException { + // Locate the AES algorithm in the catalog + CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); + SecretKey key = aes + // Retrieve the builder that works with AesKeyGenSpec - the specification for + // AES key generation + .symmetricKeyBuilder(AesKeyGenSpec.class) + // Generate a secret key according to the AES256 specification + .generateSecret(AesKeyGenSpec.aes256()); + // Log the generated key (truncated to short hex for readability) + LOG.log(Level.INFO, "AES256 key generated: {0}", Strings.toShortHexString(key.getEncoded())); + + // or just: CryptoAlgorithms.generateSecret("AES", AesKeyGenSpec.aes256()) + + return key; + } + + KeyPair generateRsaKeys() throws GeneralSecurityException { + KeyPair kp = CryptoAlgorithms.generateKeyPair("RSA", RsaKeyGenSpec.rsa4096()); + + LOG.log(Level.INFO, "RSA key public={0} private={1}", + new Object[] { Strings.toShortHexString(kp.getPublic().getEncoded()), + Strings.toShortHexString(kp.getPrivate().getEncoded()) }); + + return kp; + } + + @Test + void aesRoundStESdkLevelAPI() throws GeneralSecurityException, IOException { + LOG.info("aesRoundSmarterSdkLevelAPI - Sign then Encrypt"); + + // Sample message to encrypt + byte[] msg = randomBytes(100); + + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader(); + + // RSA-2048 keys (use registry for convenience) + KeyPair rsa = generateRsaKeys(); + + // Tag engines (SHA-256, saltLen=32) + RsaSigSpec pss = RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32); + TagEngine tagEnc = TagEngineBuilder.rsaSign(rsa.getPrivate(), pss).get(); + TagEngine tagDec = TagEngineBuilder.rsaVerify(rsa.getPublic(), pss).get(); + + // The builder stores generated IV and AAD inside the stream header + DataContent dccb = DataContentChainBuilder.encrypt().add(PlainBytesBuilder.builder().bytes(msg)) + // sign the data + .add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192)) + // and then encrypt + .add(aesBuilder).build(); + + SecretKey key = aesBuilder.generatedKey(); + LOG.log(Level.INFO, "SDK-smart: AES256 key generated {0}", Strings.toShortHexString(key.getEncoded())); + + byte[] encrypted; + try (InputStream encryptedStream = dccb.getStream()) { + // Consume the encrypted data into memory + encrypted = readAll(encryptedStream); + } + + dccb = DataContentChainBuilder.decrypt().add(PlainBytesBuilder.builder().bytes(encrypted)) + // Use the same AES key for decryption; IV and AAD are restored from the header + .add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).modeGcm(128).withHeader()) + // the decrypted stream must be verified + .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()) + // Build the pipeline + .build(); + byte[] decrypted; + try (InputStream decryptedStream = dccb.getStream()) { + // Consume the decrypted data into memory + decrypted = readAll(decryptedStream); + } + + LOG.log(Level.INFO, "original message={0} after AES roundtrip={1}", + new Object[] { Strings.toShortHexString(msg), Strings.toShortHexString(decrypted) }); + } + + @Test + void aesRoundEtSSdkLevelAPI() throws GeneralSecurityException, IOException { + LOG.info("aesRoundSmarterSdkLevelAPI - Encrypt then Sign"); + + // Sample message to encrypt + byte[] msg = randomBytes(100); + + AesDataContentBuilder aesBuilder = AesDataContentBuilder.builder().generateKey(256).modeGcm(128).withHeader(); + + // RSA-2048 keys (use registry for convenience) + KeyPair rsa = generateRsaKeys(); + + // Tag engines (SHA-256, saltLen=32) + RsaSigSpec pss = RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32); + TagEngine tagEnc = TagEngineBuilder.rsaSign(rsa.getPrivate(), pss).get(); + TagEngine tagDec = TagEngineBuilder.rsaVerify(rsa.getPublic(), pss).get(); + + // The builder stores generated IV and AAD inside the stream header + DataContent dccb = DataContentChainBuilder.encrypt().add(PlainBytesBuilder.builder().bytes(msg)) + // encrypt + .add(aesBuilder) + // and then sign + .add(new TagTrailerDataContentBuilder<>(tagEnc).bufferSize(8192)) + // + .build(); + + SecretKey key = aesBuilder.generatedKey(); + LOG.log(Level.INFO, "SDK-smart: AES256 key generated {0}", Strings.toShortHexString(key.getEncoded())); + + byte[] encrypted; + try (InputStream encryptedStream = dccb.getStream()) { + // Consume the encrypted data into memory + encrypted = readAll(encryptedStream); + } + + dccb = DataContentChainBuilder.decrypt().add(PlainBytesBuilder.builder().bytes(encrypted)) + // the stream must be verified, but encryption still runs as data flows through + .add(new TagTrailerDataContentBuilder<>(tagDec).bufferSize(8192).throwOnMismatch()) + // Use the same AES key for decryption; IV and AAD are restored from the header + .add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).modeGcm(128).withHeader()) + // Build the pipeline + .build(); + byte[] decrypted; + try (InputStream decryptedStream = dccb.getStream()) { + // Consume the decrypted data into memory + decrypted = readAll(decryptedStream); + } + + LOG.log(Level.INFO, "original message={0} after AES roundtrip={1}", + new Object[] { Strings.toShortHexString(msg), Strings.toShortHexString(decrypted) }); + } + + // helpers + + private static byte[] randomBytes(int len) { + byte[] data = new byte[len]; + new SecureRandom().nextBytes(data); + return data; + } + + private static byte[] readAll(InputStream in) throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + in.transferTo(out); + out.flush(); + return out.toByteArray(); + } + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..fc8f320 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,14 @@ +pluginManagement { + plugins { + id 'com.palantir.git-version' version '4.0.0' // Define the plugin version globally + } +} + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} + +rootProject.name = 'ZeroEcho' + +include('app', 'lib', 'samples')