Initial commit

This commit is contained in:
2025-07-30 21:40:09 +02:00
commit e3e6d8cb12
149 changed files with 21207 additions and 0 deletions

12
.gitattributes vendored Normal file
View 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

106
.gitignore vendored Normal file
View File

@@ -0,0 +1,106 @@
##---------------------------------------------------------------------------------------- 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
##---------------------------------------------------------------------------------------- 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

17
.project Normal file
View File

@@ -0,0 +1,17 @@
<?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>
</buildSpec>
<natures>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

321
.ruleset Normal file
View File

@@ -0,0 +1,321 @@
<?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="24" />
</properties>
</rule>
<rule ref="category/java/codestyle.xml/MDBAndSessionBeanNamingConvention"/>
<!-- rule ref="category/java/codestyle.xml/MethodArgumentCouldBeFinal"/ -->
<rule ref="category/java/codestyle.xml/MethodNamingConventions"/>
<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"/>
<rule ref="category/java/design.xml/CollapsibleIfStatements"/>
<rule ref="category/java/design.xml/CouplingBetweenObjects">
<properties>
<property name="threshold" value="40" />
</properties>
</rule>
<rule ref="category/java/design.xml/CyclomaticComplexity"/>
<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"/>
<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"/>
<rule ref="category/java/design.xml/TooManyMethods"/>
<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="50"/>
</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"/>
<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
View 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.

63
README.md Normal file
View File

@@ -0,0 +1,63 @@
# ZeroEcho
<img src=ZeroEcho-logo.png width=20% align="right" />
*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)**
- Modular structure allows easy integration of additional post-quantum providers
- Designed to protect against future quantum-based attacks
- Multi-Recipient Encryption (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
- AES Encryption Pipeline
- Configurable AES modes: **GCM**, **CBC**, or **CTR**
- Key sizes: **AES-128**, **AES-192**, **AES-256**
- Fluent Java builder API for fine-grained parameter control
- Offline / Air-Gapped Operation ("Cave Model")
- Generate encrypted payloads entirely offline
- Transfer via physical media (USB, SD card, etc.)
- Final upload or delivery can occur on separate, unconnected systems
- Deployment Script Generator
- Automatically builds upload scripts for encrypted payloads
- Supports staging to public endpoints (e.g., cloud drives, pastebins, file hosts)
- Useful for creating asynchronous or indirect delivery workflows
- Steganographic Embedding
- Optionally embed ciphertext into common media formats: images, audio, video
- Enables discreet transport of encrypted data over everyday channels
- Additional Authenticated Data (AAD)
- Supports AAD in AES-GCM for binding unencrypted metadata
- Ensures integrity of file context without exposing its contents
- CLI Tools & Keystore Management
**ZeroEcho is ideal for:**
- 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

46
ZeroEcho-logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 70 KiB

19
app/.classpath Normal file
View File

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

1
app/.gitignore vendored Normal file
View File

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

23
app/.project Normal file
View File

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

31
app/LICENSE Normal file
View File

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

52
app/build.gradle Normal file
View File

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

View File

@@ -0,0 +1,235 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.util.X509CertificationAuthority;
import zeroecho.util.X509CertificationAuthority.NewCertificate;
import zeroecho.util.X509Support;
/**
* A utility class for managing asymmetric cryptographic keys and X.509
* certificates via command-line interface.
* <p>
* This class supports operations such as issuing certificates (from a CSR file
* or by generating a new key pair), revoking existing certificates, and
* retrieving all certificates associated with a given username. It relies on
* command-line options for input parameters.
* </p>
*
* Supported operations:
* <ul>
* <li>Issue a certificate from a CSR file or generate a new key pair and
* certificate</li>
* <li>Revoke an existing certificate using its PEM file</li>
* <li>Retrieve all certificates issued to a specific username</li>
* </ul>
*
* Supported command-line options:
* <ul>
* <li><b>-d, --directory</b>: Directory to store Certificate Authority (CA)
* data (default: current directory)</li>
* <li><b>-o, --organization</b>: Organization name for CA (default:
* "ACME")</li>
* <li><b>-u, --username</b>: Username for certificate issuance, revocation, or
* retrieval</li>
* <li><b>-s, --subject</b>: Subject Distinguished Name (DN) for certificate
* issuance (e.g., "CN=John Doe")</li>
* <li><b>-i, --issue</b>: Issue a certificate. Optionally takes a CSR file as
* an argument. If omitted, a new key pair is generated.</li>
* <li><b>-r, --revoke</b>: Revoke a certificate using its PEM file</li>
* <li><b>-a, --getAll</b>: Retrieve all certificates associated with the given
* username</li>
* </ul>
*/
public final class AsymetricKeysManagement {
private static final Logger LOG = Logger.getLogger(AsymetricKeysManagement.class.getName());
/**
* Private constructor to prevent instantiation of this utility class.
* <p>
* All methods in this class are static and it is not intended to be
* instantiated.
* </p>
*/
private AsymetricKeysManagement() {
}
/**
* Main entry point for executing asymmetric key management operations based on
* provided command-line arguments.
* <p>
* This method parses the given arguments, validates required inputs, and
* performs the specified operation by interacting with the
* {@link X509CertificationAuthority} class.
* </p>
*
* @param args Command-line arguments passed to the application.
* @param options The set of {@link org.apache.commons.cli.Options} defining
* allowed command-line options.
* @return error-code
* @throws ParseException if command-line arguments are missing required
* options, are malformed, or mutually exclusive options
* conflict.
*
* @see X509CertificationAuthority
* @see org.apache.commons.cli.CommandLine
*/
public static int main(final String[] args, final Options options) throws ParseException { // NOPMD
final Option DIR_OPTION = Option.builder("d").longOpt("directory").hasArg().argName("dir")
.desc("Directory to store CA data (default: .)").build();
final Option ORG_OPTION = Option.builder("o").longOpt("organization").hasArg().argName("org")
.desc("Organization name (default: ACME)").build();
final Option USERNAME_OPTION = Option.builder("u").longOpt("username").hasArg().argName("username")
.desc("Username for issuance, revocation or filtering").build();
final Option SUBJECT_OPTION = Option.builder("s").longOpt("subject").hasArg().argName("subject")
.desc("Subject DN (e.g., CN=John Doe)").build();
final Option ISSUE_OPTION = Option.builder("i").longOpt("issue").optionalArg(true).argName("csrFile")
.desc("Issue a certificate from CSR PEM file or generate keypair if omitted").build();
final Option REVOKE_OPTION = Option.builder("r").longOpt("revoke").hasArg().argName("certFile")
.desc("Revoke a certificate from PEM file").build();
final Option GET_ALL_OPTION = Option.builder("a").longOpt("getAll")
.desc("Get all certificates for given username").build();
final OptionGroup OPERATIONS = new OptionGroup();
OPERATIONS.addOption(ISSUE_OPTION);
OPERATIONS.addOption(REVOKE_OPTION);
OPERATIONS.addOption(GET_ALL_OPTION);
OPERATIONS.setRequired(true); // At least one required
options.addOption(DIR_OPTION);
options.addOption(ORG_OPTION);
options.addOption(USERNAME_OPTION);
options.addOption(SUBJECT_OPTION);
options.addOptionGroup(OPERATIONS);
final CommandLine cmd;
try {
final CommandLineParser parser = new DefaultParser();
cmd = parser.parse(options, args);
final String directory = cmd.getOptionValue(DIR_OPTION, ".");
final String organization = cmd.getOptionValue(ORG_OPTION, "ACME");
final String username = cmd.getOptionValue(USERNAME_OPTION);
final String subject = cmd.getOptionValue(SUBJECT_OPTION);
final X509CertificationAuthority ca = new X509CertificationAuthority(directory, organization);
ca.initialize();
if (cmd.hasOption(ISSUE_OPTION.getOpt())) {
if (username == null) {
throw new ParseException("--username is required when issuing a certificate.");
}
final String csrFile = cmd.getOptionValue(ISSUE_OPTION.getOpt());
if (csrFile != null) {
final Certificate cert = ca.issueFromCSRFile(username, csrFile);
if (cert != null) {
X509Support.printCertificate(cert);
} else {
LOG.log(Level.SEVERE, "Certificate issuance failed.");
}
} else {
if (subject == null) {
throw new ParseException("--subject is required if no CSR file is provided.");
}
final NewCertificate cert = ca.issue(username, subject);
if (cert != null) {
X509Support.printCertificate(cert.cert());
X509Support.printPrivateKey(cert.key().getPrivate());
} else {
LOG.log(Level.SEVERE, "Certificate issuance failed.");
}
}
} else if (cmd.hasOption(REVOKE_OPTION.getOpt())) {
if (username == null) {
throw new ParseException("--username is required when revoking a certificate.");
}
final String certFile = cmd.getOptionValue(REVOKE_OPTION.getOpt());
final Certificate cert = X509Support.loadCertificate(certFile);
if (cert == null) {
LOG.log(Level.SEVERE, "Failed to load certificate from file: {0}", certFile);
return 1;
}
final boolean revoked = ca.revoke(username, cert);
if (revoked) {
System.out.println("Certificate revoked successfully:");
X509Support.printCertificate(cert);
} else {
LOG.log(Level.SEVERE, "Certificate revocation failed or certificate not found.");
}
} else if (cmd.hasOption(GET_ALL_OPTION.getOpt())) {
if (username == null) {
throw new ParseException("--username is required when issuing a certificate.");
}
final Certificate[] certs = ca.getAll(username);
if (certs.length == 0) {
System.out.println("No certificates found for username: " + username);
} else {
for (final Certificate cert : certs) {
X509Support.printCertificate(cert);
System.out.println("-----");
}
}
}
} catch (NumberFormatException | IOException | GeneralSecurityException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Cannot proceed: " + e.toString());
}
return 1;
}
return 0;
}
}

View File

@@ -0,0 +1,274 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.HexFormat;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.builder.AesBuilder;
import zeroecho.builder.DataContentChainBuilder;
import zeroecho.builder.KEMAesParametersBuilder;
import zeroecho.builder.PlainFileBuilder;
import zeroecho.data.DataContent;
import zeroecho.util.UniversalKeyStoreFile;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
/**
* Utility class providing command-line support for AES encryption and
* decryption using Key Encapsulation Mechanism (KEM) and authenticated
* encryption modes like GCM.
*
* <p>
* Supports AES modes of varying key lengths (128/192/256) and cipher types such
* as CBC, GCM, or CTR. It relies on keys provided in a ZeroEcho-compatible
* keystore and supports both public and private key operations based on the
* selected mode.
* </p>
*
* <p>
* This class is non-instantiable and should only be invoked through its static
* {@code main} method.
* </p>
*/
public final class KEMAes {
private static final Logger LOG = Logger.getLogger(KEMAes.class.getName());
private KEMAes() {
// Utility class; prevent instantiation.
}
/**
* Entry point for command-line execution.
*
* @param args command-line arguments
* @param options CLI options container (used for external extensions/tests)
* @return exit code: 0 for success, 1 for failure
* @throws ParseException if CLI parsing fails or required options are missing
*/
public static int main(final String[] args, final Options options) throws ParseException { // NOPMD
// Define and register CLI options
final Option ENCRYPT_OPTION = Option.builder("e").longOpt("encrypt").hasArg().argName("inputFile")
.desc("Encrypt the given file").build();
final Option DECRYPT_OPTION = Option.builder("d").longOpt("decrypt").hasArg().argName("inputFile")
.desc("Decrypt the given file").build();
final Option OUTPUT_OPTION = Option.builder("o").longOpt("output").hasArg().argName("outputFile")
.desc("Output file path (default: inputFile + .enc or .dec)").build();
final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("keystoreFile")
.desc("File with keys (ZeroEcho's *.keystore)").required().build();
final Option PUB_OPTION = Option.builder("p").longOpt("pubkeys").hasArg().argName("usernames")
.desc("Recipient's username list (in keystore)").build();
final Option PRIV_OPTION = Option.builder("s").longOpt("privkey").hasArg().argName("username")
.desc("Username with private key for decryption (in keystore)").build();
final Option MODE_OPTION = Option.builder("m").longOpt("mode").hasArg().argName("aesMode")
.desc("AES mode: AES-128, AES-192, AES-256 (default: " + AesMode.AES_256 + ")").build();
final Option CIPHER_TYPE_OPTION = Option.builder("c").longOpt("cipher").hasArg().argName("cipherType")
.desc("Cipher type: AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding (default: "
+ AesCipherType.GCM + ")")
.build();
final Option AAD_OPTION = Option.builder("a").longOpt("aad").hasArg().argName("hex")
.desc("Optional AAD (hex-encoded) for authenticated AES modes like GCM").build();
final OptionGroup OPERATION_GROUP = new OptionGroup();
OPERATION_GROUP.addOption(ENCRYPT_OPTION);
OPERATION_GROUP.addOption(DECRYPT_OPTION);
OPERATION_GROUP.setRequired(true);
options.addOptionGroup(OPERATION_GROUP);
options.addOption(OUTPUT_OPTION);
options.addOption(KEYSTORE_OPTION);
options.addOption(PUB_OPTION);
options.addOption(PRIV_OPTION);
options.addOption(MODE_OPTION);
options.addOption(CIPHER_TYPE_OPTION);
options.addOption(AAD_OPTION);
try {
final CommandLineParser parser = new DefaultParser();
final CommandLine cmd = parser.parse(options, args);
final boolean isEncryption = cmd.hasOption(ENCRYPT_OPTION.getOpt());
final boolean isDecryption = cmd.hasOption(DECRYPT_OPTION.getOpt());
final String inputPath = cmd.getOptionValue(ENCRYPT_OPTION.getOpt(),
cmd.getOptionValue(DECRYPT_OPTION.getOpt()));
final String outputPath = cmd.getOptionValue(OUTPUT_OPTION, inputPath + (isEncryption ? ".enc" : ".dec"));
final String keystorePath = cmd.getOptionValue(KEYSTORE_OPTION);
final AesMode mode = AesMode.fromString(cmd.getOptionValue(MODE_OPTION, AesMode.AES_256.toString()));
final AesCipherType cipher = AesCipherType
.fromString(cmd.getOptionValue(CIPHER_TYPE_OPTION, AesCipherType.GCM.toString()));
final byte[] aad = cmd.hasOption(AAD_OPTION.getOpt())
? HexFormat.of().parseHex(cmd.getOptionValue(AAD_OPTION))
: new byte[0];
final UniversalKeyStoreFile db = new UniversalKeyStoreFile(keystorePath);
if (isEncryption) {
final String pub = cmd.getOptionValue(PUB_OPTION);
if (pub == null) {
throw new ParseException("Option is required for encryption: " + PUB_OPTION.getOpt() + " ("
+ PUB_OPTION.getLongOpt() + ")");
}
final PublicKey recipient = getPublicKey(db, pub);
performEncryption(recipient, mode, cipher, aad, inputPath, outputPath);
} else if (isDecryption) {
final String priv = cmd.getOptionValue(PRIV_OPTION);
if (priv == null) {
throw new ParseException("Option is required for decryption: " + PRIV_OPTION.getOpt() + " ("
+ PRIV_OPTION.getLongOpt() + ")");
}
performDecryption(db.loadPrivateKey(priv), mode, cipher, aad, inputPath, outputPath);
}
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Error during encryption/decryption: " + e.toString());
}
return 1;
}
return 0;
}
/**
* Encrypts a file using the specified public key and AES configuration.
*
* @param recipient recipient's public key
* @param mode AES key mode (128/192/256 bits)
* @param cipher AES cipher type (GCM, CBC, etc.)
* @param aad optional Additional Authenticated Data (may be empty)
* @param inputPath path to the input file
* @param outputPath path where the encrypted file will be written
* @throws IOException if reading or writing files fails
*/
private static void performEncryption(final PublicKey recipient, final AesMode mode, final AesCipherType cipher,
final byte[] aad, final String inputPath, final String outputPath) throws IOException {
DataContent encrypted = DataContentChainBuilder.encrypt()
// input file
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL()))
// AES process
.add(AesBuilder.builder())
// configured by KEM
.add(KEMAesParametersBuilder.builder().withKey(recipient).withCipherType(cipher).withAesMode(mode)
.withAAD(aad))
// build!
.build();
try (InputStream encryptedStream = encrypted.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
encryptedStream.transferTo(fileOut);
System.err.println("Encryption complete. Output saved to: " + outputPath);
}
}
/**
* Decrypts a file using the specified private key and AES configuration.
*
* @param recipient recipient's private key
* @param mode AES key mode
* @param cipher AES cipher type
* @param aad optional Additional Authenticated Data (may be empty)
* @param inputPath path to the encrypted file
* @param outputPath path where the decrypted file will be written
* @throws IOException if reading or writing files fails
*/
private static void performDecryption(final PrivateKey recipient, final AesMode mode, final AesCipherType cipher,
final byte[] aad, final String inputPath, final String outputPath) throws IOException {
DataContent decrypted = DataContentChainBuilder.decrypt()
// input file
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL()))
// encrypted by KEM
.add(KEMAesParametersBuilder.builder().withKey(recipient).withCipherType(cipher).withAesMode(mode)
.withAAD(aad))
// AES process
.add(AesBuilder.builder())
// build!
.build();
try (InputStream decryptedStream = decrypted.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
decryptedStream.transferTo(fileOut);
System.err.println("Decryption complete. Output saved to: " + outputPath);
}
}
/**
* Retrieves a public key for the given username from the keystore.
*
* @param db the loaded keystore
* @param username username to fetch the public key for
* @return the public key
* @throws NoSuchElementException if user not found or key construction fails
*/
private static PublicKey getPublicKey(final UniversalKeyStoreFile db, final String username) {
try {
return db.loadPublicKey(username);
} catch (NoSuchElementException e) {
LOG.logp(Level.FINE, "KEMAes", "getPublicKey", "Exception", e);
throw new NoSuchElementException("unknown user <" + username + ">", e);
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException | IOException e) {
LOG.logp(Level.FINE, "KEMAes", "getPublicKey", "Exception", e);
throw new NoSuchElementException("cannot construct public key for <" + username + ">", e);
}
}
}

View File

@@ -0,0 +1,186 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.UniversalKeyStoreFile;
import zeroecho.util.X509Support;
/**
* Utility class for managing a simple file-based keystore via command-line
* interface (CLI).
* <p>
* This class provides a static {@code main} method that enables two primary
* operations:
* </p>
* <ul>
* <li>Importing a public key from a PEM-encoded X.509 certificate</li>
* <li>Generating and storing a key pair (public/private) for a specified
* owner</li>
* </ul>
* <p>
* The keystore is backed by a flat file managed by
* {@link UniversalKeyStoreFile}, and keys are stored using symbolic owner
* identifiers.
* </p>
*
* <p>
* The following command-line options are supported:
* </p>
* <ul>
* <li>{@code -k, --keystore <file>}: Path to the keystore file (required)</li>
* <li>{@code -o, --owner <name>}: Owner name used as the key identifier
* (required)</li>
* <li>{@code -c, --cert <pem-file>}: Path to a PEM-encoded X.509 certificate
* file</li>
* <li>{@code -g, --generate}: Flag indicating that a key pair should be
* generated</li>
* <li>{@code -a, --algorithm <type>}: Algorithm and key size (e.g.,
* {@code RSA:2048}, {@code EC:256}). Defaults to {@code RSA:2048}</li>
* </ul>
* <p>
* At least one of {@code --cert} or {@code --generate} must be specified.
* </p>
*
* <p>
* This class is not meant to be instantiated.
* </p>
*
* @author Leo Galambos
*/
public final class KeyStoreManagement {
private static final Logger LOG = Logger.getLogger(KeyStoreManagement.class.getName());
/**
* Private constructor to prevent instantiation of this utility class.
*/
private KeyStoreManagement() {
// Utility class; prevent instantiation.
}
/**
* Main entry point for executing keystore operations via CLI.
*
* @param args the command-line arguments to parse
* @param options the Apache Commons CLI {@link Options} object to populate with
* supported options
* @return exit code: {@code 0} on success, {@code 2} on I/O error or processing
* failure
* @throws ParseException if argument parsing fails
* @throws NoSuchAlgorithmException if the specified key generation
* algorithm is unavailable
* @throws NoSuchProviderException if the Bouncy Castle provider is
* not registered
* @throws InvalidAlgorithmParameterException if the specified key size of
* EC-algorithms is invalid
*/
public static int main(String[] args, final Options options) throws ParseException, NoSuchAlgorithmException,
NoSuchProviderException, InvalidAlgorithmParameterException {
final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("file")
.desc("Path to keystore file").required().build();
final Option OWNER_OPTION = Option.builder("o").longOpt("owner").hasArg().argName("name")
.desc("Owner name for key").required().build();
final Option CERT_OPTION = Option.builder("c").longOpt("cert").hasArg().argName("pem-file")
.desc("PEM certificate file to extract public key").build();
final Option GENERATE_OPTION = Option.builder("g").longOpt("generate").desc("Generate key pair for the owner")
.build();
final Option ALGORITHM_OPTION = Option.builder("a").longOpt("algorithm").hasArg().argName("type")
.desc("Key pair algorithm (e.g., RSA:4096, EC:256). Default is RSA:2048.").build();
options.addOption(KEYSTORE_OPTION);
options.addOption(OWNER_OPTION);
options.addOption(CERT_OPTION);
options.addOption(GENERATE_OPTION);
options.addOption(ALGORITHM_OPTION);
final CommandLineParser parser = new DefaultParser();
try {
final CommandLine cmd = parser.parse(options, args);
final String keystorePath = cmd.getOptionValue(KEYSTORE_OPTION);
final String owner = cmd.getOptionValue(OWNER_OPTION);
final String certPath = cmd.getOptionValue(CERT_OPTION);
final boolean generate = cmd.hasOption(GENERATE_OPTION);
if (certPath == null && !generate) {
throw new ParseException("You must specify either --cert or --generate");
}
final UniversalKeyStoreFile keystore = new UniversalKeyStoreFile(Paths.get(keystorePath));
if (certPath != null) {
final Certificate cert = X509Support.loadCertificate(certPath);
keystore.addPubKey(owner, cert.getPublicKey());
LOG.log(Level.INFO, "Public key added for owner: {0}", owner);
}
if (generate) {
final String algorithmString = cmd.getOptionValue(ALGORITHM_OPTION, "RSA:2048");
final KeyPair keyPair = KeyPairAlgorithm.fromString(algorithmString).generateKeyPair();
keystore.addPubKey(owner, keyPair.getPublic());
keystore.addPrivKey(owner, keyPair.getPrivate());
LOG.log(Level.INFO, "Key pair generated and added for owner: {0}", owner);
}
} catch (final IOException e) {
LOG.log(Level.SEVERE, "Unexpected error", e);
return 2;
}
return 0;
}
}

View File

@@ -0,0 +1,361 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Collection;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.builder.AesBuilder;
import zeroecho.builder.AesRandomBuilder;
import zeroecho.builder.DataContentChainBuilder;
import zeroecho.builder.MultiRecipientCryptorBuilder;
import zeroecho.builder.PlainFileBuilder;
import zeroecho.data.DataContent;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.UniversalKeyStoreFile;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
/**
* Utility class for multi-recipient AES encryption and decryption of files.
* <p>
* This class provides a command-line interface (CLI) entry point to encrypt or
* decrypt files using AES with multiple recipients public keys or a single
* private key from a keystore.
* </p>
* <p>
* Supported command-line options:
* </p>
* <ul>
* <li><b>-e / --encrypt &lt;inputFile&gt;</b>: File to encrypt.</li>
* <li><b>-d / --decrypt &lt;inputFile&gt;</b>: File to decrypt.</li>
* <li><b>-k / --keystore &lt;keystoreFile&gt;</b>: Path to keystore file
* (required).</li>
* <li><b>-p / --pubkeys &lt;usernames&gt;</b>: List of recipient usernames for
* encryption.</li>
* <li><b>-s / --privkey &lt;username&gt;</b>: Username with private key for
* decryption.</li>
* <li><b>-o / --output &lt;outputFile&gt;</b>: Output file path (optional;
* defaults to inputFile.enc or inputFile.dec).</li>
* <li><b>-m / --mode &lt;aesMode&gt;</b>: AES mode (AES-128, AES-192, AES-256).
* Default is AES-256.</li>
* <li><b>-c / --cipher &lt;cipherType&gt;</b>: Cipher transformation
* (AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding). Default is
* AES/CBC/PKCS7Padding.</li>
* <li><b>-x / --decoys &lt;algorithms&gt;</b>: List of decoy key algorithms to
* include (e.g., RSA:2048, EC:256).</li>
* </ul>
* <p>
* This is a utility class and cannot be instantiated.
* </p>
*
* @author Leo Galambos
*/
public final class MultiRecipientAes {
private static final Logger LOG = Logger.getLogger(MultiRecipientAes.class.getName());
private MultiRecipientAes() {
// Utility class; prevent instantiation.
}
/**
* Parses command-line arguments and executes either encryption or decryption
* accordingly.
* <p>
* Encryption requires:
* <ul>
* <li>Option {@code -e} with input file path to encrypt.</li>
* <li>Option {@code -p} with one or more recipient usernames whose public keys
* are used.</li>
* <li>Option {@code -k} with the keystore file containing recipient keys.</li>
* </ul>
* Decryption requires:
* <ul>
* <li>Option {@code -d} with input file path to decrypt.</li>
* <li>Option {@code -s} with the username whose private key will decrypt the
* file.</li>
* <li>Option {@code -k} with the keystore file containing the private key.</li>
* </ul>
* <p>
* Optional parameters include the output file path, AES mode, and cipher type.
* </p>
*
* @param args Command-line arguments specifying operation mode and
* parameters.
* @param options Pre-configured Apache Commons CLI {@link Options} object to
* which this method will add supported options.
* @return Exit code {@code 0} if operation was successful; {@code 1} if an
* error occurred.
* @throws ParseException If parsing the command-line arguments fails due to
* invalid or missing options.
*/
public static int main(final String[] args, final Options options) throws ParseException { // NOPMD
// Define and register CLI options
final Option ENCRYPT_OPTION = Option.builder("e").longOpt("encrypt").hasArg().argName("inputFile")
.desc("Encrypt the given file").build();
final Option DECRYPT_OPTION = Option.builder("d").longOpt("decrypt").hasArg().argName("inputFile")
.desc("Decrypt the given file").build();
final Option OUTPUT_OPTION = Option.builder("o").longOpt("output").hasArg().argName("outputFile")
.desc("Output file path (default: inputFile + .enc or .dec)").build();
final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("keystoreFile")
.desc("File with keys (ZeroEcho's *.keystore)").required().build();
final Option PUB_OPTION = Option.builder("p").longOpt("pubkeys").hasArgs().argName("list of usernames")
.desc("Recipients' usernames list (in keystore)").build();
final Option PRIV_OPTION = Option.builder("s").longOpt("privkey").hasArgs().argName("username")
.desc("Username with private key for decryption (in keystore)").build();
final Option MODE_OPTION = Option.builder("m").longOpt("mode").hasArg().argName("aesMode")
.desc("AES mode: AES-128, AES-192, AES-256 (default: AES-256)").build();
final Option CIPHER_TYPE_OPTION = Option.builder("c").longOpt("cipher").hasArg().argName("cipherType").desc(
"Cipher type: AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding (default: AES/CBC/PKCS7Padding)")
.build();
final Option DECOY_OPTION = Option.builder("x").longOpt("decoys").hasArgs().argName("keyAlgorithms")
.desc("List of decoy key algorithms (e.g., RSA:2048, EC:256)").build();
final OptionGroup OPERATION_GROUP = new OptionGroup();
OPERATION_GROUP.addOption(ENCRYPT_OPTION);
OPERATION_GROUP.addOption(DECRYPT_OPTION);
OPERATION_GROUP.setRequired(true);
options.addOptionGroup(OPERATION_GROUP);
options.addOption(OUTPUT_OPTION);
options.addOption(KEYSTORE_OPTION);
options.addOption(PUB_OPTION);
options.addOption(PRIV_OPTION);
options.addOption(MODE_OPTION);
options.addOption(CIPHER_TYPE_OPTION);
options.addOption(DECOY_OPTION);
try {
final CommandLineParser parser = new DefaultParser();
final CommandLine cmd = parser.parse(options, args);
final boolean isEncryption = cmd.hasOption(ENCRYPT_OPTION.getOpt());
final boolean isDecryption = cmd.hasOption(DECRYPT_OPTION.getOpt());
final String inputPath = cmd.getOptionValue(ENCRYPT_OPTION.getOpt(),
cmd.getOptionValue(DECRYPT_OPTION.getOpt()));
final String outputPath = cmd.getOptionValue(OUTPUT_OPTION, inputPath + (isEncryption ? ".enc" : ".dec"));
final String keystorePath = cmd.getOptionValue(KEYSTORE_OPTION);
final AesMode mode = AesMode.fromString(cmd.getOptionValue(MODE_OPTION, "AES-256"));
final AesCipherType cipher = AesCipherType
.fromString(cmd.getOptionValue(CIPHER_TYPE_OPTION, "AES/CBC/PKCS7Padding"));
final UniversalKeyStoreFile db = new UniversalKeyStoreFile(keystorePath);
if (isEncryption) {
final String[] pub = cmd.getOptionValues(PUB_OPTION);
if (pub == null) {
throw new ParseException("Option is required for encryption: " + PUB_OPTION.getOpt() + " ("
+ PUB_OPTION.getLongOpt() + ")");
}
final Collection<PublicKey> recipient = getPublicKeys(db, pub);
final Collection<PublicKey> decoy = cmd.hasOption(DECOY_OPTION)
? getDecoyKeys(cmd.getOptionValues(DECOY_OPTION))
: new HashSet<>();
performEncryption(recipient, decoy, mode, cipher, inputPath, outputPath);
} else if (isDecryption) {
final String priv = cmd.getOptionValue(PRIV_OPTION);
if (priv == null) {
throw new ParseException("Option is required for decryption: " + PRIV_OPTION.getOpt() + " ("
+ PRIV_OPTION.getLongOpt() + ")");
}
performDecryption(db.loadPrivateKey(priv), mode, cipher, inputPath, outputPath);
}
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Error during encryption/decryption: " + e.toString());
}
return 1;
}
return 0;
}
/**
* Encrypts the input file for multiple recipients using AES and writes the
* encrypted data to output file.
*
* @param recipient Collection of recipients' public keys to encrypt the AES
* key.
* @param decoy Collection of recipients' public keys (decoys) to encrypt
* the false AES key.
* @param mode AES mode (e.g., AES-128, AES-256).
* @param cipher Cipher type (e.g., AES/CBC/PKCS7Padding).
* @param inputPath Path to the plaintext input file.
* @param outputPath Path where the encrypted output file will be saved.
* @throws IOException If file reading or writing fails.
*/
private static void performEncryption(final Collection<PublicKey> recipient, final Collection<PublicKey> decoy,
final AesMode mode, final AesCipherType cipher, final String inputPath, final String outputPath)
throws IOException {
DataContent encrypted = DataContentChainBuilder.encrypt()
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL())).add(AesBuilder.builder())
.add(MultiRecipientCryptorBuilder.builder().addRecipients(recipient).addDecoys(decoy))
.add(AesRandomBuilder.builder().mode(mode).cipherType(cipher)).build();
try (InputStream encryptedStream = encrypted.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
encryptedStream.transferTo(fileOut);
System.err.println("Encryption complete. Output saved to: " + outputPath);
}
}
/**
* Decrypts the input file using the specified recipient's private key and
* writes the plaintext to output file.
*
* @param recipient Private key of the recipient used to unwrap AES key and
* decrypt the file.
* @param mode AES mode (e.g., AES-128, AES-256).
* @param cipher Cipher type (e.g., AES/CBC/PKCS7Padding).
* @param inputPath Path to the encrypted input file.
* @param outputPath Path where the decrypted output file will be saved.
* @throws IOException If file reading or writing fails.
*/
private static void performDecryption(final PrivateKey recipient, final AesMode mode, final AesCipherType cipher,
final String inputPath, final String outputPath) throws IOException {
DataContent decrypted = DataContentChainBuilder.decrypt()
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL()))
.add(AesRandomBuilder.builder().mode(mode).cipherType(cipher))
.add(MultiRecipientCryptorBuilder.builder().privateKey(recipient)).add(AesBuilder.builder()).build();
try (InputStream decryptedStream = decrypted.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
decryptedStream.transferTo(fileOut);
System.err.println("Decryption complete. Output saved to: " + outputPath);
}
}
/**
* Retrieves a collection of public keys for the specified usernames from the
* keystore.
*
* @param db The {@link UniversalKeyStoreFile} instance representing the
* keystore.
* @param usernames The list of usernames whose public keys should be fetched.
* @return Collection of {@link PublicKey} instances corresponding to the given
* usernames.
* @throws NoSuchElementException If any username is not found or their public
* key cannot be constructed.
*/
private static Collection<PublicKey> getPublicKeys(final UniversalKeyStoreFile db, final String... usernames) {
final Collection<PublicKey> set = new HashSet<>();
for (String user : usernames) {
try {
set.add(db.loadPublicKey(user));
} catch (NoSuchElementException e) {
LOG.logp(Level.FINE, "MultiRecipientAes", "getPublicKeys", "Exception", e);
throw new NoSuchElementException("unknown user <" + user + ">", e);
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException | IOException e) {
LOG.logp(Level.FINE, "MultiRecipientAes", "getPublicKeys", "Exception", e);
throw new NoSuchElementException("cannot construct public key for <" + user + ">", e);
}
}
return set;
}
/**
* Generates a collection of public keys based on the specified decoy key
* algorithms.
* <p>
* Each algorithm string should correspond to a recognized
* {@link KeyPairAlgorithm} format, such as "RSA:2048" or "EC:256". For each
* algorithm string provided, this method attempts to generate a new key pair
* and collect the public key.
* </p>
* <p>
* These keys can be used as decoys in multi-recipient encryption schemes to
* enhance security by adding plausible but non-functional recipients.
* </p>
*
* @param decoyAlgorithms an array of strings specifying the key pair algorithms
* for decoy key generation (e.g., "RSA:2048", "EC:256")
* @return a {@link Collection} of generated {@link PublicKey} instances
* corresponding to the decoy algorithms provided
* @throws NoSuchElementException if any of the specified algorithms is invalid
* or if key generation fails for any algorithm,
* wrapping the underlying cause
*/
private static Collection<PublicKey> getDecoyKeys(final String... decoyAlgorithms) {
final Collection<PublicKey> set = new HashSet<>();
for (String algStr : decoyAlgorithms) {
try {
final KeyPairAlgorithm alg = KeyPairAlgorithm.fromString(algStr);
final KeyPair pair = alg.generateKeyPair();
set.add(pair.getPublic());
} catch (IllegalArgumentException | NoSuchAlgorithmException | NoSuchProviderException
| InvalidAlgorithmParameterException e) {
LOG.log(Level.FINE, "Failed to generate decoy key for algorithm: {0}", algStr);
throw new NoSuchElementException("cannot construct key pair for <" + algStr + ">", e);
}
}
return set;
}
}

View File

