Initial commit (history reset)
This commit is contained in:
12
.gitattributes
vendored
Normal file
12
.gitattributes
vendored
Normal file
@@ -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
|
||||||
|
|
||||||
92
.gitea/workflows/release.yml
Normal file
92
.gitea/workflows/release.yml
Normal file
@@ -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
|
||||||
109
.gitignore
vendored
Normal file
109
.gitignore
vendored
Normal file
@@ -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
|
||||||
23
.project
Normal file
23
.project
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>ZeroEcho</name>
|
||||||
|
<comment></comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>net.sourceforge.pmd.eclipse.plugin.pmdBuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||||
|
<nature>net.sourceforge.pmd.eclipse.plugin.pmdNature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
||||||
356
.ruleset
Normal file
356
.ruleset
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ruleset xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
name="EgothorRuleset"
|
||||||
|
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
||||||
|
<description>Egothor preferences rule set</description>
|
||||||
|
<exclude-pattern>.*/package-info\.java</exclude-pattern>
|
||||||
|
<rule ref="category/java/bestpractices.xml/AbstractClassWithoutAbstractMethod"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/AccessorClassGeneration"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/AccessorMethodGeneration"/>
|
||||||
|
<!-- rule ref="category/java/bestpractices.xml/ArrayIsStoredDirectly"/ -->
|
||||||
|
<rule ref="category/java/bestpractices.xml/AvoidMessageDigestField"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/AvoidPrintStackTrace"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/AvoidReassigningCatchVariables"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/AvoidReassigningLoopVariables"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/AvoidReassigningParameters"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/AvoidStringBufferField"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/AvoidUsingHardCodedIP"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/CheckResultSet"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/ConstantsInInterface"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/DefaultLabelNotLastInSwitch"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/DoubleBraceInitialization"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/ExhaustiveSwitchHasDefault"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/ForLoopCanBeForeach"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/ForLoopVariableCount"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/GuardLogStatement"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/ImplicitFunctionalInterface"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/JUnit4SuitesShouldUseSuiteAnnotation"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/JUnit5TestShouldBePackagePrivate"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/JUnitUseExpected"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/LiteralsFirstInComparisons"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/LooseCoupling"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/MethodReturnsInternalArray"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/MissingOverride"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/NonExhaustiveSwitch"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/OneDeclarationPerLine"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/PreserveStackTrace"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/PrimitiveWrapperInstantiation"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/ReplaceEnumerationWithIterator"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/ReplaceHashtableWithMap"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/ReplaceVectorWithList"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/SimplifiableTestAssertion"/>
|
||||||
|
<!-- rule ref="category/java/bestpractices.xml/SystemPrintln"/ -->
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnitTestAssertionsShouldIncludeMessage"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnitTestContainsTooManyAsserts"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnitTestShouldIncludeAssert"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnitTestShouldUseAfterAnnotation"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnitTestShouldUseBeforeAnnotation"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnitTestShouldUseTestAnnotation"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnnecessaryVarargsArrayCreation"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnnecessaryWarningSuppression"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnusedAssignment"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnusedFormalParameter"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnusedLocalVariable"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnusedPrivateField"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UnusedPrivateMethod"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UseCollectionIsEmpty"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UseEnumCollections"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UseStandardCharsets"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UseTryWithResources"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/UseVarargs"/>
|
||||||
|
<rule ref="category/java/bestpractices.xml/WhileLoopWithLiteralBoolean"/>
|
||||||
|
<!-- rule ref="category/java/codestyle.xml/AtLeastOneConstructor"/ -->
|
||||||
|
<rule ref="category/java/codestyle.xml/AvoidDollarSigns"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/AvoidProtectedFieldInFinalClass"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/AvoidProtectedMethodInFinalClassNotExtending"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/AvoidUsingNativeCode"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/BooleanGetMethodName"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/CallSuperInConstructor"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/ClassNamingConventions"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/CommentDefaultAccessModifier"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/ConfusingTernary"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/ControlStatementBraces"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/EmptyControlStatement"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/EmptyMethodInAbstractClassShouldBeAbstract"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/ExtendsObject"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/FieldDeclarationsShouldBeAtStartOfClass"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/FieldNamingConventions">
|
||||||
|
<properties>
|
||||||
|
<property name="publicConstantPattern" value="[A-Z][A-Z_0-9]*" />
|
||||||
|
<property name="constantPattern" value="[A-Z][A-Z_0-9]*" />
|
||||||
|
<property name="enumConstantPattern" value="[A-Z][A-Z_0-9]*" />
|
||||||
|
<property name="finalFieldPattern" value="[_a-zA-Z][a-zA-Z_0-9]*" />
|
||||||
|
<property name="staticFieldPattern" value="[_a-zA-Z][a-zA-Z0-9]*" />
|
||||||
|
<property name="defaultFieldPattern" value="[_a-z][a-zA-Z0-9]*" />
|
||||||
|
<property name="exclusions" value="serialVersionUID,serialPersistentFields" />
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/codestyle.xml/FinalParameterInAbstractMethod"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/ForLoopShouldBeWhileLoop"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/FormalParameterNamingConventions"/>
|
||||||
|
<!-- rule ref="category/java/codestyle.xml/GenericsNaming"/ -->
|
||||||
|
<rule ref="category/java/codestyle.xml/IdenticalCatchBranches"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/LambdaCanBeMethodReference"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/LinguisticNaming"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/LocalHomeNamingConvention"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/LocalInterfaceSessionNamingConvention"/>
|
||||||
|
<!-- rule ref="category/java/codestyle.xml/LocalVariableCouldBeFinal"/ -->
|
||||||
|
<rule ref="category/java/codestyle.xml/LocalVariableNamingConventions">
|
||||||
|
<properties>
|
||||||
|
<property name="localVarPattern" value="[_a-z][a-zA-Z0-9]*" />
|
||||||
|
<property name="finalVarPattern" value="[_a-zA-Z][a-zA-Z_0-9]*" />
|
||||||
|
<property name="catchParameterPattern" value="[a-z][a-zA-Z0-9]*" />
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/codestyle.xml/LongVariable">
|
||||||
|
<properties>
|
||||||
|
<property name="minimum" value="28" />
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/codestyle.xml/MDBAndSessionBeanNamingConvention"/>
|
||||||
|
<!-- rule ref="category/java/codestyle.xml/MethodArgumentCouldBeFinal"/ -->
|
||||||
|
<rule ref="category/java/codestyle.xml/MethodNamingConventions">
|
||||||
|
<properties>
|
||||||
|
<property name="methodPattern" value="[a-z][a-zA-Z0-9_]*" />
|
||||||
|
<property name="staticPattern" value="[a-z][a-zA-Z0-9_]*" />
|
||||||
|
<property name="nativePattern" value="[a-z][a-zA-Z0-9_]*" />
|
||||||
|
<property name="junit3TestPattern" value="test[A-Z0-9][a-zA-Z0-9]*" />
|
||||||
|
<property name="junit4TestPattern" value="[a-z][a-zA-Z0-9]*" />
|
||||||
|
<property name="junit5TestPattern" value="[a-z][a-zA-Z0-9]*" />
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/codestyle.xml/NoPackage"/>
|
||||||
|
<!-- rule ref="category/java/codestyle.xml/OnlyOneReturn"/ -->
|
||||||
|
<rule ref="category/java/codestyle.xml/PackageCase"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/PrematureDeclaration"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/RemoteInterfaceNamingConvention"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/RemoteSessionInterfaceNamingConvention"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/ShortClassName"/>
|
||||||
|
<!-- rule ref="category/java/codestyle.xml/ShortMethodName"/ -->
|
||||||
|
<!-- rule ref="category/java/codestyle.xml/ShortVariable"/ -->
|
||||||
|
<rule ref="category/java/codestyle.xml/TooManyStaticImports"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UnnecessaryAnnotationValueElement"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UnnecessaryBoxing"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UnnecessaryCast"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UnnecessaryConstructor"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UnnecessaryFullyQualifiedName"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UnnecessaryImport"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UnnecessaryLocalBeforeReturn"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UnnecessaryModifier"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UnnecessaryReturn"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UnnecessarySemicolon"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UseDiamondOperator"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UseExplicitTypes"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UselessParentheses"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UselessQualifiedThis"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UseShortArrayInitializer"/>
|
||||||
|
<rule ref="category/java/codestyle.xml/UseUnderscoresInNumericLiterals"/>
|
||||||
|
<rule ref="category/java/design.xml/AbstractClassWithoutAnyMethod"/>
|
||||||
|
<rule ref="category/java/design.xml/AvoidCatchingGenericException"/>
|
||||||
|
<rule ref="category/java/design.xml/AvoidDeeplyNestedIfStmts"/>
|
||||||
|
<rule ref="category/java/design.xml/AvoidRethrowingException"/>
|
||||||
|
<rule ref="category/java/design.xml/AvoidThrowingNewInstanceOfSameException"/>
|
||||||
|
<rule ref="category/java/design.xml/AvoidThrowingNullPointerException"/>
|
||||||
|
<rule ref="category/java/design.xml/AvoidThrowingRawExceptionTypes"/>
|
||||||
|
<rule ref="category/java/design.xml/AvoidUncheckedExceptionsInSignatures"/>
|
||||||
|
<rule ref="category/java/design.xml/ClassWithOnlyPrivateConstructorsShouldBeFinal"/>
|
||||||
|
<rule ref="category/java/design.xml/CognitiveComplexity">
|
||||||
|
<properties>
|
||||||
|
<property name="reportLevel" value="45" />
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/design.xml/CollapsibleIfStatements"/>
|
||||||
|
<rule ref="category/java/design.xml/CouplingBetweenObjects">
|
||||||
|
<properties>
|
||||||
|
<property name="threshold" value="50" />
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/design.xml/CyclomaticComplexity">
|
||||||
|
<properties>
|
||||||
|
<property name="methodReportLevel" value="18" />
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/design.xml/DataClass"/>
|
||||||
|
<rule ref="category/java/design.xml/DoNotExtendJavaLangError"/>
|
||||||
|
<rule ref="category/java/design.xml/ExceptionAsFlowControl"/>
|
||||||
|
<!-- rule ref="category/java/design.xml/ExcessiveImports"/ -->
|
||||||
|
<rule ref="category/java/design.xml/ExcessiveParameterList"/>
|
||||||
|
<rule ref="category/java/design.xml/ExcessivePublicCount"/>
|
||||||
|
<rule ref="category/java/design.xml/FinalFieldCouldBeStatic"/>
|
||||||
|
<!-- rule ref="category/java/design.xml/GodClass"/ -->
|
||||||
|
<rule ref="category/java/design.xml/ImmutableField"/>
|
||||||
|
<rule ref="category/java/design.xml/InvalidJavaBean"/>
|
||||||
|
<!-- rule ref="category/java/design.xml/LawOfDemeter"/ -->
|
||||||
|
<rule ref="category/java/design.xml/LogicInversion"/>
|
||||||
|
<!-- rule ref="category/java/design.xml/LoosePackageCoupling"/ -->
|
||||||
|
<rule ref="category/java/design.xml/MutableStaticState"/>
|
||||||
|
<rule ref="category/java/design.xml/NcssCount"/>
|
||||||
|
<rule ref="category/java/design.xml/NPathComplexity">
|
||||||
|
<properties>
|
||||||
|
<property name="reportLevel" value="260" />
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/design.xml/SignatureDeclareThrowsException"/>
|
||||||
|
<rule ref="category/java/design.xml/SimplifiedTernary"/>
|
||||||
|
<rule ref="category/java/design.xml/SimplifyBooleanExpressions"/>
|
||||||
|
<rule ref="category/java/design.xml/SimplifyBooleanReturns"/>
|
||||||
|
<rule ref="category/java/design.xml/SimplifyConditional"/>
|
||||||
|
<rule ref="category/java/design.xml/SingularField"/>
|
||||||
|
<rule ref="category/java/design.xml/SwitchDensity"/>
|
||||||
|
<rule ref="category/java/design.xml/TooManyFields">
|
||||||
|
<properties>
|
||||||
|
<property name="maxfields" value="30"/>
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/design.xml/TooManyMethods">
|
||||||
|
<properties>
|
||||||
|
<property name="maxmethods" value="50"/>
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/design.xml/UselessOverridingMethod"/>
|
||||||
|
<rule ref="category/java/design.xml/UseObjectForClearerAPI"/>
|
||||||
|
<rule ref="category/java/design.xml/UseUtilityClass"/>
|
||||||
|
<rule ref="category/java/documentation.xml/CommentContent"/>
|
||||||
|
<rule ref="category/java/documentation.xml/CommentRequired">
|
||||||
|
<properties>
|
||||||
|
<property name="fieldCommentRequirement" value="Ignored"/>
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/documentation.xml/CommentSize">
|
||||||
|
<properties>
|
||||||
|
<property name="maxLines" value="150"/>
|
||||||
|
<property name="maxLineLength" value="130"/>
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/documentation.xml/UncommentedEmptyConstructor"/>
|
||||||
|
<rule ref="category/java/documentation.xml/UncommentedEmptyMethodBody"/>
|
||||||
|
<!-- rule ref="category/java/errorprone.xml/AssignmentInOperand"/ -->
|
||||||
|
<rule ref="category/java/errorprone.xml/AssignmentToNonFinalStatic"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidAccessibilityAlteration"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidAssertAsIdentifier"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidBranchingStatementAsLastInLoop"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidCallingFinalize"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidCatchingNPE"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidCatchingThrowable"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidDecimalLiteralsInBigDecimalConstructor"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidDuplicateLiterals">
|
||||||
|
<properties>
|
||||||
|
<property name="maxDuplicateLiterals" value="6"/>
|
||||||
|
<property name="skipAnnotations" value="true"/>
|
||||||
|
</properties>
|
||||||
|
</rule>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidEnumAsIdentifier"/>
|
||||||
|
<!-- rule ref="category/java/errorprone.xml/AvoidFieldNameMatchingMethodName"/ -->
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidFieldNameMatchingTypeName"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidInstanceofChecksInCatchClause"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidLiteralsInIfCondition"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidLosingExceptionInformation"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidMultipleUnaryOperators"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/AvoidUsingOctalValues"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/BrokenNullCheck"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/CallSuperFirst"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/CallSuperLast"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/CheckSkipResult"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/ClassCastExceptionWithToArray"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/CloneMethodMustBePublic"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/CloneMethodMustImplementCloneable"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/CloneMethodReturnTypeMustMatchClassName"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/CloseResource"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/CompareObjectsWithEquals"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/ComparisonWithNaN"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/ConfusingArgumentToVarargsMethod"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/ConstructorCallsOverridableMethod"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/DetachedTestCase"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/DoNotCallGarbageCollectionExplicitly"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/DoNotExtendJavaLangThrowable"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/DoNotHardCodeSDCard"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/DoNotTerminateVM"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/DoNotThrowExceptionInFinally"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/DontImportSun"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/DontUseFloatTypeForLoopIndices"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/EmptyCatchBlock"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/EmptyFinalizer"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/EqualsNull"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/FinalizeDoesNotCallSuperFinalize"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/FinalizeOnlyCallsSuperFinalize"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/FinalizeOverloaded"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/FinalizeShouldBeProtected"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/IdempotentOperations"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/ImplicitSwitchFallThrough"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/InstantiationToGetClass"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/InvalidLogMessageFormat"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/JumbledIncrementer"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/JUnitSpelling"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/JUnitStaticSuite"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/MethodWithSameNameAsEnclosingClass"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/MisplacedNullCheck"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/MissingSerialVersionUID"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/MissingStaticMethodInNonInstantiatableClass"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/MoreThanOneLogger"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/NonCaseLabelInSwitch"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/NonSerializableClass"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/NonStaticInitializer"/>
|
||||||
|
<!-- rule ref="category/java/errorprone.xml/NullAssignment"/ -->
|
||||||
|
<rule ref="category/java/errorprone.xml/OverrideBothEqualsAndHashcode"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/ProperCloneImplementation"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/ProperLogger"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/ReturnEmptyCollectionRatherThanNull"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/ReturnFromFinallyBlock"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/SimpleDateFormatNeedsLocale"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/SingleMethodSingleton"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/SingletonClassReturningNewInstance"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/StaticEJBFieldShouldBeFinal"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/StringBufferInstantiationWithChar"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/SuspiciousEqualsMethodName"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/SuspiciousHashcodeMethodName"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/SuspiciousOctalEscape"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/TestClassWithoutTestCases"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/UnconditionalIfStatement"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/UnnecessaryBooleanAssertion"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/UnnecessaryCaseChange"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/UnnecessaryConversionTemporary"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/UnusedNullCheckInEquals"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/UseCorrectExceptionLogging"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/UseEqualsToCompareStrings"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/UselessOperationOnImmutable"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/UseLocaleWithCaseConversions"/>
|
||||||
|
<rule ref="category/java/errorprone.xml/UseProperClassLoader"/>
|
||||||
|
<rule ref="category/java/multithreading.xml/AvoidSynchronizedAtMethodLevel"/>
|
||||||
|
<rule ref="category/java/multithreading.xml/AvoidSynchronizedStatement"/>
|
||||||
|
<rule ref="category/java/multithreading.xml/AvoidThreadGroup"/>
|
||||||
|
<rule ref="category/java/multithreading.xml/AvoidUsingVolatile"/>
|
||||||
|
<rule ref="category/java/multithreading.xml/DoNotUseThreads"/>
|
||||||
|
<rule ref="category/java/multithreading.xml/DontCallThreadRun"/>
|
||||||
|
<rule ref="category/java/multithreading.xml/DoubleCheckedLocking"/>
|
||||||
|
<rule ref="category/java/multithreading.xml/NonThreadSafeSingleton"/>
|
||||||
|
<rule ref="category/java/multithreading.xml/UnsynchronizedStaticFormatter"/>
|
||||||
|
<!-- rule ref="category/java/multithreading.xml/UseConcurrentHashMap"/ -->
|
||||||
|
<rule ref="category/java/multithreading.xml/UseNotifyAllInsteadOfNotify"/>
|
||||||
|
<rule ref="category/java/performance.xml/AddEmptyString"/>
|
||||||
|
<rule ref="category/java/performance.xml/AppendCharacterWithChar"/>
|
||||||
|
<rule ref="category/java/performance.xml/AvoidArrayLoops"/>
|
||||||
|
<rule ref="category/java/performance.xml/AvoidCalendarDateCreation"/>
|
||||||
|
<rule ref="category/java/performance.xml/AvoidFileStream"/>
|
||||||
|
<rule ref="category/java/performance.xml/AvoidInstantiatingObjectsInLoops"/>
|
||||||
|
<rule ref="category/java/performance.xml/BigIntegerInstantiation"/>
|
||||||
|
<rule ref="category/java/performance.xml/ConsecutiveAppendsShouldReuse"/>
|
||||||
|
<rule ref="category/java/performance.xml/ConsecutiveLiteralAppends"/>
|
||||||
|
<rule ref="category/java/performance.xml/InefficientEmptyStringCheck"/>
|
||||||
|
<rule ref="category/java/performance.xml/InefficientStringBuffering"/>
|
||||||
|
<rule ref="category/java/performance.xml/InsufficientStringBufferDeclaration"/>
|
||||||
|
<rule ref="category/java/performance.xml/OptimizableToArrayCall"/>
|
||||||
|
<rule ref="category/java/performance.xml/RedundantFieldInitializer"/>
|
||||||
|
<rule ref="category/java/performance.xml/StringInstantiation"/>
|
||||||
|
<rule ref="category/java/performance.xml/StringToString"/>
|
||||||
|
<rule ref="category/java/performance.xml/TooFewBranchesForSwitch"/>
|
||||||
|
<rule ref="category/java/performance.xml/UseArrayListInsteadOfVector"/>
|
||||||
|
<rule ref="category/java/performance.xml/UseArraysAsList"/>
|
||||||
|
<rule ref="category/java/performance.xml/UseIndexOfChar"/>
|
||||||
|
<rule ref="category/java/performance.xml/UseIOStreamsWithApacheCommonsFileItem"/>
|
||||||
|
<rule ref="category/java/performance.xml/UselessStringValueOf"/>
|
||||||
|
<rule ref="category/java/performance.xml/UseStringBufferForStringAppends"/>
|
||||||
|
<rule ref="category/java/performance.xml/UseStringBufferLength"/>
|
||||||
|
<rule ref="category/java/security.xml/HardCodedCryptoKey"/>
|
||||||
|
<rule ref="category/java/security.xml/InsecureCryptoIv"/>
|
||||||
|
</ruleset>
|
||||||
31
LICENSE
Normal file
31
LICENSE
Normal file
@@ -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.
|
||||||
74
README.md
Normal file
74
README.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# ZeroEcho
|
||||||
|
|
||||||
|
<img src=ZeroEcho-logo.png width=20% align="right" />
|
||||||
|
|
||||||
|
*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.
|
||||||
BIN
ZeroEcho-logo.png
Normal file
BIN
ZeroEcho-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
46
ZeroEcho-logo.svg
Normal file
46
ZeroEcho-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 70 KiB |
26
app/.classpath
Normal file
26
app/.classpath
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="src" output="bin/main" path="src/main/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="main"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="main,test"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="bin/test" path="src/test/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="test"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="test"/>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="bin/test" path="src/test/resources">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="test"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="test"/>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
||||||
|
<classpathentry kind="output" path="bin/default"/>
|
||||||
|
</classpath>
|
||||||
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/app/
|
||||||
29
app/.project
Normal file
29
app/.project
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>app</name>
|
||||||
|
<comment>Project app created by Buildship.</comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>net.sourceforge.pmd.eclipse.plugin.pmdBuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||||
|
<nature>net.sourceforge.pmd.eclipse.plugin.pmdNature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
||||||
31
app/LICENSE
Normal file
31
app/LICENSE
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Copyright (C) 2025, Leo Galambos
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. All advertising materials mentioning features or use of this software must
|
||||||
|
display the following acknowledgement:
|
||||||
|
This product includes software developed by the Egothor project.
|
||||||
|
|
||||||
|
4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
59
app/build.gradle
Normal file
59
app/build.gradle
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
194
app/src/main/java/zeroecho/CovertCommand.java
Normal file
194
app/src/main/java/zeroecho/CovertCommand.java
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This extension operates in two primary modes:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Embedding a binary payload across one or more EXIF metadata fields</li>
|
||||||
|
* <li>Extracting a previously embedded payload from those fields</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The following command-line options are supported:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code --embed}: Activate embedding mode (requires
|
||||||
|
* {@code --payload})</li>
|
||||||
|
* <li>{@code --extract}: Activate extraction mode</li>
|
||||||
|
* <li>{@code --jpeg <input.jpg>}: Input JPEG file to embed into or extract from
|
||||||
|
* (required)</li>
|
||||||
|
* <li>{@code --payload <file>}: File containing payload data to embed (required
|
||||||
|
* for {@code --embed})</li>
|
||||||
|
* <li>{@code --output <file>}: Output file (JPEG with embedded data, or raw
|
||||||
|
* extracted payload) (required)</li>
|
||||||
|
* <li>{@code --slots <s1;s2;...>}: Optional semicolon-separated list of EXIF
|
||||||
|
* slot definitions, each in the form {@code [group.]name[:capacity]} or a fully
|
||||||
|
* defined custom tag like
|
||||||
|
* {@code [group.]name/tag=tagId,type,count,dir[:capacity]}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Text-based EXIF slots (e.g., ASCII) will automatically encode binary payloads
|
||||||
|
* using Base64. Extraction decodes them back transparently.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This class is not meant to be instantiated.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Leo Galambos
|
||||||
|
*/
|
||||||
|
public final class CovertCommand {
|
||||||
|
private CovertCommand() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for command-line usage of the covert embedding and extraction
|
||||||
|
* tool.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<Slot> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
506
app/src/main/java/zeroecho/Guard.java
Normal file
506
app/src/main/java/zeroecho/Guard.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <h2>Overview</h2> 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}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Default behavior:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Recipients are shuffled by default to harden traffic analysis. Disable
|
||||||
|
* with {@code --no-shuffle}.</li>
|
||||||
|
* <li>The symmetric stage writes and parses its own header by default.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Keyring resolution</h2> 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.
|
||||||
|
*
|
||||||
|
* <h2>Payload selection</h2> 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}.
|
||||||
|
*
|
||||||
|
* <h2>Examples</h2> <pre>{@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
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
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: <in>.enc for encrypt, <in>.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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>KEM path</b>: 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.</li>
|
||||||
|
* <li><b>Fallback path</b>: 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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* In both cases, the created context is consumed by
|
||||||
|
* {@link MultiRecipientDataSourceBuilder#addRecipient(Object)} and is closed
|
||||||
|
* internally by the builder.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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 <file> is required when aliases are used");
|
||||||
|
}
|
||||||
|
return KeyringStore.load(Paths.get(cmd.getOptionValue(optKs)));
|
||||||
|
}
|
||||||
|
}
|
||||||
515
app/src/main/java/zeroecho/Kem.java
Normal file
515
app/src/main/java/zeroecho/Kem.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <h2>Overview</h2> 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.
|
||||||
|
*
|
||||||
|
* <h2>Examples</h2> <pre>{@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
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
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<String> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Requires the {@link Option} to declare {@code .type(Number.class)}. If the
|
||||||
|
* option is present and convertible, returns its {@code intValue()}, otherwise
|
||||||
|
* {@code null}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
673
app/src/main/java/zeroecho/KeyStoreManagement.java
Normal file
673
app/src/main/java/zeroecho/KeyStoreManagement.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <h2>Overview</h2> 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.
|
||||||
|
*
|
||||||
|
* <h2>Usage</h2> Invoked as: <pre>{@code
|
||||||
|
* ZeroEcho -K [options]
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <h2>Modes</h2> Exactly one action must be chosen:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code --list-algorithms} - list catalog algorithms and whether they
|
||||||
|
* support symmetric/asymmetric builders.</li>
|
||||||
|
* <li>{@code --list-aliases} - list aliases present in the keystore.</li>
|
||||||
|
* <li>{@code --generate} - generate a new key pair or secret and store under
|
||||||
|
* the given alias.</li>
|
||||||
|
* <li>{@code --export} - export one or more aliases as a versioned
|
||||||
|
* snippet.</li>
|
||||||
|
* <li>{@code --import} - import a versioned snippet into the keystore.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>General options</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code -k | --keystore <file>} - path to keystore file (required).</li>
|
||||||
|
* <li>{@code --overwrite} - overwrite existing aliases on conflict.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Generate options</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code --alg <id>} - algorithm id (e.g., RSA, Ed25519, AES, Frodo).</li>
|
||||||
|
* <li>{@code --alias <name>} - base alias; for asymmetric, both public and
|
||||||
|
* private entries will be created.</li>
|
||||||
|
* <li>{@code --kind sym|asym} - force symmetric or asymmetric if the algorithm
|
||||||
|
* supports both (optional).</li>
|
||||||
|
* <li>{@code --pub-suffix <sfx>} - suffix for public alias (default:
|
||||||
|
* .pub).</li>
|
||||||
|
* <li>{@code --prv-suffix <sfx>} - suffix for private alias (default:
|
||||||
|
* .prv).</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Export options</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code --aliases a,b,c} - comma-separated list of aliases to export
|
||||||
|
* (default: all).</li>
|
||||||
|
* <li>{@code --out <file|-} - output file path (default: "-" for stdout).</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Import options</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code --in <file|-} - input file path (default: "-" for stdin).</li>
|
||||||
|
* <li>{@code --overwrite} - allow replacing existing aliases when
|
||||||
|
* importing.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Examples</h2> <pre>{@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
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <h2>Exit codes</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>0 - operation succeeded</li>
|
||||||
|
* <li>non-zero - error occurred (parse error, I/O failure, or invalid
|
||||||
|
* arguments)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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<String> 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<String> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType;
|
||||||
|
AsymmetricKeyBuilder<AlgorithmKeySpec> 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<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType();
|
||||||
|
SymmetricKeyBuilder<AlgorithmKeySpec> 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<String> 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<String> parseCsv(String csv) {
|
||||||
|
List<String> 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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
330
app/src/main/java/zeroecho/Tag.java
Normal file
330
app/src/main/java/zeroecho/Tag.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <h2>Overview</h2> 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.
|
||||||
|
*
|
||||||
|
* <h2>Usage</h2> <pre>{@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 -
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <h2>Notes</h2> 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.
|
||||||
|
*
|
||||||
|
* <h4>Behavior</h4> 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 <file> 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 <alias>");
|
||||||
|
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 <alias>");
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
198
app/src/main/java/zeroecho/ZeroEcho.java
Normal file
198
app/src/main/java/zeroecho/ZeroEcho.java
Normal file
@@ -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.
|
||||||
|
* <p>
|
||||||
|
* It supports command-line options for asymmetric key management, including
|
||||||
|
* issuing certificates from CSRs or subject names, revoking certificates, and
|
||||||
|
* retrieving all certificates for a user.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* This class initializes Bouncy Castle security provider and uses Apache
|
||||||
|
* Commons CLI for command-line parsing.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Leo Galambos
|
||||||
|
*/
|
||||||
|
public final class ZeroEcho {
|
||||||
|
/**
|
||||||
|
* Logger instance for the {@code ZeroEcho} class used to log messages and
|
||||||
|
* events.
|
||||||
|
* <p>
|
||||||
|
* This logger is configured with the name of the {@code ZeroEcho} class,
|
||||||
|
* allowing for fine-grained logging control specific to this class.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public static final Logger LOG = Logger.getLogger(ZeroEcho.class.getName());
|
||||||
|
|
||||||
|
static {
|
||||||
|
BouncyCastleActivator.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor for ZeroEcho.
|
||||||
|
* <p>
|
||||||
|
* This constructor does not perform any initialization since all operations are
|
||||||
|
* handled via static methods and blocks.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private ZeroEcho() {
|
||||||
|
// No initialization needed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for the ZeroEcho application. Parses command-line arguments
|
||||||
|
* and dispatches to the appropriate subcommand for asymmetric key management or
|
||||||
|
* prints the help message.
|
||||||
|
*
|
||||||
|
* @param args command-line arguments passed to the program
|
||||||
|
*/
|
||||||
|
public static void main(final String[] args) {
|
||||||
|
final int errorCode = mainProcess(args);
|
||||||
|
|
||||||
|
if (errorCode == 0) {
|
||||||
|
System.out.println("OK");
|
||||||
|
} else {
|
||||||
|
System.out.println("ERR: " + errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the ZeroEcho application. Parses command-line arguments and
|
||||||
|
* dispatches to the appropriate subcommand for asymmetric key management or
|
||||||
|
* prints the help message.
|
||||||
|
*
|
||||||
|
* @param args command-line arguments passed to the program
|
||||||
|
* @return error-code
|
||||||
|
*/
|
||||||
|
public static int mainProcess(final String... args) {
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method automatically generates and displays a help statement based on
|
||||||
|
* the provided command-line {@link Options}. It then terminates the program
|
||||||
|
* with exit status {@code 1}.
|
||||||
|
*
|
||||||
|
* @param options The {@link Options} instance defining the available
|
||||||
|
* command-line options.
|
||||||
|
* @return always {@code 1}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
134
app/src/main/java/zeroecho/package-info.java
Normal file
134
app/src/main/java/zeroecho/package-info.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Design goals</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Single front end, multiple tools:</b> 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.</li>
|
||||||
|
* <li><b>Streaming composition:</b> 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.</li>
|
||||||
|
* <li><b>Safe, explicit defaults:</b> 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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Core ideas and algorithms</h2>
|
||||||
|
* <p>
|
||||||
|
* The tools build on two families of primitives:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Asymmetric/KEM envelopes:</b> 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.</li>
|
||||||
|
* <li><b>Trailer tags:</b> 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.</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Subcommands overview</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@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.</li>
|
||||||
|
* <li>{@link Kem} - KEM-based hybrid encryption and decryption with AES or
|
||||||
|
* ChaCha payloads, HKDF support, counters/nonces, and compact symmetric
|
||||||
|
* headers.</li>
|
||||||
|
* <li>{@link KeyStoreManagement} - maintenance of a text keyring: list
|
||||||
|
* algorithms and aliases, generate keys, and import/export versioned
|
||||||
|
* snippets.</li>
|
||||||
|
* <li>{@link Tag} - produce or verify trailer tags using digital signatures or
|
||||||
|
* message digests.</li>
|
||||||
|
* <li>{@link CovertCommand} - embed or extract a binary payload in JPEG files
|
||||||
|
* via configurable EXIF slots.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>I/O conventions and exit codes</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>All tools accept "-" for stdin/stdout when appropriate, enabling shell
|
||||||
|
* pipelines.</li>
|
||||||
|
* <li>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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Typical usage</h2> <pre>{@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
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
package zeroecho;
|
||||||
78
app/src/main/javadoc/overview.html
Normal file
78
app/src/main/javadoc/overview.html
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>ZeroEcho App Overview</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>ZeroEcho Command-Line App</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The ZeroEcho CLI is a streaming, security-first front end built on the <code>lib</code> 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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Commands</h2>
|
||||||
|
<ul>
|
||||||
|
<li><b>guard</b> - multi-recipient envelopes (public keys and/or passwords) with AES or ChaCha payloads.</li>
|
||||||
|
<li><b>kem</b> - hybrid encryption: derive a content key via a KEM (e.g., Kyber), then encrypt the payload (AES/ChaCha).</li>
|
||||||
|
<li><b>keystore</b> - manage a human-editable text keyring: list, generate, import, export.</li>
|
||||||
|
<li><b>covert</b> - embed or extract a binary payload in JPEG EXIF fields using configurable slots.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Global usage</h2>
|
||||||
|
<p>
|
||||||
|
Each command supports <code>--help</code> for exact flags and examples. Inputs and outputs are streamed;
|
||||||
|
large files do not need to be fully loaded in memory.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>I/O conventions</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Streams are processed lazily; errors in verification surface at end of stream.</li>
|
||||||
|
<li>Authenticated modes (AES-GCM, ChaCha20-Poly1305) are the default where applicable.</li>
|
||||||
|
<li>For hybrid flows, shared secrets from agreement/KEM are fed through a KDF before use.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Keyring format</h2>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Security notes</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Prefer authenticated encryption and strong KEM parameter sets.</li>
|
||||||
|
<li>Protect keyrings with OS permissions; avoid committing them to VCS.</li>
|
||||||
|
<li>Export encrypted content when targeting untrusted destinations; do not embed secrets in cleartext scripts.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Exit codes and logging</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Commands return 0 on success; non-zero indicates failure.</li>
|
||||||
|
<li>Errors go to STDERR; enable verbose logging for diagnostics as needed.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Examples (illustrative)</h2>
|
||||||
|
<pre>
|
||||||
|
# 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
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h2>System requirements</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Java 21 or newer.</li>
|
||||||
|
<li>At least one JCA provider supplying the selected algorithms (e.g., JDK defaults, Bouncy Castle, a PQC provider).</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
148
app/src/test/java/zeroecho/CovertCommandTest.java
Normal file
148
app/src/test/java/zeroecho/CovertCommandTest.java
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
354
app/src/test/java/zeroecho/GuardTest.java
Normal file
354
app/src/test/java/zeroecho/GuardTest.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <h2>Scope</h2> These tests drive
|
||||||
|
* {@link Guard#main(String[], org.apache.commons.cli.Options)} with only the
|
||||||
|
* options implemented by Guard. They exercise:
|
||||||
|
* <ul>
|
||||||
|
* <li>Password-only encryption and decryption,</li>
|
||||||
|
* <li>RSA recipients with private-key based decryption,</li>
|
||||||
|
* <li>Mixed recipients (RSA + ElGamal + password) with decoys and default
|
||||||
|
* shuffling,</li>
|
||||||
|
* <li>AES-GCM (with tag bits and AAD) and ChaCha20-Poly1305 (with AAD and
|
||||||
|
* explicit nonce).</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Keys are generated using the existing KeyStoreManagement CLI to populate a
|
||||||
|
* real KeyringStore file, as done in KemTest. The CLI persists aliases as
|
||||||
|
* {@code <alias>.pub} and {@code <alias>.prv}.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This does not require a keyring.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Keys are generated through the real KeyStoreManagement CLI and read via Guard
|
||||||
|
* with --to-alias and --priv-alias.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Verifies that default shuffling does not prevent decryption and that both
|
||||||
|
* password and private-key paths can unlock.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The CLI stores aliases as {@code <alias>.pub} and {@code <alias>.prv}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
319
app/src/test/java/zeroecho/KemTest.java
Normal file
319
app/src/test/java/zeroecho/KemTest.java
Normal file
@@ -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:
|
||||||
|
* <ul>
|
||||||
|
* <li>Verify {@code --list-kems} runs without a keyring.</li>
|
||||||
|
* <li>Iterate over <em>all</em> available KEM ids discovered via
|
||||||
|
* {@code --list-kems} and for each id perform hybrid round-trips:
|
||||||
|
* <ul>
|
||||||
|
* <li>AES-GCM with header and AAD,</li>
|
||||||
|
* <li>ChaCha20-Poly1305 with header and AAD.</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The tests:
|
||||||
|
* <ul>
|
||||||
|
* <li>use the real KeyStore format by calling {@link KeyStoreManagement} to
|
||||||
|
* generate a keypair,</li>
|
||||||
|
* <li>pass the resulting aliases into
|
||||||
|
* {@code KEMAes.main(String[], Options)},</li>
|
||||||
|
* <li>print the method name and parameters first, progress lines prefixed by
|
||||||
|
* {@code "..."},</li>
|
||||||
|
* <li>end with {@code "...ok"} on success.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
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 <em>every</em> KEM listed by {@code --list-kems}:
|
||||||
|
* <ol>
|
||||||
|
* <li>Generate a real KeyStore with a fresh keypair for the KEM,</li>
|
||||||
|
* <li>Encrypt+decrypt using AES-GCM (header+AAD),</li>
|
||||||
|
* <li>Encrypt+decrypt using ChaCha20-Poly1305 (header+AAD).</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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<String> kemIds = listKemsViaCli();
|
||||||
|
System.out.println("...discovered " + kemIds.size() + " KEM ids");
|
||||||
|
if (kemIds.isEmpty()) {
|
||||||
|
throw new GeneralSecurityException("No KEM algorithms reported by --list-kems");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> 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<String> 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<String> 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 <alias>.pub} and
|
||||||
|
* {@code <alias>.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
241
app/src/test/java/zeroecho/KeyStoreManagementTest.java
Normal file
241
app/src/test/java/zeroecho/KeyStoreManagementTest.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <h2>What it does</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>For each algorithm that exposes a default-capable asymmetric builder,
|
||||||
|
* generates a keypair.</li>
|
||||||
|
* <li>For each algorithm that exposes a default-capable symmetric builder,
|
||||||
|
* generates a secret.</li>
|
||||||
|
* <li>Reloads the keyring and materializes keys using KeyringStore.</li>
|
||||||
|
* <li>Prints progress and brief details to stdout during the run.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
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<String> 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<String> 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<CryptoAlgorithm.AsymBuilderInfo> 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<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType;
|
||||||
|
AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(st);
|
||||||
|
if (b != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasSymmetricDefault(CryptoAlgorithm alg) {
|
||||||
|
try {
|
||||||
|
List<CryptoAlgorithm.SymBuilderInfo> 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<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType();
|
||||||
|
SymmetricKeyBuilder<AlgorithmKeySpec> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
314
app/src/test/java/zeroecho/TagTest.java
Normal file
314
app/src/test/java/zeroecho/TagTest.java
Normal file
@@ -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).
|
||||||
|
*
|
||||||
|
* <h2>Scope</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Signature produce+verify with Ed25519 via a real keyring file,</li>
|
||||||
|
* <li>Signature verify failure appends marker text and returns rc=1,</li>
|
||||||
|
* <li>Digest produce+verify (SHA-256) round trip,</li>
|
||||||
|
* <li>Digest verify failure appends marker text and returns rc=1,</li>
|
||||||
|
* <li>Digest round trip over STDIN/STDOUT.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
app/src/test/java/zeroecho/ZeroEchoTest.java
Normal file
74
app/src/test/java/zeroecho/ZeroEchoTest.java
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ZeroEchoTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAsymetricOptionWithoutParamsReturnsOne() {
|
||||||
|
System.out.println("testAsymetricOptionWithoutParamsReturnsOne");
|
||||||
|
int result = ZeroEcho.mainProcess(new String[] { "-A" });
|
||||||
|
assertEquals(1, result, "Asymetric option without parameters should return 1 (error/help)");
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAesPswOptionWithoutParamsReturnsOne() {
|
||||||
|
System.out.println("testAesPswOptionWithoutParamsReturnsOne");
|
||||||
|
int result = ZeroEcho.mainProcess(new String[] { "-P" });
|
||||||
|
assertEquals(1, result, "AES-PSW option without parameters should return 1 (error/help)");
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNoOptionReturnsOne() {
|
||||||
|
System.out.println("testNoOptionReturnsOne");
|
||||||
|
int result = ZeroEcho.mainProcess(new String[] {});
|
||||||
|
assertEquals(1, result, "No options should return 1 (error/help)");
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidOptionReturnsOne() {
|
||||||
|
System.out.println("testInvalidOptionReturnsOne");
|
||||||
|
int result = ZeroEcho.mainProcess(new String[] { "-X" });
|
||||||
|
assertEquals(1, result, "Invalid option should return 1 (error/help)");
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/src/test/resources/test.jpg
Normal file
BIN
app/src/test/resources/test.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 230 KiB |
9
buildSrc/build.gradle
Normal file
9
buildSrc/build.gradle
Normal file
@@ -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()
|
||||||
|
}
|
||||||
8
buildSrc/settings.gradle
Normal file
8
buildSrc/settings.gradle
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
dependencyResolutionManagement {
|
||||||
|
// Reuse version catalog from the main build.
|
||||||
|
versionCatalogs {
|
||||||
|
create('libs', { from(files("../gradle/libs.versions.toml")) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = 'buildSrc'
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
@@ -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 = '<div style="text-align: right; padding: 5px;">Copyright © ' + currentYear +
|
||||||
|
' Egothor - Version ' + version +
|
||||||
|
' - <a href="https://gitea.egothor.org/Egothor/ZeroEcho/raw/branch/main/LICENSE">License</a>' +
|
||||||
|
'</div>'
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
2
gradle.properties
Normal file
2
gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
|
||||||
|
javadocPath=/var/www/html/javadoc/zeroecho/
|
||||||
2
gradle/libs.versions.toml
Normal file
2
gradle/libs.versions.toml
Normal file
@@ -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
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -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
|
||||||
252
gradlew
vendored
Executable file
252
gradlew
vendored
Executable file
@@ -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" "$@"
|
||||||
94
gradlew.bat
vendored
Normal file
94
gradlew.bat
vendored
Normal file
@@ -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
|
||||||
32
lib/.classpath
Normal file
32
lib/.classpath
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="src" output="bin/main" path="src/main/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="main"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="main,test"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="bin/test" path="src/test/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="test"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="test"/>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="bin/test" path="src/test/resources">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="test"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="test"/>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="bin/main" path="src/main/resources">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="main"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="main,test"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
||||||
|
<classpathentry kind="output" path="bin/default"/>
|
||||||
|
</classpath>
|
||||||
29
lib/.project
Normal file
29
lib/.project
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>lib</name>
|
||||||
|
<comment>Project lib created by Buildship.</comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>net.sourceforge.pmd.eclipse.plugin.pmdBuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||||
|
<nature>net.sourceforge.pmd.eclipse.plugin.pmdNature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
||||||
31
lib/LICENSE
Normal file
31
lib/LICENSE
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Copyright (C) 2025, Leo Galambos
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. All advertising materials mentioning features or use of this software must
|
||||||
|
display the following acknowledgement:
|
||||||
|
This product includes software developed by the Egothor project.
|
||||||
|
|
||||||
|
4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
56
lib/build.gradle
Normal file
56
lib/build.gradle
Normal file
@@ -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 = "<!-- CRYPTO_CATALOG_TABLE -->"
|
||||||
|
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")
|
||||||
|
}
|
||||||
76
lib/src/main/java/zeroecho/core/AlgorithmFamily.java
Normal file
76
lib/src/main/java/zeroecho/core/AlgorithmFamily.java
Normal file
@@ -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.
|
||||||
|
* <p>
|
||||||
|
* Each {@code AlgorithmFamily} groups primitives with similar lifecycle
|
||||||
|
* constraints, key properties, and safe usage patterns.
|
||||||
|
*
|
||||||
|
* <h2>Families</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #ASYMMETRIC}: Public-key algorithms such as signature schemes
|
||||||
|
* (Ed25519, RSA) or public-key encryption.</li>
|
||||||
|
* <li>{@link #SYMMETRIC}: Shared-key algorithms such as block/stream ciphers
|
||||||
|
* and message authentication codes.</li>
|
||||||
|
* <li>{@link #KEM}: Key encapsulation mechanisms, including post-quantum
|
||||||
|
* schemes.</li>
|
||||||
|
* <li>{@link #DIGEST}: Unkeyed hash functions and extendable-output functions
|
||||||
|
* (e.g., SHA-2, SHA-3, BLAKE3).</li>
|
||||||
|
* <li>{@link #AGREEMENT}: Key-agreement schemes (e.g., X25519, ECDH), distinct
|
||||||
|
* from KEMs but with similar goals.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>Usage:</b> 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).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
}
|
||||||
106
lib/src/main/java/zeroecho/core/Capability.java
Normal file
106
lib/src/main/java/zeroecho/core/Capability.java
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* A {@code Capability} describes one role supported by a
|
||||||
|
* {@link CryptoAlgorithm}, including:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>the algorithm identifier,</li>
|
||||||
|
* <li>its high-level {@link AlgorithmFamily},</li>
|
||||||
|
* <li>the {@link KeyUsage} role (e.g., ENCRYPT, VERIFY),</li>
|
||||||
|
* <li>the expected {@link CryptoContext} type,</li>
|
||||||
|
* <li>the accepted {@link Key} type,</li>
|
||||||
|
* <li>the accepted {@link ContextSpec} type, and</li>
|
||||||
|
* <li>a supplier for a default spec.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Purpose</h2> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Each capability corresponds to a call to
|
||||||
|
* {@link AbstractCryptoAlgorithm#capability(AlgorithmFamily, KeyUsage, Class, Class, Class, ContextConstructorKS, Supplier)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> {@code Capability} instances are immutable and safe to
|
||||||
|
* share across threads.
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public record Capability(String algorithmId, AlgorithmFamily family, KeyUsage role,
|
||||||
|
Class<? extends CryptoContext> contextType, Class<? extends Key> keyType, Class<? extends ContextSpec> specType,
|
||||||
|
Supplier<? extends ContextSpec> 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<? extends CryptoContext> contextType, Class<? extends Key> keyType,
|
||||||
|
Class<? extends ContextSpec> specType, Supplier<? extends ContextSpec> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
115
lib/src/main/java/zeroecho/core/CatalogSelector.java
Normal file
115
lib/src/main/java/zeroecho/core/CatalogSelector.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <h2>Purpose</h2> 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<String> selectByFamilyAndRoles(AlgorithmFamily family, Collection<KeyUsage> requireAllRoles) {
|
||||||
|
Objects.requireNonNull(family, "family");
|
||||||
|
Objects.requireNonNull(requireAllRoles, "requireAllRoles");
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
Set<String> 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<String> 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<String> selectByRoles(Collection<KeyUsage> requireAllRoles) {
|
||||||
|
Objects.requireNonNull(requireAllRoles, "requireAllRoles");
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
Set<String> ids = CryptoAlgorithms.available();
|
||||||
|
for (String id : ids) {
|
||||||
|
CryptoAlgorithm alg = CryptoAlgorithms.require(id);
|
||||||
|
if (alg.roles().containsAll(requireAllRoles)) {
|
||||||
|
out.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
148
lib/src/main/java/zeroecho/core/ConfluxKeys.java
Normal file
148
lib/src/main/java/zeroecho/core/ConfluxKeys.java
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.core;
|
||||||
|
|
||||||
|
import conflux.Key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared typed keys for ephemeral cryptographic parameters.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* {@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<Key<?>,Object>} or a dedicated context object) to exchange
|
||||||
|
* per-operation metadata between algorithms and higher layers.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Design goals</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Type safety</b>: each key carries its value type (e.g.,
|
||||||
|
* {@code Key<byte[]>} vs {@code Key<Integer>}).</li>
|
||||||
|
* <li><b>Namespacing</b>: all keys include the algorithm identifier in their
|
||||||
|
* name, preventing collisions when multiple algorithms share a context.</li>
|
||||||
|
* <li><b>Consistency</b>: avoids ad-hoc string constants; discoverable via
|
||||||
|
* {@link CryptoAlgorithm#listCapabilities()} and related APIs.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Instances are created via static factories; this class cannot be
|
||||||
|
* instantiated.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* IVs are required by block cipher modes such as CBC or GCM. Each call produces
|
||||||
|
* a key namespaced as {@code "crypto.<algoId>.iv"}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param algoId canonical algorithm identifier
|
||||||
|
* @return key for IV values, of type {@code byte[]}
|
||||||
|
*/
|
||||||
|
public static Key<byte[]> iv(String algoId) {
|
||||||
|
return Key.of(PREFIX + algoId + ".iv", byte[].class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a typed key for additional authenticated data (AAD).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Used in AEAD schemes such as AES-GCM to bind unencrypted headers into the
|
||||||
|
* authentication tag. Namespaced as {@code "crypto.<algoId>.aad"}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param algoId canonical algorithm identifier
|
||||||
|
* @return key for AAD values, of type {@code byte[]}
|
||||||
|
*/
|
||||||
|
public static Key<byte[]> aad(String algoId) {
|
||||||
|
return Key.of(PREFIX + algoId + ".aad", byte[].class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a typed key for a nonce value of a given algorithm.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Nonces are required by stream ciphers and AEAD modes to ensure uniqueness per
|
||||||
|
* key. Namespaced as {@code "crypto.<algoId>.nonce"}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param algoId canonical algorithm identifier
|
||||||
|
* @return key for nonce values, of type {@code byte[]}
|
||||||
|
*/
|
||||||
|
public static Key<byte[]> nonce(String algoId) {
|
||||||
|
return Key.of(PREFIX + algoId + ".nonce", byte[].class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a typed key for the authentication tag of a given algorithm.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* AEAD modes output a tag that must be preserved for decryption/verification.
|
||||||
|
* Namespaced as {@code "crypto.<algoId>.tag"}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param algoId canonical algorithm identifier
|
||||||
|
* @return key for authentication tag values, of type {@code byte[]}
|
||||||
|
*/
|
||||||
|
public static Key<byte[]> tag(String algoId) {
|
||||||
|
return Key.of(PREFIX + algoId + ".tag", byte[].class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a typed key for the number of authentication tag bits.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Some AEAD constructions allow truncated tags (e.g., 96-bit or 64-bit). This
|
||||||
|
* key represents the chosen bit-length. Namespaced as
|
||||||
|
* {@code "crypto.<algoId>.tagBits"}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param algoId canonical algorithm identifier
|
||||||
|
* @return key for tag length values, of type {@code Integer}
|
||||||
|
*/
|
||||||
|
public static Key<Integer> tagBits(String algoId) {
|
||||||
|
return Key.of(PREFIX + algoId + ".tagBits", Integer.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
846
lib/src/main/java/zeroecho/core/CryptoAlgorithm.java
Normal file
846
lib/src/main/java/zeroecho/core/CryptoAlgorithm.java
Normal file
@@ -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.
|
||||||
|
* <p>
|
||||||
|
* A {@code CryptoAlgorithm} declares:
|
||||||
|
* <ul>
|
||||||
|
* <li>Metadata: an identifier, display name, provider, and priority.</li>
|
||||||
|
* <li>Capabilities: declared features such as AEAD, streaming, or deterministic
|
||||||
|
* signatures.</li>
|
||||||
|
* <li>Roles: supported {@link KeyUsage} operations (e.g., ENCRYPT, SIGN) bound
|
||||||
|
* to concrete {@link CryptoContext} constructors.</li>
|
||||||
|
* <li>Key builders: factories for symmetric and asymmetric key material via
|
||||||
|
* {@link SymmetricKeyBuilder} and {@link AsymmetricKeyBuilder}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Metadata</h2> 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.
|
||||||
|
*
|
||||||
|
* <h2>Capabilities</h2> 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).
|
||||||
|
*
|
||||||
|
* <h2>Roles and contexts</h2> 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:
|
||||||
|
* <ol>
|
||||||
|
* <li>The binding for the role is located.</li>
|
||||||
|
* <li>The supplied key and spec are validated against the expected types.</li>
|
||||||
|
* <li>A new {@link CryptoContext} is constructed using the registered
|
||||||
|
* factory.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <h2>Key builders</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Asymmetric builders: registered via {@link #registerAsymmetricKeyBuilder}
|
||||||
|
* and accessed through {@link #asymmetricKeyBuilder(Class)} or convenience
|
||||||
|
* methods like {@link #generateKeyPair(AlgorithmKeySpec)}.</li>
|
||||||
|
* <li>Symmetric builders: registered via {@link #registerSymmetricKeyBuilder}
|
||||||
|
* and accessed through {@link #symmetricKeyBuilder(Class)} or convenience
|
||||||
|
* methods like {@link #generateSecret(AlgorithmKeySpec)}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Provider model</h2> 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.
|
||||||
|
*
|
||||||
|
* <h2>Thread safety</h2> {@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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>Security note:</b> Algorithms must enforce strong validation of keys and
|
||||||
|
* specs during registration and {@link #create(KeyUsage, Key, ContextSpec)} to
|
||||||
|
* prevent downgrade or misuse attacks.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<Capability> capabilities = new ArrayList<>();
|
||||||
|
private final Map<KeyUsage, List<RoleBinding<?, ?, ?>>> ctxBindings = new EnumMap<>(KeyUsage.class);
|
||||||
|
private final Map<Class<? extends AlgorithmKeySpec>, AsymEntry<?>> asymBuilders = new HashMap<>();
|
||||||
|
private final Map<Class<? extends AlgorithmKeySpec>, 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* When multiple providers expose the same {@link #id()}, the priority is used
|
||||||
|
* as a tiebreaker. Higher values indicate stronger preference.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* Typical values include {@code "default"}, {@code "JCA"},
|
||||||
|
* {@code "BouncyCastle"}, or a project-specific label.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<Capability> listCapabilities() {
|
||||||
|
return Collections.unmodifiableList(capabilities);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal binding record connecting a role to its required types and
|
||||||
|
* constructor.
|
||||||
|
* <p>
|
||||||
|
* 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 <C> context type
|
||||||
|
* @param <K> key type
|
||||||
|
* @param <S> spec type
|
||||||
|
*/
|
||||||
|
private static final class RoleBinding<C extends CryptoContext, K extends Key, S extends ContextSpec> {
|
||||||
|
private final Class<C> ctxType;
|
||||||
|
private final Class<K> keyType;
|
||||||
|
private final Class<S> specType;
|
||||||
|
private final ContextConstructorKS<C, K, S> ctor;
|
||||||
|
private final Supplier<? extends S> defaultSpec;
|
||||||
|
|
||||||
|
private RoleBinding(Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextConstructorKS<C, K, S> ctor,
|
||||||
|
Supplier<? extends S> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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 <C> context type
|
||||||
|
* @param <K> key type
|
||||||
|
* @param <S> spec type
|
||||||
|
* @throws NullPointerException if any class or factory argument is {@code null}
|
||||||
|
*/
|
||||||
|
protected final <C extends CryptoContext, K extends Key, S extends ContextSpec> void bind(KeyUsage role,
|
||||||
|
Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextConstructorKS<C, K, S> factory,
|
||||||
|
Supplier<? extends S> 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<KeyUsage> roles() {
|
||||||
|
return Collections.unmodifiableSet(ctxBindings.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link CryptoContext} for the given role, key, and optional
|
||||||
|
* spec.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Resolution proceeds as follows:
|
||||||
|
* </p>
|
||||||
|
* <ol>
|
||||||
|
* <li>Locate bindings for {@code role}; if none exist, throw
|
||||||
|
* {@link UnsupportedRoleException}.</li>
|
||||||
|
* <li>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.</li>
|
||||||
|
* <li>If matched, resolve the effective spec: use the provided {@code spec} or
|
||||||
|
* obtain one from the binding’s {@code defaultSpec} supplier.</li>
|
||||||
|
* <li>Invoke the factory to create a context and verify the returned type
|
||||||
|
* matches the declared {@code ctxType}.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @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 <C> context type
|
||||||
|
* @param <K> key type
|
||||||
|
* @param <S> 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 extends CryptoContext, K extends Key, S extends ContextSpec> C create(KeyUsage role, K key, S spec)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
List<RoleBinding<?, ?, ?>> list = ctxBindings.get(role);
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
throw new UnsupportedRoleException(_id + " does not support role " + role);
|
||||||
|
}
|
||||||
|
for (RoleBinding<?, ?, ?> rb0 : list) {
|
||||||
|
RoleBinding<C, K, S> rb = (RoleBinding<C, K, S>) 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.
|
||||||
|
* <p>
|
||||||
|
* Used for discovery and documentation (e.g., tool UIs).
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public static final class AsymBuilderInfo {
|
||||||
|
public final Class<? extends AlgorithmKeySpec> specType;
|
||||||
|
public final Object defaultKeySpec;
|
||||||
|
|
||||||
|
private AsymBuilderInfo(Class<? extends AlgorithmKeySpec> specType, Object defaultKeySpec) {
|
||||||
|
this.specType = specType;
|
||||||
|
this.defaultKeySpec = defaultKeySpec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal entry binding a registered asymmetric key builder to its default key
|
||||||
|
* specification supplier.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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").
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Usage</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Created during calls to
|
||||||
|
* {@link #registerAsymmetricKeyBuilder(Class, AsymmetricKeyBuilder, Supplier)}.</li>
|
||||||
|
* <li>Looked up later by {@link #asymmetricKeyBuilder(Class)} and used by
|
||||||
|
* key-generation/import convenience methods such as
|
||||||
|
* {@link #generateKeyPair(AlgorithmKeySpec)}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> Immutable once constructed; safe to share between
|
||||||
|
* threads.
|
||||||
|
*
|
||||||
|
* @param <S> the type of {@link AlgorithmKeySpec} handled by this entry
|
||||||
|
*/
|
||||||
|
private record AsymEntry<S extends AlgorithmKeySpec>(AsymmetricKeyBuilder<S> builder,
|
||||||
|
Supplier<? extends S> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Concrete algorithms call this during construction. The {@code specType} acts
|
||||||
|
* as a key for later lookup and must be unique within this algorithm.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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 <S> spec type
|
||||||
|
* @throws NullPointerException if {@code specType} or {@code builder} is
|
||||||
|
* {@code null}
|
||||||
|
*/
|
||||||
|
protected final <S extends AlgorithmKeySpec> void registerAsymmetricKeyBuilder(Class<S> specType,
|
||||||
|
AsymmetricKeyBuilder<S> builder, Supplier<? extends S> 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 <S> spec type
|
||||||
|
* @return the registered {@link AsymmetricKeyBuilder}
|
||||||
|
* @throws IllegalArgumentException if no builder is registered for
|
||||||
|
* {@code specType}
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public final <S extends AlgorithmKeySpec> AsymmetricKeyBuilder<S> asymmetricKeyBuilder(Class<S> specType) {
|
||||||
|
AsymEntry<?> e = asymBuilders.get(specType);
|
||||||
|
if (e == null) {
|
||||||
|
throw new IllegalArgumentException(_id + " has no asymmetric key builder for " + specType.getName());
|
||||||
|
}
|
||||||
|
return (AsymmetricKeyBuilder<S>) e.builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns metadata about all registered asymmetric builders.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The default spec value is best-effort; suppliers may throw, in which case
|
||||||
|
* {@code defaultKeySpec} is reported as {@code null}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return immutable list of {@link AsymBuilderInfo} descriptors
|
||||||
|
*/
|
||||||
|
public final List<AsymBuilderInfo> asymmetricBuildersInfo() {
|
||||||
|
List<AsymBuilderInfo> out = new ArrayList<>();
|
||||||
|
for (Map.Entry<Class<? extends AlgorithmKeySpec>, 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Usage</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Produced by {@link #symmetricBuildersInfo()}.</li>
|
||||||
|
* <li>Displayed to clients for inspection and documentation, but not used
|
||||||
|
* directly in cryptographic operations.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> 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<? extends AlgorithmKeySpec> specType, Object defaultKeySpec) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal entry binding a registered symmetric key builder to its optional
|
||||||
|
* default key specification supplier.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Usage</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Created during calls to
|
||||||
|
* {@link #registerSymmetricKeyBuilder(Class, SymmetricKeyBuilder, Supplier)}.</li>
|
||||||
|
* <li>Looked up internally when methods such as
|
||||||
|
* {@link #generateSecret(AlgorithmKeySpec)} or
|
||||||
|
* {@link #importSecret(AlgorithmKeySpec)} are invoked.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> 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 <S> the type of {@link AlgorithmKeySpec} handled by this
|
||||||
|
* entry
|
||||||
|
*/
|
||||||
|
private record SymEntry<S extends AlgorithmKeySpec>(SymmetricKeyBuilder<S> builder,
|
||||||
|
Supplier<? extends S> 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 <S> spec type
|
||||||
|
* @throws NullPointerException if {@code specType} or {@code builder} is
|
||||||
|
* {@code null}
|
||||||
|
*/
|
||||||
|
protected final <S extends AlgorithmKeySpec> void registerSymmetricKeyBuilder(Class<S> specType,
|
||||||
|
SymmetricKeyBuilder<S> builder, Supplier<? extends S> 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 <S> spec type
|
||||||
|
* @return the registered {@link SymmetricKeyBuilder}
|
||||||
|
* @throws IllegalArgumentException if no builder is registered for
|
||||||
|
* {@code specType}
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public final <S extends AlgorithmKeySpec> SymmetricKeyBuilder<S> symmetricKeyBuilder(Class<S> specType) {
|
||||||
|
SymEntry<?> e = symBuilders.get(specType);
|
||||||
|
if (e == null) {
|
||||||
|
throw new IllegalArgumentException(_id + " has no symmetric key builder for " + specType.getName());
|
||||||
|
}
|
||||||
|
return (SymmetricKeyBuilder<S>) e.builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns metadata about all registered symmetric builders.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The default spec value is best-effort; suppliers may throw, in which case
|
||||||
|
* {@code defaultKeySpec} is reported as {@code null}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return immutable list of {@link SymBuilderInfo} descriptors
|
||||||
|
*/
|
||||||
|
public final List<SymBuilderInfo> symmetricBuildersInfo() {
|
||||||
|
List<SymBuilderInfo> out = new ArrayList<>();
|
||||||
|
for (Map.Entry<Class<? extends AlgorithmKeySpec>, 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 <S> 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 <S extends AlgorithmKeySpec> SecretKey generateSecret(S spec) throws GeneralSecurityException {
|
||||||
|
Objects.requireNonNull(spec, "spec must not be null");
|
||||||
|
SymmetricKeyBuilder<S> b = symmetricKeyBuilder((Class<S>) 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 <S> 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 <S extends AlgorithmKeySpec> SecretKey importSecret(S spec) throws GeneralSecurityException {
|
||||||
|
Objects.requireNonNull(spec, "spec must not be null");
|
||||||
|
SymmetricKeyBuilder<S> b = symmetricKeyBuilder((Class<S>) 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 <S> concrete {@link AlgorithmKeySpec} type
|
||||||
|
* @return a freshly generated key pair
|
||||||
|
* @throws GeneralSecurityException if the supplier or builder fails
|
||||||
|
*/
|
||||||
|
private <S extends AlgorithmKeySpec> KeyPair tryGenerateWithDefault(Class<? extends AlgorithmKeySpec> specType,
|
||||||
|
AsymEntry<S> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h4>Example</h4> <pre>{@code
|
||||||
|
* CryptoAlgorithm algo = CryptoAlgorithms.require("Ed25519");
|
||||||
|
* KeyPair kp = algo.generateKeyPair();
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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<Class<? extends AlgorithmKeySpec>, AsymEntry<?>> e : asymBuilders.entrySet()) {
|
||||||
|
AsymEntry<?> entry = e.getValue();
|
||||||
|
if (entry.defaultKeySpec == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
attempted = true;
|
||||||
|
try {
|
||||||
|
// Wildcard capture lets the compiler infer <S> 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 <S> 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 <S extends AlgorithmKeySpec> KeyPair generateKeyPair(S spec) throws GeneralSecurityException {
|
||||||
|
Objects.requireNonNull(spec, "spec must not be null");
|
||||||
|
AsymmetricKeyBuilder<S> b = asymmetricKeyBuilder((Class<S>) 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 <S> 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 <S extends AlgorithmKeySpec> PublicKey importPublic(S spec) throws GeneralSecurityException {
|
||||||
|
Objects.requireNonNull(spec, "spec must not be null");
|
||||||
|
AsymmetricKeyBuilder<S> b = asymmetricKeyBuilder((Class<S>) 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 <S> 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 <S extends AlgorithmKeySpec> PrivateKey importPrivate(S spec) throws GeneralSecurityException {
|
||||||
|
Objects.requireNonNull(spec, "spec must not be null");
|
||||||
|
AsymmetricKeyBuilder<S> b = asymmetricKeyBuilder((Class<S>) spec.getClass());
|
||||||
|
return b.importPrivate(spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
609
lib/src/main/java/zeroecho/core/CryptoAlgorithms.java
Normal file
609
lib/src/main/java/zeroecho/core/CryptoAlgorithms.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* {@code CryptoAlgorithms} discovers algorithms via {@link ServiceLoader} and
|
||||||
|
* exposes:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>a registry from canonical algorithm id to implementation,</li>
|
||||||
|
* <li>policy hooks that validate requested operations before contexts are
|
||||||
|
* created,</li>
|
||||||
|
* <li>global audit wiring (listener + wrapping mode), and</li>
|
||||||
|
* <li>convenience methods for context creation and key generation/import.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Discovery & identity</h2> 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.
|
||||||
|
*
|
||||||
|
* <h2>Policy</h2> 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.
|
||||||
|
*
|
||||||
|
* <h2>Auditing</h2> 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.
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> 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<String, CryptoAlgorithm> BY_ID;
|
||||||
|
private static volatile CryptoPolicy<ContextSpec, Key> 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<String, CryptoAlgorithm> 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}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The returned set is backed by an unmodifiable registry snapshot. Use these
|
||||||
|
* identifiers with {@link #require(String)} or the convenience methods below.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return unmodifiable set of canonical algorithm ids
|
||||||
|
*/
|
||||||
|
public static Set<String> available() {
|
||||||
|
return BY_ID.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up an algorithm implementation by its canonical identifier.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param p policy to install, or {@code null} to use
|
||||||
|
* {@link CryptoPolicy#permissive()}
|
||||||
|
*/
|
||||||
|
public static void setPolicy(CryptoPolicy<ContextSpec, Key> p) {
|
||||||
|
POLICY = (p == null ? CryptoPolicy.<ContextSpec, Key>permissive() : p);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the global {@link AuditListener}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Modes</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #OFF} - No automatic wrapping of contexts (default). Only explicit
|
||||||
|
* events triggered at creation are emitted; no per-operation auditing is
|
||||||
|
* injected.</li>
|
||||||
|
*
|
||||||
|
* <li>{@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.</li>
|
||||||
|
*
|
||||||
|
* <li>{@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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public enum AuditMode {
|
||||||
|
/**
|
||||||
|
* No automatic wrapping of contexts (default).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Only explicit events emitted here (e.g.,
|
||||||
|
* {@link AuditListener#onContextCreated}) are sent to the listener;
|
||||||
|
* stream-level or per-operation auditing is not injected.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
OFF,
|
||||||
|
/**
|
||||||
|
* Wraps supported contexts in dynamic proxies that emit stream-level auditing.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
WRAP,
|
||||||
|
/**
|
||||||
|
* No wrapping and no automatic events.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The caller is responsible for emitting all relevant audit events via the
|
||||||
|
* {@link #audit()} listener.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
MANUAL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the auditing mode for subsequently created contexts.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Passing {@code null} resets the mode to {@link AuditMode#OFF}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Flow:
|
||||||
|
* </p>
|
||||||
|
* <ol>
|
||||||
|
* <li>Policy validation via
|
||||||
|
* {@link CryptoPolicy#validate(String, KeyUsage, Key, ContextSpec)}.</li>
|
||||||
|
* <li>Algorithm resolution via {@link #require(String)} and context
|
||||||
|
* construction via
|
||||||
|
* {@link CryptoAlgorithm#create(KeyUsage, Key, ContextSpec)}.</li>
|
||||||
|
* <li>Auditing behavior based on {@link #getAuditMode()}:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link AuditMode#OFF}/{@link AuditMode#MANUAL}: emit a creation event
|
||||||
|
* immediately via
|
||||||
|
* {@link AuditListener#onContextCreated(String, String, KeyUsage, Key, ContextSpec)}.</li>
|
||||||
|
* <li>{@link AuditMode#WRAP}: return a proxy (where supported) that emits
|
||||||
|
* creation and stream-level events; unknown context types are returned
|
||||||
|
* unwrapped.</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* @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 <C> context type
|
||||||
|
* @param <K> key type
|
||||||
|
* @param <S> 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 extends CryptoContext, K extends Key, S extends ContextSpec> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Equivalent to {@code create(id, role, key, null)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param id canonical algorithm identifier
|
||||||
|
* @param role desired {@link KeyUsage}
|
||||||
|
* @param key key instance for the role
|
||||||
|
* @param <C> context type
|
||||||
|
* @param <K> 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 extends CryptoContext, K extends Key> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Emits
|
||||||
|
* {@link AuditListener#onKeyGenerated(String, String, AlgorithmKeySpec, KeyPair)}
|
||||||
|
* on success.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param id canonical algorithm identifier
|
||||||
|
* @param spec algorithm-specific key specification
|
||||||
|
* @param <S> 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 <S extends AlgorithmKeySpec> KeyPair keyPair(String id, S spec) throws GeneralSecurityException {
|
||||||
|
CryptoAlgorithm algo = require(id);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
KeyPair kp = algo.asymmetricKeyBuilder((Class<S>) 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)}
|
||||||
|
* on success.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param id canonical algorithm identifier
|
||||||
|
* @param spec algorithm-specific key specification containing encoded public
|
||||||
|
* material
|
||||||
|
* @param <S> 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 <S extends AlgorithmKeySpec> PublicKey publicKey(String id, S spec) throws GeneralSecurityException {
|
||||||
|
CryptoAlgorithm algo = require(id);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
PublicKey k = algo.asymmetricKeyBuilder((Class<S>) 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)}
|
||||||
|
* on success.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param id canonical algorithm identifier
|
||||||
|
* @param spec algorithm-specific key specification containing encoded private
|
||||||
|
* material
|
||||||
|
* @param <S> 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 <S extends AlgorithmKeySpec> PrivateKey privateKey(String id, S spec)
|
||||||
|
throws GeneralSecurityException {
|
||||||
|
CryptoAlgorithm algo = require(id);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
PrivateKey k = algo.asymmetricKeyBuilder((Class<S>) 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Emits {@link AuditListener#onKeyBuilt(String, String, AlgorithmKeySpec, Key)}
|
||||||
|
* on success.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param id canonical algorithm identifier
|
||||||
|
* @param spec algorithm-specific key specification containing raw/encoded
|
||||||
|
* material
|
||||||
|
* @param <S> 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 <S extends AlgorithmKeySpec> SecretKey secretKey(String id, S spec) throws GeneralSecurityException {
|
||||||
|
CryptoAlgorithm algo = require(id);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
SecretKey k = algo.symmetricKeyBuilder((Class<S>) 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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 <S> spec type
|
||||||
|
* @return newly generated secret key
|
||||||
|
* @throws GeneralSecurityException if key generation fails
|
||||||
|
* @throws IllegalArgumentException if {@code id} is unknown
|
||||||
|
*/
|
||||||
|
public static <S extends AlgorithmKeySpec> 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 <S> spec type
|
||||||
|
* @return newly generated key pair
|
||||||
|
* @throws GeneralSecurityException if key generation fails
|
||||||
|
* @throws IllegalArgumentException if {@code id} is unknown
|
||||||
|
*/
|
||||||
|
public static <S extends AlgorithmKeySpec> 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 <S> spec type
|
||||||
|
* @return imported public key
|
||||||
|
* @throws GeneralSecurityException if import fails
|
||||||
|
* @throws IllegalArgumentException if {@code id} is unknown
|
||||||
|
*/
|
||||||
|
public static <S extends AlgorithmKeySpec> 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 <S> spec type
|
||||||
|
* @return imported private key
|
||||||
|
* @throws GeneralSecurityException if import fails
|
||||||
|
* @throws IllegalArgumentException if {@code id} is unknown
|
||||||
|
*/
|
||||||
|
public static <S extends AlgorithmKeySpec> 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 <S> spec type
|
||||||
|
* @return imported secret key
|
||||||
|
* @throws GeneralSecurityException if import fails
|
||||||
|
* @throws IllegalArgumentException if {@code id} is unknown
|
||||||
|
*/
|
||||||
|
public static <S extends AlgorithmKeySpec> SecretKey importSecret(String id, S spec)
|
||||||
|
throws GeneralSecurityException {
|
||||||
|
return require(id).importSecret(spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
322
lib/src/main/java/zeroecho/core/CryptoCatalog.java
Normal file
322
lib/src/main/java/zeroecho/core/CryptoCatalog.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* {@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:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #validate()} — sanity checks that each algorithm publishes at
|
||||||
|
* least one capability or key builder,</li>
|
||||||
|
* <li>{@link #toJson()} — a compact JSON description of algorithms,
|
||||||
|
* capabilities, and registered key builders, and</li>
|
||||||
|
* <li>{@link #toXml()} — an XML rendition of the same data.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Identity and uniqueness</h2> Algorithm ids are treated as canonical keys.
|
||||||
|
* If two providers expose the same {@linkplain CryptoAlgorithm#id() id}, the
|
||||||
|
* catalog build fails with {@link IllegalStateException}.
|
||||||
|
*
|
||||||
|
* <h2>Immutability & thread-safety</h2> After construction, the internal
|
||||||
|
* map is unmodifiable and safe to share across threads. This class performs no
|
||||||
|
* lazy discovery or on-demand mutation.
|
||||||
|
*
|
||||||
|
* <h2>Serialization formats</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>JSON</b>: produced by {@link #toJson()} as a single object with an
|
||||||
|
* {@code algorithms} array; trivial string escaping is applied.</li>
|
||||||
|
* <li><b>XML</b>: produced by {@link #toXml()} with a {@code <cryptoCatalog>}
|
||||||
|
* root; {@code &<>} escaping is applied to element text and
|
||||||
|
* attributes.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>Note:</b> 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public final class CryptoCatalog {
|
||||||
|
private final Map<String, CryptoAlgorithm> algos;
|
||||||
|
|
||||||
|
private CryptoCatalog(Map<String, CryptoAlgorithm> algos) {
|
||||||
|
this.algos = algos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovers {@link CryptoAlgorithm} implementations via {@link ServiceLoader}
|
||||||
|
* and returns an immutable catalog snapshot.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* During loading, algorithm ids are checked for uniqueness. A duplicate id
|
||||||
|
* results in an {@link IllegalStateException} to prevent ambiguous resolution.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return an immutable {@code CryptoCatalog} with all discovered algorithms
|
||||||
|
* @throws IllegalStateException if two providers declare the same algorithm id
|
||||||
|
*/
|
||||||
|
public static CryptoCatalog load() {
|
||||||
|
Map<String, CryptoAlgorithm> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The schema is:
|
||||||
|
* </p>
|
||||||
|
* <pre>{@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" }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* String values are escaped for quotes and backslashes. The method does not
|
||||||
|
* attempt to pretty-print; callers can format the output if needed.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The root element is {@code <cryptoCatalog>}. Each algorithm becomes an
|
||||||
|
* {@code <algorithm>} element with {@code id} and {@code name} attributes, plus
|
||||||
|
* nested sections for {@code <capabilities>}, {@code <asymmetricKeyBuilders>},
|
||||||
|
* and {@code <symmetricKeyBuilders>}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Element text and attribute values are escaped for {@code &, <, >}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return an XML string describing algorithms, capabilities, and key builders
|
||||||
|
*/
|
||||||
|
public String toXml() {
|
||||||
|
StringBuilder sb = new StringBuilder(4096);
|
||||||
|
sb.append("<cryptoCatalog>");
|
||||||
|
for (CryptoAlgorithm a : algos.values()) {
|
||||||
|
sb.append("<algorithm id=\"").append(esc(a.id())).append("\" name=\"").append(esc(a.displayName()))
|
||||||
|
.append("\"><capabilities>");
|
||||||
|
for (Capability cap : a.listCapabilities()) {
|
||||||
|
sb.append("<capability family=\"").append(cap.family().name()).append("\" role=\"")
|
||||||
|
.append(cap.role().name()).append("\"><contextType>")
|
||||||
|
.append(esc(cap.contextType().getSimpleName())).append("</contextType><keyType>")
|
||||||
|
.append(esc(cap.keyType().getSimpleName())).append("</keyType><specType>")
|
||||||
|
.append(esc(cap.specType().getSimpleName())).append("</specType><defaultSpec>")
|
||||||
|
.append(esc(labelOf(cap.defaultSpec().get()))).append("</defaultSpec></capability>");
|
||||||
|
}
|
||||||
|
sb.append("</capabilities><asymmetricKeyBuilders>");
|
||||||
|
for (CryptoAlgorithm.AsymBuilderInfo kb : a.asymmetricBuildersInfo()) {
|
||||||
|
sb.append("<keyBuilder specType=\"").append(esc(kb.specType.getSimpleName()))
|
||||||
|
.append("\"><defaultKeySpec>")
|
||||||
|
.append(kb.defaultKeySpec == null ? "" : esc(labelOf(kb.defaultKeySpec)))
|
||||||
|
.append("</defaultKeySpec></keyBuilder>");
|
||||||
|
}
|
||||||
|
sb.append("</asymmetricKeyBuilders><symmetricKeyBuilders>");
|
||||||
|
for (CryptoAlgorithm.SymBuilderInfo kb : a.symmetricBuildersInfo()) {
|
||||||
|
sb.append("<keyBuilder specType=\"").append(esc(kb.specType().getSimpleName()))
|
||||||
|
.append("\"><defaultKeySpec>")
|
||||||
|
.append(kb.defaultKeySpec() == null ? "" : esc(labelOf(kb.defaultKeySpec())))
|
||||||
|
.append("</defaultKeySpec></keyBuilder>");
|
||||||
|
}
|
||||||
|
sb.append("</symmetricKeyBuilders></algorithm>");
|
||||||
|
}
|
||||||
|
sb.append("</cryptoCatalog>");
|
||||||
|
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(">", ">");
|
||||||
|
}
|
||||||
|
}
|
||||||
86
lib/src/main/java/zeroecho/core/KeyUsage.java
Normal file
86
lib/src/main/java/zeroecho/core/KeyUsage.java
Normal file
@@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* <h2>Typical usages</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #SIGN}/{@link #VERIFY}: digital signature schemes (Ed25519, ECDSA,
|
||||||
|
* RSA-PSS).</li>
|
||||||
|
* <li>{@link #ENCRYPT}/{@link #DECRYPT}: confidentiality with symmetric or
|
||||||
|
* asymmetric encryption.</li>
|
||||||
|
* <li>{@link #ENCAPSULATE}/{@link #DECAPSULATE}: key encapsulation mechanisms
|
||||||
|
* (KEM).</li>
|
||||||
|
* <li>{@link #MAC}: keyed message authentication (e.g., HMAC, KMAC).</li>
|
||||||
|
* <li>{@link #DIGEST}: unkeyed hashing (e.g., SHA-256, SHA3-512).</li>
|
||||||
|
* <li>{@link #AGREEMENT}: key agreement (e.g., X25519, ECDH) to derive shared
|
||||||
|
* secrets.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>Security note:</b> Many real-world breaches trace back to cryptographic
|
||||||
|
* keys being used outside their intended purpose. Libraries should validate
|
||||||
|
* {@code KeyUsage} before performing operations.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
}
|
||||||
185
lib/src/main/java/zeroecho/core/NullKey.java
Normal file
185
lib/src/main/java/zeroecho/core/NullKey.java
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.core;
|
||||||
|
|
||||||
|
import java.security.Key;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sentinel {@link Key} representing the intentional absence of cryptographic
|
||||||
|
* key material.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* {@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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Intended uses</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Unkeyed operations such as {@code DIGEST} roles where keys are
|
||||||
|
* semantically undefined but a {@code Key} parameter is part of a common
|
||||||
|
* interface.</li>
|
||||||
|
* <li>Testing and scaffolding where a non-null {@code Key} is required to
|
||||||
|
* exercise type and flow without provisioning secrets.</li>
|
||||||
|
* <li>Metadata publication for capabilities that don’t depend on key
|
||||||
|
* material.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Security properties</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Contains no secret; {@link #getEncoded()} returns an empty byte
|
||||||
|
* array.</li>
|
||||||
|
* <li>Algorithm and format are the literal string {@code "NONE"}.</li>
|
||||||
|
* <li>All instances are equal by type; prefer the {@link #INSTANCE}
|
||||||
|
* singleton.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public static final NullKey INSTANCE = new NullKey();
|
||||||
|
|
||||||
|
private NullKey() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the algorithm identifier for this sentinel key.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The value is the literal string {@code "NONE"} to signal that no real
|
||||||
|
* cryptographic algorithm is associated with this key.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the string {@code "NONE"}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return "NONE";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the primary encoding format for this sentinel key.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The value is the literal string {@code "NONE"}; {@link #getEncoded()} returns
|
||||||
|
* an empty byte array.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the string {@code "NONE"}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getFormat() {
|
||||||
|
return "NONE";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the key in its primary encoding.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* For {@code NullKey}, this is an empty byte array because no material exists.
|
||||||
|
* Callers MAY rely on this to avoid special-casing unkeyed flows.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return a new zero-length byte array
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public byte[] getEncoded() {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares this object to another for equality.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* All {@code NullKey} instances are considered equal regardless of identity;
|
||||||
|
* equality is defined by <em>type</em> rather than state. Prefer comparing
|
||||||
|
* against {@link #INSTANCE} when intent matters, but this method ensures
|
||||||
|
* generic collections and caches behave as expected.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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)}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Derived from a fixed class-based token so that all {@code NullKey} instances
|
||||||
|
* hash identically, matching the type-based equality semantics.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return a stable hash code value
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash("NullKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a diagnostic string for logs and debugging.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The returned value is the literal {@code "NullKey"}. It contains no secrets
|
||||||
|
* and is safe for inclusion in logs and error messages.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the string {@code "NullKey"}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "NullKey";
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/src/main/java/zeroecho/core/SymmetricHeaderCodec.java
Normal file
57
lib/src/main/java/zeroecho/core/SymmetricHeaderCodec.java
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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;
|
||||||
|
}
|
||||||
178
lib/src/main/java/zeroecho/core/alg/AbstractCryptoAlgorithm.java
Normal file
178
lib/src/main/java/zeroecho/core/alg/AbstractCryptoAlgorithm.java
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* {@code AbstractCryptoAlgorithm} streamlines two common tasks during algorithm
|
||||||
|
* construction:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <ol>
|
||||||
|
* <li><b>Binding roles to runtime factories</b> 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.</li>
|
||||||
|
* <li><b>Publishing descriptive capabilities</b> by automatically creating and
|
||||||
|
* adding a {@link Capability} record that higher layers can inspect for feature
|
||||||
|
* discovery, documentation, or automated selection.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <h2>Typical usage</h2> <pre>{@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);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This single call performs two actions atomically:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Runtime binding:</b> 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.</li>
|
||||||
|
* <li><b>Metadata publication:</b> 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()}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h4>Validation</h4> 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 <C> context type
|
||||||
|
* @param <K> key type
|
||||||
|
* @param <S> spec type
|
||||||
|
* @throws NullPointerException if any class/factory/supplier argument is
|
||||||
|
* {@code null}
|
||||||
|
*/
|
||||||
|
protected <C extends CryptoContext, K extends Key, S extends ContextSpec> void capability(AlgorithmFamily family,
|
||||||
|
KeyUsage role, Class<C> ctxType, Class<K> keyType, Class<S> specType, ContextConstructorKS<C, K, S> factory,
|
||||||
|
Supplier<? extends S> defaultSpec) {
|
||||||
|
|
||||||
|
// bind runtime factory
|
||||||
|
bind(role, ctxType, keyType, specType, factory, defaultSpec);
|
||||||
|
// publish metadata
|
||||||
|
addCapability(new Capability(id(), family, role, ctxType, keyType, specType, defaultSpec));
|
||||||
|
}
|
||||||
|
}
|
||||||
134
lib/src/main/java/zeroecho/core/alg/aes/AesAlgorithm.java
Normal file
134
lib/src/main/java/zeroecho/core/alg/aes/AesAlgorithm.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Registers streaming ENCRYPT/DECRYPT contexts for the {@code "AES"} family,
|
||||||
|
* provides secure defaults (GCM/128), and exposes key builders for generation
|
||||||
|
* and import.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>Capabilities:
|
||||||
|
* <ul>
|
||||||
|
* <li>ENCRYPT/DECRYPT → {@link AesCipherContext} with {@link AesSpec}</li>
|
||||||
|
* <li>VoidSpec fallback → {@code AesSpec.gcm128(null)}</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li>Key builders:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link AesKeyGenSpec} → JCA {@link javax.crypto.KeyGenerator}</li>
|
||||||
|
* <li>{@link AesKeyImportSpec} → {@link javax.crypto.spec.SecretKeySpec}</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread‑safety</h2> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
296
lib/src/main/java/zeroecho/core/alg/aes/AesCipherContext.java
Normal file
296
lib/src/main/java/zeroecho/core/alg/aes/AesCipherContext.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
* <ul>
|
||||||
|
* <li>Decrypt + header → reads header first, hydrates context, then
|
||||||
|
* transforms</li>
|
||||||
|
* <li>Transform → initializes JCA cipher with IV/AAD (GCM), staging final
|
||||||
|
* output if necessary</li>
|
||||||
|
* <li>Encrypt + header → prepends header after IV is determined</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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<byte[]> key) {
|
||||||
|
if (ctx == null || key == null) {
|
||||||
|
return null; // NOPMD
|
||||||
|
}
|
||||||
|
return ctx.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putCtxBytes(conflux.Key<byte[]> key, byte[] value) {
|
||||||
|
if (ctx != null && key != null) {
|
||||||
|
ctx.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
lib/src/main/java/zeroecho/core/alg/aes/AesHeaderCodec.java
Normal file
174
lib/src/main/java/zeroecho/core/alg/aes/AesHeaderCodec.java
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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:
|
||||||
|
* <pre>IV | tagBits(pack7) | aadFlag(1) | [aadHash(32)]</pre>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li><b>IV</b>: 12 bytes for GCM; 16 bytes for CBC/CTR.</li>
|
||||||
|
* <li><b>tagBits</b>: 0 for non‑GCM; otherwise 96..128.</li>
|
||||||
|
* <li><b>aadFlag</b>: 1 when AAD is present; the header then includes a SHA‑256
|
||||||
|
* of the AAD.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
lib/src/main/java/zeroecho/core/alg/aes/AesKeyGenSpec.java
Normal file
107
lib/src/main/java/zeroecho/core/alg/aes/AesKeyGenSpec.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Objects of this type are immutable and thread-safe.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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";
|
||||||
|
}
|
||||||
|
}
|
||||||
164
lib/src/main/java/zeroecho/core/alg/aes/AesKeyImportSpec.java
Normal file
164
lib/src/main/java/zeroecho/core/alg/aes/AesKeyImportSpec.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Instances are consumed by the AES key builder to produce a
|
||||||
|
* {@link javax.crypto.SecretKey}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Objects of this type are immutable and thread-safe.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
362
lib/src/main/java/zeroecho/core/alg/aes/AesSpec.java
Normal file
362
lib/src/main/java/zeroecho/core/alg/aes/AesSpec.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Design</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Immutability:</b> instances are immutable and thread‑safe.</li>
|
||||||
|
* <li><b>Validation:</b> 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.</li>
|
||||||
|
* <li><b>Header codec:</b> 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.</li>
|
||||||
|
* <li><b>Runtime parameters:</b> 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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public final class AesSpec implements ContextSpec, Describable {
|
||||||
|
/**
|
||||||
|
* AES block modes supported by this implementation.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public enum Mode {
|
||||||
|
/**
|
||||||
|
* Cipher Block Chaining (CBC).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Requires a 16‑byte IV. Supports {@link Padding#PKCS5PADDING} and
|
||||||
|
* {@link Padding#NOPADDING}. Not an AEAD mode; pair with a MAC for integrity.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
CBC,
|
||||||
|
/**
|
||||||
|
* Galois/Counter Mode (GCM), an authenticated encryption mode (AEAD).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
GCM,
|
||||||
|
/**
|
||||||
|
* Counter mode (CTR).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Uses a 16‑byte nonce/IV and {@link Padding#NOPADDING}. Not an AEAD mode; pair
|
||||||
|
* with a MAC for integrity.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
CTR
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Padding schemes for CBC mode.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* GCM and CTR always use {@link #NOPADDING}.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public enum Padding {
|
||||||
|
/**
|
||||||
|
* No padding.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Use only when the plaintext length is an exact multiple of the AES block size
|
||||||
|
* (16 bytes). Applicable to CBC; mandatory for GCM/CTR.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
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}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Not thread‑safe. The resulting {@code AesSpec} is immutable.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Ignored for GCM and CTR, which always use {@link Padding#NOPADDING}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Valid only when the mode is {@link Mode#GCM}. The value must be 96, 104, 112,
|
||||||
|
* 120, or 128.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* When {@code null}, no header is written or parsed.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Validation rules:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>If mode is {@link Mode#GCM}, padding must be {@link Padding#NOPADDING}
|
||||||
|
* and {@code tagLenBits} must be 96–128 in steps of 8.</li>
|
||||||
|
* <li>If mode is not GCM, {@code tagLenBits} must be 0.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This value determines IV length expectations and whether AAD and tag length
|
||||||
|
* apply (GCM only).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the block mode used by this specification
|
||||||
|
*/
|
||||||
|
public Mode mode() {
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the selected padding scheme.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Relevant only for CBC; GCM and CTR always operate with no padding.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the padding scheme (never {@code null})
|
||||||
|
*/
|
||||||
|
public Padding padding() {
|
||||||
|
return padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the authentication tag length in bits.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* For GCM, this is one of 96, 104, 112, 120, or 128. For non‑GCM modes, this
|
||||||
|
* method returns {@code 0}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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 + ")" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
94
lib/src/main/java/zeroecho/core/alg/aes/package-info.java
Normal file
94
lib/src/main/java/zeroecho/core/alg/aes/package-info.java
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
/**
|
||||||
|
* AES algorithm implementation and runtime wiring.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Components</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Algorithm descriptor:</b> {@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.</li>
|
||||||
|
* <li><b>Streaming context:</b> {@link AesCipherContext} implements the runtime
|
||||||
|
* transform over {@code InputStream}, handling IV creation and validation,
|
||||||
|
* optional AAD, tag length (GCM), and mode-specific initialization.</li>
|
||||||
|
* <li><b>Static configuration:</b> {@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.</li>
|
||||||
|
* <li><b>Header codec:</b> {@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.</li>
|
||||||
|
* <li><b>Key specifications:</b> {@link AesKeyGenSpec} defines key-size
|
||||||
|
* parameters for generation, and {@link AesKeyImportSpec} wraps existing AES
|
||||||
|
* keys supplied as raw bytes, hex, or Base64.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Runtime parameters and context exchange</h2>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Safety and validation</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>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.</li>
|
||||||
|
* <li>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.</li>
|
||||||
|
* <li>When an AAD hash is present in the header, decryption enforces
|
||||||
|
* consistency with the caller-supplied AAD.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Algorithm descriptors are immutable and safe to share.</li>
|
||||||
|
* <li>Streaming contexts are stateful and not thread-safe.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
package zeroecho.core.alg.aes;
|
||||||
219
lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java
Normal file
219
lib/src/main/java/zeroecho/core/alg/bike/BikeAlgorithm.java
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Integration of BIKE (Bit Flipping Key Encapsulation) algorithm</h2>
|
||||||
|
*
|
||||||
|
* Declares BIKE as a {@link zeroecho.core.alg.AbstractCryptoAlgorithm} and
|
||||||
|
* registers its supported roles, contexts, and key builders within the ZeroEcho
|
||||||
|
* framework.
|
||||||
|
*
|
||||||
|
* <h3>Capabilities</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>KEM</b> - encapsulation and decapsulation of shared secrets using
|
||||||
|
* public and private keys.</li>
|
||||||
|
* <li><b>Agreement</b> - initiator and responder flows for
|
||||||
|
* {@link MessageAgreementContext} built on top of BIKE KEM.</li>
|
||||||
|
* <li><b>Asymmetric key builders</b> - generation and import of BIKE keys from
|
||||||
|
* {@link BikeKeyGenSpec}, {@link BikePublicKeySpec}, and
|
||||||
|
* {@link BikePrivateKeySpec}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Provider</h3> Uses the BouncyCastle Post-Quantum provider
|
||||||
|
* ({@code BCPQC}). The provider must be registered in the JCA {@link Security}
|
||||||
|
* before use.
|
||||||
|
*
|
||||||
|
* <h3>Example</h3> <pre>{@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);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public final class BikeAlgorithm extends AbstractCryptoAlgorithm {
|
||||||
|
/**
|
||||||
|
* Constructs and registers the BIKE algorithm with all its roles and key
|
||||||
|
* builders.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Registers:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>KEM (encapsulate/decapsulate)</li>
|
||||||
|
* <li>Agreement (initiator/responder)</li>
|
||||||
|
* <li>Asymmetric key builders for BIKE key specifications</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
193
lib/src/main/java/zeroecho/core/alg/bike/BikeKemContext.java
Normal file
193
lib/src/main/java/zeroecho/core/alg/bike/BikeKemContext.java
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>BIKE Key Encapsulation Mechanism context</h2>
|
||||||
|
*
|
||||||
|
* Implements {@link zeroecho.core.context.KemContext} for the BIKE (Bit
|
||||||
|
* Flipping Key Encapsulation) post-quantum algorithm. Encapsulation and
|
||||||
|
* decapsulation flows are separated by constructor:
|
||||||
|
* <ul>
|
||||||
|
* <li>Construct with a {@link java.security.PublicKey} to create an
|
||||||
|
* <b>encapsulation</b> context.</li>
|
||||||
|
* <li>Construct with a {@link java.security.PrivateKey} to create a
|
||||||
|
* <b>decapsulation</b> context.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Usage</h3> <pre>{@code
|
||||||
|
* BikeKemContext enc = new BikeKemContext(algo, recipientPubKey);
|
||||||
|
* KemResult r = enc.encapsulate();
|
||||||
|
*
|
||||||
|
* BikeKemContext dec = new BikeKemContext(algo, myPrivateKey);
|
||||||
|
* byte[] secret = dec.decapsulate(r.ciphertext());
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
* <p>
|
||||||
|
* No-op for BIKE; provided for API symmetry.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
lib/src/main/java/zeroecho/core/alg/bike/BikeKeyGenSpec.java
Normal file
132
lib/src/main/java/zeroecho/core/alg/bike/BikeKeyGenSpec.java
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Specification for BIKE key generation</h2>
|
||||||
|
*
|
||||||
|
* Defines algorithm parameters for generating BIKE key pairs. Encapsulates a
|
||||||
|
* chosen {@link Variant} (BIKE-128, BIKE-192, BIKE-256).
|
||||||
|
*
|
||||||
|
* <h3>Usage</h3> <pre>{@code
|
||||||
|
* // Generate a BIKE-192 key pair
|
||||||
|
* KeyPair kp = bikeAlgorithm.asymmetricKeyBuilder(BikeKeyGenSpec.class)
|
||||||
|
* .generateKeyPair(BikeKeyGenSpec.bike192());
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Implements {@link Describable} by returning the variant name.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return description string
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return variant.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
136
lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java
Normal file
136
lib/src/main/java/zeroecho/core/alg/bike/BikePrivateKeySpec.java
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Specification for a BIKE private key</h2>
|
||||||
|
*
|
||||||
|
* Wraps a BIKE private key encoded in standard PKCS#8 format. Provides
|
||||||
|
* marshalling and unmarshalling support for interchange.
|
||||||
|
*
|
||||||
|
* <h3>Usage</h3> <pre>{@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);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Fields:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code type} = "BikePrivateKeySpec"</li>
|
||||||
|
* <li>{@code pkcs8.b64} = base64 of encoded key (no padding)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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 + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
136
lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java
Normal file
136
lib/src/main/java/zeroecho/core/alg/bike/BikePublicKeySpec.java
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Specification for a BIKE public key</h2>
|
||||||
|
*
|
||||||
|
* Wraps a BIKE public key encoded in standard X.509 format. Provides
|
||||||
|
* marshalling and unmarshalling support for serialization.
|
||||||
|
*
|
||||||
|
* <h3>Usage</h3> <pre>{@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);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Fields:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code type} = "BikePublicKeySpec"</li>
|
||||||
|
* <li>{@code x509.b64} = base64 of encoded key (no padding)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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 + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
88
lib/src/main/java/zeroecho/core/alg/bike/package-info.java
Normal file
88
lib/src/main/java/zeroecho/core/alg/bike/package-info.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Scope and responsibilities</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Expose a concrete algorithm descriptor that registers BIKE roles and key
|
||||||
|
* builders.</li>
|
||||||
|
* <li>Provide a runtime context that performs encapsulation and
|
||||||
|
* decapsulation.</li>
|
||||||
|
* <li>Define key specifications for generation and for importing encoded
|
||||||
|
* keys.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Components</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Algorithm descriptor:</b> {@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.</li>
|
||||||
|
* <li><b>Runtime context:</b> {@link BikeKemContext} implements the key
|
||||||
|
* encapsulation mechanism and separates encapsulation from decapsulation by
|
||||||
|
* constructor selection.</li>
|
||||||
|
* <li><b>Key generation spec:</b> {@link BikeKeyGenSpec} selects a BIKE variant
|
||||||
|
* and is used by the key-pair builder.</li>
|
||||||
|
* <li><b>Key import specs:</b> {@link BikePublicKeySpec} and
|
||||||
|
* {@link BikePrivateKeySpec} wrap X.509 and PKCS#8 encodings and support
|
||||||
|
* marshalling for transport and storage.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Provider requirements</h2>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Algorithm descriptors are immutable and safe to share across
|
||||||
|
* threads.</li>
|
||||||
|
* <li>Runtime contexts are stateful and not thread-safe.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
package zeroecho.core.alg.bike;
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Abstract base for ChaCha family algorithms</h2>
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* <h3>Registered key builders</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link zeroecho.core.alg.chacha.ChaChaKeyGenSpec} - generates new random
|
||||||
|
* ChaCha keys of configurable size (default 256-bit).</li>
|
||||||
|
* <li>{@link zeroecho.core.alg.chacha.ChaChaKeyImportSpec} - imports externally
|
||||||
|
* supplied ChaCha keys as {@link javax.crypto.SecretKey} instances.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Notes</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>Generation uses {@link javax.crypto.KeyGenerator} with algorithm
|
||||||
|
* {@code "ChaCha20"}.</li>
|
||||||
|
* <li>Import wraps the raw key material with
|
||||||
|
* {@link javax.crypto.spec.SecretKeySpec}.</li>
|
||||||
|
* <li>Attempts to generate a key via {@code ChaChaKeyImportSpec} or import via
|
||||||
|
* {@code ChaChaKeyGenSpec} will throw
|
||||||
|
* {@link UnsupportedOperationException}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Example</h3> <pre>{@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));
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Abstract streaming cipher context for ChaCha algorithms</h2>
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* <h3>Features</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>Automatic 12-byte nonce generation (encryption) or validation
|
||||||
|
* (decryption).</li>
|
||||||
|
* <li>Optional header encoding/decoding via {@link SymmetricHeaderCodec} bound
|
||||||
|
* in the spec.</li>
|
||||||
|
* <li>Streaming transformation using {@link javax.crypto.Cipher} with staged
|
||||||
|
* {@code doFinal()}.</li>
|
||||||
|
* <li>Integration with {@link ConfluxKeys} for propagating IV/nonce
|
||||||
|
* values.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Subclass responsibilities</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>Implement {@link #jceName()} to return a JCE transformation string (e.g.,
|
||||||
|
* {@code "ChaCha20"} or {@code "ChaCha20-Poly1305"}).</li>
|
||||||
|
* <li>Implement {@link #initCipher(Cipher, byte[])} to configure parameters and
|
||||||
|
* AAD if required.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Usage</h3> <pre>{@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);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @param <S> specification type carrying ChaCha parameters
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
abstract class AbstractChaChaCipherContext<S extends ChaChaBaseSpec> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Encryption prepends optional headers; decryption consumes headers first.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
* <p>
|
||||||
|
* No-op for ChaCha contexts.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() { // NOPMD
|
||||||
|
/* no-op */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures a nonce is available in the context.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>For encryption, generates a new nonce if absent and stores it in
|
||||||
|
* context.</li>
|
||||||
|
* <li>For decryption, validates presence and correct length.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha20-Poly1305 (AEAD) algorithm</h2>
|
||||||
|
*
|
||||||
|
* 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).
|
||||||
|
*
|
||||||
|
* <h3>Capabilities</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Encrypt</b>:
|
||||||
|
* <ul>
|
||||||
|
* <li>Family: {@link zeroecho.core.AlgorithmFamily#SYMMETRIC}</li>
|
||||||
|
* <li>Usage: {@link zeroecho.core.KeyUsage#ENCRYPT}</li>
|
||||||
|
* <li>Context: {@link zeroecho.core.context.EncryptionContext}</li>
|
||||||
|
* <li>Key: {@link javax.crypto.SecretKey}</li>
|
||||||
|
* <li>Spec: {@link ChaCha20Poly1305Spec} (or
|
||||||
|
* {@link zeroecho.core.spec.VoidSpec} default)</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li><b>Decrypt</b>:
|
||||||
|
* <ul>
|
||||||
|
* <li>Family: {@link zeroecho.core.AlgorithmFamily#SYMMETRIC}</li>
|
||||||
|
* <li>Usage: {@link zeroecho.core.KeyUsage#DECRYPT}</li>
|
||||||
|
* <li>Context: {@link zeroecho.core.context.EncryptionContext}</li>
|
||||||
|
* <li>Key: {@link javax.crypto.SecretKey}</li>
|
||||||
|
* <li>Spec: {@link ChaCha20Poly1305Spec} (or
|
||||||
|
* {@link zeroecho.core.spec.VoidSpec} default)</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Defaults</h3> 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.
|
||||||
|
*
|
||||||
|
* <h3>Example</h3> <pre>{@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);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha20-Poly1305 cipher context (AEAD)</h2>
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* <h3>Behavior</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>Uses transformation {@code "ChaCha20-Poly1305"}.</li>
|
||||||
|
* <li>Initializes with {@link javax.crypto.spec.IvParameterSpec} for the
|
||||||
|
* 12-byte nonce.</li>
|
||||||
|
* <li>Supplies Additional Authenticated Data (AAD) from the active context if
|
||||||
|
* present.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Usage</h3> <pre>{@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());
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
final class ChaCha20Poly1305CipherContext extends AbstractChaChaCipherContext<ChaCha20Poly1305Spec> {
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha20-Poly1305 streaming header codec</h2>
|
||||||
|
*
|
||||||
|
* Implements {@link SymmetricHeaderCodec} for the ChaCha20-Poly1305 AEAD mode.
|
||||||
|
* Encodes a compact header that precedes the ciphertext stream and conveys:
|
||||||
|
* <ul>
|
||||||
|
* <li>a 12-byte nonce (IV) required by ChaCha20-Poly1305, and</li>
|
||||||
|
* <li>an optional SHA-256 hash of AAD to assert integrity of externally
|
||||||
|
* supplied AAD.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h3>Header layout</h3> <pre>
|
||||||
|
* [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
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <h3>Failure modes</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>Missing/invalid nonce in context when writing the header.</li>
|
||||||
|
* <li>AAD expected by header but not provided in context during read.</li>
|
||||||
|
* <li>AAD hash mismatch when verifying during read.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha20-Poly1305 algorithm specification</h2>
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* <h3>Notes</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>If {@link #header()} is {@code null}, no header is written or read;
|
||||||
|
* callers must exchange the nonce/AAD out-of-band via the context.</li>
|
||||||
|
* <li>The effective authentication tag size is 128 bits.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Example</h3> <pre>{@code
|
||||||
|
* ChaCha20Poly1305Spec spec = ChaCha20Poly1305Spec.builder()
|
||||||
|
* .header(new ChaCha20Poly1305HeaderCodec())
|
||||||
|
* .build();
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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)";
|
||||||
|
}
|
||||||
|
}
|
||||||
109
lib/src/main/java/zeroecho/core/alg/chacha/ChaChaAlgorithm.java
Normal file
109
lib/src/main/java/zeroecho/core/alg/chacha/ChaChaAlgorithm.java
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.core.alg.chacha;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha20 (stream) algorithm</h2>
|
||||||
|
*
|
||||||
|
* 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}.
|
||||||
|
*
|
||||||
|
* <h3>Capabilities</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Encrypt</b>
|
||||||
|
* <ul>
|
||||||
|
* <li>Family: {@link zeroecho.core.AlgorithmFamily#SYMMETRIC}</li>
|
||||||
|
* <li>Usage: {@link zeroecho.core.KeyUsage#ENCRYPT}</li>
|
||||||
|
* <li>Context: {@link zeroecho.core.context.EncryptionContext}</li>
|
||||||
|
* <li>Key: {@link javax.crypto.SecretKey}</li>
|
||||||
|
* <li>Spec: {@link ChaChaSpec} (or {@link zeroecho.core.spec.VoidSpec}
|
||||||
|
* default)</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li><b>Decrypt</b>
|
||||||
|
* <ul>
|
||||||
|
* <li>Family: {@link zeroecho.core.AlgorithmFamily#SYMMETRIC}</li>
|
||||||
|
* <li>Usage: {@link zeroecho.core.KeyUsage#DECRYPT}</li>
|
||||||
|
* <li>Context: {@link zeroecho.core.context.EncryptionContext}</li>
|
||||||
|
* <li>Key: {@link javax.crypto.SecretKey}</li>
|
||||||
|
* <li>Spec: {@link ChaChaSpec} (or {@link zeroecho.core.spec.VoidSpec}
|
||||||
|
* default)</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Defaults</h3> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha base specification marker</h2>
|
||||||
|
*
|
||||||
|
* Common sealed interface for all ChaCha-family specifications. Implemented by
|
||||||
|
* {@link ChaChaSpec} (raw stream cipher) and {@link ChaCha20Poly1305Spec}
|
||||||
|
* (AEAD).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Extends {@link zeroecho.core.spec.ContextSpec} to allow binding
|
||||||
|
* algorithm-specific parameters into a context.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h3>Header support</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #header()} may return a {@link SymmetricHeaderCodec} that encodes
|
||||||
|
* parameters (e.g., nonce, AAD hash) into the ciphertext stream.</li>
|
||||||
|
* <li>If {@code null}, no header is used and parameters must be managed via
|
||||||
|
* {@code CtxInterface} or other out-of-band means.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Example</h3> <pre>{@code
|
||||||
|
* ChaChaBaseSpec spec = ChaCha20Poly1305Spec.withHeader(
|
||||||
|
* new ChaCha20Poly1305HeaderCodec()
|
||||||
|
* );
|
||||||
|
* SymmetricHeaderCodec codec = spec.header(); // non-null
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha20 cipher context (stream)</h2>
|
||||||
|
*
|
||||||
|
* 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:
|
||||||
|
* <ul>
|
||||||
|
* <li>Transformation: {@code "ChaCha20"}.</li>
|
||||||
|
* <li>12-byte nonce via {@link javax.crypto.spec.ChaCha20ParameterSpec}.</li>
|
||||||
|
* <li>Initial counter sourced from {@link ChaChaSpec#initialCounter()},
|
||||||
|
* optionally overridden via {@link ConfluxKeys#tagBits(String)} in the bound
|
||||||
|
* context.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Counter handling</h3> 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<ChaChaSpec> {
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha20 streaming header codec</h2>
|
||||||
|
*
|
||||||
|
* Implements {@link SymmetricHeaderCodec} for the {@code ChaCha20} stream
|
||||||
|
* cipher. Encodes a compact header containing:
|
||||||
|
* <ul>
|
||||||
|
* <li>a 12-byte nonce (IV), and</li>
|
||||||
|
* <li>the stream counter as a 7-bit packed integer.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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"}).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h3>Header layout</h3> <pre>
|
||||||
|
* [0..11] : 12-byte nonce (IV)
|
||||||
|
* [12..N] : counter encoded via 7-bit packed integer
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <h3>Behavior</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>writeHeader</b>: requires {@code iv(id)} in context (12 bytes). Writes
|
||||||
|
* the nonce, then the counter from {@code tagBits(id)} if present, otherwise
|
||||||
|
* uses {@code 1}.</li>
|
||||||
|
* <li><b>readHeader</b>: reads nonce and counter and stores them into the
|
||||||
|
* context under the same keys.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha20 key generation specification</h2>
|
||||||
|
*
|
||||||
|
* Immutable record describing the key size for ChaCha20 key generation. Only
|
||||||
|
* 256-bit keys are permitted by the ChaCha20 design.
|
||||||
|
*
|
||||||
|
* <h3>Validation</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>The constructor enforces {@code keySizeBits == 256}.</li>
|
||||||
|
* <li>Any other size results in an {@link IllegalArgumentException}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Factory method</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #chacha256()} provides a convenient way to obtain a standard
|
||||||
|
* 256-bit spec.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Usage example</h3> <pre>{@code
|
||||||
|
* ChaChaKeyGenSpec spec = ChaChaKeyGenSpec.chacha256();
|
||||||
|
* SecretKey key = cryptoAlgorithm.generateSecret(spec);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha20 key import specification</h2>
|
||||||
|
*
|
||||||
|
* Wraps a raw ChaCha20 key for import into the ZeroEcho framework. Keys must be
|
||||||
|
* exactly 32 bytes (256 bits).
|
||||||
|
*
|
||||||
|
* <h3>Construction</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #fromRaw(byte[])} - construct from a raw byte array.</li>
|
||||||
|
* <li>{@link #fromHex(String)} - construct from a hexadecimal string.</li>
|
||||||
|
* <li>{@link #fromBase64(String)} - construct from a Base64 string.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Marshalling</h3> Keys can be serialized/deserialized using
|
||||||
|
* {@link PairSeq}:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #marshal(ChaChaKeyImportSpec)} encodes the key as Base64.</li>
|
||||||
|
* <li>{@link #unmarshal(PairSeq)} accepts fields {@code k.b64}, {@code k.hex},
|
||||||
|
* or {@code k.raw} (ISO-8859-1) to restore a spec.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Usage</h3> <pre>{@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);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Recognized fields:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code k.b64} - Base64 encoding</li>
|
||||||
|
* <li>{@code k.hex} - hexadecimal string</li>
|
||||||
|
* <li>{@code k.raw} - raw ISO-8859-1 string</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
175
lib/src/main/java/zeroecho/core/alg/chacha/ChaChaSpec.java
Normal file
175
lib/src/main/java/zeroecho/core/alg/chacha/ChaChaSpec.java
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.core.alg.chacha;
|
||||||
|
|
||||||
|
import zeroecho.core.SymmetricHeaderCodec;
|
||||||
|
import zeroecho.core.annotation.Describable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>ChaCha20 stream cipher specification</h2>
|
||||||
|
*
|
||||||
|
* Immutable parameter set for configuring a {@code ChaCha20} context. Provides
|
||||||
|
* an initial counter and an optional {@link SymmetricHeaderCodec}.
|
||||||
|
*
|
||||||
|
* <h3>Fields</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@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.</li>
|
||||||
|
* <li>{@link #header()} - optional codec for encoding/decoding a stream header
|
||||||
|
* that carries runtime parameters such as nonce and counter.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h3>Construction</h3> Use the fluent {@link Builder}: <pre>{@code
|
||||||
|
* ChaChaSpec spec = ChaChaSpec.builder()
|
||||||
|
* .initialCounter(42)
|
||||||
|
* .header(new ChaChaHeaderCodec())
|
||||||
|
* .build();
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <h3>Convenience factory</h3>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #chacha20(SymmetricHeaderCodec)} returns a spec with initial
|
||||||
|
* counter {@code 1} and the provided header codec.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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 + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
90
lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
Normal file
90
lib/src/main/java/zeroecho/core/alg/chacha/package-info.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Scope and responsibilities</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Expose a concrete algorithm descriptor that registers CMCE KEM roles and
|
||||||
|
* a KEM-backed message-agreement adapter.</li>
|
||||||
|
* <li>Provide a runtime context that performs encapsulation and
|
||||||
|
* decapsulation.</li>
|
||||||
|
* <li>Define key specifications for key-pair generation and for importing X.509
|
||||||
|
* and PKCS#8 encodings.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Components</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Algorithm descriptor:</b> {@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"}.</li>
|
||||||
|
* <li><b>Runtime context:</b> {@link zeroecho.core.alg.cmce.CmceKemContext}
|
||||||
|
* holds state for encapsulation or decapsulation depending on which constructor
|
||||||
|
* is used.</li>
|
||||||
|
* <li><b>Key generation spec:</b> {@link zeroecho.core.alg.cmce.CmceKeyGenSpec}
|
||||||
|
* selects a CMCE parameter set (variant) used by the key-pair builder.</li>
|
||||||
|
* <li><b>Key import specs:</b> {@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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Provider requirements</h2>
|
||||||
|
* <p>
|
||||||
|
* The algorithm expects the Bouncy Castle PQC provider to be installed before
|
||||||
|
* use; the descriptor verifies this when generating or importing keys.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Algorithm descriptors are immutable and safe to share across
|
||||||
|
* threads.</li>
|
||||||
|
* <li>Runtime contexts are stateful and not thread-safe.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
package zeroecho.core.alg.chacha;
|
||||||
250
lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java
Normal file
250
lib/src/main/java/zeroecho/core/alg/cmce/CmceAlgorithm.java
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Classic McEliece (CMCE) algorithm adapter</h2>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Integrates the Bouncy Castle PQC CMCE primitives into the ZeroEcho SPI. This
|
||||||
|
* algorithm publishes:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li><b>KEM</b> capabilities:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code ENCAPSULATE} using a recipient {@link PublicKey}.</li>
|
||||||
|
* <li>{@code DECAPSULATE} using a {@link PrivateKey}.</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li><b>Agreement</b> capability implemented via a KEM-backed adapter:
|
||||||
|
* <ul>
|
||||||
|
* <li>Initiator: constructs an agreement context that encapsulates to the peer
|
||||||
|
* public key.</li>
|
||||||
|
* <li>Responder: constructs an agreement context that decapsulates with the
|
||||||
|
* local private key.</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li><b>Asymmetric key builders</b>:
|
||||||
|
* <ul>
|
||||||
|
* <li>Key generation from {@link CmceKeyGenSpec} variants.</li>
|
||||||
|
* <li>Public key import from X.509 bytes.</li>
|
||||||
|
* <li>Private key import from PKCS#8 bytes.</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* <b>Provider requirement:</b> the Bouncy Castle PQC provider must be
|
||||||
|
* registered under the standard name {@code "BCPQC"} before use.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Usage example:
|
||||||
|
* </p>
|
||||||
|
* <pre>{@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);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public final class CmceAlgorithm extends AbstractCryptoAlgorithm {
|
||||||
|
/**
|
||||||
|
* Constructs and registers CMCE capabilities and key builders.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This constructor registers:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>KEM roles for {@code ENCAPSULATE} and {@code DECAPSULATE}.</li>
|
||||||
|
* <li>Agreement role wired through a KEM-backed initiator/responder
|
||||||
|
* adapter.</li>
|
||||||
|
* <li>Asymmetric key builder for {@link CmceKeyGenSpec} (generation), X.509
|
||||||
|
* public key import, and PKCS#8 private key import.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
226
lib/src/main/java/zeroecho/core/alg/cmce/CmceKemContext.java
Normal file
226
lib/src/main/java/zeroecho/core/alg/cmce/CmceKemContext.java
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Classic McEliece (CMCE) KEM context</h2>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Holds the state required to perform CMCE key encapsulation or decapsulation.
|
||||||
|
* The operational mode is determined by the constructor used:
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>PublicKey constructor - encapsulate mode</li>
|
||||||
|
* <li>PrivateKey constructor - decapsulate mode</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Usage:
|
||||||
|
* </p>
|
||||||
|
* <pre>{@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);
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Notes:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Encapsulation requires a CMCE public key; decapsulation requires a CMCE
|
||||||
|
* private key.</li>
|
||||||
|
* <li>Returned arrays are owned by the caller; callers should clear secrets
|
||||||
|
* when no longer needed.</li>
|
||||||
|
* <li>This class holds no external resources and is safe to close
|
||||||
|
* repeatedly.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* In encapsulate mode this is a {@link java.security.PublicKey}; in decapsulate
|
||||||
|
* mode it is a {@link java.security.PrivateKey}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return key used by this context; never null
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Key key() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases resources held by this context.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This implementation holds no resources and performs no action. It is safe to
|
||||||
|
* call multiple times.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
243
lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java
Normal file
243
lib/src/main/java/zeroecho/core/alg/cmce/CmceKeyGenSpec.java
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>CMCE key generation specification</h2>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Usage example:
|
||||||
|
* </p>
|
||||||
|
* <pre>{@code
|
||||||
|
* // Generate a key pair for McEliece 8192128F (256-bit security, fast)
|
||||||
|
* CmceKeyGenSpec spec = CmceKeyGenSpec.mceliece8192128f();
|
||||||
|
* KeyPair kp = alg.asymmetricKeyBuilder(CmceKeyGenSpec.class).generateKeyPair(spec);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public final class CmceKeyGenSpec implements AlgorithmKeySpec, Describable {
|
||||||
|
/**
|
||||||
|
* Enumeration of supported CMCE parameter set variants.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Each value corresponds to a named parameter set from the Classic McEliece
|
||||||
|
* post-quantum KEM standardization.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The value is simply the {@link Variant#toString()} of the selected variant.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return string description of the variant
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String description() {
|
||||||
|
return variant.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
164
lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java
Normal file
164
lib/src/main/java/zeroecho/core/alg/cmce/CmcePrivateKeySpec.java
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Classic McEliece (CMCE) private key specification</h2>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Wraps a CMCE private key in PKCS#8 (DER) encoding. This spec is used to
|
||||||
|
* import or serialize private keys into the ZeroEcho SPI.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Instances are immutable. The internal byte array is cloned on construction
|
||||||
|
* and on every accessor to prevent accidental mutation.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Marshalling</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #marshal(CmcePrivateKeySpec)} produces a {@link PairSeq} with
|
||||||
|
* Base64-encoded PKCS#8.</li>
|
||||||
|
* <li>{@link #unmarshal(PairSeq)} reconstructs a spec from that format.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Example:
|
||||||
|
* </p>
|
||||||
|
* <pre>{@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);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The input is defensively copied.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The method scans for a key named {@code "pkcs8.b64"}, decodes its value from
|
||||||
|
* Base64, and reconstructs the spec.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The output is safe to log; it does not include key material.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return a string in the form {@code CmcePrivateKeySpec[len=N]}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "CmcePrivateKeySpec[len=" + pkcs8.length + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
164
lib/src/main/java/zeroecho/core/alg/cmce/CmcePublicKeySpec.java
Normal file
164
lib/src/main/java/zeroecho/core/alg/cmce/CmcePublicKeySpec.java
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Classic McEliece (CMCE) public key specification</h2>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Wraps a CMCE public key in X.509 (DER) encoding. This spec is used to import
|
||||||
|
* or serialize public keys into the ZeroEcho SPI.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Instances are immutable. The internal byte array is cloned on construction
|
||||||
|
* and on every accessor to prevent accidental mutation.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Marshalling</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #marshal(CmcePublicKeySpec)} produces a {@link PairSeq} with
|
||||||
|
* Base64-encoded X.509 data.</li>
|
||||||
|
* <li>{@link #unmarshal(PairSeq)} reconstructs a spec from that format.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Example:
|
||||||
|
* </p>
|
||||||
|
* <pre>{@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);
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The input is defensively copied.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The method scans for a key named {@code "x509.b64"}, decodes its value from
|
||||||
|
* Base64, and reconstructs the spec.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The output is safe to log; it does not include key material.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return a string in the form {@code CmcePublicKeySpec[len=N]}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "CmcePublicKeySpec[len=" + x509.length + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
102
lib/src/main/java/zeroecho/core/alg/cmce/package-info.java
Normal file
102
lib/src/main/java/zeroecho/core/alg/cmce/package-info.java
Normal file
@@ -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.
|
||||||
|
******************************************************************************/
|
||||||
|
/**
|
||||||
|
* <h2>Classic McEliece (CMCE)</h2>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Post-quantum KEM</h2>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Contents</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link zeroecho.core.alg.cmce.CmceAlgorithm} – algorithm adapter exposing
|
||||||
|
* CMCE as a KEM and agreement primitive.</li>
|
||||||
|
* <li>{@link zeroecho.core.alg.cmce.CmceKemContext} – runtime context for
|
||||||
|
* encapsulation and decapsulation.</li>
|
||||||
|
* <li>{@link zeroecho.core.alg.cmce.CmceKeyGenSpec} – enumeration of
|
||||||
|
* standardized CMCE parameter sets (variants).</li>
|
||||||
|
* <li>{@link zeroecho.core.alg.cmce.CmcePublicKeySpec} – wrapper for
|
||||||
|
* X.509-encoded public keys.</li>
|
||||||
|
* <li>{@link zeroecho.core.alg.cmce.CmcePrivateKeySpec} – wrapper for
|
||||||
|
* PKCS#8-encoded private keys.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Security properties</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Underlying assumption: hardness of decoding binary Goppa codes.</li>
|
||||||
|
* <li>Selected as a NIST post-quantum KEM standard (2022).</li>
|
||||||
|
* <li>Public keys are large (hundreds of kilobytes), but ciphertexts and
|
||||||
|
* secrets are compact.</li>
|
||||||
|
* <li>Considered quantum-resistant and secure against known attacks.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Usage</h2> <pre>{@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);
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
package zeroecho.core.alg.cmce;
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Generic JCA-based Key Agreement Context</h2>
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Lifecycle</h2>
|
||||||
|
* <ol>
|
||||||
|
* <li>Construct with local private key and algorithm name.</li>
|
||||||
|
* <li>Call {@link #setPeerPublic(PublicKey)} with the remote party’s key.</li>
|
||||||
|
* <li>Invoke {@link #deriveSecret()} to compute the raw shared secret.</li>
|
||||||
|
* <li>Optionally call {@link #close()} (no resources are held here).</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <h2>Notes</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>The derived secret is the raw key agreement output; protocols should
|
||||||
|
* apply a KDF before using it as a symmetric key.</li>
|
||||||
|
* <li>If {@code provider} is {@code null}, the default JCA provider lookup is
|
||||||
|
* used; otherwise, the specific provider is requested.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This must be called before {@link #deriveSecret()}, otherwise the context
|
||||||
|
* cannot complete the protocol.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Internally this delegates to the JCA {@link KeyAgreement} API with the given
|
||||||
|
* {@code jcaName} and optional provider.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
/* nothing to release */
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Adapter: using a KEM as a message-based agreement primitive</h2>
|
||||||
|
*
|
||||||
|
* {@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.
|
||||||
|
*
|
||||||
|
* <h2>Roles</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link Role#INITIATOR} - encapsulates to a peer’s public key, producing a
|
||||||
|
* ciphertext (peer message) and shared secret.</li>
|
||||||
|
* <li>{@link Role#RESPONDER} - receives a peer message (ciphertext),
|
||||||
|
* decapsulates with their private key, and derives the shared secret.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Lifecycle</h2>
|
||||||
|
* <ol>
|
||||||
|
* <li>Create via {@link Builder} with a bound {@link KemContext}.</li>
|
||||||
|
* <li>Initiator calls {@link #getPeerMessage()} to obtain ciphertext to
|
||||||
|
* transmit.</li>
|
||||||
|
* <li>Responder calls {@link #setPeerMessage(byte[])} with received
|
||||||
|
* ciphertext.</li>
|
||||||
|
* <li>Both parties call {@link #deriveSecret()} to obtain the agreed
|
||||||
|
* secret.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Unlike Diffie–Hellman, KEMs are already bound to the correct key at
|
||||||
|
* construction. This method exists for interface symmetry.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Scope and responsibilities</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Expose a generic agreement context that delegates to the JCA
|
||||||
|
* {@code KeyAgreement} API for algorithms such as ECDH and XDH.</li>
|
||||||
|
* <li>Adapt KEM contexts to a two-message agreement API suitable for initiator/
|
||||||
|
* responder protocols.</li>
|
||||||
|
* <li>Preserve clear separation between algorithm descriptors, runtime
|
||||||
|
* contexts, and higher-level composition utilities.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Components</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>GenericJcaAgreementContext</b>: 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.</li>
|
||||||
|
* <li><b>KemMessageAgreementAdapter</b>: 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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Lifecycle and usage notes</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Agreement contexts should be created with the correct local key and
|
||||||
|
* configured before deriving secrets; KDF application remains the caller's
|
||||||
|
* responsibility.</li>
|
||||||
|
* <li>KEM-based adapters encapsulate or decapsulate depending on role and
|
||||||
|
* memoize results for repeated reads within a single exchange.</li>
|
||||||
|
* <li>Instances are not thread-safe; synchronize externally if they are
|
||||||
|
* shared.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
package zeroecho.core.alg.common.agreement;
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Abstract EdDSA Key-Pair Builder</h2>
|
||||||
|
*
|
||||||
|
* Base class for key generation builders targeting Edwards-curve Digital
|
||||||
|
* Signature Algorithm (EdDSA) variants such as Ed25519 and Ed448.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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()}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Responsibilities</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Expose a template method {@link #jcaKeyPairAlg()} to return the canonical
|
||||||
|
* JCA algorithm identifier.</li>
|
||||||
|
* <li>Generate key pairs using JCA without requiring extra parameters.</li>
|
||||||
|
* <li>Intentionally reject public and private key imports, as those are
|
||||||
|
* delegated to the corresponding {@code *KeySpec} builders.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> Instances are stateless. Each call to
|
||||||
|
* {@link #generateKeyPair(AlgorithmKeySpec)} acquires a new
|
||||||
|
* {@link KeyPairGenerator}, so builders are safe for concurrent use.
|
||||||
|
*
|
||||||
|
* @param <S> the algorithm-specific {@link AlgorithmKeySpec} subtype
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractEdDSAKeyGenBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyBuilder<S> {
|
||||||
|
/**
|
||||||
|
* Returns the JCA algorithm name understood by {@link KeyPairGenerator}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Implementations must return the canonical algorithm string supported by the
|
||||||
|
* JDK, e.g.:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code "Ed25519"}</li>
|
||||||
|
* <li>{@code "Ed448"}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @return the JCA algorithm identifier string
|
||||||
|
*/
|
||||||
|
protected abstract String jcaKeyPairAlg(); // e.g., "Ed25519", "Ed448"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new EdDSA key pair using JCA defaults.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Importing encoded EdDSA public keys must be done through the corresponding
|
||||||
|
* {@code *PublicKeySpec} builder class.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Importing encoded EdDSA private keys must be done through the corresponding
|
||||||
|
* {@code *PrivateKeySpec} builder class.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Abstract EdDSA Encoded Private Key Builder</h2>
|
||||||
|
*
|
||||||
|
* Base class for reconstructing EdDSA private keys (e.g., Ed25519, Ed448) from
|
||||||
|
* PKCS#8-encoded specifications.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Unlike {@link AbstractEdDSAKeyGenBuilder}, which is responsible for
|
||||||
|
* generating fresh key pairs, this class focuses on <b>importing existing
|
||||||
|
* private keys</b> from their encoded representation. Public key import and key
|
||||||
|
* pair generation are deliberately unsupported here.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Responsibilities</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Define {@link #jcaKeyFactoryAlg()} to specify the canonical JCA algorithm
|
||||||
|
* name (e.g., {@code "Ed25519"} or {@code "Ed448"}).</li>
|
||||||
|
* <li>Define {@link #encodedPkcs8(AlgorithmKeySpec)} to extract the raw
|
||||||
|
* PKCS#8-encoded private key material from the given spec.</li>
|
||||||
|
* <li>Provide an {@link #importPrivate(AlgorithmKeySpec)} implementation that
|
||||||
|
* rebuilds a {@link PrivateKey} using {@link KeyFactory} and the PKCS#8-encoded
|
||||||
|
* material.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each import
|
||||||
|
* operation creates a new {@link KeyFactory} instance internally.
|
||||||
|
*
|
||||||
|
* @param <S> the algorithm-specific key specification carrying PKCS#8 data
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractEncodedPrivateKeyBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyBuilder<S> {
|
||||||
|
/**
|
||||||
|
* Returns the canonical JCA algorithm identifier used by
|
||||||
|
* {@link KeyFactory#getInstance(String)}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Must be one of the algorithm names recognized by the JDK (e.g.,
|
||||||
|
* {@code "Ed25519"}, {@code "Ed448"}).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Subclasses must implement this to pull the encoded material from their
|
||||||
|
* {@link AlgorithmKeySpec} representation.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method uses {@link KeyFactory} initialized with the algorithm returned
|
||||||
|
* by {@link #jcaKeyFactoryAlg()} to parse the bytes provided by
|
||||||
|
* {@link #encodedPkcs8(AlgorithmKeySpec)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h2>Abstract EdDSA Encoded Public Key Builder</h2>
|
||||||
|
*
|
||||||
|
* Base class for reconstructing EdDSA public keys (e.g., Ed25519, Ed448) from
|
||||||
|
* X.509-encoded specifications.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Unlike {@link AbstractEdDSAKeyGenBuilder}, which generates new key pairs, and
|
||||||
|
* {@link AbstractEncodedPrivateKeyBuilder}, which restores private keys, this
|
||||||
|
* class focuses exclusively on <b>importing existing public keys</b> from their
|
||||||
|
* encoded form. Key pair generation and private key import are intentionally
|
||||||
|
* unsupported here.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Responsibilities</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Define {@link #jcaKeyFactoryAlg()} to return the canonical JCA algorithm
|
||||||
|
* name (e.g., {@code "Ed25519"}, {@code "Ed448"}).</li>
|
||||||
|
* <li>Define {@link #encodedX509(AlgorithmKeySpec)} to extract the raw
|
||||||
|
* X.509-encoded public key bytes from the given spec.</li>
|
||||||
|
* <li>Provide an {@link #importPublic(AlgorithmKeySpec)} implementation that
|
||||||
|
* rebuilds a {@link PublicKey} using {@link KeyFactory} and the encoded X.509
|
||||||
|
* material.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to
|
||||||
|
* {@link #importPublic(AlgorithmKeySpec)} creates a new {@link KeyFactory}.
|
||||||
|
*
|
||||||
|
* @param <S> the algorithm-specific key specification carrying X.509 data
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractEncodedPublicKeyBuilder<S extends AlgorithmKeySpec> implements AsymmetricKeyBuilder<S> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the canonical JCA algorithm identifier used by
|
||||||
|
* {@link KeyFactory#getInstance(String)}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Must be one of the algorithm names recognized by the JDK (e.g.,
|
||||||
|
* {@code "Ed25519"}, {@code "Ed448"}).
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Subclasses must implement this to pull the encoded material from their
|
||||||
|
* {@link AlgorithmKeySpec} representation.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method uses {@link KeyFactory} initialized with the algorithm returned
|
||||||
|
* by {@link #jcaKeyFactoryAlg()} to parse the bytes provided by
|
||||||
|
* {@link #encodedX509(AlgorithmKeySpec)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Responsibilities</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Bind an EdDSA variant to a {@link CryptoAlgorithm} and a key
|
||||||
|
* ({@link PrivateKey} for signing, {@link PublicKey} for verifying).</li>
|
||||||
|
* <li>Delegate {@link SignatureContext} and {@link TagEngine} behavior to the
|
||||||
|
* {@link GenericJcaSignatureContext} instance.</li>
|
||||||
|
* <li>Enforce a fixed tag length appropriate for the algorithm (64 bytes for
|
||||||
|
* Ed25519, 114 bytes for Ed448).</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2>
|
||||||
|
* <p>
|
||||||
|
* Instances are stateful and not guaranteed to be thread-safe. Use one context
|
||||||
|
* per signing or verification pipeline.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public class CommonEdDSASignatureContext implements SignatureContext {
|
||||||
|
private final GenericJcaSignatureContext delegate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a signing context for the given EdDSA algorithm.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* After calling {@code close()}, further use of this context is undefined.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Passing {@code null} clears the expected tag.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<Signature> strategy) {
|
||||||
|
delegate.setVerificationApproach(strategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the core verification predicate used by this context.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The returned predicate typically delegates to
|
||||||
|
* {@link Signature#verify(byte[])}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @return the base verification predicate
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public VerificationBiPredicate<Signature> getVerificationCore() {
|
||||||
|
return delegate.getVerificationCore();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Scope and responsibilities</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Provide abstract builders for EdDSA key-pair generation and encoded key
|
||||||
|
* import.</li>
|
||||||
|
* <li>Offer an abstract signature context that binds an EdDSA variant to the
|
||||||
|
* generic JCA-based signature context, with fixed tag lengths.</li>
|
||||||
|
* <li>Keep provider-specific concerns encapsulated in small, composable
|
||||||
|
* building blocks.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Components</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Key generation:</b>
|
||||||
|
* {@link zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder} integrates
|
||||||
|
* with {@link java.security.KeyPairGenerator} to produce new Ed25519/Ed448 key
|
||||||
|
* pairs.</li>
|
||||||
|
* <li><b>Private key import:</b>
|
||||||
|
* {@link zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder}
|
||||||
|
* reconstructs private keys from PKCS#8 encodings via
|
||||||
|
* {@link java.security.KeyFactory}.</li>
|
||||||
|
* <li><b>Public key import:</b>
|
||||||
|
* {@link zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder}
|
||||||
|
* reconstructs public keys from X.509 encodings via
|
||||||
|
* {@link java.security.KeyFactory}.</li>
|
||||||
|
* <li><b>Signature contexts:</b>
|
||||||
|
* {@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.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Design notes</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Builders are stateless and safe for concurrent use; each import or
|
||||||
|
* generation creates a fresh JCA engine internally.</li>
|
||||||
|
* <li>Import methods intentionally throw for unsupported directions (e.g.,
|
||||||
|
* public import in the private builder) to keep responsibilities clear.</li>
|
||||||
|
* <li>Signature contexts are not thread-safe; they are expected to be used for
|
||||||
|
* a single signing or verification stream at a time.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
package zeroecho.core.alg.common.eddsa;
|
||||||
80
lib/src/main/java/zeroecho/core/alg/common/package-info.java
Normal file
80
lib/src/main/java/zeroecho/core/alg/common/package-info.java
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
/**
|
||||||
|
* Common algorithm infrastructure shared across multiple cryptographic
|
||||||
|
* families.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Scope and responsibilities</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Expose abstract base classes for asymmetric key generation and encoded
|
||||||
|
* key import so that concrete algorithms can implement only variant-specific
|
||||||
|
* details.</li>
|
||||||
|
* <li>Provide generic JCA adapters for key agreement and signature processing
|
||||||
|
* that integrate with the core streaming context model.</li>
|
||||||
|
* <li>Offer thin adapters to reinterpret existing primitives (for example, KEM
|
||||||
|
* as a two-message agreement) for higher-level composition layers.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Subpackages</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link zeroecho.core.alg.common.agreement} – generic JCA-based agreement
|
||||||
|
* contexts and KEM-to-agreement adapters.</li>
|
||||||
|
* <li>{@link zeroecho.core.alg.common.eddsa} – EdDSA infrastructure: abstract
|
||||||
|
* key builders, encoded key importers, and signature contexts for Ed25519 and
|
||||||
|
* Ed448.</li>
|
||||||
|
* <li>{@link zeroecho.core.alg.common.sig} – streaming JCA-backed signature
|
||||||
|
* contexts and internal stream helpers for sign/verify pipelines.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Design notes</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Abstract builders separate generation from import paths, and unsupported
|
||||||
|
* operations fail fast with clear exceptions.</li>
|
||||||
|
* <li>Contexts adapt JCA primitives into the core’s streaming model, handling
|
||||||
|
* resource lifecycles and fixed tag lengths where applicable.</li>
|
||||||
|
* <li>All components are designed to be composable and reusable across multiple
|
||||||
|
* algorithms, reducing duplication and ensuring consistent behavior.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
package zeroecho.core.alg.common;
|
||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* {@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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Modes and usage</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>SIGN</b>: consume body bytes; on EOF call
|
||||||
|
* {@link java.security.Signature#sign()} and append a trailer of
|
||||||
|
* {@link #tagLength()} bytes.</li>
|
||||||
|
* <li><b>VERIFY</b>: 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)}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Length resolvers</h2>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Examples</h2>
|
||||||
|
* <h3>Sign with RSA-PSS</h3> <pre>
|
||||||
|
* {@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
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <h3>Verify detached RSA signature</h3> <pre>
|
||||||
|
* {@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
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2>
|
||||||
|
* <p>
|
||||||
|
* Instances are stateful, single-use, and not thread-safe. Call
|
||||||
|
* {@link #wrap(InputStream)} at most once per instance.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Determined during construction by the provided resolver and constant for this
|
||||||
|
* context's lifetime.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private final int declaredTagLen;
|
||||||
|
private byte[] expectedTag;
|
||||||
|
private VerificationBiPredicate<Signature> verificationStrategy;
|
||||||
|
|
||||||
|
// lifecycle
|
||||||
|
private boolean wrapped; // = false;
|
||||||
|
private Stream activeStream;
|
||||||
|
private boolean autoCloseActiveStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory of initialized {@link java.security.Signature} engines.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Implementations must return a ready-to-use {@code Signature} configured for
|
||||||
|
* signing or verifying with the given key.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The resolver is evaluated before the signing engine is created and may probe
|
||||||
|
* a provider when the length is not fixed by specification.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Useful for algorithms/providers where the produced length is not trivially
|
||||||
|
* known from parameters.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The resolver is evaluated during construction and should return the fixed tag
|
||||||
|
* length for verification.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The returned factory performs {@link Signature#getInstance(String)}
|
||||||
|
* (optionally with a provider) and then calls
|
||||||
|
* {@link Signature#initSign(PrivateKey)} or
|
||||||
|
* {@link Signature#initVerify(PublicKey)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* {@code
|
||||||
|
* EngineFactory f = GenericJcaSignatureContext.jcaFactory("SHA256withRSA", null);
|
||||||
|
* Signature signer = f.create(privateKey, true);
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* First resolves the produced signature length via {@code lengthResolver}, then
|
||||||
|
* creates and initializes the signing engine via {@code engineFactory}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* First resolves the expected tag length via {@code verifyLengthResolver}, then
|
||||||
|
* creates and initializes the verifying engine via {@code engineFactory}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Keeping {@code close()} non-throwing simplifies pipeline cleanup.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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)}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Computed during construction by the configured resolver and constant for the
|
||||||
|
* lifetime of this context.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<Signature> strategy) {
|
||||||
|
verificationStrategy = strategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the core verification predicate for signature comparison.
|
||||||
|
*
|
||||||
|
* @return predicate that delegates to {@link Signature#verify(byte[])}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public VerificationBiPredicate<Signature> 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<Signature> verifier() {
|
||||||
|
return verificationStrategy == null ? getVerificationCore().getThrowOnMismatch() : verificationStrategy;
|
||||||
|
}
|
||||||
|
}
|
||||||
225
lib/src/main/java/zeroecho/core/alg/common/sig/Stream.java
Normal file
225
lib/src/main/java/zeroecho/core/alg/common/sig/Stream.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Sign mode</b>: compute the signature once via {@link Signature#sign()}
|
||||||
|
* and append it as a trailer by returning it from
|
||||||
|
* {@link #produceTrailer(byte[])}.</li>
|
||||||
|
* <li><b>Verify mode</b>: compute and compare the signature at completion using
|
||||||
|
* the provided {@link VerificationBiPredicate}, surfacing the result according
|
||||||
|
* to that strategy.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Lifecycle</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #update(byte[], int, int)} - feed each chunk into the engine.</li>
|
||||||
|
* <li>{@link #produceTrailer(byte[])} - sign mode only: emit the computed
|
||||||
|
* signature once; verify mode: return 0 (no trailer).</li>
|
||||||
|
* <li>{@link #onCompleted()} - verify mode only: invoke the verification
|
||||||
|
* strategy and translate any {@link VerificationException} to
|
||||||
|
* {@link java.io.IOException}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2>
|
||||||
|
* <p>
|
||||||
|
* Not thread-safe. Bound to a single {@link java.security.Signature} engine and
|
||||||
|
* upstream stream.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
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<Signature> 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<Signature> 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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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}.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
128
lib/src/main/java/zeroecho/core/alg/common/sig/package-info.java
Normal file
128
lib/src/main/java/zeroecho/core/alg/common/sig/package-info.java
Normal file
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Design goals</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Single-use pipeline integration:</b> create a context for SIGN or
|
||||||
|
* VERIFY and call {@code wrap(InputStream)} once.</li>
|
||||||
|
* <li><b>Known tag length up front:</b> produced/expected signature length is
|
||||||
|
* resolved at construction so downstream components can append or strip
|
||||||
|
* trailers without buffering the whole stream.</li>
|
||||||
|
* <li><b>Provider encapsulation:</b> JCA provider details are hidden behind
|
||||||
|
* factories that create initialized engines.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Components</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>GenericJcaSignatureContext</b> - 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.</li>
|
||||||
|
* <li><b>Stream</b> - internal passthrough input stream that feeds chunks to
|
||||||
|
* the signature engine, emits the trailer in SIGN mode, and performs final
|
||||||
|
* verification in VERIFY mode.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Length resolution</h2>
|
||||||
|
* <p>
|
||||||
|
* Tag length is determined by resolvers supplied at construction time:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>SignLengthResolver</b> - returns the produced length (fixed or by
|
||||||
|
* probing a provider with an empty-message sign).</li>
|
||||||
|
* <li><b>VerifyLengthResolver</b> - returns the expected length (fixed or
|
||||||
|
* derived from key parameters).</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Verification approach</h2>
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <h2>Usage sketch</h2>
|
||||||
|
* <h3>Sign</h3> <pre>
|
||||||
|
* {@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
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <h3>Verify</h3> <pre>
|
||||||
|
* {@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
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <h2>Thread-safety</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Contexts and streams are stateful, not thread-safe, and intended for
|
||||||
|
* single use.</li>
|
||||||
|
* <li>Create a new context per wrapped stream.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
package zeroecho.core.alg.common.sig;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user