@@ -0,0 +1,294 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho;
import java.io.Console;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.data.DataContent;
import zeroecho.data.processing.PasswordBasedAesDecryptor;
import zeroecho.data.processing.PasswordBasedAesEncryptor;
import zeroecho.data.processing.PlainFile;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
/**
* Utility class providing AES encryption and decryption based on a password
* using PBKDF2 key derivation. This class exposes a command-line interface
* (CLI) entry point for encrypting and decrypting files with configurable
* options.
*
* <p>
* The supported command-line parameters are as follows:
* <ul>
* <li><b>-e / --encrypt &lt;inputFile&gt;</b>: Specifies the file to
* encrypt.</li>
* <li><b>-d / --decrypt &lt;inputFile&gt;</b>: Specifies the file to
* decrypt.</li>
* <li><b>-p / --password &lt;password&gt;</b>: Password for
* encryption/decryption.</li>
* <li><b>-o / --output &lt;outputFile&gt;</b>: Output file path (optional,
* defaults to input file with .enc or .dec extension).</li>
* <li><b>-m / --mode &lt;aesMode&gt;</b>: AES key size mode (AES-128, AES-192,
* AES-256). Defaults to AES-256.</li>
* <li><b>-c / --cipher &lt;cipherType&gt;</b>: Cipher transformation
* (AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding). Defaults to
* AES/CBC/PKCS7Padding.</li>
* <li><b>-n / --iterations &lt;iterations&gt;</b>: PBKDF2 iteration count.
* Defaults to 100,000.</li>
* </ul>
*
* <p>
* This class cannot be instantiated.
*/
public final class PasswordBasedAes {
private static final Logger LOG = Logger.getLogger(PasswordBasedAes.class.getName());
/**
* Private constructor to prevent instantiation.
*/
private PasswordBasedAes() {
// Utility class; prevent instantiation.
}
/**
* Main method that parses command-line arguments and performs encryption or
* decryption.
*
* @param args Command-line arguments.
* @param options Predefined CLI options to configure.
* @return Exit code: 0 if successful, 1 if an error occurred.
* @throws ParseException If argument parsing fails.
*/
public static int main(final String[] args, final Options options) throws ParseException {
// Define and register CLI options
final Option ENCRYPT_OPTION = Option.builder("e").longOpt("encrypt").hasArg().argName("inputFile")
.desc("Encrypt the given file").build();
final Option DECRYPT_OPTION = Option.builder("d").longOpt("decrypt").hasArg().argName("inputFile")
.desc("Decrypt the given file").build();
final Option PASSWORD_OPTION = Option.builder("p").longOpt("password").hasArg().argName("password")
.desc("Password used for encryption/decryption").build();
final Option OUTPUT_OPTION = Option.builder("o").longOpt("output").hasArg().argName("outputFile")
.desc("Output file path (default: inputFile + .enc or .dec)").build();
final Option MODE_OPTION = Option.builder("m").longOpt("mode").hasArg().argName("aesMode")
.desc("AES mode: AES-128, AES-192, AES-256 (default: AES-256)").build();
final Option CIPHER_TYPE_OPTION = Option.builder("c").longOpt("cipher").hasArg().argName("cipherType").desc(
"Cipher type: AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding (default: AES/CBC/PKCS7Padding)")
.build();
final Option ITER_OPTION = Option.builder("n").longOpt("iterations").hasArg().argName("iterations")
.desc("PBKDF2 iteration count (default: 100000)").build();
final Option AAD_OPTION = Option.builder("a").longOpt("aad").hasArg().argName("aadHex")
.desc("Additional Authenticated Data (AAD) as hex string (optional)").build();
final OptionGroup OPERATION_GROUP = new OptionGroup();
OPERATION_GROUP.addOption(ENCRYPT_OPTION);
OPERATION_GROUP.addOption(DECRYPT_OPTION);
OPERATION_GROUP.setRequired(true);
options.addOptionGroup(OPERATION_GROUP);
options.addOption(PASSWORD_OPTION);
options.addOption(OUTPUT_OPTION);
options.addOption(MODE_OPTION);
options.addOption(CIPHER_TYPE_OPTION);
options.addOption(ITER_OPTION);
options.addOption(AAD_OPTION);
try {
final CommandLineParser parser = new DefaultParser();
final CommandLine cmd = parser.parse(options, args);
final boolean isEncryption = cmd.hasOption(ENCRYPT_OPTION.getOpt());
final boolean isDecryption = cmd.hasOption(DECRYPT_OPTION.getOpt());
final String inputPath = cmd.getOptionValue(ENCRYPT_OPTION.getOpt(),
cmd.getOptionValue(DECRYPT_OPTION.getOpt()));
final String outputPath = cmd.getOptionValue(OUTPUT_OPTION, inputPath + (isEncryption ? ".enc" : ".dec"));
final AesMode mode = AesMode.fromString(cmd.getOptionValue(MODE_OPTION, "AES-256"));
final AesCipherType cipher = AesCipherType
.fromString(cmd.getOptionValue(CIPHER_TYPE_OPTION, "AES/CBC/PKCS7Padding"));
final int iterations = Integer.parseInt(cmd.getOptionValue(ITER_OPTION, "100000"));
String password = cmd.getOptionValue(PASSWORD_OPTION);
if (password == null) {
password = promptForPassword(isEncryption);
}
final byte[] aad = cmd.hasOption(AAD_OPTION.getOpt())
? HexFormat.of().parseHex(cmd.getOptionValue(AAD_OPTION.getOpt()))
: null;
final DataContent fileIn = new PlainFile(Path.of(inputPath).toUri().toURL());
if (isEncryption) {
performEncryption(password, iterations, aad, mode, cipher, fileIn, outputPath);
} else if (isDecryption) {
performDecryption(password, aad, mode, cipher, fileIn, outputPath);
}
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "Error during encryption/decryption: {0}", e.toString());
}
return 1;
}
return 0;
}
/**
* Prompts the user to enter a password (optionally with confirmation).
*
* @param confirm Whether to ask for password confirmation (encryption mode).
* @return The confirmed password as a string.
* @throws ParseException If user input is invalid or confirmation fails.
* @throws IOException If the system console is not available.
*/
private static String promptForPassword(final boolean confirm) throws ParseException, IOException { // NOPMD
final Console console = System.console();
if (console == null) {
throw new IOException("No console available for password input. Please use the --password option.");
}
final char[] pwdArray;
if (confirm) {
pwdArray = console.readPassword("Enter password for encryption: ");
final char[] confirmArray = console.readPassword("Confirm password: ");
if (pwdArray == null || confirmArray == null || pwdArray.length == 0) {
throw new ParseException("No password entered.");
}
if (!Arrays.equals(pwdArray, confirmArray)) {
throw new ParseException("Passwords do not match.");
}
} else {
pwdArray = console.readPassword("Enter password for decryption: ");
if (pwdArray == null || pwdArray.length == 0) {
throw new ParseException("No password entered.");
}
}
final String password = new String(pwdArray);
Arrays.fill(pwdArray, ' '); // Clear sensitive data
return password;
}
/**
* Encrypts a file using password-based AES encryption with the specified
* parameters.
*
* @param password the password used for key derivation; must not be
* {@code null}
* @param iterations the number of PBKDF2 iterations to use; must be positive
* @param aad additional authenticated data (AAD) for authenticated
* encryption modes; may be {@code null}
* @param mode the AES mode specifying key length (e.g., AES-128,
* AES-256); must not be {@code null}
* @param cipher the AES cipher configuration (e.g., CBC, GCM); must not be
* {@code null}
* @param fileIn the input data content representing the plaintext file;
* must not be {@code null}
* @param outputPath the path to write the encrypted output file; must not be
* {@code null} or empty
* @throws IOException if an I/O error occurs during reading or
* writing files
* @throws GeneralSecurityException if an error occurs during encryption or key
* derivation
*/
private static void performEncryption(final String password, final int iterations, final byte[] aad,
final AesMode mode, final AesCipherType cipher, final DataContent fileIn, final String outputPath)
throws IOException, GeneralSecurityException {
final PasswordBasedAesEncryptor encryptor = new PasswordBasedAesEncryptor(password, iterations, aad, mode,
cipher);
encryptor.setInput(fileIn);
try (InputStream encryptedStream = encryptor.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
encryptedStream.transferTo(fileOut);
System.err.println("Encryption complete. Output saved to: " + outputPath);
}
}
/**
* Decrypts a file using password-based AES decryption with the specified
* parameters.
*
* @param password the password used for key derivation; must not be
* {@code null}
* @param aad additional authenticated data (AAD) for authenticated
* encryption modes; may be {@code null}
* @param mode the AES mode specifying key length (e.g., AES-128,
* AES-256); must not be {@code null}
* @param cipher the AES cipher configuration (e.g., CBC, GCM); must not be
* {@code null}
* @param fileIn the input data content representing the encrypted file;
* must not be {@code null}
* @param outputPath the path to write the decrypted output file; must not be
* {@code null} or empty
* @throws IOException if an I/O error occurs during reading or
* writing files
* @throws GeneralSecurityException if an error occurs during decryption or key
* derivation
*/
private static void performDecryption(final String password, final byte[] aad, final AesMode mode,
final AesCipherType cipher, final DataContent fileIn, final String outputPath)
throws IOException, GeneralSecurityException {
final PasswordBasedAesDecryptor decryptor = new PasswordBasedAesDecryptor(password, aad, mode, cipher);
decryptor.setInput(fileIn);
try (InputStream decryptedStream = decryptor.getStream();
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
decryptedStream.transferTo(fileOut);
System.err.println("Decryption complete. Output saved to: " + outputPath);
}
}
}

View File

@@ -0,0 +1,208 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.MissingOptionException;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import zeroecho.util.BouncyCastleActivator;
/**
* ZeroEcho is a command-line utility for managing asymmetric keys and
* certificates, primarily focusing on Certificate Authority (CA) operations
* such as issuing, revoking, and listing certificates.
* <p>
* It supports command-line options for asymmetric key management, including
* issuing certificates from CSRs or subject names, revoking certificates, and
* retrieving all certificates for a user.
* </p>
* <p>
* This class initializes Bouncy Castle security provider and uses Apache
* Commons CLI for command-line parsing.
* </p>
*
* @author Leo Galambos
*/
public class ZeroEcho { // NOPMD by Leo Galambos on 6/1/25, 1:04PM
/**
* Logger instance for the {@code ZeroEcho} class used to log messages and
* events.
* <p>
* This logger is configured with the name of the {@code ZeroEcho} class,
* allowing for fine-grained logging control specific to this class.
* </p>
*/
public static final Logger LOG = Logger.getLogger(ZeroEcho.class.getName());
static {
BouncyCastleActivator.init();
}
/**
* Default constructor for ZeroEcho.
* <p>
* This constructor does not perform any initialization since all operations are
* handled via static methods and blocks.
* </p>
*/
private ZeroEcho() {
// No initialization needed
}
/**
* Main entry point for the ZeroEcho application. Parses command-line arguments
* and dispatches to the appropriate subcommand for asymmetric key management or
* prints the help message.
*
* @param args command-line arguments passed to the program
*/
public static void main(final String[] args) {
final int errorCode = mainProcess(args);
if (errorCode == 0) {
System.out.println("OK");
} else {
System.out.println("ERR: " + errorCode);
}
}
/**
* Entry point for the ZeroEcho application. Parses command-line arguments and
* dispatches to the appropriate subcommand for asymmetric key management or
* prints the help message.
*
* @param args command-line arguments passed to the program
* @return error-code
*/
public static int mainProcess(final String[] args) { // NOPMD by Leo Galambos on 6/11/25, 10:25PM
final Option ASYMETRIC_OPTION = Option.builder("A").longOpt("asm").desc("asymetric keys management").build();
final Option KEM_OPTION = Option.builder("E").longOpt("kem").desc("KEM encryption/decryption").build();
final Option MULTI_AES_OPTION = Option.builder("M").longOpt("multi-aes").desc("AES for multiple recipients")
.build();
final Option KEYSTORE_OPTION = Option.builder("K").longOpt("ksm").desc("key store management").build();
final Option AES_PSW_OPTION = Option.builder("P").longOpt("aes-psw").desc("AES with password").build();
final OptionGroup OPERATION_GROUP = new OptionGroup();
OPERATION_GROUP.addOption(ASYMETRIC_OPTION);
OPERATION_GROUP.addOption(AES_PSW_OPTION);
OPERATION_GROUP.addOption(MULTI_AES_OPTION);
OPERATION_GROUP.addOption(KEYSTORE_OPTION);
OPERATION_GROUP.addOption(KEM_OPTION);
OPERATION_GROUP.setRequired(true); // At least one required
Options options = new Options();
options.addOptionGroup(OPERATION_GROUP);
final CommandLineParser parser = new DefaultParser();
try {
// parse the command line arguments (allow remaining arguments for subcommands)
parser.parse(options, args, true);
switch (OPERATION_GROUP.getSelected()) {
case "A" -> {
return AsymetricKeysManagement.main(args, options = new Options().addOption(ASYMETRIC_OPTION));
}
case "E" -> {
return KEMAes.main(args, options = new Options().addOption(KEM_OPTION));
}
case "P" -> {
return PasswordBasedAes.main(args, options = new Options().addOption(AES_PSW_OPTION));
}
case "M" -> {
return MultiRecipientAes.main(args, options = new Options().addOption(MULTI_AES_OPTION));
}
case "K" -> {
return KeyStoreManagement.main(args, options = new Options().addOption(KEYSTORE_OPTION));
}
default -> {
return 1;
}
}
} catch (MissingOptionException ex) {
if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, ex.getMessage());
}
return help(options);
} catch (ParseException ex) {
if (LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, "Unexpected exception", ex.getMessage());
}
return help(options);
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
LOG.logp(Level.WARNING, "ZeroEcho", "mainProcess", e.getMessage(), e);
return -1;
} finally {
LOG.log(Level.INFO, "Completed.");
}
}
/**
* Prints the usage help message for the command-line interface of the ZeroEcho
* application.
*
* <p>
* This method automatically generates and displays a help statement based on
* the provided command-line {@link Options}. It then terminates the program
* with exit status {@code 1}.
*
* @param options The {@link Options} instance defining the available
* command-line options.
* @return always {@code 1}
*/
protected static int help(final Options options) {
// automatically generate the help statement
final HelpFormatter formatter = new HelpFormatter();
formatter.printHelp(ZeroEcho.class.getName(), options);
return 1;
}
}

View File

@@ -0,0 +1,208 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.cert.X509Certificate;
import java.util.Date;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.BouncyCastleActivator;
class AsymetricKeysManagementTest {
static {
BouncyCastleActivator.init();
}
@TempDir
Path tempDir;
private static final String USERNAME = "realUser";
private static final String SUBJECT_DN = "CN=Real Subject, O=RealOrg";
@Test
void testIssueWithRealCSR() throws Exception {
System.out.println("Running testIssueWithRealCSR...");
Path csrFile = generateCSRFile(SUBJECT_DN);
String[] args = { "-d", tempDir.toString(), "-u", USERNAME, "-i", csrFile.toString() };
Options options = new Options();
AsymetricKeysManagement.main(args, options);
// Validate output (e.g., check that a certificate was generated) - it is
// printed out to STDOUT only for now
// Path certPath = tempDir.resolve(USERNAME + ".crt");
// assertTrue(Files.exists(certPath), "Certificate should be created");
System.out.println("...ok");
}
@Test
void testIssueWithGeneratedKeypair() throws Exception {
System.out.println("Running testIssueWithGeneratedKeypair...");
String[] args = { "-d", tempDir.toString(), "-u", USERNAME, "-s", SUBJECT_DN, "-i" };
Options options = new Options();
AsymetricKeysManagement.main(args, options);
// Validate output (certificate and private key) - it is printed out to STDOUT
// only for now
// Path certPath = tempDir.resolve(USERNAME + ".crt");
// Path keyPath = tempDir.resolve(USERNAME + ".key");
// assertTrue(Files.exists(certPath), "Certificate should be created");
// assertTrue(Files.exists(keyPath), "Private key should be created");
System.out.println("...ok");
}
@Test
void testRevokeRealCertificate() throws Exception {
System.out.println("Running testRevokeRealCertificate...");
// Generate key pair and CSR within this test
KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
X500Name subject = new X500Name(SUBJECT_DN);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
PKCS10CertificationRequest csr = new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic())
.build(signer);
// Save CSR to file within tempDir
Path csrFile = tempDir.resolve("tempRevokeTest.csr");
try (JcaPEMWriter pemWriter = new JcaPEMWriter(Files.newBufferedWriter(csrFile))) {
pemWriter.writeObject(csr);
}
// Issue certificate
String[] issueArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-i", csrFile.toString() };
Options issueOptions = new Options();
AsymetricKeysManagement.main(issueArgs, issueOptions);
// Since ZeroEcho likely prints the cert but does not store it, we simulate
// loading it back
// Let's generate a self-signed cert to match the issued one (or ideally
// ZeroEcho should return it)
// Here we re-use the CSR to simulate a real certificate loading
X509Certificate issuedCert = generateSelfSignedCertificate(subject, keyPair);
// Save the issued cert to a temporary file
Path certFile = tempDir.resolve("tempIssuedCert.pem");
try (JcaPEMWriter pemWriter = new JcaPEMWriter(Files.newBufferedWriter(certFile))) {
pemWriter.writeObject(issuedCert);
}
// Now revoke the issued certificate
String[] revokeArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-r", certFile.toString() };
Options revokeOptions = new Options();
AsymetricKeysManagement.main(revokeArgs, revokeOptions);
System.out.println("...ok");
}
private static X509Certificate generateSelfSignedCertificate(X500Name subject, KeyPair keyPair) throws Exception {
long now = System.currentTimeMillis();
Date notBefore = new Date(now - 1000L * 60);
Date notAfter = new Date(now + (1000L * 60 * 60 * 24));
BigInteger serial = BigInteger.valueOf(now);
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(subject, serial, notBefore, notAfter,
subject, keyPair.getPublic());
return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME)
.getCertificate(certBuilder.build(contentSigner));
}
@Test
void testGetAllCertificates() throws Exception {
System.out.println("Running testGetAllCertificates...");
// Issue certificate first
String[] issueArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-s", SUBJECT_DN, "-i" };
AsymetricKeysManagement.main(issueArgs, new Options());
// Get all certificates for USERNAME
String[] getAllArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-a" };
AsymetricKeysManagement.main(getAllArgs, new Options());
// (Optional) You might want to capture System.out and validate contents
System.out.println("...ok");
}
@Test
void testMissingUsernameThrows() {
System.out.println("Running testMissingUsernameThrows...");
String[] args = { "-d", tempDir.toString(), "-s", SUBJECT_DN, "-i" };
Options options = new Options();
assertThrows(ParseException.class, () -> AsymetricKeysManagement.main(args, options));
System.out.println("...ok");
}
/**
* Helper: Generate a real CSR PEM file for testing.
*/
private Path generateCSRFile(String subjectDN) throws Exception {
KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
X500Name subject = new X500Name(subjectDN);
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
PKCS10CertificationRequest csr = new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic())
.build(signer);
Path csrFile = tempDir.resolve("test.csr");
try (JcaPEMWriter pemWriter = new JcaPEMWriter(Files.newBufferedWriter(csrFile))) {
pemWriter.writeObject(csr);
}
return csrFile;
}
}

View File

@@ -0,0 +1,156 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.cli.Options;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.UniversalKeyStoreFile;
class KEMAesTest {
@TempDir
Path tempDir;
private Path keystorePath;
private UniversalKeyStoreFile keystore;
private static final int DATA_SIZE = 8 * 1024;
private static final int TOTAL_USERS = 10;
private Map<String, KeyPair> kemUsers;
@BeforeAll
static void initializeCrypto() {
BouncyCastleActivator.init();
}
@BeforeEach
void setupKeystore() throws Exception {
System.out.println();
keystorePath = tempDir.resolve("kem_test.keystore");
keystore = new UniversalKeyStoreFile(keystorePath);
kemUsers = new HashMap<>();
for (int i = 0; i < TOTAL_USERS; i++) {
String username = "user-" + i + "-" + UUID.randomUUID();
String[] args = { "-k", keystorePath.toString(), "-o", username, "-g", "-a",
KeyPairAlgorithm.KYBER_512.toString() };
int exit = KeyStoreManagement.main(args, new Options());
assertEquals(0, exit, "Key generation failed for " + username);
KeyPair kp = new KeyPair(keystore.loadPublicKey(username), keystore.loadPrivateKey(username));
kemUsers.put(username, kp);
}
}
@Test
void testKEMAesEncryptionDecryption() throws Exception {
System.out.println("testKEMAesEncryptionDecryption");
// Write random input
Path inputFile = tempDir.resolve("plain_input.dat");
byte[] inputContent = new byte[DATA_SIZE];
new Random().nextBytes(inputContent);
Files.write(inputFile, inputContent);
// Encrypt using the first user's public key
String encryptUser = kemUsers.keySet().iterator().next();
Path encryptedFile = tempDir.resolve("encrypted_output.dat");
System.out.println("Encrypting as recipient: " + encryptUser);
String[] encryptArgs = { "-e", inputFile.toString(), "-k", keystorePath.toString(), "-p", encryptUser, "-o",
encryptedFile.toString() };
int encryptExit = KEMAes.main(encryptArgs, new Options());
assertEquals(0, encryptExit, "Encryption failed");
assertTrue(Files.exists(encryptedFile), "Encrypted file not created");
// Decrypt using corresponding private key
Path decryptedFile = tempDir.resolve("decrypted_output.dat");
String[] decryptArgs = { "-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s", encryptUser, "-o",
decryptedFile.toString() };
System.out.println("Decrypting as recipient: " + encryptUser);
int decryptExit = KEMAes.main(decryptArgs, new Options());
assertEquals(0, decryptExit, "Decryption failed");
assertTrue(Files.exists(decryptedFile), "Decrypted file not created");
byte[] decrypted = Files.readAllBytes(decryptedFile);
assertArrayEquals(inputContent, decrypted, "Decrypted content does not match");
// Attempt decryption with wrong private key (should fail)
List<String> others = kemUsers.keySet().stream().filter(k -> !k.equals(encryptUser))
.collect(Collectors.toList());
assertFalse(others.isEmpty(), "No alternative user for invalid decryption test");
for (String wrongUser : others) {
Path wrongOutput = tempDir.resolve("wrong_decrypt_output.dat");
String[] wrongDecryptArgs = { "-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s",
wrongUser, "-o", wrongOutput.toString() };
System.out.println("Attempting decryption as wrong user: " + wrongUser);
int wrongDecryptExit = KEMAes.main(wrongDecryptArgs, new Options());
assertEquals(1, wrongDecryptExit, "Expected decryption failure with wrong key");
}
System.out.println("...test completed");
}
}

View File

@@ -0,0 +1,130 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.nio.file.Path;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.cli.Options;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.KeyPairAlgorithm;
import zeroecho.util.CryptoAlgorithmsNames;
import zeroecho.util.UniversalKeyStoreFile;
class KeyStoreManagementTest {
@TempDir
Path tempDir;
private static final Map<String, KeyPairAlgorithm> ownerToAlgorithm = new HashMap<>();
@BeforeAll
public static void setupCryptoProvider() {
BouncyCastleActivator.init();
}
@Test
public void testGenerateAndVerifyAllKeyPairAlgorithms() throws Exception {
System.out.println("testGenerateAndVerifyAllKeyPairAlgorithms");
Path keystorePath = tempDir.resolve("test-keystore.txt");
List<String> argsList = new ArrayList<>();
Options options = new Options();
for (KeyPairAlgorithm algorithm : KeyPairAlgorithm.SERIALIZABLE_ALGORITHMS) {
if (algorithm == KeyPairAlgorithm.ELGAMAL_2048) {
// too slow => skip over
continue;
}
if (algorithm.getAlgorithmName().equals(CryptoAlgorithmsNames.FRODO.displayName())) {
// unsupported
continue;
}
String owner = "owner_" + UUID.randomUUID();
ownerToAlgorithm.put(owner, algorithm);
System.out.println("...generate new user " + owner + " with " + algorithm.toString());
argsList.clear();
argsList.add("--keystore");
argsList.add(keystorePath.toString());
argsList.add("--owner");
argsList.add(owner);
argsList.add("--generate");
argsList.add("--algorithm");
argsList.add(algorithm.toString());
int result = KeyStoreManagement.main(argsList.toArray(new String[0]), options);
assertEquals(0, result, "KeyStoreManagement should return 0 for success");
}
// Verify contents of keystore
UniversalKeyStoreFile keystore = new UniversalKeyStoreFile(keystorePath);
for (Map.Entry<String, KeyPairAlgorithm> entry : ownerToAlgorithm.entrySet()) {
String owner = entry.getKey();
System.out.println("...loading keys for " + owner + " algorithm(" + entry.getValue() + ")");
PublicKey publicKey = keystore.loadPublicKey(owner);
PrivateKey privateKey = keystore.loadPrivateKey(owner);
assertNotNull(publicKey, "Public key should be present for " + owner);
assertNotNull(privateKey, "Private key should be present for " + owner);
// Optional: Check algorithm consistency
assertEquals(entry.getValue().getAlgorithmName(), CryptoAlgorithmsNames.fromString(publicKey.getAlgorithm()).displayName(),
"Public key algorithm mismatch for " + owner);
assertEquals(entry.getValue().getAlgorithmName(), CryptoAlgorithmsNames.fromString(privateKey.getAlgorithm()).displayName(),
"Private key algorithm mismatch for " + owner);
}
System.out.println("...ok");
}
}

View File

@@ -0,0 +1,203 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.cli.Options;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.BouncyCastleActivator;
import zeroecho.util.UniversalKeyStoreFile;
class MultiRecipientAesTest {
@TempDir
Path tempDir;
private Path keystorePath;
private UniversalKeyStoreFile keystore;
private static final int DATA_SIZE = 16 * 1024;
private static final int TOTAL_OWNERS = 20;
private static final int RECIPIENTS_COUNT = 10;
private Map<String, KeyPair> ownerKeyPairs;
@BeforeAll
public static void setupCryptoProvider() {
BouncyCastleActivator.init();
}
@BeforeEach
void setupKeystore() throws Exception {
System.out.println("");
keystorePath = tempDir.resolve("test.keystore");
keystore = new UniversalKeyStoreFile(keystorePath);
ownerKeyPairs = new HashMap<>();
// Use KeyStoreManagement to generate 20 RSA:2048 key pairs for random owners
Options options = new Options();
for (int i = 0; i < TOTAL_OWNERS; i++) {
String owner = "owner-" + i + "-" + UUID.randomUUID();
System.out.println("");
String[] args = { "-k", keystorePath.toString(), "-o", owner, "-g", "-a", "RSA:2048" };
int exitCode = KeyStoreManagement.main(args, options);
assertEquals(0, exitCode, "Key generation failed for owner " + owner);
// Reload keys after each addition
KeyPair kp = new KeyPair(keystore.loadPublicKey(owner), keystore.loadPrivateKey(owner));
ownerKeyPairs.put(owner, kp);
}
System.out.println("");
}
@Test
void testEncryptDecryptWithMultiRecipientAes() throws Exception {
System.out.println("testEncryptDecryptWithMultiRecipientAes");
// Prepare test file with random data
Path inputFile = tempDir.resolve("plaintext.dat");
byte[] originalContent = new byte[DATA_SIZE]; // 1 KB random data
System.out.println("...preparing test data, size=" + DATA_SIZE + " bytes");
new Random().nextBytes(originalContent);
Files.write(inputFile, originalContent);
// Pick 10 random owners for encryption recipients
List<String> allOwners = new ArrayList<>(ownerKeyPairs.keySet());
Collections.shuffle(allOwners);
List<String> recipientOwners = allOwners.subList(0, RECIPIENTS_COUNT);
System.out.println("...encrypting for " + RECIPIENTS_COUNT + " recipients");
// Encrypt with public keys of the 10 recipients
Path encryptedFile = tempDir.resolve("encrypted.dat");
List<String> encryptArgs = new ArrayList<>();
encryptArgs.add("-e");
encryptArgs.add(inputFile.toString());
encryptArgs.add("-k");
encryptArgs.add(keystorePath.toString());
encryptArgs.add("-p");
encryptArgs.addAll(recipientOwners);
encryptArgs.add("-o");
encryptArgs.add(encryptedFile.toString());
int encryptExit = MultiRecipientAes.main(encryptArgs.toArray(new String[0]), new Options());
assertEquals(0, encryptExit, "Encryption failed");
assertTrue(Files.exists(encryptedFile), "Encrypted file not created");
// Pick one owner from recipients to decrypt
String decryptOwner = recipientOwners.get(0);
Path decryptedFile = tempDir.resolve("decrypted.dat");
System.out.println("...testing decryption for the user " + decryptOwner);
List<String> decryptArgs = List.of("-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s",
decryptOwner, "-o", decryptedFile.toString());
int decryptExit = MultiRecipientAes.main(decryptArgs.toArray(new String[0]), new Options());
assertEquals(0, decryptExit, "Decryption failed for recipient owner");
System.out.println("...verifying decrypted content matches original");
// Verify decrypted content matches original
byte[] decryptedContent = Files.readAllBytes(decryptedFile);
assertArrayEquals(originalContent, decryptedContent, "Decrypted content does not match original");
// Pick owner NOT in recipients and test that decryption produces incorrect
// output or fails
List<String> nonRecipients = allOwners.stream().filter(o -> !recipientOwners.contains(o))
.collect(Collectors.toList());
assertFalse(nonRecipients.isEmpty(), "No non-recipient owners to test");
String nonRecipientOwner = nonRecipients.get(0);
Path decryptedWrongFile = tempDir.resolve("decrypted_wrong.dat");
System.out.println("...testing whether decryption fails for the user " + nonRecipientOwner
+ " who was not amongst recipients");
List<String> decryptWrongArgs = List.of("-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s",
nonRecipientOwner, "-o", decryptedWrongFile.toString());
int wrongDecryptExit = MultiRecipientAes.main(decryptWrongArgs.toArray(new String[0]), new Options());
// Decryption may or may not fail gracefully, but output should not match
// original content
assertEquals(1, wrongDecryptExit, "Decryption with non-recipient owner failed unexpectedly");
// The decryptedWrongFile should NOT exist because decryption failed (no output
// file created)
assertFalse(Files.exists(decryptedWrongFile), "Decrypted file should NOT exist for non-recipient owner");
// Trying to read should throw NoSuchFileException
assertThrows(NoSuchFileException.class, () -> {
Files.readAllBytes(decryptedWrongFile);
});
System.out.println("...ok");
}
}

View File

@@ -0,0 +1,135 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.cli.Options;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import zeroecho.util.BouncyCastleActivator;
/**
* Unit tests for {@link PasswordBasedAes} using its static CLI-based main
* method.
*/
class PasswordBasedAesTest {
static {
BouncyCastleActivator.init();
}
@TempDir
Path tempDir;
private static final String PASSWORD = "secure-password";
/**
* Tests that encryption followed by decryption restores the original plain
* text.
*/
@Test
void testEncryptAndDecryptShortText() throws Exception {
String content = "This is a simple secret message.";
Path inputFile = writeTempFile("short.txt", content);
Path encryptedFile = tempDir.resolve("short.txt.enc");
Path decryptedFile = tempDir.resolve("short.txt.dec");
runEncryption(inputFile, encryptedFile);
runDecryption(encryptedFile, decryptedFile);
String decrypted = Files.readString(decryptedFile, StandardCharsets.UTF_8);
assertEquals(content, decrypted.trim());
}
/**
* Tests encryption/decryption of a longer file to ensure streaming works.
*/
@Test
void testEncryptAndDecryptLongFile() throws Exception {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10000; i++) {
builder.append("Line ").append(i).append(": This is a long test line.\n");
}
String longText = builder.toString();
Path inputFile = writeTempFile("long.txt", longText);
Path encryptedFile = tempDir.resolve("long.txt.enc");
Path decryptedFile = tempDir.resolve("long.txt.dec");
runEncryption(inputFile, encryptedFile);
runDecryption(encryptedFile, decryptedFile);
String decrypted = Files.readString(decryptedFile, StandardCharsets.UTF_8);
assertEquals(longText, decrypted);
}
/**
* Ensures that a nonexistent input file causes no crash (is caught internally).
*/
@Test
void testNonexistentInputHandledGracefully() {
String[] args = { "-e", "nonexistent.txt", "-p", PASSWORD, "-o", "out.bin" };
assertDoesNotThrow(() -> PasswordBasedAes.main(args, new Options()));
}
// --- Helper methods ---
private Path writeTempFile(String name, String content) throws IOException {
Path file = tempDir.resolve(name);
Files.writeString(file, content, StandardCharsets.UTF_8);
return file;
}
private void runEncryption(Path input, Path output) throws Exception {
String[] args = { "-e", input.toString(), "-p", PASSWORD, "-o", output.toString() };
PasswordBasedAes.main(args, new Options());
assertTrue(Files.exists(output));
}
private void runDecryption(Path input, Path output) throws Exception {
String[] args = { "-d", input.toString(), "-p", PASSWORD, "-o", output.toString() };
PasswordBasedAes.main(args, new Options());
assertTrue(Files.exists(output));
}
}

View File

@@ -0,0 +1,74 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class ZeroEchoTest {
@Test
void testAsymetricOptionWithoutParamsReturnsOne() {
System.out.println("testAsymetricOptionWithoutParamsReturnsOne");
int result = ZeroEcho.mainProcess(new String[] { "-A" });
assertEquals(1, result, "Asymetric option without parameters should return 1 (error/help)");
System.out.println("...ok");
}
@Test
void testAesPswOptionWithoutParamsReturnsOne() {
System.out.println("testAesPswOptionWithoutParamsReturnsOne");
int result = ZeroEcho.mainProcess(new String[] { "-P" });
assertEquals(1, result, "AES-PSW option without parameters should return 1 (error/help)");
System.out.println("...ok");
}
@Test
void testNoOptionReturnsOne() {
System.out.println("testNoOptionReturnsOne");
int result = ZeroEcho.mainProcess(new String[] {});
assertEquals(1, result, "No options should return 1 (error/help)");
System.out.println("...ok");
}
@Test
void testInvalidOptionReturnsOne() {
System.out.println("testInvalidOptionReturnsOne");
int result = ZeroEcho.mainProcess(new String[] { "-X" });
assertEquals(1, result, "Invalid option should return 1 (error/help)");
System.out.println("...ok");
}
}

9
buildSrc/build.gradle Normal file
View 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
View 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'

View File

@@ -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'
}

View File

@@ -0,0 +1,131 @@
/*
* 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'
}
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.1.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)
}
withJavadocJar()
withSourcesJar()
}
javadoc {
failOnError = false
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.bottom = "Copyright &copy; 2025 Egothor"
source = sourceSets.main.allJava
}
tasks.named('test') {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}
if (project.hasProperty('giteaToken') && project.giteaToken) {
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
}
}
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
}

View File

@@ -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'
}

1
gradle.properties Normal file
View File

@@ -0,0 +1 @@
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g

View 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

Binary file not shown.

View 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
View 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
View 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

26
lib/.classpath Normal file
View File

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

23
lib/.project Normal file
View File

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

31
lib/LICENSE Normal file
View File

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

13
lib/build.gradle Normal file
View File

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

View File

@@ -0,0 +1,93 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.builder;
import zeroecho.data.DataContent;
import zeroecho.data.processing.AesDecryptor;
import zeroecho.data.processing.AesEncryptor;
/**
* Builder interface for constructing AES encryption or decryption
* {@link DataContent} instances.
* <p>
* The builder can be configured to build either an AES encryptor or decryptor.
* The constructed {@code DataContent} will perform the corresponding
* cryptographic operation.
*
* <p>
* Usage example:
*
* <pre>{@code
* DataContent encryptor = AesBuilder.builder().build(true);
*
* DataContent decryptor = AesBuilder.builder().build(false);
* }</pre>
*/
public interface AesBuilder extends DataContentBuilder<DataContent> { // NOPMD
/**
* Creates a new instance of the default AES builder implementation.
*
* @return a new {@code AesBuilder}
*/
static AesBuilder builder() {
return new DefaultAesContentBuilder();
}
/**
* Default implementation of the {@link AesBuilder} interface.
* <p>
* Builds an AES encryption or decryption content instance depending on the
* {@code encrypt} parameter passed to {@link #build(boolean)}.
* </p>
*/
final class DefaultAesContentBuilder implements AesBuilder {
private DefaultAesContentBuilder() {
}
/**
* Builds and returns an AES encryptor or decryptor.
*
* @param encrypt {@code true} to build an {@link AesEncryptor}, {@code false}
* to build an {@link AesDecryptor}
* @return a {@link DataContent} instance configured for encryption or
* decryption
*/
@Override
public DataContent build(final boolean encrypt) {
return encrypt ? new AesEncryptor(null) : new AesDecryptor(null);
}
}
}

View File

@@ -0,0 +1,197 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.builder;
import java.util.Objects;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
import zeroecho.data.processing.SecretAesRandom;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
/**
* A builder interface for constructing {@link SecretAesRandom} instances, which
* produce AES-based pseudorandom data wrapped as {@link PlainContent}.
*
* <p>
* This builder allows configuring:
* <ul>
* <li>The AES mode (which determines key length: 128, 192, or 256 bits)</li>
* <li>The AES cipher type (e.g., CBC, GCM)</li>
* <li>An optional input source that feeds into the random generator</li>
* <li>Additional Authenticated Data (AAD) to be associated with AES cipher
* operations</li>
* </ul>
*
* <p>
* The {@link #build(boolean)} method produces the configured instance and
* accepts a boolean flag indicating whether the content should operate in
* encryption or decryption mode. When an input is provided via
* {@link #input(DataContent)}, the meaning of this flag becomes relevant:
* </p>
* <ul>
* <li>If {@code encrypt} is {@code true}, the builder produces a randomizer
* suitable for encryption.</li>
* <li>If {@code encrypt} is {@code false}, the builder configures the content
* for decryption based on the input.</li>
* <li>If no input is provided, the {@code encrypt} flag may be ignored.</li>
* </ul>
*
* <p>
* Usage example:
* </p>
* <pre>{@code
* PlainContent random = AesRandomBuilder.builder()
* .mode(AesMode.AES_256)
* .cipherType(AesCipherType.CBC)
* .withAad(new byte[] { ... })
* .build(true);
* }</pre>
*/
public interface AesRandomBuilder extends DataContentBuilder<PlainContent> {
/**
* Sets the AES mode, which determines the key length (128, 192, or 256 bits).
*
* @param mode the AES mode; must not be null
* @return this builder instance for method chaining
* @throws NullPointerException if {@code mode} is null
*/
AesRandomBuilder mode(AesMode mode);
/**
* Sets the AES cipher type (e.g., CBC, GCM).
*
* @param cipherType the cipher type; must not be null
* @return this builder instance for method chaining
* @throws NullPointerException if {@code cipherType} is null
*/
AesRandomBuilder cipherType(AesCipherType cipherType);
/**
* Sets the input {@link DataContent} to be used as source.
*
* @param input the data content to wrap; must not be null
* @return this builder instance for method chaining
* @throws NullPointerException if {@code input} is null
*/
AesRandomBuilder input(DataContent input);
/**
* Sets the Additional Authenticated Data (AAD) to be associated with the AES
* cipher.
*
* @param aad the additional authenticated data to use; may be {@code null} if
* none
* @return this builder instance for method chaining
*/
AesRandomBuilder withAad(byte[] aad);
/**
* Creates a new instance of the default builder implementation.
*
* @return a new {@code AesRandomBuilder}
*/
static AesRandomBuilder builder() {
return new DefaultAesRandomBuilder();
}
/**
* Default implementation of the {@link AesRandomBuilder} interface.
* <p>
* Builds a {@link SecretAesRandom} instance with the configured AES mode,
* cipher type, and AAD.
* </p>
*/
final class DefaultAesRandomBuilder implements AesRandomBuilder {
private AesMode modeField;
private AesCipherType cipherTypeField;
private DataContent source;
private byte[] aadField;
private DefaultAesRandomBuilder() {
}
@Override
public AesRandomBuilder mode(final AesMode mode) {
this.modeField = Objects.requireNonNull(mode, "mode must not be null");
return this;
}
@Override
public AesRandomBuilder cipherType(final AesCipherType cipherType) {
this.cipherTypeField = Objects.requireNonNull(cipherType, "cipherType must not be null");
return this;
}
@Override
public AesRandomBuilder input(final DataContent input) {
this.source = Objects.requireNonNull(input, "input must not be null");
return this;
}
@Override
public AesRandomBuilder withAad(final byte[] aad) {
this.aadField = aad; // NOPMD
return this;
}
/**
* Builds and returns the {@link SecretAesRandom} instance configured by this
* builder.
*
* @param encrypt {@code true} to configure for encryption mode, {@code false}
* for decryption mode
* @return a configured {@link PlainContent} instance representing the AES
* random content
* @throws IllegalStateException if AES mode or cipher type has not been set
*/
@Override
public PlainContent build(final boolean encrypt) {
if (modeField == null) {
throw new IllegalStateException("AES mode must be set before building");
}
if (cipherTypeField == null) {
throw new IllegalStateException("AES cipher type must be set before building");
}
final SecretAesRandom secretAesRandom = new SecretAesRandom(modeField, cipherTypeField, aadField);
if (source != null) {
secretAesRandom.setInput(source);
}
return secretAesRandom;
}
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.builder;
import zeroecho.data.DataContent;
/**
* A builder interface for constructing instances of {@link DataContent}.
* <p>
* This interface enables a uniform and extensible way to create various
* {@code DataContent} implementations in a fluent, builder-based style. It is
* intended to be used with builder classes that encapsulate construction
* parameters and logic specific to each {@code DataContent} subtype.
* <p>
* The {@link #build(boolean)} method configures the resulting
* {@code DataContent} instance for either encryption or decryption mode. When
* encryption is selected, additional metadata (such as headers) may be added to
* the output stream; this metadata must be preserved and reused during
* decryption.
* <p>
* Typical usage example:
*
* <pre>{@code
* DataContent content = SomeDataContentBuilder.builder()
* .parameterX(...)
* .parameterY(...)
* .build(true); // true for encryption mode
* }</pre>
*
* @param <T> the type of {@link DataContent} produced by this builder
*
* @author Leo Galambos
*/
public interface DataContentBuilder<T extends DataContent> { // NOPMD
/**
* Constructs and returns a new {@link DataContent} instance based on the
* builder's current configuration.
*
* @param encrypt whether the resulting {@code DataContent} should be configured
* for encryption ({@code true}) or decryption ({@code false}).
* In encryption mode, additional headers or metadata may be
* inserted into the stream which must be retained for successful
* decryption.
* @return the constructed {@code DataContent} instance
*/
T build(boolean encrypt);
}

View File

@@ -0,0 +1,158 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.builder;
import zeroecho.data.DataContent;
/**
* A builder interface for constructing a chain of {@link DataContent}
* components, where each content unit passes its output as the input to the
* next one.
*
* <p>
* This builder simplifies the composition of processing pipelines such as:
* <ul>
* <li>Encryption with multiple layers (e.g., compression → encryption →
* signing)</li>
* <li>Decryption chains (e.g., verification → decryption → decompression)</li>
* </ul>
*
* <p>
* The builder supports both encryption and decryption modes, which are applied
* consistently across all {@link DataContentBuilder} instances in the chain.
* </p>
*
* <p>
* Usage example: <pre>{@code
* DataContent chain = DataContentChainBuilder.encrypt()
* .add(PlainStringBuilder.builder().value("hello"))
* .add(DerivedAesParametersBuilder.builder().password("secret"))
* .build();
* }</pre>
*/
public interface DataContentChainBuilder {
/**
* Adds a {@link DataContentBuilder} to the chain and links its output to the
* input of the previously added {@code DataContent}, if any.
*
* <p>
* The {@link DataContent} produced by this builder becomes the new tail of the
* chain.
* </p>
*
* @param builder the builder producing the next {@code DataContent}; must not
* be null
* @return this chain builder instance, allowing for fluent chaining
* @throws NullPointerException if {@code builder} is null
*/
DataContentChainBuilder add(DataContentBuilder<? extends DataContent> builder);
/**
* Finalizes the chain and returns the tail {@link DataContent} instance.
*
* <p>
* The returned content may internally reference earlier content via
* {@link DataContent#setInput(DataContent)} chaining.
* </p>
*
* @return the last {@code DataContent} in the chain, or {@code null} if no
* builders were added
*/
DataContent build();
/**
* Creates a new {@code DataContentChainBuilder} configured for encryption mode.
*
* <p>
* All added {@link DataContentBuilder} instances will receive {@code true} for
* their {@code build(boolean encrypt)} method, indicating encryption behavior.
* </p>
*
* @return a new chain builder in encryption mode
*/
static DataContentChainBuilder encrypt() {
return new DefaultDataContentChainBuilder(true);
}
/**
* Creates a new {@code DataContentChainBuilder} configured for decryption mode.
*
* <p>
* All added {@link DataContentBuilder} instances will receive {@code false} for
* their {@code build(boolean encrypt)} method, indicating decryption behavior.
* </p>
*
* @return a new chain builder in decryption mode
*/
static DataContentChainBuilder decrypt() {
return new DefaultDataContentChainBuilder(false);
}
/**
* Default implementation of {@link DataContentChainBuilder}.
*
* <p>
* Maintains a reference to the tail of the content chain. Each added builder is
* built in the specified encryption/decryption mode and connected via
* {@link DataContent#setInput(DataContent)} to the previously built content.
* </p>
*/
final class DefaultDataContentChainBuilder implements DataContentChainBuilder {
private DataContent tail;
private final boolean encrypt;
private DefaultDataContentChainBuilder(final boolean encrypt) {
this.encrypt = encrypt;
}
@Override
public DataContentChainBuilder add(final DataContentBuilder<? extends DataContent> builder) {
final DataContent previous = tail;
tail = builder.build(encrypt);
if (previous != null) {
tail.setInput(previous);
}
return this;
}
@Override
public DataContent build() {
return tail;
}
}
}

View File

@@ -0,0 +1,222 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.builder;
import java.security.spec.InvalidKeySpecException;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.SecretContent;
import zeroecho.data.processing.SecretDerivedAesParameters;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
/**
* A builder interface for constructing {@link SecretDerivedAesParameters}
* instances that encapsulate AES encryption parameters derived from a password.
*
* <p>
* This builder enables configuration of password-based encryption using PBKDF2
* (or a similar KDF), including selection of key strength, cipher type,
* iteration count, and optional Additional Authenticated Data (AAD) for AEAD
* cipher modes.
* </p>
*
* <p>
* Typical usage:
* </p>
*
* <pre>{@code
* SecretContent secret = DerivedAesParametersBuilder.builder()
* .password("myStrongPassword")
* .iterations(100_000)
* .mode(AesMode.AES_256)
* .cipherType(AesCipherType.GCM)
* .withAad(aadBytes)
* .build(true); // true for encryption mode
* }</pre>
*
* <p>
* If invalid parameters are provided (e.g., nulls or unsupported key specs),
* {@link IllegalArgumentException} will be thrown during the build process.
* </p>
*/
public interface DerivedAesParametersBuilder extends DataContentBuilder<SecretContent> {
/**
* Sets the password used to derive the AES key.
* <p>
* This password must be non-null and should have sufficient entropy for secure
* encryption.
* </p>
*
* @param password the password string; must not be {@code null} or empty
* @return this builder instance
* @throws IllegalArgumentException if password is null or empty
*/
DerivedAesParametersBuilder password(String password);
/**
* Specifies the number of iterations for the key derivation function.
* <p>
* Higher iteration counts slow down brute-force attacks, but may increase
* processing time.
* </p>
*
* @param iterations number of iterations; must be a positive integer
* @return this builder instance
* @throws IllegalArgumentException if iterations is not positive
*/
DerivedAesParametersBuilder iterations(int iterations);
/**
* Defines the AES mode to use, which determines the derived key length (e.g.,
* 128-bit, 192-bit, or 256-bit).
*
* @param mode the AES key size mode; must not be {@code null}
* @return this builder instance
* @throws NullPointerException if {@code mode} is null
*/
DerivedAesParametersBuilder mode(AesMode mode);
/**
* Specifies the AES cipher type (e.g., CBC or GCM) to be used for encryption or
* decryption.
*
* @param cipherType the cipher mode; must not be {@code null}
* @return this builder instance
* @throws NullPointerException if {@code cipherType} is null
*/
DerivedAesParametersBuilder cipherType(AesCipherType cipherType);
/**
* Sets Additional Authenticated Data (AAD) for the AES cipher.
* <p>
* This data will be included in the authentication tag for AEAD cipher modes
* such as GCM. It may be {@code null} if no AAD is used.
* </p>
*
* @param aad the additional authenticated data bytes, or {@code null}
* @return this builder instance
*/
DerivedAesParametersBuilder withAad(byte[] aad);
/**
* Creates a new builder instance with default values:
* <ul>
* <li>{@link AesMode#AES_256}</li>
* <li>{@link AesCipherType#CBC}</li>
* <li>{@code iterations = AesSupport.KEY_ITERATIONS}</li>
* <li>{@code aad = null}</li>
* </ul>
*
* @return a new {@code DerivedAesParametersBuilder}
*/
static DerivedAesParametersBuilder builder() {
return new DefaultDerivedAesParametersBuilder();
}
/**
* Default implementation of the {@link DerivedAesParametersBuilder} interface.
* <p>
* Builds a {@link SecretDerivedAesParameters} instance configured with the
* supplied parameters.
* <p>
* On failure to create the secret content due to invalid parameters or key
* specification errors, this builder logs the error and rethrows as an
* {@link IllegalArgumentException}.
*/
final class DefaultDerivedAesParametersBuilder implements DerivedAesParametersBuilder {
private static final Logger LOG = Logger
.getLogger(DerivedAesParametersBuilder.DefaultDerivedAesParametersBuilder.class.getName());
private String passwordField;
private int iterationsField = AesSupport.KEY_ITERATIONS;
private AesMode modeField = AesMode.AES_256;
private AesCipherType cipherTypeField = AesCipherType.CBC;
private byte[] aadField;
private DefaultDerivedAesParametersBuilder() {
}
@Override
public DerivedAesParametersBuilder password(final String password) {
if (password == null || password.isEmpty()) {
throw new IllegalArgumentException("password must not be null or empty");
}
this.passwordField = password;
return this;
}
@Override
public DerivedAesParametersBuilder iterations(final int iterations) {
if (iterations <= 0) {
throw new IllegalArgumentException("iterations must be positive");
}
this.iterationsField = iterations;
return this;
}
@Override
public DerivedAesParametersBuilder mode(final AesMode mode) {
this.modeField = Objects.requireNonNull(mode, "mode must not be null");
return this;
}
@Override
public DerivedAesParametersBuilder cipherType(final AesCipherType cipherType) {
this.cipherTypeField = Objects.requireNonNull(cipherType, "cipherType must not be null");
return this;
}
@Override
public DerivedAesParametersBuilder withAad(final byte[] aad) {
this.aadField = aad; // NOPMD
return this;
}
@Override
public SecretContent build(final boolean encrypt) {
try {
return new SecretDerivedAesParameters(passwordField, iterationsField, aadField, modeField,
cipherTypeField, encrypt);
} catch (IllegalArgumentException | InvalidKeySpecException e) {
LOG.log(Level.WARNING, "Exception during build", e);
throw new IllegalArgumentException(e);
}
}
}
}

View File

@@ -0,0 +1,257 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.builder;
import java.io.IOException;
import java.security.Key;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Objects;
import zeroecho.data.processing.SecretKEMAesParameters;
import zeroecho.util.KeySupport;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.asymmetric.AsymmetricContext;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
/**
* Builder for {@link SecretKEMAesParameters}, allowing construction from either
* a {@link PublicKey} (for encryption) or a {@link PrivateKey} (for
* decryption).
*
* <p>
* This builder ensures that the provided key corresponds to a
* {@link KEMAsymmetricContext}. If the key type is not supported by the KEM
* infrastructure, the {@link #build(boolean)} method will fail.
* </p>
*
* <p>
* Validation rules:
* </p>
* <ul>
* <li>For encryption mode ({@code encrypt = true}), the resulting
* {@link KEMAsymmetricContext} must have a {@code null} extractor (meaning it
* can only generate encapsulated keys, not decapsulate).</li>
* <li>For decryption mode ({@code encrypt = false}), the resulting
* {@link KEMAsymmetricContext} must have a {@code null} generator (meaning it
* can only extract secrets, not generate them).</li>
* </ul>
*/
public interface KEMAesParametersBuilder extends DataContentBuilder<SecretKEMAesParameters> {
/**
* Sets the asymmetric key used to build the KEM context.
*
* <p>
* The provided key determines the operational mode:
* </p>
* <ul>
* <li>Public key → encryption mode.</li>
* <li>Private key → decryption mode.</li>
* </ul>
*
* @param key the public or private key; must not be {@code null}
* @return this builder instance for method chaining
* @throws NullPointerException if {@code key} is {@code null}
*/
KEMAesParametersBuilder withKey(Key key);
/**
* Sets the AES mode.
*
* @param aesMode the AES mode (e.g., {@link AesMode#AES_128},
* {@link AesMode#AES_256}); must not be {@code null}
* @return this builder instance for method chaining
* @throws NullPointerException if {@code aesMode} is {@code null}
*/
KEMAesParametersBuilder withAesMode(AesMode aesMode);
/**
* Sets the AES cipher type.
*
* @param cipherType the cipher type (e.g., {@link AesCipherType#CBC},
* {@link AesCipherType#GCM}); must not be {@code null}
* @return this builder instance for method chaining
* @throws NullPointerException if {@code cipherType} is {@code null}
*/
KEMAesParametersBuilder withCipherType(AesCipherType cipherType);
/**
* Sets optional Additional Authenticated Data (AAD) for authenticated modes.
*
* @param aad the AAD bytes; may be {@code null}
* @return this builder instance for method chaining
*/
KEMAesParametersBuilder withAAD(byte[] aad);
/**
* Creates a new {@code KEMAesParametersBuilder} instance with default settings:
* <ul>
* <li>{@link AesMode#AES_256}</li>
* <li>{@link AesCipherType#GCM}</li>
* <li>empty AAD ({@code new byte[0]})</li>
* </ul>
*
* @return a new builder instance
*/
static KEMAesParametersBuilder builder() {
return new DefaultKEMAesParametersBuilder();
}
/**
* Default implementation of {@link KEMAesParametersBuilder}.
*
* <p>
* Performs parameter validation and constructs a {@link SecretKEMAesParameters}
* instance based on the configured key, AES mode, cipher type, and optional
* AAD.
* </p>
*/
final class DefaultKEMAesParametersBuilder implements KEMAesParametersBuilder {
private Key key; // May be PublicKey or PrivateKey
private AesMode aesMode = AesMode.AES_256;
private AesCipherType cipherType = AesCipherType.GCM;
private byte[] aad = {};
private DefaultKEMAesParametersBuilder() {
}
@Override
public KEMAesParametersBuilder withKey(final Key key) {
this.key = Objects.requireNonNull(key, "key must not be null");
return this;
}
@Override
public KEMAesParametersBuilder withAesMode(final AesMode aesMode) {
this.aesMode = Objects.requireNonNull(aesMode, "aesMode must not be null");
return this;
}
@Override
public KEMAesParametersBuilder withCipherType(final AesCipherType cipherType) {
this.cipherType = Objects.requireNonNull(cipherType, "cipherType must not be null");
return this;
}
@Override
public KEMAesParametersBuilder withAAD(final byte[] aad) {
this.aad = aad; // NOPMD
return this;
}
/**
* Builds a {@link SecretKEMAesParameters} instance.
*
* <p>
* The method determines whether encryption or decryption mode should be used
* based on the provided key type:
* </p>
* <ul>
* <li>{@link java.security.PublicKey} → encryption</li>
* <li>{@link java.security.PrivateKey} → decryption</li>
* </ul>
*
* <p>
* Validation includes:
* </p>
* <ul>
* <li>Ensuring the key corresponds to a KEM-capable context.</li>
* <li>Verifying generator/extractor presence depending on the mode.</li>
* </ul>
*
* @param encrypt {@code true} for encryption mode; {@code false} for decryption
* mode
* @return a fully configured {@link SecretKEMAesParameters} instance
* @throws IllegalArgumentException if the key type is invalid, if mode and key
* mismatch, or if the underlying context
* cannot be created
* @throws NullPointerException if required fields are not set
*/
@Override
public SecretKEMAesParameters build(final boolean encrypt) { // NOPMD
Objects.requireNonNull(key, "Key must be set before building");
Objects.requireNonNull(aesMode, "AES mode must be set before building");
Objects.requireNonNull(cipherType, "Cipher type must be set before building");
try {
AsymmetricContext ctx = switch (key) { // NOPMD
case PublicKey pubk -> {
if (!encrypt) {
throw new IllegalArgumentException("Decrypt needs a private key");
}
yield KeySupport.fromKey(pubk);
}
case PrivateKey privk -> {
if (encrypt) {
throw new IllegalArgumentException("Encrypt needs a public key");
}
yield KeySupport.fromKey(privk);
}
default -> {
throw new IllegalArgumentException("Provided key does not correspond to a KEM-capable context: "
+ key.getClass().getName());
}
};
if (!(ctx instanceof KEMAsymmetricContext kemContext)) {
throw new IllegalArgumentException(
"Provided key does not correspond to a KEM-capable context: " + key.getClass().getName());
}
// Validate context capability - just to be sure - if it fails here,
// KeySupport.fromKey is buggy
if (encrypt) {
if (kemContext.generator() == null) {
throw new IllegalArgumentException(
"KEM generator is not defined; this key cannot be used for encryption.");
}
} else {
if (kemContext.extractor() == null) {
throw new IllegalArgumentException(
"KEM extractor is not defined; this key cannot be used for decryption.");
}
}
return new SecretKEMAesParameters(kemContext, aesMode, cipherType, aad);
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}
}

View File

@@ -0,0 +1,247 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.builder;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import zeroecho.data.EncryptedContent;
import zeroecho.data.processing.SecretMultiRecipientCryptor;
/**
* A builder interface for creating instances of
* {@link SecretMultiRecipientCryptor}, supporting either encryption with
* multiple public keys or decryption with a private key.
* <p>
* This builder enables fluent and type-safe construction of multi-recipient
* cryptographic content handlers. It enforces mutual exclusivity between
* encryption and decryption modes: you may either add one or more recipients
* (and optionally decoys) for encryption, or specify a private key for
* decryption, but not both.
*
* <h2>Usage Examples</h2>
*
* <h3>Encryption with recipients and decoys:</h3> <pre>{@code
* EncryptedContent cryptor = MultiRecipientCryptorBuilder.builder()
* .addRecipient(publicKey1)
* .addRecipient(publicKey2)
* .addDecoy(decoyKey1)
* .addDecoy(decoyKey2)
* .build(true); // true = encryption mode
* }</pre>
*
* <h3>Decryption with private key:</h3> <pre>{@code
* EncryptedContent cryptor = MultiRecipientCryptorBuilder.builder()
* .privateKey(myPrivateKey)
* .build(false); // false = decryption mode
* }</pre>
*
* <p>
* Attempting to mix public recipients/decoys and a private key within the same
* builder instance will result in an {@link IllegalStateException}.
*
* @see SecretMultiRecipientCryptor
* @see EncryptedContent
*/
public interface MultiRecipientCryptorBuilder extends DataContentBuilder<EncryptedContent> {
/**
* Adds a collection of public key recipients for encryption mode. If a private
* key has already been configured, this call will throw an
* {@link IllegalStateException}.
*
* @param recipients a non-null collection of public keys
* @return this builder instance
* @throws NullPointerException if {@code recipients} is null
* @throws IllegalStateException if the builder is already configured with a
* private key
*/
MultiRecipientCryptorBuilder addRecipients(Collection<PublicKey> recipients);
/**
* Adds a single public key recipient for encryption mode. Multiple calls are
* cumulative. If a private key has already been configured, this call will
* throw an {@link IllegalStateException}.
*
* @param recipient a non-null public key
* @return this builder instance
* @throws NullPointerException if {@code recipient} is null
* @throws IllegalStateException if the builder is already configured with a
* private key
*/
MultiRecipientCryptorBuilder addRecipient(PublicKey recipient);
/**
* Configures the builder for decryption using the specified private key. Once
* set, any previously added public key recipients will be ignored. This method
* cannot be used if recipients were already added.
*
* @param privateKey a non-null private key
* @return this builder instance
* @throws NullPointerException if {@code privateKey} is null
* @throws IllegalStateException if recipients have already been configured
*/
MultiRecipientCryptorBuilder privateKey(PrivateKey privateKey);
/**
* Adds a collection of decoy public keys for encryption mode. These keys will
* be indistinguishable from actual recipients but are not able to decrypt the
* message. Useful for obfuscating true recipients.
*
* @param decoys a non-null collection of public keys
* @return this builder instance
* @throws NullPointerException if {@code decoys} is null
* @throws IllegalStateException if the builder is already configured with a
* private key
*/
MultiRecipientCryptorBuilder addDecoys(Collection<PublicKey> decoys);
/**
* Adds a single decoy public key for encryption mode. Multiple calls are
* cumulative. Decoys do not participate in decryption.
*
* @param decoy a non-null public key
* @return this builder instance
* @throws NullPointerException if {@code decoy} is null
* @throws IllegalStateException if the builder is already configured with a
* private key
*/
MultiRecipientCryptorBuilder addDecoy(PublicKey decoy);
/**
* Creates a new builder instance.
*
* @return a fresh {@code MultiRecipientCryptorBuilder}
*/
static MultiRecipientCryptorBuilder builder() {
return new DefaultMultiRecipientCryptorBuilder();
}
/**
* Default implementation of {@link MultiRecipientCryptorBuilder}.
*/
final class DefaultMultiRecipientCryptorBuilder implements MultiRecipientCryptorBuilder {
private final Set<PublicKey> allRecipients = new HashSet<>();
private final Set<PublicKey> allDecoys = new HashSet<>();
private PrivateKey privateKeyField;
private DefaultMultiRecipientCryptorBuilder() {
}
@Override
public MultiRecipientCryptorBuilder addRecipients(final Collection<PublicKey> recipients) {
if (privateKeyField == null) {
Objects.requireNonNull(recipients, "recipients must not be null");
} else {
throw new IllegalStateException(
"Both public keys and the private key cannot be configured for the cryptor.");
}
allRecipients.addAll(recipients);
return this;
}
@Override
public MultiRecipientCryptorBuilder addRecipient(final PublicKey recipient) {
if (privateKeyField == null) {
Objects.requireNonNull(recipient, "recipient must not be null");
} else {
throw new IllegalStateException(
"Both public keys and the private key cannot be configured for the cryptor.");
}
allRecipients.add(recipient);
return this;
}
@Override
public MultiRecipientCryptorBuilder privateKey(final PrivateKey privateKey) {
if (!allRecipients.isEmpty() || !allDecoys.isEmpty()) {
throw new IllegalStateException(
"Both public keys and the private key cannot be configured for the cryptor.");
}
Objects.requireNonNull(privateKey, "privateKey must not be null");
this.privateKeyField = privateKey;
return this;
}
@Override
public MultiRecipientCryptorBuilder addDecoys(final Collection<PublicKey> decoys) {
if (privateKeyField != null) {
throw new IllegalStateException("Cannot add decoys when a private key is configured.");
}
Objects.requireNonNull(decoys, "decoys must not be null");
allDecoys.addAll(decoys);
return this;
}
@Override
public MultiRecipientCryptorBuilder addDecoy(final PublicKey decoy) {
if (privateKeyField != null) {
throw new IllegalStateException("Cannot add decoy when a private key is configured.");
}
Objects.requireNonNull(decoy, "decoy must not be null");
allDecoys.add(decoy);
return this;
}
@Override
public EncryptedContent build(final boolean encrypt) {
if (privateKeyField != null) {
if (encrypt) {
throw new IllegalStateException("You requested encryption with a private key.");
}
return new SecretMultiRecipientCryptor(privateKeyField);
}
if (allRecipients.isEmpty() && allDecoys.isEmpty()) {
throw new IllegalStateException("No recipients or decoys configured for encryption.");
}
if (!encrypt) {
throw new IllegalStateException("You requested decryption with public keys.");
}
PublicKey[] recipients = allRecipients.toArray(new PublicKey[0]);
PublicKey[] decoys = allDecoys.toArray(new PublicKey[0]);
return new SecretMultiRecipientCryptor(recipients, decoys);
}
}
}

View File

@@ -0,0 +1,110 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.builder;
import java.util.Objects;
import zeroecho.data.PlainContent;
import zeroecho.data.processing.PlainBytes;
/**
* Builder interface for constructing {@link PlainBytes} instances that
* encapsulate unencrypted byte array content.
* <p>
* This builder allows specifying a raw byte array to be wrapped as
* {@code PlainContent}, which can be used as input for cryptographic operations
* or as standalone plaintext data.
* <p>
* The {@link #build(boolean)} method accepts an {@code encrypt} flag for API
* consistency, but the flag has no effect for plain byte content.
*
* <p>
* <strong>Usage example:</strong>
*
* <pre>{@code
* PlainContent content = PlainBytesBuilder.builder()
* .bytes(new byte[] { 1, 2, 3, 4 })
* .build(false); // encrypt flag is ignored for PlainBytes
* }</pre>
*
* @see PlainBytes
*/
public interface PlainBytesBuilder extends DataContentBuilder<PlainContent> {
/**
* Sets the byte array content to be wrapped by {@link PlainBytes}.
*
* @param bytes the byte array; must not be null
* @return this builder instance for method chaining
* @throws NullPointerException if {@code bytes} is null
*/
PlainBytesBuilder bytes(byte[] bytes);
/**
* Creates a new instance of the default builder implementation.
*
* @return a new {@code PlainBytesBuilder}
*/
static PlainBytesBuilder builder() {
return new DefaultPlainBytesBuilder();
}
/**
* Default implementation of the {@link PlainBytesBuilder} interface.
* <p>
* Builds a {@link PlainBytes} instance wrapping the specified byte array.
* </p>
*/
final class DefaultPlainBytesBuilder implements PlainBytesBuilder {
private byte[] bytesField;
private DefaultPlainBytesBuilder() {
}
@Override
public PlainBytesBuilder bytes(final byte[] bytes) {
this.bytesField = Objects.requireNonNull(bytes, "bytes must not be null");
return this;
}
@Override
public PlainContent build(final boolean encrypt) {
if (bytesField == null) {
throw new IllegalStateException("bytes must be set before building");
}
return new PlainBytes(bytesField);
}
}
}

View File

@@ -0,0 +1,105 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.builder;
import java.net.URL;
import java.util.Objects;
import zeroecho.data.PlainContent;
import zeroecho.data.processing.PlainFile;
/**
* Builder interface for constructing {@link PlainContent} instances that
* represent unencrypted file-based content sourced from a {@link URL}.
* <p>
* This builder allows specifying the source URL of a file to be used as plain
* (non-encrypted) content. It is typically used as input for cryptographic
* operations or for representing raw file data.
* <p>
* The {@link #build(boolean)} method accepts an {@code encrypt} flag to comply
* with the {@link DataContentBuilder} interface; however, this flag is ignored
* for plain content types.
* <p>
* <strong>Usage example:</strong>
*
* <pre>{@code
* PlainContent plainFile = PlainFileBuilder.builder()
* .url(new URL("file:///path/to/file.txt"))
* .build(true); // encrypt flag is ignored for PlainFile
* }</pre>
*
* @see PlainFile
*/
public interface PlainFileBuilder extends DataContentBuilder<PlainContent> {
/**
* Sets the URL of the file to be used as the plain content source.
*
* @param url the URL of the file; must not be {@code null}
* @return this builder instance
*/
PlainFileBuilder url(URL url);
/**
* Creates a new instance of the default builder implementation.
*
* @return a new {@code PlainFileBuilder}
*/
static PlainFileBuilder builder() {
return new DefaultPlainFileBuilder();
}
/**
* Default implementation of the {@link PlainFileBuilder} interface.
* <p>
* Builds a {@link PlainFile} instance using the specified URL.
*/
final class DefaultPlainFileBuilder implements PlainFileBuilder {
private URL urlField;
private DefaultPlainFileBuilder() {
}
@Override
public PlainFileBuilder url(final URL url) {
this.urlField = Objects.requireNonNull(url);
return this;
}
@Override
public PlainContent build(final boolean encrypt) {
return new PlainFile(urlField);
}
}
}

View File

@@ -0,0 +1,101 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.builder;
import zeroecho.data.processing.PlainString;
/**
* Builder interface for constructing {@link PlainString} instances, which
* represent unencrypted textual content.
* <p>
* This builder supports specifying a string value to be encapsulated and
* optionally used in cryptographic processing pipelines, although the value
* itself remains unencrypted.
* <p>
* The {@link #build(boolean)} method accepts an {@code encrypt} flag to comply
* with the {@link DataContentBuilder} interface. However, this flag is ignored
* because {@code PlainString} represents unencrypted data.
* <p>
* Usage example:
*
* <pre>{@code
* PlainString plainText = PlainStringBuilder.builder()
* .value("Hello, World!")
* .build(true); // encrypt flag is ignored for PlainString
* }</pre>
*/
public interface PlainStringBuilder extends DataContentBuilder<PlainString> {
/**
* Creates a new instance of the default builder implementation.
*
* @return a new {@code PlainStringBuilder}
*/
static PlainStringBuilder builder() {
return new DefaultPlainStringBuilder();
}
/**
* Sets the string value that the {@link PlainString} instance will contain.
*
* @param value the string value; may be {@code null} or empty depending on
* usage
* @return this builder instance
*/
PlainStringBuilder value(String value);
/**
* Default implementation of the {@link PlainStringBuilder} interface.
* <p>
* Builds a {@link PlainString} instance using the specified string value.
*/
final class DefaultPlainStringBuilder implements PlainStringBuilder {
private String valueField;
private DefaultPlainStringBuilder() {
}
@Override
public PlainStringBuilder value(final String value) {
this.valueField = value;
return this;
}
@Override
public PlainString build(final boolean encrypt) {
return new PlainString(valueField);
}
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.builder;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
import zeroecho.data.processing.ReclassifiedPlain;
/**
* Builder interface for constructing {@link ReclassifiedPlain} instances, which
* wrap an existing {@link DataContent} stream and reclassify it as plain
* content without modifying the underlying data.
* <p>
* Typically used to repurpose an encrypted or transformed stream back into a
* plain data stream for further processing.
* <p>
* Usage example:
*
* <pre>{@code
* PlainContent reclassified = ReclassifiedPlainBuilder.builder().build();
* }</pre>
*/
@Deprecated
public interface ReclassifiedPlainBuilder extends DataContentBuilder<PlainContent> { // NOPMD
/**
* Creates a new instance of the default {@code ReclassifiedPlainBuilder}.
*
* @return a new builder instance
*/
static ReclassifiedPlainBuilder builder() {
return new DefaultReclassifiedPlainBuilder();
}
/**
* Default implementation of {@link ReclassifiedPlainBuilder}.
*/
final class DefaultReclassifiedPlainBuilder implements ReclassifiedPlainBuilder {
private DefaultReclassifiedPlainBuilder() {
}
@Override
public PlainContent build(final boolean encrypt) {
return new ReclassifiedPlain();
}
}
}

View File

@@ -0,0 +1,89 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/**
* Provides builder interfaces and implementations for constructing various
* {@link zeroecho.data.DataContent} types and assembling content processing
* pipelines, including encryption and decryption workflows.
* <p>
* This package offers a modular and extensible approach to build
* {@code DataContent} chains in a fluent style. It currently supports:
* <ul>
* <li>Plain content builders such as {@link PlainStringBuilder} and
* {@link PlainFileBuilder}</li>
* <li>Symmetric encryption parameter builders, including
* {@link DerivedAesParametersBuilder} and {@link AesBuilder}</li>
* <li>Utility builders like {@link ReclassifiedPlainBuilder} for stream
* reclassification</li>
* </ul>
* <p>
* The builders can be combined to form flexible data processing pipelines. Each
* builder produces an instance of {@code DataContent} or its subtype, which can
* be chained by setting the input stream of the next stage.
* <p>
* <b>Example usage:</b>
*
* <pre>{@code
* DataContent output = DataContentChainBuilder.builder()
* // Start from a plain string input
* .add(PlainStringBuilder.builder().value("Example plaintext"))
* // Encrypt the input
* .add(AesBuilder.builder().encrypt())
* // Apply derived AES parameters with a password
* .add(DerivedAesParametersBuilder.builder().password("secretPassword").iterations(10000).mode(AesMode.AES_256)
* .cipherType(AesCipherType.CBC))
* // Reclassify encrypted stream as plain for further processing
* .add(ReclassifiedPlainBuilder.builder())
* // Decrypt using the same password-derived parameters
* .add(DerivedAesParametersBuilder.builder().password("secretPassword").iterations(10000).mode(AesMode.AES_256)
* .cipherType(AesCipherType.CBC))
* .add(AesBuilder.builder().decrypt())
* // Build the final data content chain
* .build();
* String decrypted = output.toText();
* }</pre>
* <p>
* Future extensions will include additional cryptographic parameter builders
* (e.g., for asymmetric algorithms), and support for other data content
* transformations.
*
* @author Leo Galambos
* @see zeroecho.data.DataContent
* @see PlainStringBuilder
* @see PlainFileBuilder
* @see DerivedAesParametersBuilder
* @see AesBuilder
* @see ReclassifiedPlainBuilder
*/
package zeroecho.builder;

View File

@@ -0,0 +1,178 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.covert;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Queue;
import java.util.TreeMap;
import zeroecho.util.RandomSupport;
/**
* A utility class for generating pseudo-random textual content based on
* predefined character frequency distributions.
* <p>
* The {@code TextualCodec} class contains a nested {@link Generator} class that
* can be configured with a set of character frequencies (e.g., English letter
* frequencies) to produce text that mimics the statistical distribution of the
* given language or character set.
*/
public class TextualCodec { // NOPMD
/**
* Private constructor to prevent instantiation of {@code TextualCodec}.
* <p>
* This class is intended to be used as a utility container for static members
* and should not be instantiated.
*/
private TextualCodec() {
}
/**
* Generates characters or strings using a frequency-based distribution.
* <p>
* This generator uses a cumulative frequency table internally to map random
* numbers to characters, enabling the creation of realistic-looking text that
* follows the given character frequency distribution. The generator also avoids
* consecutive duplicate characters by employing a simple queuing mechanism.
*/
public static class Generator {
/**
* Internal map representing the cumulative frequency ranges mapped to
* characters.
*/
private final NavigableMap<Double, Character> ranges = new TreeMap<>();
/**
* The maximum value of the cumulative frequency range.
*/
private final double maxRange;
/**
* A predefined English character frequency distribution including the space
* character, based on typical usage in English text.
*/
public final static Map<Character, Double> ENGLISH = Map.ofEntries(Map.entry('a', 8.2), Map.entry('b', 1.5),
Map.entry('c', 2.8), Map.entry('d', 4.3), Map.entry('e', 12.7), Map.entry('f', 2.2),
Map.entry('g', 2.0), Map.entry('h', 6.1), Map.entry('i', 7.0), Map.entry('j', 0.15),
Map.entry('k', 0.77), Map.entry('l', 4.0), Map.entry('m', 2.4), Map.entry('n', 6.7),
Map.entry('o', 7.5), Map.entry('p', 1.9), Map.entry('q', 0.095), Map.entry('r', 6.0),
Map.entry('s', 6.3), Map.entry('t', 9.1), Map.entry('u', 2.8), Map.entry('v', 0.98),
Map.entry('w', 2.4), Map.entry('x', 0.15), Map.entry('y', 2.0), Map.entry('z', 0.074),
Map.entry(' ', 25.4));
/**
* A default generator using the {@link #ENGLISH} frequency distribution.
*/
public final static Generator EN = new Generator(ENGLISH);
private Character lastChar = '~';
private final Queue<Character> backlog = new ArrayDeque<>();
/**
* Constructs a new {@code Generator} with the specified character frequency
* distribution.
*
* @param frequencies a map of characters to their relative frequencies (must be
* non-negative)
*/
public Generator(Map<Character, Double> frequencies) {
double cumulative = 0.0;
for (Map.Entry<Character, Double> entry : frequencies.entrySet()) {
double freq = entry.getValue();
if (freq <= 0) {
continue;
}
ranges.put(cumulative, entry.getKey());
cumulative = cumulative + freq;
}
maxRange = cumulative;
}
/**
* Generates a string of the specified length using the configured character
* frequency distribution. Consecutive duplicate characters are avoided when
* possible.
*
* @param length the number of characters to generate
* @return a randomly generated string
*/
public String getText(int length) {
StringBuffer sb = new StringBuffer();
while (length-- > 0) { // NOPMD
sb.append(getChar());
}
return sb.toString();
}
/**
* Returns the next randomly generated character, avoiding consecutive
* duplicates when possible.
*
* @return the next character in the generated sequence
*/
public char getChar() {
if (backlog.isEmpty() || lastChar.equals(backlog.peek())) {
Character next = getChar(RandomSupport.getRandom().nextDouble(maxRange));
while (lastChar.equals(next)) {
backlog.add(next);
next = getChar(RandomSupport.getRandom().nextDouble(maxRange));
}
lastChar = next;
return lastChar;
}
return lastChar = backlog.poll();
}
/**
* Returns a character based on the provided value in the frequency range.
*
* @param value a value between 0 (inclusive) and {@code maxRange} (exclusive)
* @return the corresponding character for the specified value
* @throws IllegalArgumentException if the value is out of range
*/
public char getChar(double value) {
if (value < 0.0 || value >= maxRange) {
throw new IllegalArgumentException("Value must be in [0.0, " + maxRange + ")");
}
Map.Entry<Double, Character> entry = ranges.floorEntry(value);
return (entry == null) ? ranges.firstEntry().getValue() : entry.getValue();
}
}
}

View File

@@ -0,0 +1,166 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.covert.jpeg;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter;
import org.apache.commons.imaging.formats.tiff.fieldtypes.AbstractFieldType;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputField;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
/**
* Embeds binary payloads into the EXIF metadata of a JPEG file using predefined
* {@link SlotType}s.
* <p>
* This class enables lossless modification of JPEG images by injecting custom
* data into specific EXIF fields. These fields are typically unused or
* repurposable for application-specific steganography or metadata tagging.
* </p>
*
* <p>
* The embedding process respects total capacity constraints and allows per-slot
* overrides to fine-tune the number of bytes stored in each {@link SlotType}.
* </p>
*/
public class JpegExifEmbedder {
/**
* Optional overrides for the default capacity of each {@link SlotType}. This
* allows finer control over how the payload is partitioned and embedded.
*/
private final Map<SlotType, Integer> capacityOverrides = new EnumMap<>(SlotType.class); // NOPMD
/**
* Constructs a new instance of {@code JpegExifEmbedder}.
* <p>
* This default constructor initializes the embedder without any specific
* configuration. Further setup may be required before using it to embed EXIF
* metadata into JPEG images.
*/
public JpegExifEmbedder() { // NOPMD
// empty
}
/**
* Overrides the default capacity for a specific EXIF {@link SlotType}.
*
* @param slot the EXIF slot whose capacity should be overridden
* @param bytes the new capacity in bytes for this slot
*/
public void overrideCapacity(SlotType slot, int bytes) {
capacityOverrides.put(slot, bytes);
}
/**
* Embeds the given payload into the EXIF metadata of a JPEG file and writes the
* result to the provided output stream.
* <p>
* The payload is split across the available EXIF {@link SlotType}s. The
* embedding respects the total capacity defined either by defaults or the
* overridden values.
* </p>
*
* @param jpegPath the path to the source JPEG file
* @param payloadInput the input stream containing the binary payload to embed
* @param jpegOutput the output stream to which the modified JPEG is written
* @throws IOException if file access or modification fails
* @throws IllegalArgumentException if the payload exceeds the total EXIF
* capacity
*/
public void embed(Path jpegPath, InputStream payloadInput, OutputStream jpegOutput) throws IOException {
byte[] jpegBytes = Files.readAllBytes(jpegPath);
byte[] payload = payloadInput.readAllBytes();
int totalCapacity = 0;
for (SlotType slot : SlotType.values()) {
totalCapacity += capacityOverrides.getOrDefault(slot, slot.defaultCapacity);
}
if (payload.length > totalCapacity) {
throw new IllegalArgumentException("Payload too large. Max capacity: " + totalCapacity + " bytes.");
}
Map<SlotType, byte[]> slotMap = splitPayload(payload);
JpegImageMetadata jpegMetadata = (JpegImageMetadata) Imaging.getMetadata(jpegBytes);
TiffOutputSet outputSet = (jpegMetadata != null && jpegMetadata.getExif() != null)
? jpegMetadata.getExif().getOutputSet()
: new TiffOutputSet();
TiffOutputDirectory exifDirectory = outputSet.getOrCreateExifDirectory();
for (Map.Entry<SlotType, byte[]> entry : slotMap.entrySet()) {
SlotType slot = entry.getKey();
byte[] data = entry.getValue();
exifDirectory.removeField(slot.tagInfo);
exifDirectory.add(new TiffOutputField(slot.tagInfo, AbstractFieldType.BYTE, data.length, data)); // NOPMD
}
try (ByteArrayInputStream jpegInputStream = new ByteArrayInputStream(jpegBytes)) {
new ExifRewriter().updateExifMetadataLossless(jpegInputStream, jpegOutput, outputSet);
}
}
private Map<SlotType, byte[]> splitPayload(byte[] payload) {
Map<SlotType, byte[]> result = new LinkedHashMap<>(); // NOPMD
int offset = 0;
for (SlotType slot : SlotType.values()) {
int capacity = capacityOverrides.getOrDefault(slot, slot.defaultCapacity);
if (offset > payload.length) {
break;
}
int chunkSize = Math.min(capacity, payload.length - offset);
byte[] chunk = Arrays.copyOfRange(payload, offset, offset + chunkSize);
result.put(slot, chunk);
offset += chunkSize;
}
return result;
}
}

View File

@@ -0,0 +1,140 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.covert.jpeg;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumMap;
import java.util.Map;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
import org.apache.commons.imaging.formats.tiff.TiffField;
/**
* Extracts embedded binary payloads from the EXIF metadata of a JPEG image.
* <p>
* This class is designed to reverse the embedding process performed by
* {@code JpegExifEmbedder}. It reads a JPEG file, locates specific EXIF fields
* designated for data storage (as defined by {@link SlotType}), and
* reconstructs the original payload into a binary stream.
* </p>
*
* <p>
* The extraction respects custom per-slot capacity overrides, ensuring that
* embedded payloads that don't fully utilize the maximum slot size can be
* detected by an early termination during read.
* </p>
*
* <p>
* This class assumes that the EXIF fields were populated sequentially and that
* the payload ends when a field is smaller than its declared or default
* capacity.
* </p>
*/
public class JpegExifExtractor {
/**
* Optional overrides for the capacity of each {@link SlotType}. These can be
* used to control how much data is expected from each slot during extraction.
*/
private final Map<SlotType, Integer> capacityOverrides = new EnumMap<>(SlotType.class); // NOPMD
/**
* Constructs a new instance of {@code JpegExifExtractor}.
* <p>
* This default constructor creates an extractor that can be used to read and
* retrieve EXIF metadata from JPEG image files.
*/
public JpegExifExtractor() { // NOPMD
// empty
}
/**
* Sets a custom capacity for the given slot. This allows the extractor to
* determine how much data to expect from each EXIF field.
*
* @param slot the slot whose capacity is being overridden
* @param bytes the maximum number of bytes expected from this slot
*/
public void overrideCapacity(SlotType slot, int bytes) {
capacityOverrides.put(slot, bytes);
}
/**
* Extracts an embedded binary payload from the EXIF fields of the specified
* JPEG image.
* <p>
* This method reads the JPEG file, parses its EXIF metadata, and collects
* binary data from all configured {@link SlotType} entries in the order they
* are defined. If a field contains fewer bytes than its capacity (default or
* overridden), extraction is stopped early, assuming the end of the payload has
* been reached.
* </p>
*
* @param jpegPath the path to the JPEG file containing embedded data
* @param payloadOutput the output stream to which the extracted binary data
* will be written
* @throws IOException if an I/O error occurs during file reading
* or writing
* @throws IllegalArgumentException if the JPEG file does not contain valid EXIF
* metadata
*/
public void extract(Path jpegPath, OutputStream payloadOutput) throws IOException {
byte[] jpegBytes = Files.readAllBytes(jpegPath);
JpegImageMetadata jpegMetadata = (JpegImageMetadata) Imaging.getMetadata(jpegBytes);
if (jpegMetadata == null || jpegMetadata.getExif() == null) {
throw new IllegalArgumentException("No EXIF metadata found.");
}
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
for (SlotType slot : SlotType.values()) {
TiffField field = jpegMetadata.findExifValue(slot.tagInfo);
if (field != null && field.getByteArrayValue() != null) {
final byte[] data = field.getByteArrayValue();
buffer.write(data);
if (data.length < capacityOverrides.getOrDefault(slot, slot.defaultCapacity)) {
break;
}
}
}
payloadOutput.write(buffer.toByteArray());
}
}

View File

@@ -0,0 +1,93 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.covert.jpeg;
import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo;
/**
* Represents predefined EXIF metadata slots that can be used for embedding or
* extracting textual data within a JPEG image's metadata.
* <p>
* Each slot is associated with a specific {@link TagInfo} EXIF tag and a
* default capacity (in bytes), which can be used to determine how much data can
* be stored in that slot.
*/
public enum SlotType {
/**
* EXIF tag used for user comments. Commonly used for storing textual
* annotations.
*/
USER_COMMENT(ExifTagConstants.EXIF_TAG_USER_COMMENT, 4096),
/**
* EXIF tag typically used by camera manufacturers to store proprietary data.
*/
MAKER_NOTE(ExifTagConstants.EXIF_TAG_MAKER_NOTE, 4096),
/**
* EXIF tag indicating the version of the EXIF specification used.
*/
EXIF_VERSION(ExifTagConstants.EXIF_TAG_EXIF_VERSION, 1024),
/**
* EXIF tag representing the software used to process or generate the image.
*/
SOFTWARE(ExifTagConstants.EXIF_TAG_SOFTWARE, 2048);
/**
* The EXIF {@link TagInfo} associated with this slot.
*/
public final TagInfo tagInfo; // NOPMD
/**
* The default storage capacity (in bytes) for this EXIF slot.
*/
public final int defaultCapacity;
/**
* Constructs a {@code SlotType} with the specified EXIF tag and default
* capacity.
*
* @param tagInfo the {@link TagInfo} representing the EXIF tag
* @param defaultCapacity the default capacity in bytes for this slot
*/
SlotType(TagInfo tagInfo, int defaultCapacity) {
this.tagInfo = tagInfo;
this.defaultCapacity = defaultCapacity;
}
}

View File

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

View File

@@ -0,0 +1,61 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Provides implementations of covert data embedding and extraction techniques.
* <p>
* The {@code zeroecho.covert} package includes utilities for hiding binary
* payloads within standard media formats, particularly images, using
* metadata-based steganography and other non-invasive channels. These
* techniques are designed to conceal the presence of data without altering the
* visible or functional aspects of the host content.
* <p>
* Current capabilities focus on:
* <ul>
* <li>Embedding encrypted or unstructured data into JPEG EXIF metadata
* fields</li>
* <li>Configurable allocation across multiple EXIF fields with capacity
* enforcement</li>
* <li>Reversible extraction pipelines for recovery of embedded payloads</li>
* <li>Modular design suitable for extending to new carrier formats and
* methods</li>
* </ul>
* Future support may include techniques based on file structure abuse, protocol
* headers, or steganographic encoding in binary or multimedia content.
* <p>
* This package is intended for research, secure communication, and
* privacy-preserving applications where the existence of a payload must be
* obfuscated or made non-obvious.
*/
package zeroecho.covert;

View File

@@ -0,0 +1,131 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
/**
* Represents a generic unit of data content. This may include plain, secret, or
* encrypted forms of data.
*/
public interface DataContent { // NOPMD
/**
* Default maximum number of bytes that {@link #toBytes()} will read. This
* prevents accidental memory overload when reading large content into RAM.
*/
int DEFAULT_MAX_READ_SIZE = 32 * 1024 * 1024; // 32 MB
/**
* Returns an {@link InputStream} representing the content's data.
* <p>
* The stream may be raw or transformed depending on the content type.
*
* @return input stream of the content; never {@code null}
* @throws IOException if an I/O error occurs opening the stream
*/
InputStream getStream() throws IOException;
/**
* Returns the content's data as a byte array, with a size limit of 16MB by
* default.
* <p>
* This method is intended for small to medium-sized content. If the content
* exceeds {@link #DEFAULT_MAX_READ_SIZE}, a {@link IllegalStateException} is
* thrown.
*
* @return the content data as {@code byte[]}
* @throws RuntimeException if reading from the stream fails
* @throws IllegalStateException if content exceeds allowed memory limit
*/
default byte[] toBytes() {
try (InputStream in = getStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
final byte[] buffer = new byte[4096];
int total = 0;
int n;
while ((n = in.read(buffer)) != -1) { // NOPMD
total += n;
if (total > DEFAULT_MAX_READ_SIZE) {
throw new IllegalStateException("Content too large to buffer (" + total + " bytes)");
}
out.write(buffer, 0, n);
}
return out.toByteArray();
} catch (IOException e) {
throw new UncheckedIOException("Failed to read content stream", e);
}
}
/**
* Returns the content's data as a UTF-8 encoded string.
* <p>
* The default implementation converts {@code toBytes()} using UTF-8.
* <strong>Warning:</strong> This method is only safe if the content represents
* valid UTF-8 text. For arbitrary binary data (e.g., encrypted content), this
* may produce invalid or garbled output.
*
* @return the content data as {@code String}
* @throws RuntimeException if reading from the stream fails
* @throws IllegalArgumentException if the bytes are not valid UTF-8 (optional)
*/
default String toText() {
return new String(toBytes(), StandardCharsets.UTF_8);
}
/**
* Connects this content to the output of a previous stage in the pipeline.
* <p>
* This method is used to supply the input data that this content will process
* (e.g., a {@code PlainContent} being encrypted, or an encrypted content being
* decrypted).
* </p>
*
* @param input the {@code DataContent} instance providing upstream data; may be
* {@code null} if this is the start of the pipeline
* @throws UnsupportedOperationException if not overridden by a subclass
* @throws IllegalArgumentException if the input is not allowed by the
* implementation
*
* @implSpec The default implementation of this method throws
* {@link UnsupportedOperationException}. Subclasses that support
* chaining must override this method.
*/
default void setInput(DataContent input) {
throw new UnsupportedOperationException("This content type does not accept input chaining.");
}
}

View File

@@ -0,0 +1,45 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data;
/**
* Represents encrypted content, which is the result of an encryption process
* and can be safely deployed to a public space without security concerns.
* Deployment methods may include saving to a file, writing to standard output,
* or using steganography for enhanced secrecy.
*/
public interface EncryptedContent extends DataContent { // NOPMD
}

View File

@@ -0,0 +1,99 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data;
/**
* An extension of {@link DataContent} that provides export capabilities in
* different modes.
* <p>
* This interface is intended for data sources that support not only raw binary
* streaming but also platform-specific script export, such as Bash or Windows
* CMD. The export mode determines how the underlying content is transformed or
* wrapped.
*
* <p>
* Typical use cases include:
* <ul>
* <li>Uploading images or data directly to a remote server (RAW mode)</li>
* <li>Generating self-contained Bash scripts that embed the encoded data
* (BASH_SCRIPT mode)</li>
* <li>Generating CMD scripts for use on Windows systems (CMD_SCRIPT mode)</li>
* </ul>
*
* <p>
* Implementations are responsible for adapting the content to the selected
* {@code ExportMode}.
*
* @see ExportableDataContent.ExportMode
* @see DataContent
*/
public interface ExportableDataContent extends DataContent {
/**
* Enumeration of supported export modes.
*/
enum ExportMode {
/**
* Raw mode returns the content as a direct InputStream, without any
* transformation or encoding.
*/
RAW,
/**
* Bash script mode returns a shell script with embedded Base64-encoded content,
* suitable for execution on Unix-like systems.
*/
BASH_SCRIPT,
/**
* CMD script mode returns a Windows batch file containing encoded content that
* can be decoded and used locally.
*/
CMD_SCRIPT
}
/**
* Returns the current export mode.
*
* @return the export mode that determines how the content will be formatted or
* transformed
*/
ExportMode getExportMode();
/**
* Sets the desired export mode.
*
* @param mode the export mode to use; must not be {@code null}
* @throws NullPointerException if the mode is {@code null}
*/
void setExportMode(ExportMode mode);
}

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data;
/**
* Represents a plain content that includes some original content, such as data
* from interactive input, a file, or the result of decrypting encrypted
* content.
*/
public interface PlainContent extends DataContent { // NOPMD
}

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data;
/**
* Represents secret content, which is a specific type of plain content that
* stores a secret phrase. This phrase may be obtained from input, read from a
* file, or generated dynamically.
*/
public interface SecretContent extends PlainContent { // NOPMD
}

View File

@@ -0,0 +1,104 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data.output;
import java.util.Objects;
import zeroecho.data.DataContent;
import zeroecho.data.ExportableDataContent;
/**
* An abstract base class for exportable data content, providing default
* implementations for managing input data and export mode.
*
* <p>
* This class is intended to be extended by concrete implementations that
* generate various forms of export output, such as raw binary streams, shell
* scripts, or platform-specific wrappers.
* </p>
*
* <p>
* It manages:
* <ul>
* <li>The {@link DataContent} input that is to be exported</li>
* <li>The {@link ExportMode} that controls how the export is formatted</li>
* </ul>
*
* <p>
* Implementing classes must override {@link ExportableDataContent#getStream()}
* to provide the actual export logic based on the configured mode.
* </p>
*/
abstract class AbstractExportableDataContent implements ExportableDataContent {
/**
* The export mode (RAW, BASH_SCRIPT, CMD_SCRIPT, etc.). Defaults to RAW.
*/
protected ExportMode mode = ExportMode.RAW;
/**
* The input data to be exported. Must be non-null before use.
*/
protected DataContent input;
/**
* Sets the input {@link DataContent} for export.
*
* @param input the input data to be exported
* @throws NullPointerException if {@code input} is {@code null}
*/
@Override
public void setInput(DataContent input) {
this.input = Objects.requireNonNull(input, "Input content cannot be null.");
}
/**
* Returns the currently selected export mode.
*
* @return the {@link ExportMode} in use
*/
@Override
public ExportMode getExportMode() {
return mode;
}
/**
* Sets the export mode to control how the data is output.
*
* @param mode the desired {@link ExportMode}
*/
@Override
public void setExportMode(ExportMode mode) {
this.mode = mode;
}
}

View File

@@ -0,0 +1,194 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data.output;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.Base64.Encoder;
/**
* A streaming {@link InputStream} that encodes binary input data into Base64
* format with optional line prefixes and suffixes for each encoded line.
*
* <p>
* This class is designed for script-friendly output formatting, such as
* generating platform-specific batch or shell script lines where each line
* might begin with a command (e.g., {@code echo }) and end with a
* platform-specific line terminator (e.g., {@code \n} for UNIX-like systems or
* {@code \r\n} for Windows).
* </p>
*
* <p>
* It supports chunked streaming and avoids holding the entire Base64-encoded
* content in memory, making it suitable for large binary inputs. Lines are
* split according to the specified maximum line length and are composed as
* follows:
* </p>
*
* <pre>
* [prefix][base64-encoded data][suffix]
* </pre>
*
* <p>
* If a suffix is defined, the actual Base64 data per line will be truncated to
* {@code lineLength - suffix.length} characters to ensure total line length
* does not exceed {@code lineLength}.
* </p>
*
* <p>
* This stream does not automatically insert newlines between lines unless
* explicitly provided via the {@code suffix} argument (e.g.,
* {@code "\n".getBytes()}).
* </p>
*
* <p>
* <strong>Usage example:</strong>
* </p>
*
* <pre>{@code
* InputStream source = new FileInputStream("input.bin");
* InputStream encoded = new Base64Stream(
* source,
* "echo ".getBytes(StandardCharsets.UTF_8),
* 76,
* "\n".getBytes(StandardCharsets.UTF_8)
* );
* encoded.transferTo(System.out);
* }</pre>
*
* @author Leo Galambos
*/
public class Base64Stream extends InputStream {
InputStream source;
InputStream in, prefix, suffix;
Encoder base64;
int pos = 0;
final int lineLength;
int lineBreak;
/**
* Constructs a new {@code Base64Stream}.
*
* @param source the raw binary input stream to be Base64 encoded
* @param linePrefix optional prefix to prepend at the beginning of each line
* (e.g., {@code "echo "}); can be {@code null} for no prefix
* @param lineLength the total maximum length of each output line, including any
* prefix and suffix
* @param lineSuffix optional suffix to append at the end of each line (e.g.,
* newline); can be {@code null}
*/
public Base64Stream(InputStream source, byte[] linePrefix, int lineLength, byte[] lineSuffix) {
this.source = source;
this.lineLength = lineLength;
in = InputStream.nullInputStream();
base64 = Base64.getEncoder();
if (linePrefix != null) {
prefix = new ByteArrayInputStream(linePrefix);
}
if (lineSuffix != null) {
suffix = new ByteArrayInputStream(lineSuffix);
}
lineBreak = lineLength - ((lineSuffix == null) ? 0 : lineSuffix.length);
}
@Override
public int read() throws IOException {
byte[] result = { 0 };
int count = read(result, 0, 1);
return (count == 0) ? -1 : result[0] & 0xff;
}
InputStream is = InputStream.nullInputStream();
int breakPos;
boolean closed = false;
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (pos == lineLength) {
pos = 0;
}
if (pos == 0 && prefix != null) {
prefix.reset();
breakPos = Integer.MAX_VALUE;
is = prefix;
} else {
if (pos == lineBreak && suffix != null && !closed) {
suffix.reset();
breakPos = Integer.MAX_VALUE;
is = suffix;
} else {
if (in.available() == 0) {
ensureData();
}
breakPos = lineBreak;
is = in;
}
}
int l = Math.min(Math.min(is.available(), len), breakPos - pos);
pos += l;
return is.read(b, off, l);
}
final static int TRIPLES_INPUT = 1000;
/**
* Reads the next block of raw bytes from the source and encodes them into
* Base64. Handles stream exhaustion and final line suffix if applicable.
*/
private void ensureData() throws IOException {
byte[] buf = source.readNBytes(3 * TRIPLES_INPUT);
if (buf.length == 0 && suffix != null && !closed) {
System.out.println("suffix");
closed = true;
suffix.reset();
breakPos = lineBreak = Integer.MAX_VALUE;
in = suffix;
return;
}
if (buf.length != 3 * TRIPLES_INPUT) {
in = new ByteArrayInputStream(base64.encode(buf));
} else {
in = new ByteArrayInputStream(base64.withoutPadding().encode(buf));
}
}
}

View File

@@ -0,0 +1,231 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data.output;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.SequenceInputStream;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* {@code PiwigoExportDataContent} is a specialized exportable content class
* that supports uploading an image file to a Piwigo photo gallery server,
* either directly via HTTP POST or by generating platform-specific scripts for
* deferred uploading.
*
* <p>
* Depending on the export mode (RAW, BASH_SCRIPT, CMD_SCRIPT), it can:
* <ul>
* <li>Upload the image to a Piwigo server directly using HTTP
* multipart/form-data</li>
* <li>Generate a Bash script that decodes the base64-encoded image and uploads
* it using curl</li>
* <li>Generate a CMD (Windows batch) script that reconstructs the image using
* certutil and uploads it</li>
* </ul>
*
* <p>
* This class integrates with {@code AbstractExportableDataContent} and expects
* its {@code input} field to be set before invoking {@link #getStream()}.
* </p>
*
* <p>
* The image is uploaded using the {@code pwg.images.add} method of the Piwigo
* API.
* </p>
*/
class PiwigoExportDataContent extends AbstractExportableDataContent {
private final String imageFileName;
private final String piwigoUrl;
private final String username;
private final String password;
private final String albumId;
/**
* Constructs a new exportable Piwigo upload object.
*
* @param imageFileName the name of the image file to assign during upload or
* script output
* @param piwigoUrl the URL of the Piwigo API endpoint
* @param username the Piwigo username for authentication
* @param password the Piwigo password
* @param albumId the ID of the Piwigo album to which the image will be
* uploaded
*/
public PiwigoExportDataContent(String imageFileName, String piwigoUrl, String username, String password,
String albumId) {
this.imageFileName = imageFileName;
this.piwigoUrl = piwigoUrl;
this.username = username;
this.password = password;
this.albumId = albumId;
}
/**
* Returns an {@code InputStream} that provides either the raw upload stream, or
* a platform-specific script depending on the export mode.
*
* @return the resulting {@code InputStream}
* @throws IOException if reading the input or creating the stream fails
*/
@Override
public InputStream getStream() throws IOException {
if (input == null) {
throw new IllegalStateException("Input not set.");
}
return switch (mode) {
case RAW -> performDirectUpload(input.getStream());
case BASH_SCRIPT -> generateBashScript(input.getStream());
case CMD_SCRIPT -> generateCmdScript(input.getStream());
};
}
/**
* Performs a direct upload of the image data to the Piwigo server using a
* multipart/form-data HTTP POST request.
*
* @param dataStream the input stream of the binary image data
* @return the server's response stream
* @throws IOException if the upload fails
*/
private InputStream performDirectUpload(InputStream dataStream) throws IOException {
String boundary = "----Boundary" + System.currentTimeMillis();
URL url = URI.create(piwigoUrl).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
try (OutputStream out = conn.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
writeFormField(writer, boundary, "method", "pwg.images.add");
writeFormField(writer, boundary, "username", username);
writeFormField(writer, boundary, "password", password);
writeFormField(writer, boundary, "category", albumId);
writer.write("--" + boundary + "\r\n");
writer.write("Content-Disposition: form-data; name=\"image\"; filename=\"" + imageFileName + "\"\r\n");
writer.write("Content-Type: image/jpeg\r\n\r\n");
writer.flush();
dataStream.transferTo(out);
out.flush();
writer.write("\r\n--" + boundary + "--\r\n");
writer.flush();
}
InputStream responseStream;
try {
responseStream = conn.getInputStream();
} catch (IOException e) {
InputStream errorStream = conn.getErrorStream();
if (errorStream != null) {
return errorStream;
}
return new ByteArrayInputStream(("Error: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
}
return responseStream;
}
/**
* Writes a single form field as part of a multipart/form-data HTTP request.
*
* @param writer the writer to output the field to
* @param boundary the multipart boundary
* @param name the name of the form field
* @param value the value of the form field
* @throws IOException if writing fails
*/
private void writeFormField(Writer writer, String boundary, String name, String value) throws IOException {
writer.write("--" + boundary + "\r\n");
writer.write("Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n");
writer.write(value + "\r\n");
}
/**
* Generates a Bash script that reconstructs the image using a Base64 heredoc
* block and uploads it using {@code curl}.
*
* @param originalStream the original binary stream of the image
* @return a stream containing the complete shell script
*/
private InputStream generateBashScript(InputStream originalStream) {
InputStream header = new ByteArrayInputStream(("#!/bin/bash\nset -e\n\ncurl -X POST \"" + piwigoUrl + "\" \\\n"
+ " -F method=\"pwg.images.add\" \\\n" + " -F username=\"" + username + "\" \\\n" + " -F password=\""
+ password + "\" \\\n" + " -F category=\"" + albumId + "\" \\\n" + " -F image=@<(base64 -d <<'EOF'\n")
.getBytes(StandardCharsets.UTF_8));
@SuppressWarnings("resource")
InputStream body = new Base64Stream(originalStream, null, 76, new byte[] { 10 });
InputStream footer = new ByteArrayInputStream("EOF\n)\n".getBytes(StandardCharsets.UTF_8));
return new SequenceInputStream(new SequenceInputStream(header, body), footer);
}
/**
* Generates a CMD batch script that reconstructs the image using certutil and
* uploads it using {@code curl}.
*
* @param originalStream the original binary stream of the image
* @return a stream containing the complete Windows batch script
*/
private InputStream generateCmdScript(InputStream originalStream) {
InputStream header = new ByteArrayInputStream(
("@echo off\nsetlocal\necho -----BEGIN BASE64----- > tmp.b64\n").getBytes(StandardCharsets.UTF_8));
@SuppressWarnings("resource")
InputStream body = new Base64Stream(originalStream, "echo ".getBytes(), 76, " >> tmp.b64\r\n".getBytes());
InputStream footer = new ByteArrayInputStream(
("echo -----END BASE64----- >> tmp.b64\n" + "certutil -decode tmp.b64 \"" + imageFileName + "\" >nul\n"
+ "del tmp.b64\n" + "curl -X POST \"" + piwigoUrl + "\" ^\n" + " -F method=pwg.images.add ^\n"
+ " -F username=" + username + " ^\n" + " -F password=" + password + " ^\n" + " -F category="
+ albumId + " ^\n" + " -F image=@" + imageFileName + "\n" + "del \"" + imageFileName + "\"\n")
.getBytes(StandardCharsets.UTF_8));
return new SequenceInputStream(new SequenceInputStream(header, body), footer);
}
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Provides classes for exporting binary data in various formats including:
* <ul>
* <li>Raw binary stream uploads</li>
* <li>Platform-specific shell script encodings (e.g., Bash or Windows CMD)</li>
* <li>Base64 transformation and line-wrapping</li>
* </ul>
*
* <p>
* The core components of this package include:
* </p>
* <ul>
* <li>{@link zeroecho.data.output.Base64Stream} a stream that applies Base64
* encoding and formats output per-line with optional prefixes and
* suffixes.</li>
* <li>{@link zeroecho.data.output.AbstractExportableDataContent} a reusable
* base class for any exportable content, managing input and export mode.</li>
* <li>{@link zeroecho.data.output.PiwigoExportDataContent} an implementation
* that exports images to a Piwigo gallery, supporting direct upload or
* script-based methods.</li>
* </ul>
*
* <p>
* These tools allow platform-neutral data packaging and transport, and support
* automation scenarios.
* </p>
*/
package zeroecho.data.output;

View File

@@ -0,0 +1,53 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/**
* Provides abstractions for representing different types of content used in
* secure data handling.
* <p>
* The package defines the following interfaces:
* <ul>
* <li>{@link PlainContent} Represents unprocessed or original content
* obtained from input, files, or after decryption.</li>
* <li>{@link SecretContent} Represents sensitive plain content, typically a
* secret phrase that may be input manually, read from a file, or generated
* dynamically.</li>
* <li>{@link EncryptedContent} Represents the result of encrypting content,
* which can be safely shared or stored publicly. It may also support
* steganographic methods for additional secrecy.</li>
* </ul>
* These interfaces form the foundation for building secure data exchange
* mechanisms and cryptographic workflows.
*/
package zeroecho.data;

View File

@@ -0,0 +1,139 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data.processing;
import java.util.Objects;
import conflux.Ctx;
import conflux.Key;
import zeroecho.data.DataContent;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
/**
* A base class for AES encryption and decryption utilities.
*
* <p>
* This class manages AES key material, initialization vectors (IVs), optional
* Additional Authenticated Data (AAD), cipher type selection, and block size
* configuration. It is designed to be extended by more specific encryption or
* decryption implementations that rely on a consistent context for AES
* parameter management.
* </p>
*
* <p>
* Instances automatically populate the shared {@link Ctx} context with
* AES-related parameters, ensuring they are available for use by cipher
* initialization routines.
* </p>
*
* @author Leo Galambos
*/
public class AesCommon {
/**
* Parameter key for AES block size.
*/
public static final Key<Integer> BLOCK_SIZE = Key.of("aes.block.size", Integer.class);
/**
* Parameter key for AES encryption key bytes.
*/
public static final Key<byte[]> KEY = Key.of("aes.key", byte[].class);
/**
* Parameter key for AES initialization vector (IV) bytes.
*/
public static final Key<byte[]> IV = Key.of("aes.iv", byte[].class);
/**
* Parameter key for AES mode.
*/
public static final Key<AesMode> MODE = Key.of("aes.mode", AesMode.class);
/**
* Parameter key for AES cipher type.
*/
public static final Key<AesCipherType> CIPHER_TYPE = Key.of("aes.cipher.type", AesCipherType.class);
/**
* Parameter key for Additional Authenticated Data (AAD) used in AEAD modes such
* as AES-GCM. This value must be identical for both encryption and decryption
* to ensure authentication succeeds.
*/
public static final Key<byte[]> AAD = Key.of("aes.aad", byte[].class);
/**
* The source data content to be encrypted or decrypted.
*/
protected DataContent source;
/**
* Constructs an {@code AesCommon} instance and initializes the encryption
* context with AES-specific parameters.
*
* <p>
* This constructor sets the required AES block size in the shared {@link Ctx}
* context, and, if {@code params} is non-null, it stores the provided AES mode,
* key material, IV, cipher type, and any AAD values. The {@link Ctx} singleton
* acts as a storage mechanism to hold the encryption-related state needed for
* downstream operations.
* </p>
*
* @param params the {@link AesParameters} to be saved into the encryption
* context; may be {@code null} if no user parameters are to be
* configured
*/
public AesCommon(final AesParameters params) {
Ctx.INSTANCE.put(BLOCK_SIZE, AesSupport.BLOCK_SIZE);
if (params != null) {
params.save(Ctx.INSTANCE);
}
}
/**
* Sets the input data content to be encrypted or decrypted.
*
* @param input the {@link DataContent} to set as source
* @throws NullPointerException if {@code input} is {@code null}
*/
public void setInput(final DataContent input) {
Objects.requireNonNull(input, "input must not be null");
source = input;
}
}

View File

@@ -0,0 +1,109 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.data.PlainContent;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
/**
* Decrypts AES-encrypted content from an input stream.
*
* This class extends {@link AesCommon} and implements {@link PlainContent},
* providing functionality to retrieve a decrypted {@link InputStream} from an
* encrypted source using AES parameters such as key, IV, and mode.
*
* This class itself does not perform password-based key derivation; subclasses
* should implement such logic if needed.
*
* @author Leo Galambos
*/
public class AesDecryptor extends AesCommon implements PlainContent {
/** Logger for internal messages and error reporting. */
private static final Logger LOG = Logger.getLogger(AesDecryptor.class.getName());
/**
* Constructs an {@code AesDecryptor} initialized with the specified AES
* parameters.
*
* @param params the AES parameters including mode, key, and IV
*/
public AesDecryptor(final AesParameters params) {
super(params);
}
/**
* Returns a decrypted {@link InputStream} by applying AES decryption on the
* input stream provided by the underlying source.
*
* This method retrieves cryptographic parameters including the encryption key,
* initialization vector (IV), and cipher type from a contextual configuration
* (via {@link Ctx}). It then delegates decryption to
* {@link AesSupport#decrypt(byte[], byte[], byte[], AesCipherType, InputStream)}.
*
* If the source stream is {@code null}, a {@link NullPointerException} is
* thrown. If any decryption error occurs (e.g., invalid parameters or corrupted
* input), it is logged and rethrown as an {@link IOException}.
*
* @return the decrypted {@code InputStream}
* @throws IOException if decryption fails or an I/O error occurs
* during stream processing
* @throws NullPointerException if the input stream retrieved from the source is
* {@code null}
*/
@Override
public InputStream getStream() throws IOException {
final InputStream previousInput = source.getStream();
Objects.requireNonNull(previousInput, "input stream must not be null");
try {
return AesSupport.decrypt(Ctx.INSTANCE.get(KEY), Ctx.INSTANCE.get(IV), Ctx.INSTANCE.get(AAD),
Ctx.INSTANCE.get(CIPHER_TYPE), previousInput);
} catch (IllegalArgumentException e) {
LOG.logp(Level.WARNING, "AesDecryptor", "getStream", "Exception during decryption", e);
throw new IOException(e);
}
}
}

View File

@@ -0,0 +1,111 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.data.EncryptedContent;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
/**
* Encrypts the provided content using AES and exposes the result as an
* {@link InputStream}.
*
* The encryption key, IV, and mode are specified by the configured AES
* parameters. The source content is streamed and encrypted on-the-fly using
* {@link AesSupport#encrypt}.
*
* This implementation logs encryption errors and wraps them in
* {@link IOException}s to ensure compatibility with streaming APIs.
*
* @author Leo Galambos
*/
public class AesEncryptor extends AesCommon implements EncryptedContent {
/** Logger for internal messages and error reporting. */
private static final Logger LOG = Logger.getLogger(AesEncryptor.class.getName());
/**
* Constructs an {@code AesEncryptor} initialized with the specified AES
* parameters.
*
* @param params the AES parameters including mode, key, and IV
* @throws IllegalArgumentException if {@code params} is {@code null}
*/
public AesEncryptor(final AesParameters params) {
super(params);
}
/**
* Returns an {@link InputStream} that applies AES encryption to the underlying
* input data.
*
* This method retrieves the original {@code InputStream} from the configured
* {@code source}, verifies its presence, and then wraps it in an AES-encrypting
* stream using the encryption context provided by {@link Ctx}. The encryption
* parameters used include the AES key, initialization vector (IV), and cipher
* type, all of which must be pre-configured in the context.
*
* If the original stream is {@code null}, or if encryption setup fails due to
* invalid or missing parameters, this method throws an {@link IOException}. Any
* internal {@link IllegalArgumentException} is logged and rethrown as an
* {@code IOException} to ensure consistent error handling.
*
* @return an {@code InputStream} that provides AES-encrypted data as it is read
* @throws IOException if the input stream is missing or encryption
* setup fails
* @throws NullPointerException if the source stream is {@code null}
*/
@Override
public InputStream getStream() throws IOException {
final InputStream previousInput = source.getStream();
Objects.requireNonNull(previousInput, "input stream must not be null");
try {
return AesSupport.encrypt(Ctx.INSTANCE.get(KEY), Ctx.INSTANCE.get(IV), Ctx.INSTANCE.get(AAD),
Ctx.INSTANCE.get(CIPHER_TYPE), previousInput);
} catch (IllegalArgumentException e) {
LOG.logp(Level.WARNING, "AesEncryptor", "getStream", "Exception", e);
throw new IOException(e);
}
}
}

View File

@@ -0,0 +1,144 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
/**
* A KEM-based AES decryptor implementation of {@link PlainContent}.
* <p>
* This class uses a {@link KEMAsymmetricContext} configured with a key
* extractor to perform key decapsulation, combined with AES decryption for the
* payload.
* </p>
* <p>
* The decrypted content can be accessed as a stream after setting the encrypted
* input content.
* </p>
*/
public final class KEMDecryptor implements PlainContent {
private static final Logger LOG = Logger.getLogger(KEMDecryptor.class.getName());
/**
* The KEM context configured for decapsulation (extractor must be non-null).
*/
private final KEMAsymmetricContext kemContext;
/**
* AES cipher variant used for symmetric decryption.
*/
private final AesCipherType cipherType;
/**
* Optional Additional Authenticated Data used during decryption; may be
* {@code null}.
*/
private final byte[] aad;
/**
* The encrypted content input to be decrypted.
* <p>
* Must be set before calling {@link #getStream()}.
* </p>
*/
private DataContent encryptedContent;
/**
* Constructs a KEM-based AES decryptor.
*
* @param kemContext the KEM context configured for decapsulation mode; must
* have a non-null extractor for key decapsulation
* @param cipherType the AES cipher type used during encryption (must match)
* @param aad optional Additional Authenticated Data for AEAD ciphers;
* may be {@code null}
* @throws IllegalArgumentException if {@code kemContext} has no extractor or if
* {@code cipherType} is {@code null}
*/
public KEMDecryptor(KEMAsymmetricContext kemContext, AesCipherType cipherType, byte[] aad) {
if (kemContext.extractor() == null) {
throw new IllegalArgumentException(
"KEMDecryptor requires a KEMAsymmetricContext in decapsulation mode (extractor must not be null)");
}
this.kemContext = kemContext;
this.cipherType = Objects.requireNonNull(cipherType, "cipherType must not be null");
this.aad = aad; // NOPMD
}
/**
* Sets the encrypted input content to be decrypted.
*
* @param input the encrypted {@link DataContent} input; must not be
* {@code null}
* @throws IllegalArgumentException if {@code input} is {@code null}
*/
@Override
public void setInput(DataContent input) {
if (input == null) {
throw new IllegalArgumentException("Input DataContent cannot be null");
}
this.encryptedContent = input;
}
/**
* Returns an {@link InputStream} providing the decrypted plaintext.
* <p>
* This method requires that the encrypted input content has been set via
* {@link #setInput(DataContent)}.
* </p>
*
* @return an input stream yielding the decrypted plaintext; never {@code null}
* @throws IllegalStateException if no encrypted content has been set prior to
* invocation
* @throws IOException if an I/O error occurs during stream retrieval
* or decryption
*/
@Override
public InputStream getStream() throws IOException {
if (encryptedContent == null) {
throw new IllegalStateException("No encrypted content set for decryption");
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Starting KEM decryption using cipher type: " + cipherType);
}
return kemContext.getDecryptedStream(encryptedContent.getStream(), cipherType, aad);
}
}

View File

@@ -0,0 +1,174 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.DataContent;
import zeroecho.data.EncryptedContent;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
/**
* Implements encryption of content using a Key Encapsulation Mechanism (KEM)
* combined with AES symmetric encryption.
* <p>
* This class wraps a {@link KEMAsymmetricContext} configured with a KEM key
* generator and applies AES encryption (specified by {@link AesCipherType}) to
* the input data. The encryption output stream consists of the concatenation of
* the KEM encapsulated key, the AES initialization vector (IV), and the
* AES-encrypted payload.
* <p>
* Optionally supports Additional Authenticated Data (AAD) to be included in
* authenticated cipher modes (e.g., AES-GCM).
* <p>
* Usage:
* <ul>
* <li>Create an instance with a properly initialized
* {@code KEMAsymmetricContext} in encapsulation mode.</li>
* <li>Set the input content using {@link #setInput(DataContent)}.</li>
* <li>Obtain the encrypted output stream via {@link #getStream()}.</li>
* </ul>
* <p>
* Note: The {@code KEMAsymmetricContext} must have a non-null generator to
* perform encryption.
*
* @implSpec The {@link #getStream()} method produces an InputStream that yields
* the concatenation of:
* <ol>
* <li>KEM encapsulated key bytes</li>
* <li>AES initialization vector bytes</li>
* <li>Encrypted payload bytes</li>
* </ol>
* This stream never returns null and throws IOException on failure.
*/
public class KEMEncryptor implements EncryptedContent {
private static final Logger LOG = Logger.getLogger(KEMEncryptor.class.getName());
/**
* The KEM context configured for encapsulation (generator must be non-null).
*/
private final KEMAsymmetricContext kemContext;
/**
* AES cipher variant used for symmetric decryption.
*/
private final AesCipherType cipherType;
/**
* Optional Additional Authenticated Data used during decryption; may be
* {@code null}.
*/
private final byte[] aad;
/**
* The input content to be encrypted.
* <p>
* Must be set before {@link #getStream()} is called. It represents the
* plaintext data that will be encrypted using the KEM + AES process.
*/
private DataContent inputContent;
/**
* Constructs a new KEM-based AES encryptor.
*
* @param kemContext the KEM context configured for encapsulation mode; must
* have a non-null generator for key encapsulation
* @param cipherType the AES cipher variant to use for symmetric encryption
* (e.g., CBC, GCM)
* @param aad optional Additional Authenticated Data bytes for AEAD
* ciphers; may be {@code null} if not applicable
* @throws IllegalArgumentException if the {@code kemContext} does not have a
* generator or if {@code cipherType} is
* {@code null}
*/
public KEMEncryptor(KEMAsymmetricContext kemContext, AesCipherType cipherType, byte[] aad) {
if (kemContext.generator() == null) {
throw new IllegalArgumentException(
"KEMEncryptor requires a KEMAsymmetricContext in encapsulation mode (generator must not be null)");
}
this.kemContext = kemContext;
this.cipherType = Objects.requireNonNull(cipherType, "cipherType must not be null");
this.aad = aad; // NOPMD
}
/**
* Sets the input content that will be encrypted.
*
* @param input the {@link DataContent} providing the plaintext data; must not
* be {@code null}
* @throws IllegalArgumentException if {@code input} is {@code null}
*/
@Override
public void setInput(DataContent input) {
if (input == null) {
throw new IllegalArgumentException("Input DataContent cannot be null");
}
this.inputContent = input;
}
/**
* Returns an {@link InputStream} that yields the encrypted data stream.
* <p>
* The stream consists of the concatenation of:
* <ul>
* <li>KEM encapsulated key bytes</li>
* <li>AES initialization vector (IV)</li>
* <li>AES-encrypted payload</li>
* </ul>
* <p>
* This method requires that the input content has been set via
* {@link #setInput(DataContent)}.
*
* @return an input stream providing the encrypted content; never {@code null}
* @throws IllegalStateException if no input content has been set prior to
* invocation
* @throws IOException if an I/O error occurs during stream retrieval
* or encryption
*/
@Override
public InputStream getStream() throws IOException {
if (inputContent == null) {
throw new IllegalStateException("No input content set for encryption");
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Starting KEM encryption using cipher type: " + cipherType);
}
return kemContext.getEncryptedStream(inputContent.getStream(), cipherType, aad);
}
}

View File

@@ -0,0 +1,193 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.util.IOUtil;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
import zeroecho.util.aes.DerivedAesParameters;
/**
* A password-based AES decryption utility that reads key derivation parameters
* (salt and iteration count) from the encrypted stream header and performs
* on-the-fly decryption using the derived cryptographic key and IV.
*
* This class is designed to complement {@link PasswordBasedAesEncryptor}, which
* prepends the salt and iteration count to the encrypted stream. The decryptor
* reads these values to securely re-derive the same AES key and IV using the
* configured {@link AesMode} and {@link AesCipherType}.
*
* Internally, it uses PBKDF2 (Password-Based Key Derivation Function 2) to
* derive the key material and delegates decryption to the {@link AesDecryptor}
* superclass.
*
* The shared {@link Ctx} context is used to temporarily store cryptographic
* configuration and derived parameters during decryption. The password is held
* in memory only long enough to perform re-derivation and is not retained
* beyond that.
*
* If the encrypted stream header is malformed, or if the key derivation fails,
* an exception is raised to indicate potential tampering or incompatibility.
*
* @see PasswordBasedAesEncryptor
* @see AesSupport#rederiveKeyAndIv(String, byte[], int, AesMode, byte[],
* AesCipherType)
*
* @author Leo Galambos
*/
public class PasswordBasedAesDecryptor extends AesDecryptor {
private static final Logger LOG = Logger.getLogger(PasswordBasedAesDecryptor.class.getName());
private final String password;
/**
* Constructs a PasswordBasedAesDecryptor with password, AES mode, and default
* cipher type (CBC).
*
* @param password the password for key derivation
* @param mode the AES mode (128, 192, or 256-bit)
* @throws IllegalArgumentException if arguments are invalid
* @throws InvalidKeySpecException if key derivation fails
*/
public PasswordBasedAesDecryptor(final String password, final AesMode mode) throws InvalidKeySpecException {
this(password, null, mode, AesCipherType.CBC);
}
/**
* Constructs a password-based AES decryptor with specified AES mode, cipher
* type, and Additional Authenticated Data (AAD).
*
* <p>
* This decryptor uses the provided password to derive the AES key for
* decryption. The AES mode and cipher type define the algorithm parameters
* (e.g., key size and block mode). The optional AAD is used for authenticated
* encryption modes like GCM to ensure integrity and authenticity of additional
* associated data.
* </p>
*
* <p>
* The provided AAD bytes will be stored in the global context for use during
* decryption. If the cipher mode does not support AAD or if no AAD is needed,
* this parameter can be {@code null}.
* </p>
*
* @param password the password used for AES key derivation; must not be
* {@code null}
* @param aad additional authenticated data bytes; may be {@code null} if
* not used
* @param mode the AES mode indicating key length (e.g., AES-128,
* AES-256); must not be {@code null}
* @param cipherType the AES cipher type (e.g., CBC, GCM); must not be
* {@code null}
* @throws InvalidKeySpecException if key derivation or initialization fails
* @throws NullPointerException if {@code password}, {@code mode}, or
* {@code cipherType} is {@code null}
*/
public PasswordBasedAesDecryptor(final String password, final byte[] aad, final AesMode mode,
final AesCipherType cipherType) throws InvalidKeySpecException {
super(null);
Ctx.INSTANCE.put(MODE, mode);
Ctx.INSTANCE.put(CIPHER_TYPE, cipherType);
if (aad != null) {
Ctx.INSTANCE.put(AAD, aad);
}
this.password = password;
}
/**
* Returns an {@link InputStream} that decrypts AES-encrypted data using a key
* and IV derived from the password.
* <p>
* This method expects the encrypted input stream to begin with:
* <ul>
* <li>A packed 7-bit encoded salt length</li>
* <li>The salt bytes</li>
* <li>A packed 7-bit encoded PBKDF iteration count</li>
* </ul>
* These parameters are used to re-derive the decryption key and IV using PBKDF,
* which are then used to decrypt the rest of the stream.
*
* @return a decrypted {@link InputStream}
* @throws IOException if header parsing or stream decryption fails
* @throws IllegalStateException if key derivation fails due to an invalid or
* corrupt state
*/
@Override
public InputStream getStream() throws IOException {
final InputStream in = source.getStream();
// Read salt and iteration count from input stream header
final int saltLength = IOUtil.readPack7I(in);
if (saltLength > 4 * AesSupport.BLOCK_SIZE) {
throw new IOException("Salt length " + saltLength + " is weird");
}
final byte[] salt = in.readNBytes(saltLength);
final int iterations = IOUtil.readPack7I(in);
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "processing AES (iterations={0} salt={1})",
new Object[] { iterations, Arrays.toString(salt) });
}
final DerivedAesParameters params;
try {
params = AesSupport.rederiveKeyAndIv(password, salt, iterations, Ctx.INSTANCE.get(MODE),
Ctx.INSTANCE.get(AAD), Ctx.INSTANCE.get(CIPHER_TYPE));
params.save(Ctx.INSTANCE);
} catch (InvalidKeySpecException e) {
LOG.logp(Level.WARNING, "PasswordBasedAesDecryptor", "getStream", "Exception", e);
throw new IllegalStateException("Failed to generate key: invalid state", e);
}
try {
return AesSupport.decrypt(Ctx.INSTANCE.get(KEY), Ctx.INSTANCE.get(IV), Ctx.INSTANCE.get(AAD),
Ctx.INSTANCE.get(CIPHER_TYPE), in);
} catch (IllegalArgumentException e) {
LOG.logp(Level.WARNING, "PasswordBasedAesDecryptor", "getStream", "Exception during decryption", e);
throw new IOException(e);
}
}
}

View File

@@ -0,0 +1,180 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data.processing;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.util.IOUtil;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
import zeroecho.util.aes.DerivedAesParameters;
/**
* A password-based AES encryption utility that derives a cryptographic key and
* IV using PBKDF2 (Password-Based Key Derivation Function 2) and performs AES
* encryption on a given input stream.
* <p>
* This class encapsulates the logic required to securely derive AES parameters
* from a user-supplied password and to stream-encrypt data using the derived
* values.
* <p>
* A header containing the salt and iteration count is prepended to the
* encrypted stream, allowing the corresponding decryption mechanism to
* re-derive the key for symmetric operations. The underlying encryption is
* performed using the {@link AesEncryptor} superclass.
* <p>
* For security and immutability reasons, critical cryptographic parameters
* (iterations, mode, cipher type) are made read-only after construction.
* Attempts to modify them will result in an {@link IllegalStateException}.
*
* @see AesSupport#deriveKeyAndIv(String, int, AesMode, byte[], AesCipherType)
* @see PasswordBasedAesDecryptor
*
* @author Leo Galambos
*/
public class PasswordBasedAesEncryptor extends AesEncryptor {
private static final Logger LOG = Logger.getLogger(PasswordBasedAesEncryptor.class.getName());
/**
* Constructs a PasswordBasedAesEncryptor using a password, iteration count, AES
* mode, and default cipher type (CBC).
*
* @param password the password for key derivation
* @param iterations the PBKDF2 iteration count
* @param mode the AES mode (128, 192, or 256-bit)
* @throws IllegalArgumentException if parameters are invalid
* @throws InvalidKeySpecException if key derivation fails
*/
public PasswordBasedAesEncryptor(final String password, final int iterations, final AesMode mode)
throws InvalidKeySpecException {
this(password, iterations, null, mode, AesCipherType.CBC);
}
/**
* Constructs a {@code PasswordBasedAesEncryptor} instance that derives a
* cryptographic key and initialization vector (IV) from a given password using
* the specified number of iterations, AES mode, associated data (AAD), and
* cipher type.
*
* <p>
* This constructor uses a password-based key derivation function to generate
* the necessary encryption parameters, which are then passed to the parent
* {@link AesCommon} class.
* </p>
*
* <p>
* To ensure the integrity of derived cryptographic material, listeners are
* registered on sensitive context parameters (iterations, mode, and cipher
* type). Any attempt to modify these parameters after construction will result
* in an {@link IllegalStateException}.
* </p>
*
* @param password the password used to derive the encryption key and IV
* @param iterations the number of iterations for key derivation
* @param mode the AES mode to use (e.g., 128-bit, 192-bit, 256-bit)
* @param aad additional authenticated data (AAD) to be included in the
* derived parameters; may be {@code null} if unused
* @param cipherType the cipher type to use (e.g., CBC, GCM, CTR)
*
* @throws IllegalArgumentException if any parameter is invalid
* @throws InvalidKeySpecException if the key derivation fails due to an
* invalid specification
*
* @see AesSupport#deriveKeyAndIv(String, int, AesMode, byte[], AesCipherType)
*/
public PasswordBasedAesEncryptor(final String password, final int iterations, final byte[] aad, final AesMode mode,
final AesCipherType cipherType) throws InvalidKeySpecException {
super(AesSupport.deriveKeyAndIv(password, iterations, mode, aad, cipherType));
Ctx.INSTANCE.addListener(DerivedAesParameters.ITERATIONS, newValue -> {
throw new IllegalStateException(DerivedAesParameters.ITERATIONS
+ " value cannot be modified because it would affect previously computed crypto material");
});
Ctx.INSTANCE.addListener(MODE, newValue -> {
throw new IllegalStateException(
MODE + " value cannot be modified because it would affect previously computed crypto material");
});
Ctx.INSTANCE.addListener(CIPHER_TYPE, newValue -> {
throw new IllegalStateException(CIPHER_TYPE
+ " value cannot be modified because it would affect previously computed crypto material");
});
}
/**
* Returns an {@link InputStream} that provides AES-encrypted data derived from
* the configured password-based key and IV.
*
* This method prepends a header to the encrypted stream containing the salt and
* iteration count used for key derivation, which is necessary for corresponding
* decryption processes to re-derive the correct key.
*
* The encryption itself is delegated to the superclass's
* {@link AesEncryptor#getStream()}, which performs on-the-fly encryption of the
* underlying source data.
*
* @return an input stream of AES-encrypted content including a header with salt
* and iterations
* @throws IOException if encryption or stream access fails
*/
@Override
public InputStream getStream() throws IOException {
// Compose header: salt + iterations
final byte[] salt = Ctx.INSTANCE.get(DerivedAesParameters.SALT);
final int iterations = Ctx.INSTANCE.get(DerivedAesParameters.ITERATIONS);
final ByteArrayOutputStream header = new ByteArrayOutputStream();
IOUtil.writePack7I(header, salt.length);
header.write(salt);
IOUtil.writePack7I(header, iterations);
// salt is not a secret, so we can log it
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "processing AES (iterations={0} salt={1}",
new Object[] { iterations, Arrays.toString(salt) });
}
final InputStream encryptedStream = super.getStream();
return new SequenceInputStream(new ByteArrayInputStream(header.toByteArray()), encryptedStream);
}
}

View File

@@ -0,0 +1,135 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data.processing;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
/**
* An implementation of {@link PlainContent} that encapsulates a byte array.
*
* Provides read-only access to the content through an {@link InputStream}
* backed by the internal byte buffer.
*
* This class represents the start of a {@link DataContent} processing chain,
* thus it does not accept input from preceding content.
*
* @author Leo Galambos
*/
public class PlainBytes implements PlainContent {
/**
* Logger instance for the {@code PlainBytes} class used to log runtime
* information, warnings, or errors. It is statically initialized with the class
* name to ensure consistent logging context throughout the class.
*/
private static final Logger LOG = Logger.getLogger(PlainBytes.class.getName());
/**
* Byte array buffer that holds the raw data managed by this instance.
*/
protected final byte[] buffer;
/**
* Constructs a new {@code PlainBytes} instance by copying the provided byte
* array.
*
* @param buffer the byte array to wrap
*/
public PlainBytes(final byte[] buffer) {
Objects.requireNonNull(buffer, "PlainBytes cannot operate with null buffer");
this.buffer = Arrays.copyOf(buffer, buffer.length);
}
/**
* Constructs a new {@code PlainBytes} instance with a buffer of the specified
* length.
*
* @param length the size of the internal byte buffer to allocate
* @throws NegativeArraySizeException if {@code length} is negative
*/
protected PlainBytes(final int length) {
this.buffer = new byte[length];
}
/**
* {@inheritDoc}
*
* {@code PlainBytes} represents the start of a {@link DataContent} chain and
* therefore must not have any input. Calling this method with a
* non-{@code null} argument will result in an exception.
*
*
* @param input the preceding {@link DataContent}, which must be {@code null}
* @throws IllegalArgumentException if {@code input} is not {@code null}
*/
@Override
public void setInput(final DataContent input) {
if (input != null) {
throw new IllegalArgumentException(
getClass().getName() + " must be the first element in a DataContent chain; it cannot accept input");
}
}
/**
* Returns an {@link InputStream} for reading the byte array.
*
* @return a new {@link ByteArrayInputStream}
* @throws IOException if an I/O error occurs
*/
@Override
public InputStream getStream() throws IOException {
LOG.log(Level.INFO, "opening byte array for read, length={0}", buffer.length);
return new ByteArrayInputStream(buffer);
}
/**
* Returns a copy of the internal byte buffer.
*
* @return a new byte array containing the data from the internal buffer
*/
@Override
public byte[] toBytes() {
return Arrays.copyOf(buffer, buffer.length);
}
}

View File

@@ -0,0 +1,110 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
/**
* A {@link PlainContent} implementation that reads content from a file-like
* URL.
*/
public final class PlainFile implements PlainContent {
/**
* Logger instance for the {@code PlainFile} class used for logging debug,
* informational, and error messages. Initialized with the class name to provide
* context-specific logging output.
*/
private static final Logger LOG = Logger.getLogger(PlainFile.class.getName());
/**
* URL representing the location of the file associated with this instance.
*
* This URL may point to a file on the local file system, a remote resource, or
* any other location accessible via the {@link java.net.URL} protocol. It is
* final and set during construction.
*/
private final URL location;
/**
* Constructs a PlainFile from the specified URL.
*
* @param location the file URL; must not be null
*/
public PlainFile(final URL location) {
Objects.requireNonNull(location, "URL must not be null");
this.location = location;
}
/**
* {@inheritDoc}
*
* {@code PlainFile} represents the start of a {@link DataContent} chain and
* therefore must not have any input. Calling this method with a
* non-{@code null} argument will result in an exception.
*
* @param input the preceding {@link DataContent}, which must be {@code null}
* @throws IllegalArgumentException if {@code input} is not {@code null}
*/
@Override
public void setInput(final DataContent input) {
if (input != null) {
throw new IllegalArgumentException(
getClass().getName() + " must be the first element in a DataContent chain; it cannot accept input");
}
}
/**
* Returns an {@link InputStream} for reading from the file.
*
* @return an {@link InputStream} from the URL
* @throws IOException if an I/O error occurs opening the stream
*/
@Override
public InputStream getStream() throws IOException {
LOG.log(Level.INFO, "opening {0}", location);
return location.openStream();
}
}

View File

@@ -0,0 +1,126 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data.processing;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
/**
* A {@link PlainContent} implementation that wraps a UTF-8 string.
*
* @author Leo Galambos
*/
public class PlainString implements PlainContent {
/**
* Logger instance for the {@code PlainString} class used to log informational,
* debug, or error messages related to the classs operations.
*
* The logger is initialized with the name of the {@code PlainString} class,
* enabling targeted and organized logging output.
*/
private static final Logger LOG = Logger.getLogger(PlainString.class.getName());
/**
* The plain text content represented by this instance.
*/
protected String str;
/**
* Constructs a PlainString with the given string.
*
* @param str the plain text content; must not be null
*/
public PlainString(final String str) {
Objects.requireNonNull(str, "plain string must not be null");
this.str = str;
}
/**
* Returns the original string content.
*
* @return the string
*/
@Override
public String toText() {
return str;
}
/**
* {@inheritDoc}
*
* {@code PlainString} represents the start of a {@link DataContent} chain and
* therefore must not have any input. Calling this method with a
* non-{@code null} argument will result in an exception.
*
* @param input the preceding {@link DataContent}, which must be {@code null}
* @throws IllegalArgumentException if {@code input} is not {@code null}
*/
@Override
public void setInput(final DataContent input) {
if (input != null) {
throw new IllegalArgumentException(
getClass().getName() + " must be the first element in a DataContent chain; it cannot accept input");
}
}
/**
* Returns an {@link InputStream} of the UTF-8 encoded string.
*
* @return a {@link ByteArrayInputStream}
*/
@Override
public InputStream getStream() {
LOG.log(Level.FINE, "opening \"{0}\"", str);
return new ByteArrayInputStream(toBytes());
}
/**
* Returns the UTF-8 encoded byte array of the string.
*
* @return a byte array representation
*/
@Override
public byte[] toBytes() {
return str.getBytes(StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,99 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data.processing;
import java.io.IOException;
import java.io.InputStream;
import zeroecho.data.DataContent;
import zeroecho.data.PlainContent;
/**
* A {@link PlainContent} implementation that reclassifies an existing
* {@link DataContent} instance as plain (unencrypted) without modifying the
* underlying data stream.
*
* This is useful in cryptographic pipelines where the actual content remains
* unchanged, but its classification as "plain" is semantically important for
* subsequent processing.
*/
@Deprecated
public class ReclassifiedPlain implements PlainContent {
private DataContent previous;
/**
* Creates a new {@code ReclassifiedPlain} instance without any initial input.
*
* The input must be later provided using {@link #setInput(DataContent)}.
*/
public ReclassifiedPlain() {
this(null);
}
/**
* Constructs a {@code ReclassifiedPlain} with the specified upstream content.
*
* @param previous the upstream {@link DataContent} to be reclassified as plain
*/
public ReclassifiedPlain(final DataContent previous) {
super();
this.previous = previous;
}
/**
* Sets the upstream {@link DataContent} that this plain content reclassifies.
*
* @param input the content to treat as plain
*/
@Override
public void setInput(final DataContent input) {
previous = input;
}
/**
* Returns the input stream from the upstream content without any
* transformation.
*
* @return the raw input stream from the upstream content
* @throws IOException if the underlying content fails to provide a stream
*/
@Override
public InputStream getStream() throws IOException {
if (previous == null) {
throw new IllegalStateException("Input content is not set for ReclassifiedPlain");
}
return previous.getStream();
}
}

View File

@@ -0,0 +1,165 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data.processing;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Arrays;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.data.DataContent;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
/**
* A {@link SecretRandom} implementation that generates a random AES key and IV
* using the specified {@link AesMode} and {@link AesCipherType}.
*
* <p>
* The combined key and IV are stored in the internal buffer and propagated to
* the shared {@link Ctx} for use by other components that rely on AES
* encryption or decryption.
*
* <p>
* When the {@code SECRET} context value is updated, this class listens and
* splits the secret value into separate key and IV portions, updating both the
* internal buffer and the relevant context entries.
*
* <p>
* Likewise, changes to the {@code KEY} context value will update the buffer
* accordingly.
*
* <p>
* This class does not perform encryption or decryption itself. Instead, it
* makes the key/IV accessible to other components and exposes the raw input
* stream.
*
* @author Leo Galambos
*/
public class SecretAesRandom extends SecretRandom {
private static final Logger LOG = Logger.getLogger(SecretAesRandom.class.getName());
private DataContent source;
/**
* Creates a new {@code SecretAesRandom} instance by generating a random AES
* key, IV, and optionally incorporating Additional Authenticated Data (AAD).
*
* <p>
* The generated values are saved to the {@link Ctx} and stored internally in a
* buffer. Listeners are registered to propagate updates between the
* {@code SECRET}, {@code KEY}, and {@code IV}.
* </p>
*
* @param mode the AES mode determining the key length (e.g., AES-128 or
* AES-256)
* @param cipherType the AES cipher type (e.g., CBC, GCM)
* @param aad optional Additional Authenticated Data (AAD) associated
* with the generated parameters; may be {@code null} if
* unused
*
* @throws IllegalArgumentException if key or IV generation fails
*
* @see AesSupport#generateKeyAndIV(AesMode, AesCipherType, byte[])
*/
public SecretAesRandom(final AesMode mode, final AesCipherType cipherType, final byte[] aad) {
super(mode.getKeyLengthBytes() + cipherType.getIVLengthBytes(), false);
final AesParameters params;
try {
params = AesSupport.generateKeyAndIV(mode, cipherType, aad);
} catch (IllegalArgumentException | NoSuchAlgorithmException | NoSuchProviderException e) {
LOG.logp(Level.WARNING, "SecretAesRandom", "SecretAesRandom", "Exception", e);
throw new IllegalArgumentException(e);
}
System.arraycopy(params.key().getKey(), 0, buffer, 0, mode.getKeyLengthBytes());
System.arraycopy(params.iv(), 0, buffer, mode.getKeyLengthBytes(), cipherType.getIVLengthBytes());
params.save(Ctx.INSTANCE);
Ctx.INSTANCE.addListener(SECRET, newValue -> {
Ctx.INSTANCE.put(AesCommon.KEY, Arrays.copyOf(newValue, mode.getKeyLengthBytes()));
Ctx.INSTANCE.put(AesCommon.IV, Arrays.copyOfRange(newValue, mode.getKeyLengthBytes(),
mode.getKeyLengthBytes() + cipherType.getIVLengthBytes()));
});
Ctx.INSTANCE.addListener(AesCommon.KEY, newValue -> {
System.arraycopy(newValue, 0, buffer, 0, mode.getKeyLengthBytes());
});
Ctx.INSTANCE.addListener(SECRET, newValue -> {
System.arraycopy(newValue, 0, buffer, mode.getKeyLengthBytes(), cipherType.getIVLengthBytes());
});
}
/**
* Sets the source {@link DataContent} for this instance.
* <p>
* This class does not modify or encrypt the input data—it only passes through
* the stream as-is via {@link #getStream()}.
*
* @param input the data source to use (must not be null)
* @throws NullPointerException if {@code input} is null
*/
@Override
public void setInput(final DataContent input) {
Objects.requireNonNull(input, "input must not be null");
source = input;
}
/**
* Returns the raw input stream from the previously set {@link DataContent}.
* <p>
* This stream is not encrypted or modified. Consumers are expected to use the
* AES key and IV made available through the {@link Ctx} for encryption or
* decryption operations.
*
* @return the raw input stream
* @throws IOException if the underlying stream cannot be opened
*/
@Override
public InputStream getStream() throws IOException {
return source.getStream();
}
}

View File

@@ -0,0 +1,266 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data.processing;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.data.DataContent;
import zeroecho.data.SecretContent;
import zeroecho.util.IOUtil;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
import zeroecho.util.aes.DerivedAesParameters;
/**
* A {@link SecretContent} implementation that uses a password-based key
* derivation function (PBKDF) to produce AES encryption parameters (key and
* IV).
*
* <p>
* This class supports both encryption and decryption:
* <ul>
* <li><strong>Encryption mode</strong>:
* <ul>
* <li>Derives the AES key and IV immediately upon construction.</li>
* <li>Stores the derived parameters, salt, and iteration count in a shared
* {@link Ctx}.</li>
* <li>The output stream prepends salt and iteration count as a header.</li>
* </ul>
* </li>
* <li><strong>Decryption mode</strong>:
* <ul>
* <li>Expects the input stream to start with a salt and iteration count
* header.</li>
* <li>Key and IV are re-derived dynamically in {@link #getStream()} using the
* extracted values.</li>
* <li>Mode and cipher type must be known in advance and are stored in
* {@link Ctx} at construction.</li>
* </ul>
* </li>
* </ul>
*
* <p>
* Derived parameters and configuration values are stored in {@link Ctx} using
* standard keys like {@code KEY}, {@code IV}, {@code SALT}, {@code ITERATIONS},
* {@code MODE}, {@code CIPHER_TYPE}, and {@code BLOCK_SIZE}.
*
* @author Leo Galambos
*/
public class SecretDerivedAesParameters implements SecretContent {
private static final Logger LOG = Logger.getLogger(SecretDerivedAesParameters.class.getName());
private DataContent source;
private final String password;
private final boolean encrypt;
/**
* Constructs an AES key derivation context with default cipher type
* ({@code CBC}).
* <p>
* This constructor performs full setup for encryption or preps configuration
* for decryption.
* </p>
*
* @param password the password used for PBKDF key derivation (must not be
* null)
* @param iterations the number of PBKDF iterations
* @param mode the AES mode (e.g., {@code AES-128}, {@code AES-256})
* @param encrypt true for encryption (derive key now), false for decryption
* (key derived later)
* @throws IllegalArgumentException if arguments are invalid
* @throws InvalidKeySpecException if key derivation fails (only in encryption
* mode)
*/
public SecretDerivedAesParameters(final String password, final int iterations, final AesMode mode,
final boolean encrypt) throws InvalidKeySpecException {
this(password, iterations, null, mode, AesCipherType.CBC, encrypt);
}
/**
* Constructs an AES key derivation context with explicit cipher type and
* optional Additional Authenticated Data (AAD).
*
* <p>
* In encryption mode, key derivation is performed immediately using PBKDF2, and
* the derived parameters (including AAD) are saved in {@link Ctx}. In
* decryption mode, the AES mode and cipher type are stored in {@link Ctx}, but
* key derivation is deferred until {@link #getStream()} reads the salt and
* iteration count from the input stream.
* </p>
*
* @param password the password used for PBKDF2 key derivation; must not be
* {@code null}
* @param iterations the PBKDF2 iteration count; should be at least 100,000 for
* adequate security
* @param aad optional Additional Authenticated Data (AAD); may be
* {@code null} if unused
* @param mode the AES mode (e.g., {@code AES-128}, {@code AES-256}); must
* not be {@code null}
* @param cipherType the AES cipher type (e.g., {@code CBC}, {@code GCM}); must
* not be {@code null}
* @param encrypt {@code true} for encryption mode (immediate derivation),
* {@code false} for decryption mode
*
* @throws IllegalArgumentException if any argument is invalid
* @throws InvalidKeySpecException if key derivation fails (only in encryption
* mode)
*
* @see AesSupport#deriveKeyAndIv(String, int, AesMode, byte[], AesCipherType)
*/
public SecretDerivedAesParameters(final String password, final int iterations, final byte[] aad, final AesMode mode,
final AesCipherType cipherType, final boolean encrypt) throws InvalidKeySpecException {
this.password = password;
this.encrypt = encrypt;
if (encrypt) {
final DerivedAesParameters params = AesSupport.deriveKeyAndIv(password, iterations, mode, aad, cipherType);
params.save(Ctx.INSTANCE);
} else {
Ctx.INSTANCE.put(AesCommon.CIPHER_TYPE, cipherType);
Ctx.INSTANCE.put(AesCommon.MODE, mode);
if (aad != null) {
Ctx.INSTANCE.put(AesCommon.AAD, aad);
}
}
Ctx.INSTANCE.put(AesCommon.BLOCK_SIZE, AesSupport.BLOCK_SIZE);
}
/**
* Sets the data source for encryption or decryption.
* <p>
* The actual processing mode depends on the {@code encrypt} flag provided
* during construction:
* <ul>
* <li><b>Encryption</b>: the stream will prepend salt and iteration header
* before actual data.</li>
* <li><b>Decryption</b>: the stream will read salt and iteration header to
* re-derive the key and IV.</li>
* </ul>
*
* @param input the input content to wrap (must not be null)
* @throws NullPointerException if {@code input} is null
*/
@Override
public void setInput(final DataContent input) {
Objects.requireNonNull(input, "input must not be null");
source = input;
}
/**
* Returns a stream that wraps the input content, handling AES encryption or
* decryption.
*
* <ul>
* <li><b>Encryption mode</b>:
* <ul>
* <li>Prepends a header (salt length, salt bytes, and iteration count).</li>
* <li>Returns a concatenated stream: header + encrypted content.</li>
* </ul>
* </li>
* <li><b>Decryption mode</b>:
* <ul>
* <li>Reads the header (salt and iterations) from the input stream.</li>
* <li>Re-derives the AES key and IV using stored password, mode, and cipher
* type.</li>
* <li>Returns the input stream positioned after the header.</li>
* </ul>
* </li>
* </ul>
*
* @return the wrapped input stream with header handling
* @throws IOException if stream reading or header parsing fails
* @throws IllegalStateException if key derivation fails during decryption
*/
@Override
public InputStream getStream() throws IOException {
if (encrypt) {
// encryption
final byte[] salt = Ctx.INSTANCE.get(DerivedAesParameters.SALT);
final int iterations = Ctx.INSTANCE.get(DerivedAesParameters.ITERATIONS);
final ByteArrayOutputStream header = new ByteArrayOutputStream();
IOUtil.write(header, salt);
IOUtil.writePack7I(header, iterations);
// salt is not a secret, so we can log it
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "processing AES (iterations={0} salt={1}",
new Object[] { iterations, Arrays.toString(salt) });
}
final InputStream encryptedStream = source.getStream();
return new SequenceInputStream(new ByteArrayInputStream(header.toByteArray()), encryptedStream);
} else {
// decryption
// Read salt and iteration count from input stream header
final InputStream in = source.getStream();
final byte[] salt = IOUtil.read(in, 4 * AesSupport.BLOCK_SIZE);
final int iterations = IOUtil.readPack7I(in);
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "processing AES (iterations={0} salt={1})",
new Object[] { iterations, Arrays.toString(salt) });
}
try {
final AesMode mode = Ctx.INSTANCE.get(AesCommon.MODE);
final AesCipherType cipherType = Ctx.INSTANCE.get(AesCommon.CIPHER_TYPE);
final byte[] aad = Ctx.INSTANCE.get(AesCommon.AAD);
final DerivedAesParameters params = AesSupport.rederiveKeyAndIv(password, salt, iterations, mode, aad,
cipherType);
Ctx.INSTANCE.put(AesCommon.KEY, params.key().getKey());
Ctx.INSTANCE.put(AesCommon.IV, params.iv());
Ctx.INSTANCE.put(DerivedAesParameters.SALT, params.salt());
Ctx.INSTANCE.put(DerivedAesParameters.ITERATIONS, iterations);
} catch (InvalidKeySpecException e) {
LOG.logp(Level.WARNING, "PasswordBasedAesDecryptor", "getStream", "Exception", e);
throw new IllegalStateException("Failed to generate key: invalid state", e);
}
return in;
}
}
}

View File

@@ -0,0 +1,253 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data.processing;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import zeroecho.data.DataContent;
import zeroecho.data.SecretContent;
import zeroecho.util.IOUtil;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesMode;
import zeroecho.util.aes.AesSupport;
import zeroecho.util.aes.BasicAesParameters;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
/**
* Provides AES encryption and decryption parameter management using a Key
* Encapsulation Mechanism (KEM).
* <p>
* This class integrates a {@link KEMAsymmetricContext} to securely derive AES
* keys and initialization vectors, supporting both encryption and decryption
* workflows:
* </p>
* <ul>
* <li><strong>Encryption mode:</strong> Uses the KEM generator to derive AES
* parameters, prepends the encapsulated key and IV to the data stream, and
* stores them in {@link Ctx} for downstream processing.</li>
* <li><strong>Decryption mode:</strong> Reads the encapsulated key and IV from
* the input stream, performs KEM decapsulation to reconstruct the AES key, and
* populates {@link Ctx} for subsequent AES operations.</li>
* </ul>
*
* <p>
* The class implements {@link SecretContent} and can be chained into
* {@code DataContent} pipelines for streaming encryption/decryption.
* </p>
*
* <h2>Usage</h2> <pre>{@code
* SecretKEMAesParameters params =
* new SecretKEMAesParameters(kemContext, aesMode, cipherType, aad);
* params.setInput(dataContent);
* InputStream stream = params.getStream();
* }</pre>
*
* <p>
* Thread-safety: Instances are not thread-safe. Each encryption or decryption
* pipeline must use its own {@code SecretKEMAesParameters}.
* </p>
*/
public class SecretKEMAesParameters implements SecretContent {
private static final Logger LOG = Logger.getLogger(SecretKEMAesParameters.class.getName());
/** KEM context used for encapsulation/decapsulation. */
private final KEMAsymmetricContext kemContext;
/** Indicates encryption (true) or decryption (false). */
private final boolean encrypt;
/** Data source for processing. */
private DataContent source;
/**
* Creates a new KEM-based AES parameter provider.
*
* <p>
* Depending on whether the provided {@link KEMAsymmetricContext} is configured
* for encryption or decryption, this constructor initializes the internal
* context and populates {@link Ctx} with the required AES parameters.
* </p>
*
* <ul>
* <li>In encryption mode, derives AES key material and IV using KEM and stores
* them in {@link Ctx}.</li>
* <li>In decryption mode, records the cipher type, AES mode, and optional
* Additional Authenticated Data (AAD) into {@link Ctx}.</li>
* </ul>
*
* @param kemContext the KEM context used for encapsulation/decapsulation; must
* not be {@code null}
* @param mode the AES mode specifying key length; must not be
* {@code null}
* @param cipherType the AES cipher type (CBC, GCM, etc.); must not be
* {@code null}
* @param aad optional Additional Authenticated Data (AAD); may be
* {@code null}
* @throws NullPointerException if any required parameter is {@code null}
*/
public SecretKEMAesParameters(final KEMAsymmetricContext kemContext, final AesMode mode,
final AesCipherType cipherType, final byte[] aad) {
this.kemContext = Objects.requireNonNull(kemContext, "kemContext must not be null");
this.encrypt = kemContext.extractor() == null;
if (encrypt) {
final BasicAesParameters params = AesSupport.deriveFromKEM(kemContext, mode, cipherType, aad);
params.save(Ctx.INSTANCE);
} else {
Ctx.INSTANCE.put(AesCommon.CIPHER_TYPE, Objects.requireNonNull(cipherType, "cipherType must not be null"));
Ctx.INSTANCE.put(AesCommon.MODE, Objects.requireNonNull(mode, "mode must not be null"));
if (aad != null) {
Ctx.INSTANCE.put(AesCommon.AAD, aad);
}
}
Ctx.INSTANCE.put(AesCommon.BLOCK_SIZE, AesSupport.BLOCK_SIZE);
}
/**
* Sets the input data content for subsequent processing.
* <p>
* This must be called prior to invoking {@link #getStream()}.
* </p>
*
* @param input the non-null input content to be processed
* @throws NullPointerException if {@code input} is {@code null}
*/
@Override
public void setInput(final DataContent input) {
this.source = Objects.requireNonNull(input, "input must not be null");
}
/**
* Returns an {@link InputStream} configured for either encryption or
* decryption.
*
* <ul>
* <li>In encryption mode, the stream begins with a header containing the
* encapsulated KEM data and IV, followed by the original content.</li>
* <li>In decryption mode, the method reads the encapsulated KEM data and IV,
* derives the AES key, updates {@link Ctx}, and returns the remaining
* stream.</li>
* </ul>
*
* @return a prepared {@link InputStream} suitable for AES processing
* @throws IOException if an error occurs while preparing the stream
*/
@Override
public InputStream getStream() throws IOException {
return encrypt ? buildEncryptionStream() : buildDecryptionStream();
}
/**
* Constructs an encryption stream by generating and prepending a KEM header.
* <p>
* The header contains:
* </p>
* <ul>
* <li>Encapsulated KEM data (used for key reconstruction during
* decryption).</li>
* <li>Initialization Vector (IV) required by the AES cipher.</li>
* </ul>
*
* @return an {@link InputStream} combining the header and original data content
* @throws IOException if writing to the header fails
*/
private InputStream buildEncryptionStream() throws IOException {
ByteArrayOutputStream header = new ByteArrayOutputStream();
byte[] encapsulated = kemContext.getEncapsulation();
byte[] iv = Ctx.INSTANCE.get(AesCommon.IV);
IOUtil.write(header, encapsulated);
IOUtil.write(header, iv);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Encryption header: encap=" + encapsulated.length + " iv=" + iv.length);
}
// --- Combine header + original stream ---
return new SequenceInputStream(new ByteArrayInputStream(header.toByteArray()), source.getStream());
}
/**
* Constructs a decryption stream by consuming the KEM header and restoring AES
* parameters.
*
* <p>
* Steps performed:
* </p>
* <ul>
* <li>Reads encapsulated KEM data and the IV from the input stream.</li>
* <li>Performs KEM decapsulation to rederive the AES key.</li>
* <li>Stores the derived key and IV into {@link Ctx} for downstream AES
* operations.</li>
* </ul>
*
* @return the remaining {@link InputStream} after header consumption
* @throws IOException if reading from the stream fails
* @throws IllegalArgumentException if the IV length does not match the expected
* size
*/
private InputStream buildDecryptionStream() throws IOException {
InputStream in = source.getStream();
AesCipherType cipherType = Ctx.INSTANCE.get(AesCommon.CIPHER_TYPE);
// --- Read encapsulated & IV ---
byte[] encapsulated = IOUtil.read(in, 30_000);
byte[] iv = IOUtil.read(in, cipherType.getIVLengthBytes());
if (iv.length != cipherType.getIVLengthBytes()) {
throw new IllegalArgumentException(cipherType + " requires IV length " + cipherType.getIVLengthBytes()
+ ", but only IV with " + iv.length + "available");
}
// --- KEM decapsulation ---
byte[] key = AesSupport.rederiveFromKEM(kemContext, encapsulated, Ctx.INSTANCE.get(AesCommon.MODE));
Ctx.INSTANCE.put(AesCommon.KEY, key);
Ctx.INSTANCE.put(AesCommon.IV, iv);
return in;
}
}

View File

@@ -0,0 +1,338 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.data.processing;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.security.auth.DestroyFailedException;
import org.bouncycastle.crypto.DataLengthException;
import conflux.Ctx;
import conflux.Key;
import zeroecho.data.DataContent;
import zeroecho.data.EncryptedContent;
import zeroecho.util.IOUtil;
import zeroecho.util.KeySupport;
import zeroecho.util.RandomSupport;
import zeroecho.util.asymmetric.AsymmetricContext;
import zeroecho.util.asymmetric.AsymmetricStreamBuilder;
import zeroecho.util.asymmetric.ClassicAsymmetricContext;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
import zeroecho.util.asymmetric.SignatureAsymmetricContext;
/**
* A cryptographic content wrapper that supports multi-recipient encryption and
* single-recipient decryption using asymmetric keys.
* <p>
* {@code SecretMultiRecipientCryptor} operates on {@link DataContent} instances
* and provides an {@link InputStream} that transparently performs encryption or
* decryption depending on the configured keys and input content type:
*
* <ul>
* <li>When constructed with public keys, the cryptor operates in <b>encryption
* mode</b>: a secret is encrypted once for each recipient and prepended as a
* header before the actual content stream.</li>
* <li>When constructed with a private key, the cryptor operates in
* <b>decryption mode</b>: the encrypted secret is extracted and decrypted from
* the header, and made available in the runtime context.</li>
* </ul>
*
* <p>
* This class uses a shared key reference ({@code SECRET}) to store the
* recovered or generated secret in the global context ({@link Ctx}) for
* downstream use.
*
* <p>
* Logging is kept at an operational level and avoids exposing sensitive data.
*
* @author Leo Galambos
*/
public class SecretMultiRecipientCryptor implements EncryptedContent {
private static final Logger LOG = Logger.getLogger(SecretMultiRecipientCryptor.class.getName());
private final PublicKey recipient[];
private final PublicKey decoy[];
private final PrivateKey privKey;
private DataContent source;
/**
* A typed key used for storing or retrieving secret data as a byte array.
* <p>
* The key identifier is "secret.data" and it expects values of type
* {@code byte[]}.
* </p>
*/
protected static final Key<byte[]> SECRET = Key.of("secret.data", byte[].class);
private record TaggedPublicKey(boolean decoy, PublicKey publicKey) {
}
/**
* Constructs a new {@code SecretMultiRecipientCryptor} configured for
* encryption using the specified recipients' public keys and optional decoy
* public keys.
* <p>
* The recipient public keys are used to encrypt the secret key for legitimate
* recipients. The decoy public keys are included to provide plausible
* deniability or to obfuscate the true set of recipients, but do not correspond
* to actual decryption capabilities.
* </p>
*
* @param recipient an array of {@link PublicKey} objects representing the
* legitimate recipients of the encrypted data
* @param decoy an array of {@link PublicKey} objects representing decoy
* recipients that cannot decrypt the data; may be empty or
* {@code null} if no decoys are desired
*/
public SecretMultiRecipientCryptor(final PublicKey[] recipient, final PublicKey[] decoy) { // NOPMD
super();
this.recipient = (recipient == null) ? new PublicKey[0] : recipient;
this.decoy = (decoy == null) ? new PublicKey[0] : decoy;
this.privKey = null;
}
/**
* Constructs a new {@code SecretMultiRecipientCryptor} configured for
* decryption with the provided private key.
*
* @param privKey the private key for decrypting the secret
*/
public SecretMultiRecipientCryptor(final PrivateKey privKey) {
super();
this.recipient = null;
this.decoy = null;
this.privKey = privKey;
}
/**
* Sets the input {@link DataContent} that this cryptor will wrap. This must be
* called before invoking {@link #getStream()}.
*
* @param input the data content to be encrypted or decrypted; must not be
* {@code null}
* @throws NullPointerException if {@code input} is {@code null}
*/
@Override
public void setInput(final DataContent input) {
Objects.requireNonNull(input, "input must not be null");
source = input;
}
/**
* Returns an {@link InputStream} representing either encrypted or decrypted
* data based on the configured mode:
*
* <ul>
* <li><b>Decryption Mode</b> (if a private key is set): Attempts to extract and
* decrypt the shared secret from the stream header. The decrypted secret is
* validated by hash and stored in the context under {@link #SECRET}. The
* remaining stream is returned for use.</li>
* <li><b>Encryption Mode</b> (if public keys are set): Encrypts the shared
* secret separately for each recipient, constructs a header from these
* encrypted blocks, appends a zero-length marker, and returns a stream
* combining the header and the original content.</li>
* </ul>
*
* @return an input stream for the processed content
* @throws IOException if the secret cannot be found or decrypted in
* decryption mode, or if an I/O error occurs
* @throws IllegalStateException if {@link #setInput(DataContent)} was not
* called before invocation
*/
@Override
public InputStream getStream() throws IOException {
if (privKey != null) {
return decrypt();
} else {
return encrypt();
}
}
private InputStream encrypt() throws IOException {
final ByteArrayOutputStream pubChunk = new ByteArrayOutputStream();
final ByteArrayOutputStream header = new ByteArrayOutputStream();
// valid secret
final ByteArrayOutputStream secretItem = new ByteArrayOutputStream();
final byte[] secret = Ctx.INSTANCE.get(SECRET);
IOUtil.writePack7I(secretItem, Arrays.hashCode(secret));
secretItem.write(secret);
final byte hashAndSecret[] = secretItem.toByteArray();
final ByteArrayInputStream secretStream = new ByteArrayInputStream(hashAndSecret);
// false secret - decoy
final ByteArrayOutputStream decoyItem = new ByteArrayOutputStream();
final byte[] decoysecret = RandomSupport.generateRandom(secret.length);
IOUtil.writePack7I(decoyItem, Arrays.hashCode(decoysecret));
decoyItem.write(decoysecret);
final byte hashAndDecoy[] = decoyItem.toByteArray();
final ByteArrayInputStream decoyStream = new ByteArrayInputStream(hashAndDecoy);
// prepare
final List<TaggedPublicKey> items = new ArrayList<>();
for (PublicKey item : recipient) {
items.add(new TaggedPublicKey(false, item));
}
for (PublicKey item : decoy) {
items.add(new TaggedPublicKey(true, item));
}
Collections.shuffle(items, RandomSupport.getRandom());
final byte[] aad = new byte[4];
int aadCounter = 0;
for (TaggedPublicKey item : items) {
aad[0] = (byte) (aadCounter >>> 24);
aad[1] = (byte) (aadCounter >>> 16);
aad[2] = (byte) (aadCounter >>> 8);
aad[3] = (byte) aadCounter;
aadCounter++;
try (AsymmetricContext ctx = KeySupport.fromKey(item.publicKey())) {
constructEncryptor(ctx).withInputStream(item.decoy() ? decoyStream : secretStream).withKEMCipherAad(aad)
.buildEncryptingStream().transferTo(pubChunk);
IOUtil.write(header, pubChunk.toByteArray());
pubChunk.reset();
if (item.decoy()) {
decoyStream.reset();
} else {
secretStream.reset();
}
} catch (DestroyFailedException e) {
throw new IOException(e);
}
}
IOUtil.writePack7I(header, 0);
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "header for {0} recipients, {1} decoys, size {2} bytes",
new Object[] { recipient.length, decoy.length, header.size() });
}
final InputStream encryptedStream = source.getStream();
return new SequenceInputStream(new ByteArrayInputStream(header.toByteArray()), encryptedStream);
}
private AsymmetricStreamBuilder constructEncryptor(AsymmetricContext context) {
return switch (context) {
case ClassicAsymmetricContext ctx -> // NOPMD
AsymmetricStreamBuilder.newBuilder().withCipherEngine(ctx.cipher()).withKey(ctx.key());
case KEMAsymmetricContext ctx -> // NOPMD
AsymmetricStreamBuilder.newBuilder().withKEMGenerator(ctx.generator()).withKey(ctx.key());
case SignatureAsymmetricContext ctx -> // NOPMD
throw new IllegalArgumentException(ctx.toString() + " cannot be used for encryption");
};
}
private InputStream decrypt() throws IOException { // NOPMD
final InputStream in = source.getStream();
try (AsymmetricContext ctx = KeySupport.fromKey(privKey)) {
final AsymmetricStreamBuilder asb = constructDecryptor(ctx);
final ByteArrayOutputStream decrypted = new ByteArrayOutputStream();
final byte[] aad = new byte[4];
int aadCounter = 0;
for (byte pubChunkByte[] = IOUtil.read(in, 20 * 1024); pubChunkByte.length > 0; pubChunkByte = IOUtil
.read(in, 20 * 1024)) {
if (decrypted.size() == 0) {
// the secret was not yet found
aad[0] = (byte) (aadCounter >>> 24);
aad[1] = (byte) (aadCounter >>> 16);
aad[2] = (byte) (aadCounter >>> 8);
aad[3] = (byte) aadCounter;
aadCounter++;
try {
final InputStream is = asb.withInputStream(new ByteArrayInputStream(pubChunkByte)) // NOPMD
.withKEMCipherAad(aad).buildDecryptingStream();
final int hash = IOUtil.readPack7I(is);
is.transferTo(decrypted);
final byte[] candidate = decrypted.toByteArray();
if (hash != Arrays.hashCode(candidate)) {
decrypted.reset();
}
} catch (IOException | DataLengthException | DestroyFailedException e) {
if (LOG.isLoggable(Level.INFO)) {
LOG.info(e.toString());
}
decrypted.reset();
} catch (IllegalArgumentException e) {
// thrown by PQ algos when something is wrong
if (LOG.isLoggable(Level.INFO)) {
LOG.info("Exception from PQ " + e);
}
decrypted.reset();
}
}
}
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "header with {0} recipients, secret size {1} bytes",
new Object[] { aadCounter, decrypted.size() });
}
if (decrypted.size() == 0) {
throw new IOException("secret was not found in the header");
}
Ctx.INSTANCE.put(SECRET, decrypted.toByteArray());
return in;
}
}
private AsymmetricStreamBuilder constructDecryptor(AsymmetricContext context) {
return switch (context) {
case ClassicAsymmetricContext ctx -> // NOPMD
AsymmetricStreamBuilder.newBuilder().withKey(ctx.key()).withCipherEngine(ctx.cipher());
case KEMAsymmetricContext ctx -> // NOPMD
AsymmetricStreamBuilder.newBuilder().withKEMExtractor(ctx.extractor()).withKey(ctx.key());
case SignatureAsymmetricContext ctx -> // NOPMD
throw new IllegalArgumentException(ctx.toString() + " cannot be used for decryption");
};
}
}

View File

@@ -0,0 +1,81 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data.processing;
import conflux.Ctx;
import conflux.Key;
import zeroecho.data.SecretContent;
import zeroecho.util.Password;
/**
* A {@link SecretContent} implementation that encapsulates a passwordKey
* string. This class extends {@link PlainString} and enforces immutability of
* the passwordKey after construction.
* <p>
* Passwords can be generated randomly or provided explicitly. Once set,
* attempts to change the passwordKey via parameters will cause an exception.
* <p>
* This class supports applying parameters from a map and collecting its state
* back into a map, using the key {@link #PASSWORD}.
*
* @author Leo Galambos
*/
public class SecretPassword extends PlainString implements SecretContent {
private final Key<String> PASSWORD = Key.of("secret.password", String.class);
/**
* Constructs a {@code SecretPassword} with a randomly generated printable
* passwordKey of the specified length.
*
* @param length the length of the generated passwordKey
* @throws IllegalArgumentException if {@code length} is less than or equal to
* zero
*/
public SecretPassword(final int length) {
super(Password.generatePrintablePassword(length));
Ctx.INSTANCE.put(PASSWORD, str);
}
/**
* Constructs a {@code SecretPassword} wrapping the specified passwordKey
* string. The passwordKey may be {@code null}.
*
* @param password the passwordKey string, or {@code null}
*/
public SecretPassword(final String password) {
super(password);
Ctx.INSTANCE.put(PASSWORD, str);
}
}

View File

@@ -0,0 +1,148 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.data.processing;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.logging.Level;
import java.util.logging.Logger;
import conflux.Ctx;
import conflux.Key;
import zeroecho.data.SecretContent;
import zeroecho.util.Password;
import zeroecho.util.RandomSupport;
/**
* A secure byte container that holds cryptographically random data, intended
* for use in encryption-related operations such as secret generation, key
* material storage, or secure content handling.
* <p>
* {@code SecretRandom} extends {@link PlainBytes} and implements
* {@link SecretContent}, offering additional semantics for managing sensitive
* data. All instances are stored in a shared context via a predefined secret
* key reference to allow later retrieval or secure reuse.
* <p>
* Multiple constructors are provided to support different secure initialization
* modes:
* <ul>
* <li>Randomly filled buffer of a specified size</li>
* <li>Wrapping an existing secure byte array</li>
* <li>Seed-based randomization using a secure hash and salt</li>
* </ul>
* <p>
* Logging is limited to operational messages and avoids revealing any sensitive
* content.
*
* @author Leo Galambos
*/
public class SecretRandom extends PlainBytes implements SecretContent {
/**
* Logger instance scoped to this class, used for operational messages related
* to secure random number generation and management.
*
* Note: Care is taken not to log the sensitive content of the secret bytes.
*/
private static final Logger LOG = Logger.getLogger(SecretRandom.class.getName());
/**
* A typed key for secret data as a byte array, delegated from
* {@link SecretMultiRecipientCryptor#SECRET}.
*/
protected static final Key<byte[]> SECRET = SecretMultiRecipientCryptor.SECRET;
/**
* Constructs a {@code SecretRandom} instance with a buffer of the specified
* length.
* <p>
* If {@code fillWithRandom} is {@code true}, the buffer is filled with
* cryptographically secure random bytes. The resulting buffer is also stored in
* the shared context using a secret key.
* </p>
*
* @param length the length (in bytes) of the buffer to be allocated
* @param fillWithRandom if {@code true}, fills the buffer with secure random
* data
*/
public SecretRandom(final int length, final boolean fillWithRandom) {
super(length);
LOG.log(Level.INFO, "generating random, {0} bytes", length);
if (fillWithRandom) {
RandomSupport.generateRandom(buffer);
}
Ctx.INSTANCE.put(SECRET, buffer);
}
/**
* Creates a new instance by copying a pre-existing cryptographically secure
* byte array.
*
* @param random the secure byte array to use as secret content
*/
public SecretRandom(final byte[] random) {
super(random);
Ctx.INSTANCE.put(SECRET, buffer);
}
/**
* Constructs a new {@code SecretRandom} instance with a securely randomized
* byte buffer.
* <p>
* The buffer is initialized with cryptographically strong random data,
* generated using a combination of a user-provided {@code seed} string and an
* internal random salt. The resulting byte array is non-deterministic and
* unique across multiple invocations, even with the same seed.
* <p>
* The randomness is generated via the
* {@link Password#generateRandom(byte[], String)} method, which uses a secure
* hash (SHA-256) of the seed and salt to initialize a {@link SecureRandom}
* instance. This ensures that the internal buffer is filled with high-entropy,
* unpredictable bytes suitable for secret material or key generation purposes.
*
* @param length the size of the internal byte buffer
* @param seed a user-defined seed string that influences the randomness
* generation; must not be {@code null}
* @throws NoSuchAlgorithmException if the cryptographic algorithm (SHA-256 or
* SecureRandom) is not available
* @throws NegativeArraySizeException if {@code length} is negative
* @throws NullPointerException if {@code seed} is {@code null}
*/
public SecretRandom(final int length, final String seed) throws NoSuchAlgorithmException {
super(length);
Password.generateRandom(buffer, seed);
Ctx.INSTANCE.put(SECRET, buffer);
}
}

View File

@@ -0,0 +1,79 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/**
* Provides a comprehensive set of classes for AES encryption and decryption,
* password-based key derivation, and secure content handling.
*
* <p>
* This package contains the core AES cryptographic components:
* <ul>
* <li>{@link AesCommon} — common AES utilities and base class for encryptors
* and decryptors</li>
* <li>{@link AesEncryptor} and {@link AesDecryptor} — stream-based AES
* encryption and decryption handlers</li>
* <li>{@link PasswordBasedAesEncryptor} and {@link PasswordBasedAesDecryptor} —
* AES implementations using password-derived keys (via PBKDF2)</li>
* <li>{@link SecretRandom} — abstract base class for secure byte sequence
* generators</li>
* <li>{@link SecretAesRandom} — deterministic AES-based implementation of
* {@code SecretRandom}</li>
* <li>{@link SecretDerivedAesParameters} — encapsulates derived AES keys and
* IVs with contextual integrity checks</li>
* <li>{@link SecretMultiRecipientCryptor} — supports encrypting data for
* multiple recipients using independent key derivation paths</li>
* <li>{@link SecretPassword} — immutable password container designed for secure
* handling and comparison</li>
* </ul>
*
* <p>
* The package also offers plain content wrappers for handling unencrypted data
* sources:
* <ul>
* <li>{@link PlainBytes} — wraps raw byte arrays as content sources</li>
* <li>{@link PlainString} — wraps UTF-8 strings as content sources</li>
* <li>{@link PlainFile} — treats file-based or URL-based content as input
* sources</li>
* </ul>
*
* <p>
* This package is designed around streaming APIs and centralized context-based
* cryptographic parameter management using the shared {@link conflux.Ctx}
* object. It facilitates secure encryption workflows with support for dynamic
* derivation, deterministic streams, and multi-party encryption use cases.
* </p>
*
* @author Leo Galambos
*/
package zeroecho.data.processing;

View File

@@ -0,0 +1,51 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.operations;
import zeroecho.data.DataContent;
import zeroecho.data.EncryptedContent;
import zeroecho.data.PlainContent;
import zeroecho.data.SecretContent;
/**
* Defines an operation that decrypts {@link EncryptedContent} using a
* {@link SecretContent} and returns a {@link DataContent}. The result is
* typically a {@link PlainContent}, but more complex encryption-decryption
* schemes may yield other types of {@link DataContent}, especially in cases
* involving multiple encryption layers for added complexity or obfuscation.
*/
public interface Decryption {
}

View File

@@ -0,0 +1,56 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.operations;
import zeroecho.data.EncryptedContent;
/**
* Defines an operation for publishing {@link EncryptedContent} to public or
* shared locations.
* <p>
* The deployment process ensures that sensitive content, once encrypted, can be
* safely disseminated in a variety of forms and mediums—without revealing its
* meaning or presence. Common deployment strategies include:
* <ul>
* <li>Saving to files on local or remote systems</li>
* <li>Writing to standard output (e.g., console or logs)</li>
* <li>Embedding in other data structures or media via steganography</li>
* <li>Distributing through network endpoints or URLs</li>
* </ul>
* In advanced use cases, deployment may also aim to obscure the very existence
* of the content, making it resistant to detection or suspicion.
*/
public interface Deployment {
}

View File

@@ -0,0 +1,46 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.operations;
import zeroecho.data.EncryptedContent;
import zeroecho.data.SecretContent;
/**
* Defines an operation that transforms arbitrary content into
* {@link EncryptedContent} using a {@link SecretContent} as the encryption key
* or secret.
*/
public interface Encryption {
}

View File

@@ -0,0 +1,64 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/**
* Provides abstractions for securely transforming and publishing sensitive data
* in encrypted form across public or shared environments.
* <p>
* The primary objective of this package is to enable the secure dissemination
* of original {@link zeroecho.data.PlainContent} by converting it into
* {@link zeroecho.data.EncryptedContent}, which can be safely shared—even in
* hostile or public environments—without exposing the underlying message.
* <p>
* This package defines the following interfaces:
* <ul>
* <li>{@link Encryption} Transforms any {@link zeroecho.data.DataContent},
* typically a {@link zeroecho.data.PlainContent}, into
* {@link zeroecho.data.EncryptedContent} using a
* {@link zeroecho.data.SecretContent} (e.g., a passphrase or key).</li>
* <li>{@link Decryption} Uses a {@link zeroecho.data.SecretContent} to
* recover the original {@link zeroecho.data.DataContent} from an
* {@link zeroecho.data.EncryptedContent}. In most cases, this yields a
* {@link zeroecho.data.PlainContent}, though more complex encryption chains are
* supported.</li>
* <li>{@link Deployment} Publishes the {@link zeroecho.data.EncryptedContent}
* to publicly accessible locations such as files, console output, URLs, or
* hidden within other data structures using techniques like steganography,
* making the content optionally hard to detect.</li>
* </ul>
* This framework supports layered encryption, flexible content handling, and
* covert deployment strategies for maximum confidentiality and plausible
* deniability in public communication or archival.
*/
package zeroecho.operations;

View File

@@ -0,0 +1,106 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.util;
import java.security.Security;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
/**
* Provides initialization support for the Bouncy Castle cryptographic provider.
* <p>
* This utility class automatically adds the Bouncy Castle provider to the Java
* Security framework upon class loading. It also offers an explicit
* initialization method for optional use.
* <p>
* The class is declared as {@code final} and has a private constructor to
* prevent instantiation, emphasizing its utility nature.
*
* <p>
* Example usage:
*
* <pre>{@code
* BouncyCastleActivator.init();
* }</pre>
*
* @author Leo Galambos
*/
public final class BouncyCastleActivator {
/**
* Logger instance for the {@code BouncyCastleActivator} class, used to log
* messages related to the initialization and management of the Bouncy Castle
* security provider.
* <p>
* Initialized with the class name to ensure logs are specific and traceable to
* this component. Useful for debugging issues during cryptographic provider
* setup.
* </p>
*/
private static final Logger LOG = Logger.getLogger(BouncyCastleActivator.class.getName());
/**
* Static initializer that registers the Bouncy Castle provider with the Java
* Security framework. Logs the initialization process.
*/
static {
LOG.log(Level.INFO, "BouncyCastle provider initialization");
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
LOG.log(Level.INFO, "BouncyCastle PQC provider initialization");
if (Security.getProvider(BouncyCastlePQCProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastlePQCProvider());
}
}
/**
* Explicitly logs the activation of the Bouncy Castle provider. This method can
* be called to confirm provider activation.
*/
static public void init() {
LOG.log(Level.INFO, "BrouncyCastle activated");
}
/**
* Private constructor to prevent instantiation of this utility class.
*/
private BouncyCastleActivator() {
// this is a utility class
}
}

View File

@@ -0,0 +1,217 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.util;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
/**
* Enumeration of supported cryptographic algorithm names used in
* {@code KeyPairAlgorithm}.
*
* <p>
* Each name includes a human-readable display name that is used for external
* representation, such as in logs or UIs. The internal enum name (e.g.,
* {@code ML_KEM}) can still be accessed via {@link #name()}, while
* {@link #toString()} and {@link #displayName()} return the readable form.
*
* <p>
* This enum can be used in {@code switch-case} structures for logic branching
* on algorithm families.
*/
public enum CryptoAlgorithmsNames {
/** EdDSA (Edwards-curve Digital Signature Algorithm). */
ED25519("Ed25519"),
/** RSA public-key encryption and signature algorithm. */
RSA("RSA"),
/** DSA (Digital Signature Algorithm). */
DSA("DSA"),
/** EC (Elliptic Curve) public-key cryptography. */
EC("EC"),
/** ElGamal public-key encryption algorithm. */
ELGAMAL("ElGamal"),
/** NaccacheStern public-key cryptosystem. */
/// NACCACHE("NaccacheStern"),
/** McEliece code-based public-key encryption scheme. */
MCELIECE("McEliece"),
/**
* ML-KEM (Kyber), a post-quantum lattice-based KEM (Key Encapsulation
* Mechanism).
*/
KYBER("ML-KEM"),
/** SPHINCS+, a stateless hash-based digital signature scheme. */
SPHINCS_PLUS("SPHINCS+"),
/** NewHope, a post-quantum lattice-based KEM. */
NEWHOPE("NewHope"),
/**
* FrodoKEM, a post-quantum lattice-based KEM based on learning with errors
* (LWE).
*/
FRODO("Frodo");
private final String displayNameField;
private static final Map<String, CryptoAlgorithmsNames> BY_NAME = new HashMap<>(); // NOPMD
static {
for (CryptoAlgorithmsNames n : values()) {
BY_NAME.put(n.displayNameField.toUpperCase(Locale.ROOT), n); // case-insensitive match
}
}
/**
* Constructs an algorithm name enum with a human-readable name.
*
* @param displayName the external string representation of the algorithm name
*/
CryptoAlgorithmsNames(String displayName) {
this.displayNameField = displayName;
}
/**
* Returns the external display name of the algorithm.
*
* @return the human-readable algorithm name
*/
public String displayName() {
return displayNameField;
}
/**
* Returns the string representation of the algorithm, same as
* {@link #displayName()}.
*
* @return the human-readable algorithm name
*/
@Override
public String toString() {
return displayNameField;
}
/**
* Parses a human-readable algorithm name and returns the corresponding
* {@code CryptoAlgorithmsNames} enum constant. Matching is case-insensitive.
* <p>
* The method also normalizes the algorithm name extracted from a public or
* private key's format or metadata.
* <p>
* Some cryptographic providers, like BouncyCastle, use detailed algorithm names
* (e.g., "SPHINCS+-SHA2-256S") to specify variant or parameter information.
* This method maps such names to more general algorithm identifiers (e.g.,
* "SPHINCS+").
* <p>
* This ensures consistent algorithm identification regardless of the variant or
* parameter set.
*
* @param name the name of the algorithm (e.g., "RSA", "ML-KEM",
* "SPHINCS+-SHA2-256S")
* @return the corresponding {@code CryptoAlgorithmsNames} enum constant
* @throws IllegalArgumentException if the name is unknown
*/
public static CryptoAlgorithmsNames fromString(String name) { // NOPMD
if (name == null) {
throw new IllegalArgumentException("Algorithm name must not be null.");
}
if (name.startsWith("ML-KEM")) {
name = "ML-KEM"; // NOPMD
} else {
if (name.startsWith("EdDSA")) {
name = "Ed25519";
} else {
if (name.startsWith("Frodo")) {
name = "Frodo";
} else {
final int i = name.indexOf('-');
if (i != -1) {
name = name.substring(0, i);
}
}
}
}
final CryptoAlgorithmsNames result = BY_NAME.get(name.trim().toUpperCase(Locale.ROOT));
if (result == null) {
throw new IllegalArgumentException("Unknown algorithm name: " + name);
}
return result;
}
/**
* Returns a {@link KeyFactory} instance for the algorithm represented by this
* enum constant.
* <p>
* This method first attempts to obtain the {@code KeyFactory} from the classic
* Bouncy Castle provider ({@link BouncyCastleProvider}). If the algorithm is
* not supported there — typically in the case of post-quantum cryptographic
* (PQC) algorithms — it falls back to the Bouncy Castle PQC provider
* ({@link BouncyCastlePQCProvider}).
* </p>
*
* @return a {@code KeyFactory} for the algorithm associated with this enum
* constant
* @throws NoSuchAlgorithmException if the algorithm is not available from
* either provider
* @throws NoSuchProviderException if the required Bouncy Castle provider is
* not registered
*/
public KeyFactory getFactory() throws NoSuchAlgorithmException, NoSuchProviderException {
try {
// Classic algo ?
return KeyFactory.getInstance(displayNameField, BouncyCastleProvider.PROVIDER_NAME);
} catch (NoSuchAlgorithmException x) {
// PQC algo ?
return KeyFactory.getInstance(displayNameField, BouncyCastlePQCProvider.PROVIDER_NAME);
}
}
}

View File

@@ -0,0 +1,286 @@
/*******************************************************************************
* Copyright (C) 2024, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.util;
import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* Utility class for performing efficient I/O operations with support for packed
* integers. This class provides static methods to write and read data with a
* compact variable-length encoding ("Pack7") and custom handling of UUIDs and
* UTF-8 strings.
* <p>
* <b>Design rationale:</b> Using static methods allows developers to perform
* encoding and decoding operations directly on existing {@link InputStream} and
* {@link OutputStream} instances without the need for additional stream
* wrappers such as {@link FilterInputStream} or {@link FilterOutputStream}.
* This approach eliminates extra object creation and method call overhead,
* potentially improving performance, especially in high-throughput scenarios.
* </p>
* <p>
* <b>Usage:</b> These methods can be used independently with any I/O stream to
* achieve custom serialization formats, such as length-prefixed UTF-8 strings,
* packed integers, or custom binary structures.
* </p>
*/
public final class IOUtil { // NOPMD by Leo Galambos on 6/1/25, 4:30PM
/**
* Private constructor to prevent instantiation of this utility class.
*/
private IOUtil() {
// this is a utility class
}
/**
* Writes a byte array to the output stream with its length encoded as a packed
* 7-bit integer.
*
* @param out the output stream
* @param buf the byte array to write
* @throws IOException if an I/O error occurs
*/
public static void write(final OutputStream out, final byte[] buf) throws IOException {
writePack7I(out, buf.length);
out.write(buf);
}
/**
* Writes a UUID to the output stream as two big-endian long values.
*
* @param out the output stream
* @param uuid the UUID to write
* @throws IOException if an I/O error occurs
*/
public static void write(final OutputStream out, final UUID uuid) throws IOException {
writeLong(out, uuid.getMostSignificantBits());
writeLong(out, uuid.getLeastSignificantBits());
}
/**
* Writes a UTF-8 encoded string to the output stream with its length as a
* packed 7-bit integer.
*
* @param out the output stream
* @param str the string to write
* @throws IOException if an I/O error occurs
*/
public static void writeUTF8(final OutputStream out, final String str) throws IOException {
final byte[] buf = str.getBytes(StandardCharsets.UTF_8);
writePack7I(out, buf.length);
out.write(buf);
}
/**
* Writes a long value to the output stream in big-endian order (8 bytes).
*
* @param out the output stream
* @param val the long value to write
* @throws IOException if an I/O error occurs
*/
public static void writeLong(final OutputStream out, long val) throws IOException {
final byte[] buf = new byte[8];
for (int i = buf.length; --i >= 0;) { // NOPMD by Leo Galambos on 6/1/25, 4:32PM
buf[i] = (byte) (val & 0xff);
val = val >>> 8; // NOPMD by Leo Galambos on 6/1/25, 4:33PM
}
out.write(buf);
}
/**
* Reads a UUID from the input stream, composed of two big-endian long values.
*
* @param in the input stream
* @return the UUID read from the stream
* @throws IOException if an I/O error occurs or if the stream ends prematurely
*/
public static UUID readUUID(final InputStream in) throws IOException {
final long msb = readLong(in);
final long lsb = readLong(in);
return new UUID(msb, lsb);
}
/**
* Reads a UTF-8 encoded string from the input stream. The string is prefixed
* with a packed 7-bit integer indicating its length. To prevent excessive
* allocation, the maximum allowable length must be specified.
*
* @param in the input stream
* @param maxLength the maximum allowable length of the string in bytes
* @return the string read from the stream
* @throws IOException if an I/O error occurs, the length exceeds maxLength, or
* if the stream ends prematurely
*/
public static String readUTF8(final InputStream in, final int maxLength) throws IOException {
final int len = readPack7I(in);
if (len > maxLength) {
throw new IOException("readUTF8 length " + len + " exceeds maximum allowed length " + maxLength);
}
final byte[] buf = new byte[len];
if (in.readNBytes(buf, 0, buf.length) != buf.length) {
throw new EOFException("readUTF8 EOF");
}
return new String(buf, StandardCharsets.UTF_8);
}
/**
* Reads a long value from the input stream in big-endian order (8 bytes).
*
* @param in the input stream
* @return the long value read
* @throws IOException if an I/O error occurs or if the stream ends prematurely
*/
public static long readLong(final InputStream in) throws IOException {
final byte[] buff = new byte[8];
if (in.readNBytes(buff, 0, buff.length) != buff.length) {
throw new EOFException("readLong EOF");
}
long result = 0;
for (final byte b : buff) {
result = (result << 8) | (b & 0xffL);
}
return result;
}
/**
* Writes an integer to the output stream using packed 7-bit encoding (variable
* length).
*
* @param out the output stream
* @param val the integer value to write
* @throws IOException if an I/O error occurs
*/
public static void writePack7I(final OutputStream out, int val) throws IOException {
final byte[] buff = new byte[5];
int idx = buff.length;
while ((val & ~0x7f) != 0) {
buff[--idx] = (byte) (val & 0x7f);
val = val >>> 7; // NOPMD by Leo Galambos on 6/1/25, 4:35PM
}
buff[--idx] = (byte) val;
buff[buff.length - 1] |= 0x80;
out.write(buff, idx, buff.length - idx);
}
/**
* Writes a long value to the output stream using packed 7-bit encoding
* (variable length).
*
* @param out the output stream
* @param val the long value to write
* @throws IOException if an I/O error occurs
*/
public static void writePack7L(final OutputStream out, long val) throws IOException {
final byte[] buff = new byte[10];
int idx = buff.length;
while ((val & ~0x7fL) != 0) {
buff[--idx] = (byte) (val & 0x7f);
val = val >>> 7; // NOPMD by Leo Galambos on 6/1/25, 4:37PM
}
buff[--idx] = (byte) val;
buff[buff.length - 1] |= 0x80;
out.write(buff, idx, buff.length - idx);
}
/**
* Reads a byte array from the input stream. The length of the array is
* specified as a packed 7-bit integer prefix. To prevent excessive allocation,
* the maximum allowable length must be specified.
*
* @param in the input stream
* @param maxLength the maximum allowable length of the byte array
* @return the byte array read
* @throws IOException if an I/O error occurs, the length exceeds maxLength, or
* if the stream ends prematurely
*/
public static byte[] read(final InputStream in, final int maxLength) throws IOException {
final int len = readPack7I(in);
if (len > maxLength || len < 0) {
throw new IOException("read length " + len + " exceeds maximum allowed length " + maxLength);
}
final byte[] result = new byte[len];
if (in.readNBytes(result, 0, result.length) != result.length) {
throw new EOFException("read EOF");
}
return result;
}
/**
* Reads an integer from the input stream using packed 7-bit encoding (variable
* length).
*
* @param in the input stream
* @return the integer value read
* @throws IOException if an I/O error occurs or if the stream ends prematurely
*/
public static int readPack7I(final InputStream in) throws IOException {
int result = in.read();
if (result > 0x7f) { // NOPMD by Leo Galambos on 6/1/25, 4:38PM
return result & 0x7f;
}
int i;
for (i = in.read(); i < 0x80; i = in.read()) {
result = (result << 7) | i;
}
return (result << 7) | (i & 0x7f);
}
/**
* Reads a long value from the input stream using packed 7-bit encoding
* (variable length).
*
* @param in the input stream
* @return the long value read
* @throws IOException if an I/O error occurs or if the stream ends prematurely
*/
public static long readPack7L(final InputStream in) throws IOException {
long result = in.read();
if (result > 0x7f) { // NOPMD by Leo Galambos on 6/1/25, 4:38PM
return result & 0x7fL;
}
int i;
for (i = in.read(); i < 0x80; i = in.read()) {
result = (result << 7) | i;
}
return (result << 7) | (i & 0x7f);
}
}

View File

@@ -0,0 +1,516 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.util;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.ECGenParameterSpec;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.KeyGenerationParameters;
import org.bouncycastle.crypto.generators.ElGamalParametersGenerator;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ElGamalParameters;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ElGamalParameterSpec;
import org.bouncycastle.pqc.crypto.frodo.FrodoKeyGenerationParameters;
import org.bouncycastle.pqc.crypto.frodo.FrodoKeyPairGenerator;
import org.bouncycastle.pqc.crypto.frodo.FrodoParameters;
import org.bouncycastle.pqc.crypto.frodo.FrodoPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.frodo.FrodoPublicKeyParameters;
import org.bouncycastle.pqc.crypto.newhope.NHKeyPairGenerator;
import org.bouncycastle.pqc.crypto.newhope.NHPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.newhope.NHPublicKeyParameters;
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
import org.bouncycastle.pqc.jcajce.spec.KyberParameterSpec;
import org.bouncycastle.pqc.jcajce.spec.SPHINCSPlusParameterSpec;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McElieceKeyGenerationParameters;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McElieceKeyPairGenerator;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McElieceParameters;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McEliecePrivateKeyParameters;
import org.bouncycastle.pqc.legacy.crypto.mceliece.McEliecePublicKeyParameters;
import zeroecho.util.bc.FrodoPrivateKey;
import zeroecho.util.bc.FrodoPublicKey;
import zeroecho.util.bc.McEliecePrivateKey;
import zeroecho.util.bc.McEliecePublicKey;
import zeroecho.util.bc.NewHopePrivateKey;
import zeroecho.util.bc.NewHopePublicKey;
/**
* Enumeration representing supported asymmetric key pair algorithms along with
* their key sizes and post-quantum cryptography (PQC) status.
*
* <p>
* This enum provides constants for classical cryptographic algorithms such as
* RSA, DSA, EC, ElGamal, and EdDSA, as well as various post-quantum algorithms
* like Kyber, SPHINCS+, NewHope, FrodoKEM, and McEliece.
* </p>
*
* <p>
* Each enum constant includes the algorithm's name, key size (or security level
* for PQC algorithms), and whether it is a post-quantum cryptographic
* algorithm.
* </p>
*
* <p>
* Functionality includes:
* <ul>
* <li>Generating key pairs using the Bouncy Castle (classic and PQC)
* providers.</li>
* <li>Converting Java {@link PublicKey} and {@link PrivateKey} objects to
* Bouncy Castle {@link org.bouncycastle.crypto.params.AsymmetricKeyParameter}
* instances.</li>
* <li>Parsing string representations of algorithms in the format
* "ALGORITHM:KEYSIZE" (case-insensitive) to corresponding enum constants.</li>
* </ul>
*
* <p>
* Note: The {@link #generateKeyPair()} method automatically adds the necessary
* Bouncy Castle providers if not already present.
* </p>
*
* <p>
* The {@link #generateKeyPair()} method uses the Bouncy Castle provider to
* generate the key pair, and the enum supports parsing from string format
* (e.g., {@code "RSA:2048"}) via {@link #fromString(String)}.
* </p>
*
* <p>
* Example usage: <pre>{@code
* KeyPairAlgorithm alg = KeyPairAlgorithm.RSA_2048;
* KeyPair keyPair = alg.generateKeyPair();
* }</pre>
*
* @author Leo Galambos
*/
public enum KeyPairAlgorithm {
/**
* EdDSA (Edwards-curve Digital Signature Algorithm) with 255-bit key size.
*/
ED25519(CryptoAlgorithmsNames.ED25519, 255),
/**
* RSA algorithm with 1024-bit key size.
*/
RSA_1024(CryptoAlgorithmsNames.RSA, 1024),
/**
* RSA algorithm with 2048-bit key size.
*/
RSA_2048(CryptoAlgorithmsNames.RSA, 2048),
/**
* RSA algorithm with 4096-bit key size.
*/
RSA_4096(CryptoAlgorithmsNames.RSA, 4096),
/**
* DSA algorithm with 2048-bit key size.
*/
DSA_2048(CryptoAlgorithmsNames.DSA, 2048),
/**
* EC algorithm with 256-bit key size (e.g., secp256r1).
*/
EC_P256(CryptoAlgorithmsNames.EC, 256),
/**
* EC algorithm with 384-bit key size (e.g., secp384r1).
*/
EC_P384(CryptoAlgorithmsNames.EC, 384),
/**
* ElGamal algorithm with 512-bit key size.
*/
ELGAMAL_512(CryptoAlgorithmsNames.ELGAMAL, 512),
/**
* ElGamal algorithm with 1024-bit key size.
*/
ELGAMAL_1024(CryptoAlgorithmsNames.ELGAMAL, 1024),
/**
* ElGamal algorithm with 2048-bit key size.
*/
ELGAMAL_2048(CryptoAlgorithmsNames.ELGAMAL, 2048),
/// NaccacheStern algorithm with 2048-bit key size.
/// NACCACHE_2048(CryptoAlgorithmsNames.NACCACHE, 2048),
/**
* McEliece algorithm with 256-bit security level.
*/
MCELIECE_256(CryptoAlgorithmsNames.MCELIECE, 256, true),
/**
* ML-KEM (Kyber) algorithm with 512-bit security level.
*/
KYBER_512(CryptoAlgorithmsNames.KYBER, 512, true),
/**
* ML-KEM (Kyber) algorithm with 768-bit security level.
*/
KYBER_768(CryptoAlgorithmsNames.KYBER, 768, true),
/**
* ML-KEM (Kyber) algorithm with 1024-bit security level.
*/
KYBER_1024(CryptoAlgorithmsNames.KYBER, 1024, true),
/**
* SPHINCS+ algorithm with 128-bit security level.
*/
SPHINCS_PLUS_128S(CryptoAlgorithmsNames.SPHINCS_PLUS, 128, true),
/**
* SPHINCS+ algorithm with 192-bit security level.
*/
SPHINCS_PLUS_192S(CryptoAlgorithmsNames.SPHINCS_PLUS, 192, true),
/**
* SPHINCS+ algorithm with 256-bit security level.
*/
SPHINCS_PLUS_256S(CryptoAlgorithmsNames.SPHINCS_PLUS, 256, true),
/**
* NewHope algorithm with 512-bit parameter set.
*/
NEWHOPE_512(CryptoAlgorithmsNames.NEWHOPE, 512, true),
/**
* NewHope algorithm with 1024-bit parameter set.
*/
NEWHOPE_1024(CryptoAlgorithmsNames.NEWHOPE, 1024, true),
/**
* FrodoKEM algorithm with 640-bit parameter set.
*/
FRODOKEM_640(CryptoAlgorithmsNames.FRODO, 640, true),
/**
* FrodoKEM algorithm with 976-bit parameter set.
*/
FRODOKEM_976(CryptoAlgorithmsNames.FRODO, 976, true),
/**
* FrodoKEM algorithm with 1344-bit parameter set.
*/
FRODOKEM_1344(CryptoAlgorithmsNames.FRODO, 1344, true);
/**
* Algorithms for which key pairs are serializable (i.e., keys return non-null
* from {@code getEncoded()}).
*/
public static final KeyPairAlgorithm[] SERIALIZABLE_ALGORITHMS = {
/// EdDSA (Edwards-curve Digital Signature Algorithm) with 255-bit key size.
ED25519,
/// RSA with 1024-bit key size.
RSA_1024,
/// RSA with 2048-bit key size.
RSA_2048,
/// RSA with 4096-bit key size.
RSA_4096,
/// DSA with 2048-bit key size.
DSA_2048,
/// Elliptic Curve with 256-bit key size (e.g., secp256r1).
EC_P256,
/// Elliptic Curve with 384-bit key size (e.g., secp384r1).
EC_P384,
/// ElGamal with 512-bit key size.
ELGAMAL_512,
/// ElGamal with 1024-bit key size.
ELGAMAL_1024,
/// ElGamal with 2048-bit key size.
ELGAMAL_2048,
/// ML-KEM / Kyber with 512-bit parameter.
KYBER_512,
/// ML-KEM / Kyber with 768-bit parameter.
KYBER_768,
/// ML-KEM / Kyber with 1024-bit parameter.
KYBER_1024,
/// SPHINCS+ with 128-bit security level (SHAKE-256).
SPHINCS_PLUS_128S,
/// SPHINCS+ with 192-bit security level (SHAKE-256).
SPHINCS_PLUS_192S,
/// SPHINCS+ with 256-bit security level (SHAKE-256).
SPHINCS_PLUS_256S,
/// FrodoKEM algorithm with 640-bit parameter set.
FRODOKEM_640,
/// FrodoKEM algorithm with 976-bit parameter set.
FRODOKEM_976,
/// FrodoKEM algorithm with 1344-bit parameter set.
FRODOKEM_1344 };
private static final Map<String, KeyPairAlgorithm> BY_STRING = new HashMap<>(); // NOPMD
static {
for (KeyPairAlgorithm alg : values()) {
BY_STRING.put(alg.toString().toUpperCase(Locale.ROOT), alg);
}
}
private final CryptoAlgorithmsNames algorithmName;
private final int keySize;
private final boolean pqc;
KeyPairAlgorithm(CryptoAlgorithmsNames algorithmName, int keySize) {
this(algorithmName, keySize, false);
}
KeyPairAlgorithm(CryptoAlgorithmsNames algorithmName, int keySize, boolean pqc) {
this.algorithmName = algorithmName;
this.keySize = keySize;
this.pqc = pqc;
}
/**
* Converts a given {@link PublicKey} into the corresponding Bouncy Castle
* {@link org.bouncycastle.crypto.params.AsymmetricKeyParameter}.
*
* @param key the public key to convert
* @return the corresponding BC asymmetric key parameter
* @throws IOException if the key encoding is invalid or conversion fails
*/
public AsymmetricKeyParameter getKeyParameter(PublicKey key) throws IOException {
return pqc ? org.bouncycastle.pqc.crypto.util.PublicKeyFactory.createKey(key.getEncoded())
: org.bouncycastle.crypto.util.PublicKeyFactory.createKey(key.getEncoded());
}
/**
* Converts a given {@link PrivateKey} into the corresponding Bouncy Castle
* {@link org.bouncycastle.crypto.params.AsymmetricKeyParameter}.
*
* @param key the private key to convert
* @return the corresponding BC asymmetric key parameter
* @throws IOException if the key encoding is invalid or conversion fails
*/
public AsymmetricKeyParameter getKeyParameter(PrivateKey key) throws IOException {
return pqc ? org.bouncycastle.pqc.crypto.util.PrivateKeyFactory.createKey(key.getEncoded())
: org.bouncycastle.crypto.util.PrivateKeyFactory.createKey(key.getEncoded());
}
/**
* Returns the standard cryptographic algorithm name (e.g., "RSA", "EC", "DSA")
* associated with this enum constant.
*
* @return the algorithm name as a {@code String}
*/
public String getAlgorithmName() {
return algorithmName.toString();
}
/**
* Returns the key size or security level in bits for this algorithm.
*
* <p>
* For classical algorithms, this corresponds to the actual key size (e.g., 2048
* bits for RSA). For post-quantum algorithms, this corresponds to the security
* level or parameter set size.
* </p>
*
* @return the key size in bits, or a negative number if undefined
*/
public int getKeySize() {
return keySize;
}
/**
* Returns a string representation of this algorithm in the format
* {@code "ALGORITHM:KEYSIZE"}, e.g., {@code "RSA:2048"} or
* {@code "McEliece:256"}. This format is consistent for parsing and display.
*
* @return the formatted string representation of the algorithm and key size
*/
@Override
public String toString() {
return algorithmName + ":" + keySize;
}
/**
* Generates a new {@link KeyPair} for this algorithm using the Bouncy Castle
* providers.
*
* <p>
* This method automatically adds and configures the Bouncy Castle and Bouncy
* Castle PQC providers if not already present in the Java Security environment.
* </p>
*
* @return a newly generated {@link KeyPair} instance for this algorithm
* @throws NoSuchAlgorithmException if the algorithm is not supported
* @throws NoSuchProviderException if the required Bouncy Castle
* provider is not registered or
* available
* @throws InvalidAlgorithmParameterException if algorithm parameters are
* invalid
*/
public KeyPair generateKeyPair() // NOPMD
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
switch (this) {
case ED25519: {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(algorithmName.displayName());
keyPairGenerator.initialize(255, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
case RSA_1024:
case RSA_2048:
case RSA_4096:
case DSA_2048: {
KeyPairGenerator gen = KeyPairGenerator.getInstance(algorithmName.displayName(),
BouncyCastleProvider.PROVIDER_NAME);
gen.initialize(keySize);
return gen.generateKeyPair();
}
case EC_P256:
case EC_P384: {
KeyPairGenerator gen = KeyPairGenerator.getInstance(algorithmName.displayName(),
BouncyCastleProvider.PROVIDER_NAME);
gen.initialize(new ECGenParameterSpec("secp" + keySize + "r1"));
return gen.generateKeyPair();
}
case ELGAMAL_512:
case ELGAMAL_1024:
case ELGAMAL_2048: {
ElGamalParametersGenerator paramGen = new ElGamalParametersGenerator();
paramGen.init(keySize, 20, RandomSupport.getRandom());
ElGamalParameters params = paramGen.generateParameters();
ElGamalParameterSpec spec = new ElGamalParameterSpec(params.getP(), params.getG());
KeyPairGenerator gen = KeyPairGenerator.getInstance(algorithmName.displayName(),
BouncyCastleProvider.PROVIDER_NAME);
gen.initialize(spec);
return gen.generateKeyPair();
}
case MCELIECE_256: {
McElieceKeyPairGenerator generator = new McElieceKeyPairGenerator();
McElieceKeyGenerationParameters params = new McElieceKeyGenerationParameters(RandomSupport.getRandom(),
new McElieceParameters());
generator.init(params);
AsymmetricCipherKeyPair kp = generator.generateKeyPair();
McEliecePublicKeyParameters pubParams = (McEliecePublicKeyParameters) kp.getPublic();
McEliecePrivateKeyParameters privParams = (McEliecePrivateKeyParameters) kp.getPrivate();
PublicKey pub = new McEliecePublicKey(pubParams);
PrivateKey priv = new McEliecePrivateKey(privParams);
return new KeyPair(pub, priv);
}
case KYBER_512:
case KYBER_768:
case KYBER_1024: {
KeyPairGenerator gen = KeyPairGenerator.getInstance("Kyber", BouncyCastlePQCProvider.PROVIDER_NAME);
KyberParameterSpec paramSpec = switch (this) {
case KYBER_512 -> KyberParameterSpec.kyber512;
case KYBER_768 -> KyberParameterSpec.kyber768;
case KYBER_1024 -> KyberParameterSpec.kyber1024;
default -> throw new IllegalStateException("Unexpected Kyber variant");
};
gen.initialize(paramSpec, RandomSupport.getRandom());
return gen.generateKeyPair();
}
case SPHINCS_PLUS_128S:
case SPHINCS_PLUS_192S:
case SPHINCS_PLUS_256S: {
SPHINCSPlusParameterSpec params = switch (this) {
case SPHINCS_PLUS_128S -> SPHINCSPlusParameterSpec.shake_128s;
case SPHINCS_PLUS_192S -> SPHINCSPlusParameterSpec.shake_192s;
case SPHINCS_PLUS_256S -> SPHINCSPlusParameterSpec.shake_256s;
default -> throw new IllegalStateException("Unexpected SPHINCS variant");
};
KeyPairGenerator gen = KeyPairGenerator.getInstance("SPHINCSPlus",
BouncyCastlePQCProvider.PROVIDER_NAME);
gen.initialize(params);
return gen.generateKeyPair();
}
case NEWHOPE_512:
case NEWHOPE_1024: {
NHKeyPairGenerator generator = new NHKeyPairGenerator();
generator.init(new KeyGenerationParameters(RandomSupport.getRandom(), keySize));
AsymmetricCipherKeyPair kp = generator.generateKeyPair();
NHPublicKeyParameters pubParams = (NHPublicKeyParameters) kp.getPublic();
NHPrivateKeyParameters privParams = (NHPrivateKeyParameters) kp.getPrivate();
PublicKey pub = new NewHopePublicKey(pubParams);
PrivateKey priv = new NewHopePrivateKey(privParams);
return new KeyPair(pub, priv);
}
case FRODOKEM_640:
case FRODOKEM_976:
case FRODOKEM_1344: {
FrodoParameters params = switch (this) {
case FRODOKEM_640 -> FrodoParameters.frodokem640aes;
case FRODOKEM_976 -> FrodoParameters.frodokem976aes;
case FRODOKEM_1344 -> FrodoParameters.frodokem1344aes;
default -> throw new IllegalStateException("Unexpected Frodo variant");
};
FrodoKeyPairGenerator generator = new FrodoKeyPairGenerator();
generator.init(new FrodoKeyGenerationParameters(RandomSupport.getRandom(), params));
AsymmetricCipherKeyPair kp = generator.generateKeyPair();
FrodoPublicKeyParameters pubParams = (FrodoPublicKeyParameters) kp.getPublic();
FrodoPrivateKeyParameters privParams = (FrodoPrivateKeyParameters) kp.getPrivate();
PublicKey pub = new FrodoPublicKey(pubParams);
PrivateKey priv = new FrodoPrivateKey(privParams);
return new KeyPair(pub, priv);
}
}
return null;
}
/**
* Parses a string representation of an algorithm in the format
* {@code "ALGORITHM:KEYSIZE"} (case-insensitive) and returns the matching
* {@link KeyPairAlgorithm} enum constant.
*
* @param value the string to parse, e.g., "RSA:2048"
* @return the corresponding {@code KeyPairAlgorithm} constant
* @throws IllegalArgumentException if the input is null, malformed, or not
* recognized
*/
public static KeyPairAlgorithm fromString(String value) {
if (value == null || !value.contains(":")) {
throw new IllegalArgumentException("Expected format: ALGORITHM:KEYSIZE (e.g., RSA:2048)");
}
final KeyPairAlgorithm alg = BY_STRING.get(value.trim().toUpperCase(Locale.ROOT));
if (alg == null) {
throw new IllegalArgumentException("Unsupported key pair algorithm: " + value);
}
return alg;
}
}

View File

@@ -0,0 +1,425 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.util; // NOPMD
import java.io.IOException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.DSAKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.crypto.interfaces.DHKey;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.engines.ElGamalEngine;
import org.bouncycastle.crypto.engines.RSAEngine;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.DSAParameters;
import org.bouncycastle.crypto.params.DSAPrivateKeyParameters;
import org.bouncycastle.crypto.params.DSAPublicKeyParameters;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.params.ElGamalParameters;
import org.bouncycastle.crypto.params.ElGamalPrivateKeyParameters;
import org.bouncycastle.crypto.params.ElGamalPublicKeyParameters;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.pqc.crypto.frodo.FrodoKEMExtractor;
import org.bouncycastle.pqc.crypto.frodo.FrodoKEMGenerator;
import org.bouncycastle.pqc.crypto.frodo.FrodoPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.frodo.FrodoPublicKeyParameters;
import org.bouncycastle.pqc.crypto.mlkem.MLKEMExtractor;
import org.bouncycastle.pqc.crypto.mlkem.MLKEMGenerator;
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPrivateKeyParameters;
import org.bouncycastle.pqc.crypto.mlkem.MLKEMPublicKeyParameters;
import org.bouncycastle.pqc.crypto.util.PrivateKeyFactory;
import org.bouncycastle.pqc.crypto.util.PublicKeyFactory;
import org.bouncycastle.pqc.jcajce.interfaces.SPHINCSPlusPrivateKey;
import org.bouncycastle.pqc.jcajce.interfaces.SPHINCSPlusPublicKey;
import zeroecho.util.asymmetric.AsymmetricContext;
import zeroecho.util.asymmetric.ClassicAsymmetricContext;
import zeroecho.util.asymmetric.KEMAsymmetricContext;
import zeroecho.util.asymmetric.SignatureAsymmetricContext;
import zeroecho.util.bc.FrodoPrivateKey;
import zeroecho.util.bc.FrodoPublicKey;
/**
* Utility class for converting between standard Java {@link Key} objects and
* BouncyCastle {@link AsymmetricKeyParameter} with optional cipher context.
* <p>
* Supports serialization and deserialization of public and private keys to/from
* a standardized string format, as well as conversion to
* {@link AsymmetricContext} wrappers compatible with BouncyCastle.
* </p>
*
* <h2>Supported Algorithms</h2>
* <ul>
* <li>RSA</li>
* <li>DSA</li>
* <li>EC</li>
* <li>ElGamal</li>
* <li>SPHINCS+</li>
* <li>KEM-based (e.g., Kyber, Frodo)</li>
* </ul>
*
* <h2>String Format</h2> <pre>{@code
* ALGORITHM:BIT_LENGTH:BASE64_ENCODED_KEY
* Example: RSA:2048:MIIBIjANBgkqhkiG9...
* }</pre>
*
* <p>
* Note that algorithms which do not support encryption (e.g., DSA, EC) will
* have a <code>null</code> cipher in their {@link AsymmetricContext}.
* </p>
*/
public final class KeySupport {
private KeySupport() {
// Utility class; do not instantiate
}
/**
* Converts a standard Java {@link PublicKey} into an {@link AsymmetricContext},
* which contains the corresponding BouncyCastle key parameter and an optional
* encryption engine if applicable.
*
* @param pubKey the public key to convert
* @return an {@link AsymmetricContext} wrapping the key and optional cipher
* @throws IOException if key decoding fails
* @throws IllegalArgumentException if the algorithm is unsupported
*/
public static AsymmetricContext fromKey(final PublicKey pubKey) throws IOException { // NOPMD
return switch (CryptoAlgorithmsNames.fromString(pubKey.getAlgorithm())) {
case CryptoAlgorithmsNames.ED25519 -> {
byte[] keyBytes = pubKey.getEncoded(); // full SubjectPublicKeyInfo DER-encoded
SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(keyBytes);
byte[] keyData = spki.getPublicKeyData().getBytes();
Ed25519PublicKeyParameters keyParam = new Ed25519PublicKeyParameters(keyData, 0);
yield new SignatureAsymmetricContext(keyParam); // , null);
}
case CryptoAlgorithmsNames.RSA -> {
final RSAPublicKey rsa = (RSAPublicKey) pubKey;
final RSAKeyParameters keyParam = new RSAKeyParameters(false, rsa.getModulus(),
rsa.getPublicExponent());
yield new ClassicAsymmetricContext(keyParam, new org.bouncycastle.crypto.encodings.OAEPEncoding(
new RSAEngine(), new SHA256Digest(), new byte[0]));
}
case CryptoAlgorithmsNames.DSA -> {
final DSAPublicKey dsa = (DSAPublicKey) pubKey;
final DSAParams p = dsa.getParams();
final DSAParameters dsaParams = new DSAParameters(p.getP(), p.getQ(), p.getG());
final DSAPublicKeyParameters keyParam = new DSAPublicKeyParameters(dsa.getY(), dsaParams);
yield new SignatureAsymmetricContext(keyParam); // , null); // Signing only
}
case CryptoAlgorithmsNames.EC -> {
final ECPublicKey ec = (ECPublicKey) pubKey;
final ECNamedCurveSpec ecSpec = (ECNamedCurveSpec) ec.getParams();
final ECNamedCurveParameterSpec bcSpec = ECNamedCurveTable.getParameterSpec(ecSpec.getName());
final ECPoint point = bcSpec.getCurve().createPoint(ec.getW().getAffineX(), ec.getW().getAffineY());
final ECDomainParameters domainParams = new ECDomainParameters(bcSpec.getCurve(), bcSpec.getG(),
bcSpec.getN(), bcSpec.getH(), bcSpec.getSeed());
final ECPublicKeyParameters keyParam = new ECPublicKeyParameters(point, domainParams);
yield new SignatureAsymmetricContext(keyParam); // , null); // Signing only
}
case CryptoAlgorithmsNames.ELGAMAL -> {
final DHPublicKey elg = (DHPublicKey) pubKey;
final DHParameterSpec elParams = elg.getParams();
final ElGamalParameters params = new ElGamalParameters(elParams.getP(), elParams.getG());
final ElGamalPublicKeyParameters keyParam = new ElGamalPublicKeyParameters(elg.getY(), params);
yield new ClassicAsymmetricContext(keyParam, new ElGamalEngine());
}
case CryptoAlgorithmsNames.SPHINCS_PLUS -> {
final SPHINCSPlusPublicKey sphincs = (SPHINCSPlusPublicKey) pubKey;
final AsymmetricKeyParameter keyParam = PublicKeyFactory.createKey(sphincs.getEncoded());
yield new SignatureAsymmetricContext(keyParam); // , (EncapsulatedSecretGenerator) null);
}
case CryptoAlgorithmsNames.KYBER -> {
// Kyber
final MLKEMPublicKeyParameters keyParam = (MLKEMPublicKeyParameters) PublicKeyFactory
.createKey(pubKey.getEncoded());
yield new KEMAsymmetricContext(keyParam, new MLKEMGenerator(RandomSupport.getRandom()));
}
case CryptoAlgorithmsNames.FRODO -> {
// Frodo
final FrodoPublicKeyParameters keyParam = ((FrodoPublicKey) pubKey).getKeyParameters();
yield new KEMAsymmetricContext(keyParam, new FrodoKEMGenerator(RandomSupport.getRandom()));
}
default -> {
throw new IllegalArgumentException("Unsupported algorithm: " + pubKey.getAlgorithm());
}
};
}
/**
* Converts a standard Java {@link PrivateKey} into an
* {@link AsymmetricContext}, which contains the corresponding BouncyCastle key
* parameter and an optional encryption engine if applicable.
*
* @param privKey the private key to convert
* @return an {@link AsymmetricContext} wrapping the key and optional cipher
* @throws IOException if key decoding fails
* @throws IllegalArgumentException if the algorithm is unsupported
*/
public static AsymmetricContext fromKey(final PrivateKey privKey) throws IOException { // NOPMD
return switch (CryptoAlgorithmsNames.fromString(privKey.getAlgorithm())) {
case CryptoAlgorithmsNames.ED25519 -> {
byte[] keyBytes = privKey.getEncoded();
PrivateKeyInfo pki = PrivateKeyInfo.getInstance(keyBytes);
byte[] keyData = pki.parsePrivateKey().toASN1Primitive().getEncoded();
Ed25519PrivateKeyParameters keyParam = new Ed25519PrivateKeyParameters(keyData, 0);
yield new SignatureAsymmetricContext(keyParam); // , null);
}
case CryptoAlgorithmsNames.RSA -> {
final RSAPrivateKey rsa = (RSAPrivateKey) privKey;
final RSAKeyParameters keyParam = new RSAKeyParameters(true, rsa.getModulus(),
rsa.getPrivateExponent());
yield new ClassicAsymmetricContext(keyParam, new org.bouncycastle.crypto.encodings.OAEPEncoding(
new RSAEngine(), new SHA256Digest(), new byte[0]));
}
case CryptoAlgorithmsNames.DSA -> {
final DSAPrivateKey dsa = (DSAPrivateKey) privKey;
final DSAParams p = dsa.getParams();
final DSAParameters dsaParams = new DSAParameters(p.getP(), p.getQ(), p.getG());
final DSAPrivateKeyParameters keyParam = new DSAPrivateKeyParameters(dsa.getX(), dsaParams);
yield new SignatureAsymmetricContext(keyParam); // , null); // Signing only
}
case CryptoAlgorithmsNames.EC -> {
final ECPrivateKey ecPrivateKey = (ECPrivateKey) privKey;
final ECNamedCurveSpec ecNamedSpec = (ECNamedCurveSpec) ecPrivateKey.getParams();
final ECNamedCurveParameterSpec bcSpec = ECNamedCurveTable.getParameterSpec(ecNamedSpec.getName());
final ECDomainParameters domainParams = new ECDomainParameters(bcSpec.getCurve(), bcSpec.getG(),
bcSpec.getN(), bcSpec.getH(), bcSpec.getSeed());
final ECPrivateKeyParameters keyParam = new ECPrivateKeyParameters(ecPrivateKey.getS(), domainParams);
yield new SignatureAsymmetricContext(keyParam); // , null); // Signing only
}
case CryptoAlgorithmsNames.ELGAMAL -> {
final DHPrivateKey elg = (DHPrivateKey) privKey;
final DHParameterSpec elParams = elg.getParams();
final ElGamalParameters params = new ElGamalParameters(elParams.getP(), elParams.getG());
final ElGamalPrivateKeyParameters keyParam = new ElGamalPrivateKeyParameters(elg.getX(), params);
yield new ClassicAsymmetricContext(keyParam, new ElGamalEngine());
}
case CryptoAlgorithmsNames.SPHINCS_PLUS -> {
final SPHINCSPlusPrivateKey sphincs = (SPHINCSPlusPrivateKey) privKey;
final AsymmetricKeyParameter keyParam = PrivateKeyFactory.createKey(sphincs.getEncoded());
yield new SignatureAsymmetricContext(keyParam); // , (EncapsulatedSecretExtractor) null);
}
case CryptoAlgorithmsNames.KYBER -> {
final MLKEMPrivateKeyParameters keyParam = (MLKEMPrivateKeyParameters) PrivateKeyFactory
.createKey(privKey.getEncoded());
yield new KEMAsymmetricContext(keyParam, new MLKEMExtractor(keyParam));
}
case CryptoAlgorithmsNames.FRODO -> {
// Frodo
final FrodoPrivateKeyParameters keyParam = ((FrodoPrivateKey) privKey).getKeyParameters();
yield new KEMAsymmetricContext(keyParam, new FrodoKEMExtractor(keyParam));
}
default -> {
throw new IllegalArgumentException("Unsupported algorithm: " + privKey.getAlgorithm());
}
};
}
/**
* Serializes a {@link Key} into a string format for storage or transmission.
* <p>
* Format: <code>ALGORITHM:BIT_LENGTH:BASE64_ENCODED_KEY</code>
* </p>
*
* @param key the key to serialize
* @return serialized string representation of the key
*/
public static String serializeKey(final Key key) {
final CryptoAlgorithmsNames algorithm = CryptoAlgorithmsNames.fromString(key.getAlgorithm());
final int keySize;
switch (algorithm) {
case CryptoAlgorithmsNames.RSA -> keySize = ((RSAKey) key).getModulus().bitLength();
case CryptoAlgorithmsNames.DSA -> keySize = ((DSAKey) key).getParams().getP().bitLength();
case CryptoAlgorithmsNames.EC -> keySize = ((ECKey) key).getParams().getCurve().getField().getFieldSize();
case CryptoAlgorithmsNames.ELGAMAL -> keySize = ((DHKey) key).getParams().getP().bitLength(); // ElGamal
// keys often
// use DH
// interface
case CryptoAlgorithmsNames.SPHINCS_PLUS -> {
// Approximate bit length from encoded length (not precisely meaningful for PQC)
keySize = key.getEncoded().length * 8;
}
case CryptoAlgorithmsNames.KYBER -> {
// Kyber: ML-KEM-1024 => KYBER.displayName() + "-" + {keySize}
keySize = Integer
.parseInt(key.getAlgorithm().substring(CryptoAlgorithmsNames.KYBER.displayName().length() + 1));
}
default -> {
// fallback
keySize = key.getEncoded().length * 8; // fallback
}
}
final String base64 = Base64.getEncoder().encodeToString(key.getEncoded());
return algorithm + ":" + keySize + ":" + base64;
}
/**
* Deserializes a public key from a string previously generated using a matching
* serialization format (e.g., by {@link #serializeKey(Key)}).
*
* <p>
* The expected format of the input string is:
* </p>
*
* <pre>{@code
* ALGORITHM:BIT_LENGTH:BASE64_ENCODED_KEY
* }</pre>
*
* <p>
* The {@code BIT_LENGTH} component is ignored during parsing and is only used
* for informational purposes.
* </p>
*
* <p>
* This method reconstructs a {@link PublicKey} instance using the
* {@link java.security.spec.X509EncodedKeySpec} and the {@link KeyFactory} for
* the specified algorithm. The BouncyCastle security provider is required to be
* registered beforehand.
* </p>
*
* @param serialized the serialized string representation of the public key
* @return the deserialized {@link PublicKey} object
* @throws IllegalArgumentException if the serialized string is malformed or
* does not contain exactly three parts
* @throws NoSuchAlgorithmException if the algorithm specified in the serialized
* string is not supported
* @throws NoSuchProviderException if the BouncyCastle provider is not
* available
* @throws InvalidKeySpecException if the key specification is invalid or
* incompatible with the algorithm
*/
public static PublicKey deserializePublicKey(final String serialized)
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
final int PARTS = 3;
final String[] parts = serialized.split(":", PARTS);
if (parts.length != PARTS) {
throw new IllegalArgumentException("Invalid serialized key format");
}
final CryptoAlgorithmsNames algorithm = CryptoAlgorithmsNames.fromString(parts[0]);
final KeyFactory keyFactory = algorithm.getFactory();
final byte[] encoded = Base64.getDecoder().decode(parts[2]);
final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
return keyFactory.generatePublic(keySpec);
}
/**
* Deserializes a private key from a string representation.
*
* <p>
* The expected format of the input string is:
* </p>
*
* <pre>{@code
* ALGORITHM:BIT_LENGTH:BASE64_ENCODED_PKCS8_KEY
* }</pre>
*
* <p>
* Example:
* </p>
*
* <pre>{@code
* RSA:2048:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBK...
* }</pre>
*
* <p>
* This method supports keys for standard algorithms such as RSA, DSA, EC, and
* ElGamal, provided they are properly encoded in PKCS#8 format and recognized
* by the BouncyCastle provider.
* </p>
* <p>
* The bit length field is ignored during deserialization, as the key strength
* is derived from the actual key material.
* </p>
*
* @param serialized the serialized private key string in the format
* {@code ALGORITHM:BIT_LENGTH:BASE64}
* @return the reconstructed {@link PrivateKey} instance
* @throws IllegalArgumentException if the serialized format is invalid
* @throws NoSuchAlgorithmException if the algorithm is not supported
* @throws NoSuchProviderException if the BouncyCastle provider is not
* available
* @throws InvalidKeySpecException if the key specification is invalid or the
* key cannot be reconstructed
*/
public static PrivateKey deserializePrivateKey(final String serialized)
throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
final int PARTS = 3;
final String[] parts = serialized.split(":", PARTS);
if (parts.length != PARTS) {
throw new IllegalArgumentException("Invalid serialized key format");
}
final CryptoAlgorithmsNames algorithm = CryptoAlgorithmsNames.fromString(parts[0]);
final KeyFactory keyFactory = algorithm.getFactory();
final byte[] encoded = Base64.getDecoder().decode(parts[2]);
return keyFactory.generatePrivate(new java.security.spec.PKCS8EncodedKeySpec(encoded));
}
}

View File

@@ -0,0 +1,182 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.util;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
/**
* An abstract adapter that converts an {@link OutputStream}-based
* transformation (such as encryption or compression) into an
* {@link InputStream}-based one. This class reads data from a given
* {@link InputStream} (the {@code previousInput}), processes it through a
* transformation {@link OutputStream} (e.g., encryption, compression), and
* exposes the transformed data via the standard {@link InputStream} API.
* <p>
* Subclasses must initialize the {@code transformationOut} field with a proper
* {@link OutputStream} that writes transformed data into the internal buffer
* ({@code baos}). This initialization must be done <strong>after construction
* and before any read operation</strong>, via a subclass- specific method such
* as an <code>initialize(...)</code> method.
* </p>
*
* <h2>Subclass Contract</h2>
* <ul>
* <li>Set {@code transformationOut} to a valid {@link OutputStream} that writes
* to {@code baos}.</li>
* <li>Do <em>not</em> call virtual methods like initialization from
* constructors.</li>
* <li>Call the initialization method before performing any reads.</li>
* </ul>
*
* @author Leo Galambos
*/
@Deprecated
public abstract class OutputToInputStreamAdapter extends InputStream {
/**
* Default buffer size in bytes. Typically set to 8 KB (8 * 1024 bytes).
*/
protected static final int DEFAULT_BUF_SIZE = 8 * 1024;
/**
* Buffer size in bytes used by this instance. Initialized during object
* construction and remains constant.
*/
protected final int BUF_SIZE;
/**
* The original input stream containing untransformed data.
*/
protected final InputStream previousInput;
/**
* A buffer holding transformed data.
*/
protected ByteArrayOutputStream baos;
/**
* The output stream performing transformation and writing to {@code baos}. Must
* be initialized by subclasses before use.
*/
protected OutputStream transformationOut;
/**
* Input stream initialized to a no-op {@code InputStream} that contains no
* data.
* <p>
* This stream is set to {@link InputStream#nullInputStream()}, which is a
* convenient way to avoid {@code null} values while providing a valid,
* non-functional stream. Useful as a default placeholder until a real stream is
* assigned.
* </p>
*/
private InputStream bais = nullInputStream();
/**
* Constructs a new adapter wrapping the specified input stream with a given
* buffer size for transformation output.
*
* @param previousInput The input stream to read untransformed data from.
* @param bufSize The buffer size for the transformation output buffer.
*/
public OutputToInputStreamAdapter(final InputStream previousInput, final int bufSize) {
super();
Objects.requireNonNull(previousInput, "input stream must not be null");
this.previousInput = previousInput;
this.BUF_SIZE = bufSize;
baos = new ByteArrayOutputStream(BUF_SIZE);
}
/**
* Constructs a new adapter wrapping the specified input stream using the
* default buffer size.
*
* @param previousInput The input stream to read untransformed data from.
*/
public OutputToInputStreamAdapter(final InputStream previousInput) {
this(previousInput, DEFAULT_BUF_SIZE);
}
@Override
public int read() throws IOException {
ensureData();
return bais.read();
}
@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
ensureData();
return bais.read(b, off, len);
}
@Override
public void close() throws IOException {
previousInput.close();
}
/**
* Reads untransformed data from {@code previousInput}, writes it to
* {@code transformationOut} for processing, and buffers the transformed data in
* {@code baos} for reading.
*
* @throws IOException If an I/O error occurs during reading or transformation.
*/
private void ensureData() throws IOException {
if (bais.available() > 0 || baos == null) {
return;
}
bais = null; // NOPMD by Leo Galambos on 6/1/25, 4:09PM
final byte[] chunk = previousInput.readNBytes(BUF_SIZE);
if (chunk.length == 0) {
transformationOut.close();
final byte[] finalBytes = baos.toByteArray();
bais = new ByteArrayInputStream(finalBytes);
baos = null; // NOPMD
return;
} else {
transformationOut.write(chunk);
}
final byte[] chunkBytes = baos.toByteArray();
bais = new ByteArrayInputStream(chunkBytes);
baos.reset();
}
}

View File

@@ -0,0 +1,147 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.util;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.locks.ReentrantLock;
/**
* Utility class for generating random passwords and secure random byte arrays.
* <p>
* This class provides methods to:
* <ul>
* <li>Generate cryptographically secure random byte arrays of specified
* length.</li>
* <li>Generate random passwords composed of characters derived from random
* bytes.</li>
* </ul>
* <p>
* A single instance of {@link SecureRandom} is used unless
* {@code UNSAFE_SINGLE_SECURE} is set to {@code false}, in which case a new
* {@link SecureRandom} instance is created for each operation.
* <p>
* This class is thread-safe through use of a {@link ReentrantLock}.
*
* @author Leo Galambos
*/
public final class Password {
/**
* Private constructor to prevent instantiation of this utility class.
*/
private Password() {
// this is a utility class
}
/**
* Generates a random password by filling the provided byte array with
* cryptographically strong random bytes. The randomness is influenced by a
* combination of a user-supplied seed string and a randomly generated salt,
* ensuring that the result is both secure and non-deterministic across multiple
* invocations with the same input.
* <p>
* Internally, the method uses the SHA-256 digest of the concatenation of the
* seed and a 16-byte random salt to derive a seed for a {@link SecureRandom}
* instance. This seed is mixed into the internal state of the
* {@code SecureRandom} generator to produce a high-entropy, unpredictable byte
* sequence. As a result, the output differs each time the method is called,
* even with the same seed and password buffer.
* <p>
* Note: The generated salt is not returned or stored. If you require
* reproducible output or need to verify the result later, you must persist the
* salt separately.
*
* @param password the byte array to be filled with random password bytes; must
* not be {@code null}
* @param seed a user-supplied string used to influence the randomness
* generation; must not be {@code null}
* @return the same {@code password} byte array, now filled with
* cryptographically strong random data
* @throws NoSuchAlgorithmException if the SHA-256 digest or strong
* {@code SecureRandom} implementation is not
* available
* @throws NullPointerException if {@code password} or {@code seed} is
* {@code null}
*/
public static byte[] generateRandom(final byte[] password, final String seed) throws NoSuchAlgorithmException {
final byte[] salt = new byte[16];
RandomSupport.getRandom().nextBytes(salt);
// Combine input + salt
final byte[] seedBytes = seed.getBytes(StandardCharsets.UTF_8);
final byte[] combined = new byte[seedBytes.length + salt.length];
System.arraycopy(seedBytes, 0, combined, 0, seedBytes.length);
System.arraycopy(salt, 0, combined, seedBytes.length, salt.length);
// Derive seed using SHA-256
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
final byte[] rndSeed = digest.digest(combined);
// Seed SecureRandom (deterministic per run, but uses a fresh salt)
final SecureRandom seededRandom = SecureRandom.getInstanceStrong();
seededRandom.setSeed(rndSeed); // mixes with internal state
// Generate random output
seededRandom.nextBytes(password);
return password;
}
/**
* Generates a cryptographically secure random password consisting of printable
* ASCII characters.
*
* @param length the desired length of the password; must be positive
* @return a random password string using printable characters in the ASCII
* range 33126
* @throws IllegalArgumentException if {@code length} is less than or equal to
* zero
*/
public static String generatePrintablePassword(final int length) {
if (length <= 0) {
throw new IllegalArgumentException("Password length must be greater than zero");
}
final StringBuilder password = new StringBuilder(length);
for (int i = 0; i < length; i++) {
final int ascii = RandomSupport.getRandom().nextInt('~' - '!' + 1) + '!';
password.append((char) ascii);
}
return password.toString();
}
}

View File

@@ -0,0 +1,131 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.util;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utility class providing support for secure random number generation.
* <p>
* This class encapsulates logic for generating cryptographically secure random
* data using Java's {@link SecureRandom}. It optionally supports a singleton
* instance pattern for the {@code SecureRandom} generator, controlled by the
* {@code UNSAFE_SINGLE_SECURE} flag.
* </p>
*
* <p>
* <strong>Note:</strong> Reusing a single {@code SecureRandom} instance can
* improve performance, but may reduce randomness guarantees in some
* multi-threaded or long-lived contexts. Always assess your threat model and
* performance needs when toggling {@code UNSAFE_SINGLE_SECURE}.
* </p>
*
* @author Leo Galambos
*/
public final class RandomSupport {
private static final Logger LOG = Logger.getLogger(RandomSupport.class.getName());
/** Flag indicating whether a single {@link SecureRandom} instance is reused. */
private final static boolean UNSAFE_SINGLE_SECURE = true;
/** Lock for thread-safe access to the {@link SecureRandom} instance. */
private static Lock instanceLock = new ReentrantLock();
/** Shared {@link SecureRandom} instance. */
private static SecureRandom RANDOM;
static {
try {
RANDOM = SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
LOG.logp(Level.WARNING, "Password", "", "NoSuchAlgorithmException", e);
RANDOM = new SecureRandom();
}
}
private RandomSupport() {
// this is a utility class
}
/**
* Retrieves a {@link SecureRandom} instance.
* <p>
* If {@code UNSAFE_SINGLE_SECURE} is true, returns a shared instance;
* otherwise, creates a new {@link SecureRandom} instance.
*
* @return A {@link SecureRandom} instance for random number generation.
*/
public static SecureRandom getRandom() {
instanceLock.lock();
try {
if (UNSAFE_SINGLE_SECURE) {
return RANDOM;
} else {
LOG.log(Level.INFO, "creating a new SecureRandom");
return new SecureRandom();
}
} finally {
instanceLock.unlock();
}
}
/**
* Generates a secure random byte array of the specified size.
*
* @param size The size of the byte array to generate.
* @return A byte array filled with cryptographically secure random bytes.
*/
public static byte[] generateRandom(final int size) {
return generateRandom(new byte[size]);
}
/**
* Fills the given byte array with random bytes and returns it.
* <p>
* This method uses a thread-safe {@code SecureRandom} instance to generate
* cryptographically strong random values.
*
* @param buffer the byte array to fill with random bytes
* @return the same byte array, now containing random data
* @throws NullPointerException if {@code buffer} is {@code null}
*/
public static byte[] generateRandom(final byte[] buffer) {
getRandom().nextBytes(buffer);
return buffer;
}
}

View File

@@ -0,0 +1,523 @@
/**
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package zeroecho.util;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidParameterException;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.StreamCipher;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import zeroecho.util.aes.AesCipherType;
import zeroecho.util.aes.AesParameters;
import zeroecho.util.aes.AesSupport;
/**
* A builder for creating {@link InputStream}s that apply symmetric encryption
* or decryption using a variety of cipher types (block, stream, or AEAD).
* <p>
* Supports Bouncy Castle's {@link BufferedBlockCipher}, {@link StreamCipher},
* and {@link AEADBlockCipher} implementations. This builder validates
* configuration and initializes the cipher before wrapping the input stream in
* a cipher-specific processing stream.
*/
public final class SymmetricStreamBuilder {
/** Internal buffer size used during stream processing. */
public static final int BUF_SIZE = 4096;
private InputStream in;
private Object cipher;
private CipherParameters params;
private SymmetricStreamBuilder() {
// private constructor
}
/**
* Creates a new instance of {@code SymmetricStreamBuilder}.
*
* @return a new builder instance
*/
public static SymmetricStreamBuilder newBuilder() {
return new SymmetricStreamBuilder();
}
/**
* Sets the input stream that will be encrypted or decrypted.
*
* @param in the source {@link InputStream}
* @return this builder instance
*/
public SymmetricStreamBuilder withInputStream(final InputStream in) {
this.in = in;
return this;
}
/**
* Configures the cipher to be used for symmetric encryption or decryption.
*
* This method accepts either a predefined {@link AesCipherType}, or a concrete
* cipher instance such as:
* <ul>
* <li>{@link org.bouncycastle.crypto.StreamCipher}</li>
* <li>{@link org.bouncycastle.crypto.BufferedBlockCipher}</li>
* <li>{@link org.bouncycastle.crypto.modes.AEADBlockCipher}</li>
* </ul>
* If an {@link AesCipherType} is provided, it is internally converted into the
* appropriate cipher instance via its {@code createCipher()} method.
*
* Any unsupported cipher type will result in an
* {@link InvalidParameterException}.
*
* @param cipher the cipher type or instance to use for stream processing; must
* be an instance of {@code AesCipherType}, {@code StreamCipher},
* {@code BufferedBlockCipher}, or {@code AEADBlockCipher}
*
* @return this {@code SymmetricStreamBuilder} instance for fluent chaining
*
* @throws InvalidParameterException if the provided cipher is of an unsupported
* type
*
* @see AesCipherType
* @see org.bouncycastle.crypto.StreamCipher
* @see org.bouncycastle.crypto.BufferedBlockCipher
* @see org.bouncycastle.crypto.modes.AEADBlockCipher
*/
public SymmetricStreamBuilder withCipher(final Object cipher) {
this.cipher = switch (cipher) {
case AesCipherType aes -> aes.createCipher();
case BufferedBlockCipher buff -> buff;
case StreamCipher stream -> stream;
case AEADBlockCipher aead -> aead;
default -> throw new InvalidParameterException("Unsupported cipher type: " + cipher.getClass().getName());
};
return this;
}
/**
* Sets the cipher parameters (e.g., key, IV, nonce).
*
* @param params the cipher parameters
* @return this builder instance
*/
public SymmetricStreamBuilder withParameters(final CipherParameters params) {
this.params = params;
return this;
}
/**
* Configures this {@link SymmetricStreamBuilder} with both the AES cipher
* instance and its associated parameters, derived from the specified
* {@link AesParameters}.
*
* <p>
* This method performs two key operations:
* </p>
* <ul>
* <li>Initializes the cipher by invoking {@link AesCipherType#createCipher()}
* on {@code params.cipherType()}.</li>
* <li>Generates the corresponding {@link CipherParameters} using the provided
* secret key, initialization vector (IV), and optional Additional Authenticated
* Data (AAD).</li>
* </ul>
*
* <p>
* If the selected cipher type supports AEAD (e.g., AES-GCM), the
* {@code params.aad()} value will be incorporated into the parameter set. For
* non-AEAD modes, any provided AAD is ignored.
* </p>
*
* @param params the {@link AesParameters} object containing the cipher type,
* secret key, IV, and optional AAD; must not be {@code null}
* @return this {@code SymmetricStreamBuilder} instance for method chaining
* @throws NullPointerException if {@code params} is {@code null}
*/
public SymmetricStreamBuilder withCipherAndParameters(final AesParameters params) {
withCipher(params.cipherType().createCipher());
this.params = AesSupport.getParameters(params.cipherType(), params.key(), params.iv(), params.aad());
return this;
}
/**
* Builds a stream that encrypts data from the configured input stream.
*
* @return an {@link InputStream} that provides encrypted output
* @throws IllegalStateException if the builder is not fully configured
*/
public InputStream buildEncryptingStream() {
return buildStream(true);
}
/**
* Builds a stream that decrypts data from the configured input stream.
*
* @return an {@link InputStream} that provides decrypted output
* @throws IllegalStateException if the builder is not fully configured
*/
public InputStream buildDecryptingStream() {
return buildStream(false);
}
private InputStream buildStream(final boolean forEncryption) {
if (cipher == null || in == null || params == null) {
throw new IllegalStateException("InputStream, cipher, and parameters must be provided.");
}
return switch (cipher) {
case BufferedBlockCipher bbc -> {
bbc.init(forEncryption, params);
yield new BufferedBlockCipherInputStream(in, bbc);
}
case AEADBlockCipher aead -> {
aead.init(forEncryption, params);
yield new AEADBlockCipherInputStream(in, aead);
}
case StreamCipher sc -> {
sc.init(forEncryption, params);
yield new StreamCipherInputStream(in, sc);
}
default -> throw new IllegalStateException("Unsupported cipher type: " + cipher.getClass().getName());
};
}
}
/**
* Abstract base class for cipher-based {@link InputStream} implementations that
* process encrypted or decrypted data using a symmetric cipher.
*
* Manages buffered input/output handling, stream reading, and stream
* finalization logic. Concrete subclasses implement cipher-specific processing
* through the {@link #processCipher} method.
*/
abstract class SymmetricCipherInputStreamBase extends InputStream {
/**
* The underlying input stream supplying raw (encrypted or plaintext) data.
*/
protected final InputStream in;
/**
* Internal buffer used to read data from the underlying input stream.
*/
protected final byte[] inputBuffer = new byte[SymmetricStreamBuilder.BUF_SIZE];
/**
* Buffer holding the output from cipher processing.
*/
protected final byte[] outputBuffer;
/**
* Current read position within the output buffer.
*/
protected int outputPos;
/**
* Total number of valid bytes currently in the output buffer.
*/
protected int outputLen;
/**
* Indicates whether the cipher has been finalized (no more data to process).
*/
protected boolean finalProcessed;
/**
* Constructs a cipher input stream with the specified underlying input and
* output buffer size.
*
* @param in the underlying input stream
* @param outputBufferSize the size of the buffer to hold processed output
*/
protected SymmetricCipherInputStreamBase(final InputStream in, final int outputBufferSize) {
super();
this.in = in;
this.outputBuffer = new byte[outputBufferSize];
}
/**
* Reads a single byte from the encrypted or decrypted stream.
*
* @return the byte read, or {@code -1} if end of stream
* @throws IOException if an I/O or cipher processing error occurs
*/
@Override
public int read() throws IOException {
if (outputPos >= outputLen && !fillBuffer()) {
return -1;
}
return outputBuffer[outputPos++] & 0xFF;
}
/**
* Reads up to {@code len} bytes into the given buffer, starting at {@code off}.
*
* @param b the destination buffer
* @param off the start offset
* @param len the maximum number of bytes to read
* @return the number of bytes read, or {@code -1} if end of stream
* @throws IOException if an I/O or cipher processing error occurs
*/
@Override
public int read(final byte[] b, final int off, final int len) throws IOException {
if (outputPos >= outputLen && !fillBuffer()) {
return -1;
}
final int toCopy = Math.min(len, outputLen - outputPos);
System.arraycopy(outputBuffer, outputPos, b, off, toCopy);
outputPos += toCopy;
return toCopy;
}
/**
* Attempts to refill the output buffer by reading from the input stream and
* processing the data with the cipher.
* <p>
* If the end of input is reached, this method finalizes the cipher (if
* applicable) and marks the stream as complete.
*
* @return {@code true} if new output was generated and is available to read,
* {@code false} if end of stream was reached and no more output is
* available
* @throws IOException if cipher processing fails
*/
protected boolean fillBuffer() throws IOException {
if (finalProcessed) {
return false;
}
int read;
try {
do {
read = in.readNBytes(inputBuffer, 0, inputBuffer.length);
outputLen = processCipher(inputBuffer, read, outputBuffer);
} while (outputLen == 0 && read > 0);
finalProcessed = read == 0;
outputPos = 0;
return outputLen > 0;
} catch (DataLengthException | IllegalStateException | InvalidCipherTextException e) {
throw new IOException("Cipher processing failed", e);
}
}
/**
* Processes a chunk of data using the configured cipher.
* <p>
* Implementations must detect {@code inputLen == 0} to perform finalization if
* the cipher supports it (e.g., for block or AEAD ciphers).
* <p>
* For stream ciphers, this typically means returning {@code 0} as no
* finalization is needed.
*
* @param input the buffer containing input data; contents are undefined if
* {@code inputLen == 0}
* @param inputLen the number of bytes to process, or {@code 0} to finalize the
* cipher
* @param output the buffer into which processed output is written
* @return the number of bytes written to the output buffer
*
* @throws DataLengthException if the input data is too large for the
* cipher
* @throws IllegalStateException if the cipher has not been properly
* initialized
* @throws InvalidCipherTextException if finalization fails due to invalid
* padding or corrupted ciphertext
* @throws Exception if any other unexpected error occurs
* during processing
*/
protected abstract int processCipher(byte[] input, int inputLen, byte[] output) throws InvalidCipherTextException;
/**
* Closes the underlying input stream and releases any associated resources.
*
* @throws IOException if an I/O error occurs during closing
*/
@Override
public void close() throws IOException {
in.close();
}
}
/**
* An {@link InputStream} that applies a {@link BufferedBlockCipher} to
* transform the data during reading.
* <p>
* This stream supports both encryption and decryption depending on how the
* cipher is initialized.
*/
class BufferedBlockCipherInputStream extends SymmetricCipherInputStreamBase {
private final BufferedBlockCipher cipher;
/**
* Creates a new stream that wraps the given input and processes it using a
* {@link BufferedBlockCipher}.
*
* @param in the input stream to wrap
* @param cipher the initialized {@link BufferedBlockCipher}
*/
public BufferedBlockCipherInputStream(final InputStream in, final BufferedBlockCipher cipher) {
super(in, cipher.getOutputSize(SymmetricStreamBuilder.BUF_SIZE));
this.cipher = cipher;
}
/**
* Processes a block of input using the {@link BufferedBlockCipher}.
* <p>
* If {@code inputLen == 0}, the cipher is finalized via
* {@link BufferedBlockCipher#doFinal(byte[], int)}, completing any remaining
* buffered input and writing final output (including padding, if applicable).
* Otherwise, the method delegates to
* {@link BufferedBlockCipher#processBytes(byte[], int, int, byte[], int)}.
*
* @param input the input buffer containing plaintext or ciphertext bytes;
* ignored when {@code inputLen == 0}
* @param inputLen number of bytes to process from the input buffer, or
* {@code 0} to trigger finalization
* @param output the output buffer to write processed data into
* @return the number of bytes written to the output buffer
*
* @throws DataLengthException if the input data is too large for the
* cipher
* @throws IllegalStateException if the cipher has not been properly
* initialized
* @throws InvalidCipherTextException if finalization fails due to invalid
* padding or corrupted ciphertext
*/
@Override
protected int processCipher(final byte[] input, final int inputLen, final byte[] output)
throws InvalidCipherTextException {
if (inputLen == 0) {
return cipher.doFinal(output, 0);
}
return cipher.processBytes(input, 0, inputLen, output, 0);
}
}
/**
* An {@link InputStream} that applies an {@link AEADBlockCipher} to transform
* the data during reading.
* <p>
* AEAD (Authenticated Encryption with Associated Data) ciphers require
* finalization to validate authentication tags.
*/
class AEADBlockCipherInputStream extends SymmetricCipherInputStreamBase {
private final AEADBlockCipher cipher;
/**
* Creates a new stream that wraps the given input and processes it using an
* {@link AEADBlockCipher}.
*
* @param in the input stream to wrap
* @param cipher the initialized {@link AEADBlockCipher}
*/
public AEADBlockCipherInputStream(final InputStream in, final AEADBlockCipher cipher) {
super(in, cipher.getOutputSize(SymmetricStreamBuilder.BUF_SIZE) + AesSupport.BLOCK_SIZE);
this.cipher = cipher;
}
/**
* Processes a block of input using the {@link AEADBlockCipher}.
* <p>
* If {@code inputLen == 0}, the cipher is finalized via
* {@link AEADBlockCipher#doFinal(byte[], int)}, completing encryption or
* decryption and verifying the authentication tag. If authentication fails, an
* {@link InvalidCipherTextException} is thrown.
* <p>
* For normal processing, the method delegates to
* {@link AEADBlockCipher#processBytes(byte[], int, int, byte[], int)} to
* transform the input data into ciphertext or plaintext.
*
* @param input the input buffer containing plaintext or ciphertext data;
* ignored when {@code inputLen == 0}
* @param inputLen number of bytes to process, or {@code 0} to finalize and
* verify authentication
* @param output the output buffer to receive processed data
* @return the number of bytes written to the output buffer
*
* @throws IllegalStateException if the cipher is not properly initialized
* @throws InvalidCipherTextException if finalization fails due to
* authentication tag mismatch
*/
@Override
protected int processCipher(final byte[] input, final int inputLen, final byte[] output)
throws InvalidCipherTextException {
if (inputLen == 0) {
return cipher.doFinal(output, 0);
}
return cipher.processBytes(input, 0, inputLen, output, 0);
}
}
/**
* An {@link InputStream} that applies a {@link StreamCipher} to transform the
* data during reading.
* <p>
* Stream ciphers operate byte-by-byte and do not require finalization.
*/
class StreamCipherInputStream extends SymmetricCipherInputStreamBase {
private final StreamCipher cipher;
/**
* Creates a new stream that wraps the given input and processes it using a
* {@link StreamCipher}.
*
* @param in the input stream to wrap
* @param cipher the initialized {@link StreamCipher}
*/
public StreamCipherInputStream(final InputStream in, final StreamCipher cipher) {
super(in, SymmetricStreamBuilder.BUF_SIZE);
this.cipher = cipher;
}
/**
* Processes a block of input using the stream cipher. Finalization is a no-op
* for stream ciphers (returns 0 bytes).
*
* @param input input buffer
* @param inputLen number of bytes to process, or {@code 0} to signal end of
* stream
* @param output output buffer
* @return number of bytes written to output
*/
@Override
protected int processCipher(final byte[] input, final int inputLen, final byte[] output) {
if (inputLen == 0) {
// No finalization for StreamCipher, just indicate end of stream
return 0;
}
cipher.processBytes(input, 0, inputLen, output, 0);
return inputLen;
}
}

View File

@@ -0,0 +1,265 @@
/*******************************************************************************
* Copyright (C) 2025, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.util;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.PosixFilePermission;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* UniversalKeyStoreFile provides a simple, secure mechanism for storing and
* retrieving cryptographic public and private keys using a plain text file. It
* supports various key algorithms, including asymmetric and post-quantum, and
* ensures minimal file permissions for security.
* <p>
* The keys are stored as key-value pairs where the key is a symbolic name with
* suffix "_pub" or "_priv", and the value is formatted as:
*
* <pre>
* Algorithm:Length:Base64EncodedKey
* </pre>
* <p>
* File permissions are restricted to read and write for the owner (chmod 600).
*/
public class UniversalKeyStoreFile {
/**
* Logger instance for the {@code UniversalKeyStoreFile} class, used to log
* informational, debug, and error messages related to keystore file operations.
* <p>
* Initialized with the class name to enable clear and contextual logging.
* </p>
*/
private static final Logger LOG = Logger.getLogger(UniversalKeyStoreFile.class.getName());
/**
* File system path pointing to the keystore file managed by this instance.
* <p>
* This path is immutable after construction and represents the physical
* location of the keystore file, which may be used for reading or writing
* keystore data.
* </p>
*/
private final Path filePath;
/**
* Constructs a UniversalKeyStoreFile with the given path. If the file does not
* exist, it is created with secure permissions (rw-------).
*
* @param filePath The path of the file to use for storing keys.
*/
public UniversalKeyStoreFile(final Path filePath) {
this.filePath = filePath;
try {
if (!Files.exists(filePath)) {
Files.createFile(filePath);
}
setSecurePermissions(filePath);
} catch (IOException e) {
throw new UncheckedIOException("Error creating or securing key store file", e);
}
}
/**
* Constructs a UniversalKeyStoreFile with the given file name. If the file does
* not exist, it is created with secure permissions (rw-------).
*
* @param fileName The name of the file to use for storing keys.
*/
public UniversalKeyStoreFile(final String fileName) {
this(Path.of(fileName));
}
/**
* Sets the file permissions to rw------- (owner read/write only).
*
* @param path The path of the file to secure.
* @throws IOException If an I/O error occurs.
*/
private void setSecurePermissions(final Path path) throws IOException {
try {
// Set POSIX permissions: rw------- (owner read/write)
final Set<PosixFilePermission> perms = Set.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE);
Files.setPosixFilePermissions(path, perms);
} catch (UnsupportedOperationException e) {
LOG.log(Level.WARNING, "POSIX file permissions not supported on this platform.");
}
}
/**
* Adds a public key to the underlying storage for the specified owner.
* <p>
* The key must support standard encoding, i.e., {@link PublicKey#getEncoded()}
* must return a non-null value. If the key's format is {@code null}, indicating
* it is not serializable (e.g., some post-quantum keys), an
* {@link IllegalArgumentException} will be thrown.
*
* @param owner the identifier for the key owner; must not be {@code null}
* @param pubKey the public key to store; must support encoding
* @throws IllegalArgumentException if the key does not support encoding (i.e.,
* {@code getFormat() == null})
* @throws NullPointerException if either {@code owner} or {@code pubKey} is
* {@code null}
*/
public void addPubKey(final String owner, final PublicKey pubKey) {
if (pubKey.getEncoded() == null) {
throw new IllegalArgumentException(pubKey.toString() + " is not serializable");
}
writeEntry(owner + "_pub", KeySupport.serializeKey(pubKey));
}
/**
* Adds a private key to the underlying storage for the specified owner.
* <p>
* The key must be serializable, i.e., it must support encoding via
* {@link PrivateKey#getEncoded()}. If the key's format is {@code null},
* indicating it does not support standard encoding (e.g., some post-quantum
* keys), an {@link IllegalArgumentException} will be thrown.
*
* @param owner the identifier for the key owner; must not be {@code null}
* @param privKey the private key to store; must support encoding
* @throws IllegalArgumentException if the key does not support encoding (i.e.,
* {@code getFormat() == null})
* @throws NullPointerException if either {@code owner} or {@code privKey}
* is {@code null}
*/
public void addPrivKey(final String owner, final PrivateKey privKey) {
if (privKey.getEncoded() == null) {
throw new IllegalArgumentException(privKey.toString() + " is not serializable");
}
writeEntry(owner + "_priv", KeySupport.serializeKey(privKey));
}
/**
* Writes an entry to the key store file in the format key=value.
*
* @param key The key for the entry (e.g., owner_pub).
* @param value The value for the entry, including metadata and Base64 key.
*/
private void writeEntry(final String key, final String value) {
final String entry = key + "=" + value + System.lineSeparator();
try {
Files.writeString(filePath, entry, StandardOpenOption.APPEND);
} catch (IOException e) {
throw new UncheckedIOException("Error writing to key store file", e);
}
}
/**
* Loads a public key for the specified owner from the key store.
* <p>
* This method retrieves a line from the key store matching the pattern
* {@code owner_pub}, parses it to extract the key algorithm and Base64-encoded
* key data, and reconstructs the {@link PublicKey} using the {@code BC} (Bouncy
* Castle) provider.
* </p>
*
* @param owner the symbolic name of the key owner (must not be {@code null})
* @return the reconstructed {@link PublicKey} instance
* @throws NoSuchProviderException if the Bouncy Castle provider is not
* available
* @throws NoSuchAlgorithmException if the algorithm is not supported
* @throws InvalidKeySpecException if the key specification is invalid
* @throws NoSuchElementException if the key for the given owner is not found
* @throws IOException if an I/O error occurs while accessing the
* key store
*/
public PublicKey loadPublicKey(final String owner)
throws IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
final String line = findLine(owner + "_pub");
if (line == null) {
throw new NoSuchElementException("Public key for " + owner + " not found.");
}
final String[] parts = line.split("=", 2);
return KeySupport.deserializePublicKey(parts[1]);
}
/**
* Loads a private key for the specified owner from the key store.
*
* @param owner The symbolic name of the key owner.
* @return The reconstructed private key.
* @throws NoSuchElementException If the key is not found.
* @throws IOException If the key cannot be reconstructed.
* @throws NoSuchProviderException If the key cannot be reconstructed.
* @throws NoSuchAlgorithmException If the key cannot be reconstructed.
* @throws InvalidKeySpecException If the key cannot be reconstructed.
*/
public PrivateKey loadPrivateKey(final String owner)
throws IOException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException {
final String line = findLine(owner + "_priv");
if (line == null) {
throw new NoSuchElementException("Private key for " + owner + " not found.");
}
final String[] parts = line.split("=", 2);
return KeySupport.deserializePrivateKey(parts[1]);
}
/**
* Finds a line in the key store file corresponding to the specified key.
*
* @param key The key to search for.
* @return The line containing the key, or null if not found.
* @throws IOException If an I/O error occurs.
*/
private String findLine(final String key) throws IOException {
return Files.lines(filePath).filter(line -> line.startsWith(key + "=")).findFirst().orElse(null);
}
/**
* Clears all entries from the key store file.
*/
public void clearStore() {
try {
Files.writeString(filePath, "", StandardOpenOption.TRUNCATE_EXISTING);
} catch (IOException e) {
throw new UncheckedIOException("Error clearing key store file", e);
}
}
}

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