Initial commit
This commit is contained in:
12
.gitattributes
vendored
Normal file
12
.gitattributes
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#
|
||||||
|
# https://help.github.com/articles/dealing-with-line-endings/
|
||||||
|
#
|
||||||
|
# Linux start script should use lf
|
||||||
|
/gradlew text eol=lf
|
||||||
|
|
||||||
|
# These are Windows script files and should use crlf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
|
||||||
|
# Binary files should be left untouched
|
||||||
|
*.jar binary
|
||||||
|
|
||||||
106
.gitignore
vendored
Normal file
106
.gitignore
vendored
Normal 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
17
.project
Normal 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
321
.ruleset
Normal 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
31
LICENSE
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Copyright (C) 2024, Leo Galambos
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. All advertising materials mentioning features or use of this software must
|
||||||
|
display the following acknowledgement:
|
||||||
|
This product includes software developed by the Egothor project.
|
||||||
|
|
||||||
|
4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
63
README.md
Normal file
63
README.md
Normal 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
BIN
ZeroEcho-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
46
ZeroEcho-logo.svg
Normal file
46
ZeroEcho-logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 70 KiB |
19
app/.classpath
Normal file
19
app/.classpath
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="src" output="bin/main" path="src/main/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="main"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="main,test"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="bin/test" path="src/test/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="test"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="test"/>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
||||||
|
<classpathentry kind="output" path="bin/default"/>
|
||||||
|
</classpath>
|
||||||
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/app/
|
||||||
23
app/.project
Normal file
23
app/.project
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<projectDescription>
|
||||||
|
<name>app</name>
|
||||||
|
<comment>Project app created by Buildship.</comment>
|
||||||
|
<projects>
|
||||||
|
</projects>
|
||||||
|
<buildSpec>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
<buildCommand>
|
||||||
|
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
||||||
|
<arguments>
|
||||||
|
</arguments>
|
||||||
|
</buildCommand>
|
||||||
|
</buildSpec>
|
||||||
|
<natures>
|
||||||
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
||||||
|
</natures>
|
||||||
|
</projectDescription>
|
||||||
31
app/LICENSE
Normal file
31
app/LICENSE
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Copyright (C) 2025, Leo Galambos
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. All advertising materials mentioning features or use of this software must
|
||||||
|
display the following acknowledgement:
|
||||||
|
This product includes software developed by the Egothor project.
|
||||||
|
|
||||||
|
4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
52
app/build.gradle
Normal file
52
app/build.gradle
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
plugins {
|
||||||
|
id 'buildlogic.java-application-conventions'
|
||||||
|
id 'com.palantir.git-version' version '4.0.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
group 'org.egothor'
|
||||||
|
version gitVersion(prefix:'release@')
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'org.apache.commons:commons-text'
|
||||||
|
implementation 'commons-cli:commons-cli'
|
||||||
|
implementation project(':lib')
|
||||||
|
// might be removed if I move BC ops to the lib
|
||||||
|
testImplementation 'org.bouncycastle:bcpkix-jdk18on'
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
// Define the main class for the application.
|
||||||
|
mainClass = 'zeroecho.ZeroEcho'
|
||||||
|
}
|
||||||
|
|
||||||
|
jar {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
'Main-Class': application.mainClass,
|
||||||
|
'Implementation-Title': rootProject.name,
|
||||||
|
'Implementation-Version': "${version}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include compiled classes from main source set
|
||||||
|
from sourceSets.main.output
|
||||||
|
|
||||||
|
dependsOn configurations.runtimeClasspath
|
||||||
|
from {
|
||||||
|
configurations.runtimeClasspath.collect { dep ->
|
||||||
|
if (dep.isDirectory()) {
|
||||||
|
dep
|
||||||
|
} else {
|
||||||
|
zipTree(dep).matching {
|
||||||
|
exclude 'META-INF/LICENSE.*'
|
||||||
|
exclude 'META-INF/*.SF'
|
||||||
|
exclude 'META-INF/*.DSA'
|
||||||
|
exclude 'META-INF/*.RSA'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
|
||||||
|
}
|
||||||
235
app/src/main/java/zeroecho/AsymetricKeysManagement.java
Normal file
235
app/src/main/java/zeroecho/AsymetricKeysManagement.java
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.CommandLine;
|
||||||
|
import org.apache.commons.cli.CommandLineParser;
|
||||||
|
import org.apache.commons.cli.DefaultParser;
|
||||||
|
import org.apache.commons.cli.Option;
|
||||||
|
import org.apache.commons.cli.OptionGroup;
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.apache.commons.cli.ParseException;
|
||||||
|
|
||||||
|
import zeroecho.util.X509CertificationAuthority;
|
||||||
|
import zeroecho.util.X509CertificationAuthority.NewCertificate;
|
||||||
|
import zeroecho.util.X509Support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility class for managing asymmetric cryptographic keys and X.509
|
||||||
|
* certificates via command-line interface.
|
||||||
|
* <p>
|
||||||
|
* This class supports operations such as issuing certificates (from a CSR file
|
||||||
|
* or by generating a new key pair), revoking existing certificates, and
|
||||||
|
* retrieving all certificates associated with a given username. It relies on
|
||||||
|
* command-line options for input parameters.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* Supported operations:
|
||||||
|
* <ul>
|
||||||
|
* <li>Issue a certificate from a CSR file or generate a new key pair and
|
||||||
|
* certificate</li>
|
||||||
|
* <li>Revoke an existing certificate using its PEM file</li>
|
||||||
|
* <li>Retrieve all certificates issued to a specific username</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* Supported command-line options:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>-d, --directory</b>: Directory to store Certificate Authority (CA)
|
||||||
|
* data (default: current directory)</li>
|
||||||
|
* <li><b>-o, --organization</b>: Organization name for CA (default:
|
||||||
|
* "ACME")</li>
|
||||||
|
* <li><b>-u, --username</b>: Username for certificate issuance, revocation, or
|
||||||
|
* retrieval</li>
|
||||||
|
* <li><b>-s, --subject</b>: Subject Distinguished Name (DN) for certificate
|
||||||
|
* issuance (e.g., "CN=John Doe")</li>
|
||||||
|
* <li><b>-i, --issue</b>: Issue a certificate. Optionally takes a CSR file as
|
||||||
|
* an argument. If omitted, a new key pair is generated.</li>
|
||||||
|
* <li><b>-r, --revoke</b>: Revoke a certificate using its PEM file</li>
|
||||||
|
* <li><b>-a, --getAll</b>: Retrieve all certificates associated with the given
|
||||||
|
* username</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public final class AsymetricKeysManagement {
|
||||||
|
private static final Logger LOG = Logger.getLogger(AsymetricKeysManagement.class.getName());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to prevent instantiation of this utility class.
|
||||||
|
* <p>
|
||||||
|
* All methods in this class are static and it is not intended to be
|
||||||
|
* instantiated.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private AsymetricKeysManagement() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for executing asymmetric key management operations based on
|
||||||
|
* provided command-line arguments.
|
||||||
|
* <p>
|
||||||
|
* This method parses the given arguments, validates required inputs, and
|
||||||
|
* performs the specified operation by interacting with the
|
||||||
|
* {@link X509CertificationAuthority} class.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param args Command-line arguments passed to the application.
|
||||||
|
* @param options The set of {@link org.apache.commons.cli.Options} defining
|
||||||
|
* allowed command-line options.
|
||||||
|
* @return error-code
|
||||||
|
* @throws ParseException if command-line arguments are missing required
|
||||||
|
* options, are malformed, or mutually exclusive options
|
||||||
|
* conflict.
|
||||||
|
*
|
||||||
|
* @see X509CertificationAuthority
|
||||||
|
* @see org.apache.commons.cli.CommandLine
|
||||||
|
*/
|
||||||
|
public static int main(final String[] args, final Options options) throws ParseException { // NOPMD
|
||||||
|
|
||||||
|
final Option DIR_OPTION = Option.builder("d").longOpt("directory").hasArg().argName("dir")
|
||||||
|
.desc("Directory to store CA data (default: .)").build();
|
||||||
|
final Option ORG_OPTION = Option.builder("o").longOpt("organization").hasArg().argName("org")
|
||||||
|
.desc("Organization name (default: ACME)").build();
|
||||||
|
final Option USERNAME_OPTION = Option.builder("u").longOpt("username").hasArg().argName("username")
|
||||||
|
.desc("Username for issuance, revocation or filtering").build();
|
||||||
|
final Option SUBJECT_OPTION = Option.builder("s").longOpt("subject").hasArg().argName("subject")
|
||||||
|
.desc("Subject DN (e.g., CN=John Doe)").build();
|
||||||
|
|
||||||
|
final Option ISSUE_OPTION = Option.builder("i").longOpt("issue").optionalArg(true).argName("csrFile")
|
||||||
|
.desc("Issue a certificate from CSR PEM file or generate keypair if omitted").build();
|
||||||
|
final Option REVOKE_OPTION = Option.builder("r").longOpt("revoke").hasArg().argName("certFile")
|
||||||
|
.desc("Revoke a certificate from PEM file").build();
|
||||||
|
final Option GET_ALL_OPTION = Option.builder("a").longOpt("getAll")
|
||||||
|
.desc("Get all certificates for given username").build();
|
||||||
|
|
||||||
|
final OptionGroup OPERATIONS = new OptionGroup();
|
||||||
|
OPERATIONS.addOption(ISSUE_OPTION);
|
||||||
|
OPERATIONS.addOption(REVOKE_OPTION);
|
||||||
|
OPERATIONS.addOption(GET_ALL_OPTION);
|
||||||
|
OPERATIONS.setRequired(true); // At least one required
|
||||||
|
|
||||||
|
options.addOption(DIR_OPTION);
|
||||||
|
options.addOption(ORG_OPTION);
|
||||||
|
options.addOption(USERNAME_OPTION);
|
||||||
|
options.addOption(SUBJECT_OPTION);
|
||||||
|
options.addOptionGroup(OPERATIONS);
|
||||||
|
|
||||||
|
final CommandLine cmd;
|
||||||
|
try {
|
||||||
|
final CommandLineParser parser = new DefaultParser();
|
||||||
|
cmd = parser.parse(options, args);
|
||||||
|
|
||||||
|
final String directory = cmd.getOptionValue(DIR_OPTION, ".");
|
||||||
|
final String organization = cmd.getOptionValue(ORG_OPTION, "ACME");
|
||||||
|
final String username = cmd.getOptionValue(USERNAME_OPTION);
|
||||||
|
final String subject = cmd.getOptionValue(SUBJECT_OPTION);
|
||||||
|
|
||||||
|
final X509CertificationAuthority ca = new X509CertificationAuthority(directory, organization);
|
||||||
|
ca.initialize();
|
||||||
|
|
||||||
|
if (cmd.hasOption(ISSUE_OPTION.getOpt())) {
|
||||||
|
if (username == null) {
|
||||||
|
throw new ParseException("--username is required when issuing a certificate.");
|
||||||
|
}
|
||||||
|
final String csrFile = cmd.getOptionValue(ISSUE_OPTION.getOpt());
|
||||||
|
if (csrFile != null) {
|
||||||
|
final Certificate cert = ca.issueFromCSRFile(username, csrFile);
|
||||||
|
if (cert != null) {
|
||||||
|
X509Support.printCertificate(cert);
|
||||||
|
} else {
|
||||||
|
LOG.log(Level.SEVERE, "Certificate issuance failed.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (subject == null) {
|
||||||
|
throw new ParseException("--subject is required if no CSR file is provided.");
|
||||||
|
}
|
||||||
|
final NewCertificate cert = ca.issue(username, subject);
|
||||||
|
if (cert != null) {
|
||||||
|
X509Support.printCertificate(cert.cert());
|
||||||
|
X509Support.printPrivateKey(cert.key().getPrivate());
|
||||||
|
} else {
|
||||||
|
LOG.log(Level.SEVERE, "Certificate issuance failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (cmd.hasOption(REVOKE_OPTION.getOpt())) {
|
||||||
|
if (username == null) {
|
||||||
|
throw new ParseException("--username is required when revoking a certificate.");
|
||||||
|
}
|
||||||
|
final String certFile = cmd.getOptionValue(REVOKE_OPTION.getOpt());
|
||||||
|
final Certificate cert = X509Support.loadCertificate(certFile);
|
||||||
|
if (cert == null) {
|
||||||
|
LOG.log(Level.SEVERE, "Failed to load certificate from file: {0}", certFile);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean revoked = ca.revoke(username, cert);
|
||||||
|
if (revoked) {
|
||||||
|
System.out.println("Certificate revoked successfully:");
|
||||||
|
X509Support.printCertificate(cert);
|
||||||
|
} else {
|
||||||
|
LOG.log(Level.SEVERE, "Certificate revocation failed or certificate not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (cmd.hasOption(GET_ALL_OPTION.getOpt())) {
|
||||||
|
if (username == null) {
|
||||||
|
throw new ParseException("--username is required when issuing a certificate.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Certificate[] certs = ca.getAll(username);
|
||||||
|
if (certs.length == 0) {
|
||||||
|
System.out.println("No certificates found for username: " + username);
|
||||||
|
} else {
|
||||||
|
for (final Certificate cert : certs) {
|
||||||
|
X509Support.printCertificate(cert);
|
||||||
|
System.out.println("-----");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException | IOException | GeneralSecurityException e) {
|
||||||
|
if (LOG.isLoggable(Level.SEVERE)) {
|
||||||
|
LOG.log(Level.SEVERE, "Cannot proceed: " + e.toString());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
274
app/src/main/java/zeroecho/KEMAes.java
Normal file
274
app/src/main/java/zeroecho/KEMAes.java
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.CommandLine;
|
||||||
|
import org.apache.commons.cli.CommandLineParser;
|
||||||
|
import org.apache.commons.cli.DefaultParser;
|
||||||
|
import org.apache.commons.cli.Option;
|
||||||
|
import org.apache.commons.cli.OptionGroup;
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.apache.commons.cli.ParseException;
|
||||||
|
|
||||||
|
import zeroecho.builder.AesBuilder;
|
||||||
|
import zeroecho.builder.DataContentChainBuilder;
|
||||||
|
import zeroecho.builder.KEMAesParametersBuilder;
|
||||||
|
import zeroecho.builder.PlainFileBuilder;
|
||||||
|
import zeroecho.data.DataContent;
|
||||||
|
import zeroecho.util.UniversalKeyStoreFile;
|
||||||
|
import zeroecho.util.aes.AesCipherType;
|
||||||
|
import zeroecho.util.aes.AesMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class providing command-line support for AES encryption and
|
||||||
|
* decryption using Key Encapsulation Mechanism (KEM) and authenticated
|
||||||
|
* encryption modes like GCM.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Supports AES modes of varying key lengths (128/192/256) and cipher types such
|
||||||
|
* as CBC, GCM, or CTR. It relies on keys provided in a ZeroEcho-compatible
|
||||||
|
* keystore and supports both public and private key operations based on the
|
||||||
|
* selected mode.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This class is non-instantiable and should only be invoked through its static
|
||||||
|
* {@code main} method.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class KEMAes {
|
||||||
|
private static final Logger LOG = Logger.getLogger(KEMAes.class.getName());
|
||||||
|
|
||||||
|
private KEMAes() {
|
||||||
|
// Utility class; prevent instantiation.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for command-line execution.
|
||||||
|
*
|
||||||
|
* @param args command-line arguments
|
||||||
|
* @param options CLI options container (used for external extensions/tests)
|
||||||
|
* @return exit code: 0 for success, 1 for failure
|
||||||
|
* @throws ParseException if CLI parsing fails or required options are missing
|
||||||
|
*/
|
||||||
|
public static int main(final String[] args, final Options options) throws ParseException { // NOPMD
|
||||||
|
// Define and register CLI options
|
||||||
|
final Option ENCRYPT_OPTION = Option.builder("e").longOpt("encrypt").hasArg().argName("inputFile")
|
||||||
|
.desc("Encrypt the given file").build();
|
||||||
|
final Option DECRYPT_OPTION = Option.builder("d").longOpt("decrypt").hasArg().argName("inputFile")
|
||||||
|
.desc("Decrypt the given file").build();
|
||||||
|
final Option OUTPUT_OPTION = Option.builder("o").longOpt("output").hasArg().argName("outputFile")
|
||||||
|
.desc("Output file path (default: inputFile + .enc or .dec)").build();
|
||||||
|
final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("keystoreFile")
|
||||||
|
.desc("File with keys (ZeroEcho's *.keystore)").required().build();
|
||||||
|
final Option PUB_OPTION = Option.builder("p").longOpt("pubkeys").hasArg().argName("usernames")
|
||||||
|
.desc("Recipient's username list (in keystore)").build();
|
||||||
|
final Option PRIV_OPTION = Option.builder("s").longOpt("privkey").hasArg().argName("username")
|
||||||
|
.desc("Username with private key for decryption (in keystore)").build();
|
||||||
|
final Option MODE_OPTION = Option.builder("m").longOpt("mode").hasArg().argName("aesMode")
|
||||||
|
.desc("AES mode: AES-128, AES-192, AES-256 (default: " + AesMode.AES_256 + ")").build();
|
||||||
|
final Option CIPHER_TYPE_OPTION = Option.builder("c").longOpt("cipher").hasArg().argName("cipherType")
|
||||||
|
.desc("Cipher type: AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding (default: "
|
||||||
|
+ AesCipherType.GCM + ")")
|
||||||
|
.build();
|
||||||
|
final Option AAD_OPTION = Option.builder("a").longOpt("aad").hasArg().argName("hex")
|
||||||
|
.desc("Optional AAD (hex-encoded) for authenticated AES modes like GCM").build();
|
||||||
|
|
||||||
|
final OptionGroup OPERATION_GROUP = new OptionGroup();
|
||||||
|
OPERATION_GROUP.addOption(ENCRYPT_OPTION);
|
||||||
|
OPERATION_GROUP.addOption(DECRYPT_OPTION);
|
||||||
|
OPERATION_GROUP.setRequired(true);
|
||||||
|
|
||||||
|
options.addOptionGroup(OPERATION_GROUP);
|
||||||
|
options.addOption(OUTPUT_OPTION);
|
||||||
|
options.addOption(KEYSTORE_OPTION);
|
||||||
|
options.addOption(PUB_OPTION);
|
||||||
|
options.addOption(PRIV_OPTION);
|
||||||
|
options.addOption(MODE_OPTION);
|
||||||
|
options.addOption(CIPHER_TYPE_OPTION);
|
||||||
|
options.addOption(AAD_OPTION);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final CommandLineParser parser = new DefaultParser();
|
||||||
|
final CommandLine cmd = parser.parse(options, args);
|
||||||
|
|
||||||
|
final boolean isEncryption = cmd.hasOption(ENCRYPT_OPTION.getOpt());
|
||||||
|
final boolean isDecryption = cmd.hasOption(DECRYPT_OPTION.getOpt());
|
||||||
|
|
||||||
|
final String inputPath = cmd.getOptionValue(ENCRYPT_OPTION.getOpt(),
|
||||||
|
cmd.getOptionValue(DECRYPT_OPTION.getOpt()));
|
||||||
|
final String outputPath = cmd.getOptionValue(OUTPUT_OPTION, inputPath + (isEncryption ? ".enc" : ".dec"));
|
||||||
|
final String keystorePath = cmd.getOptionValue(KEYSTORE_OPTION);
|
||||||
|
final AesMode mode = AesMode.fromString(cmd.getOptionValue(MODE_OPTION, AesMode.AES_256.toString()));
|
||||||
|
final AesCipherType cipher = AesCipherType
|
||||||
|
.fromString(cmd.getOptionValue(CIPHER_TYPE_OPTION, AesCipherType.GCM.toString()));
|
||||||
|
final byte[] aad = cmd.hasOption(AAD_OPTION.getOpt())
|
||||||
|
? HexFormat.of().parseHex(cmd.getOptionValue(AAD_OPTION))
|
||||||
|
: new byte[0];
|
||||||
|
|
||||||
|
final UniversalKeyStoreFile db = new UniversalKeyStoreFile(keystorePath);
|
||||||
|
|
||||||
|
if (isEncryption) {
|
||||||
|
final String pub = cmd.getOptionValue(PUB_OPTION);
|
||||||
|
if (pub == null) {
|
||||||
|
throw new ParseException("Option is required for encryption: " + PUB_OPTION.getOpt() + " ("
|
||||||
|
+ PUB_OPTION.getLongOpt() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
final PublicKey recipient = getPublicKey(db, pub);
|
||||||
|
|
||||||
|
performEncryption(recipient, mode, cipher, aad, inputPath, outputPath);
|
||||||
|
} else if (isDecryption) {
|
||||||
|
final String priv = cmd.getOptionValue(PRIV_OPTION);
|
||||||
|
if (priv == null) {
|
||||||
|
throw new ParseException("Option is required for decryption: " + PRIV_OPTION.getOpt() + " ("
|
||||||
|
+ PRIV_OPTION.getLongOpt() + ")");
|
||||||
|
}
|
||||||
|
performDecryption(db.loadPrivateKey(priv), mode, cipher, aad, inputPath, outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
|
||||||
|
if (LOG.isLoggable(Level.SEVERE)) {
|
||||||
|
LOG.log(Level.SEVERE, "Error during encryption/decryption: " + e.toString());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts a file using the specified public key and AES configuration.
|
||||||
|
*
|
||||||
|
* @param recipient recipient's public key
|
||||||
|
* @param mode AES key mode (128/192/256 bits)
|
||||||
|
* @param cipher AES cipher type (GCM, CBC, etc.)
|
||||||
|
* @param aad optional Additional Authenticated Data (may be empty)
|
||||||
|
* @param inputPath path to the input file
|
||||||
|
* @param outputPath path where the encrypted file will be written
|
||||||
|
* @throws IOException if reading or writing files fails
|
||||||
|
*/
|
||||||
|
private static void performEncryption(final PublicKey recipient, final AesMode mode, final AesCipherType cipher,
|
||||||
|
final byte[] aad, final String inputPath, final String outputPath) throws IOException {
|
||||||
|
|
||||||
|
DataContent encrypted = DataContentChainBuilder.encrypt()
|
||||||
|
// input file
|
||||||
|
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL()))
|
||||||
|
// AES process
|
||||||
|
.add(AesBuilder.builder())
|
||||||
|
// configured by KEM
|
||||||
|
.add(KEMAesParametersBuilder.builder().withKey(recipient).withCipherType(cipher).withAesMode(mode)
|
||||||
|
.withAAD(aad))
|
||||||
|
// build!
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (InputStream encryptedStream = encrypted.getStream();
|
||||||
|
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
|
||||||
|
encryptedStream.transferTo(fileOut);
|
||||||
|
System.err.println("Encryption complete. Output saved to: " + outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a file using the specified private key and AES configuration.
|
||||||
|
*
|
||||||
|
* @param recipient recipient's private key
|
||||||
|
* @param mode AES key mode
|
||||||
|
* @param cipher AES cipher type
|
||||||
|
* @param aad optional Additional Authenticated Data (may be empty)
|
||||||
|
* @param inputPath path to the encrypted file
|
||||||
|
* @param outputPath path where the decrypted file will be written
|
||||||
|
* @throws IOException if reading or writing files fails
|
||||||
|
*/
|
||||||
|
private static void performDecryption(final PrivateKey recipient, final AesMode mode, final AesCipherType cipher,
|
||||||
|
final byte[] aad, final String inputPath, final String outputPath) throws IOException {
|
||||||
|
|
||||||
|
DataContent decrypted = DataContentChainBuilder.decrypt()
|
||||||
|
// input file
|
||||||
|
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL()))
|
||||||
|
// encrypted by KEM
|
||||||
|
.add(KEMAesParametersBuilder.builder().withKey(recipient).withCipherType(cipher).withAesMode(mode)
|
||||||
|
.withAAD(aad))
|
||||||
|
// AES process
|
||||||
|
.add(AesBuilder.builder())
|
||||||
|
// build!
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (InputStream decryptedStream = decrypted.getStream();
|
||||||
|
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
|
||||||
|
decryptedStream.transferTo(fileOut);
|
||||||
|
System.err.println("Decryption complete. Output saved to: " + outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a public key for the given username from the keystore.
|
||||||
|
*
|
||||||
|
* @param db the loaded keystore
|
||||||
|
* @param username username to fetch the public key for
|
||||||
|
* @return the public key
|
||||||
|
* @throws NoSuchElementException if user not found or key construction fails
|
||||||
|
*/
|
||||||
|
private static PublicKey getPublicKey(final UniversalKeyStoreFile db, final String username) {
|
||||||
|
try {
|
||||||
|
return db.loadPublicKey(username);
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
LOG.logp(Level.FINE, "KEMAes", "getPublicKey", "Exception", e);
|
||||||
|
|
||||||
|
throw new NoSuchElementException("unknown user <" + username + ">", e);
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException | IOException e) {
|
||||||
|
LOG.logp(Level.FINE, "KEMAes", "getPublicKey", "Exception", e);
|
||||||
|
|
||||||
|
throw new NoSuchElementException("cannot construct public key for <" + username + ">", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
186
app/src/main/java/zeroecho/KeyStoreManagement.java
Normal file
186
app/src/main/java/zeroecho/KeyStoreManagement.java
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.CommandLine;
|
||||||
|
import org.apache.commons.cli.CommandLineParser;
|
||||||
|
import org.apache.commons.cli.DefaultParser;
|
||||||
|
import org.apache.commons.cli.Option;
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.apache.commons.cli.ParseException;
|
||||||
|
|
||||||
|
import zeroecho.util.KeyPairAlgorithm;
|
||||||
|
import zeroecho.util.UniversalKeyStoreFile;
|
||||||
|
import zeroecho.util.X509Support;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for managing a simple file-based keystore via command-line
|
||||||
|
* interface (CLI).
|
||||||
|
* <p>
|
||||||
|
* This class provides a static {@code main} method that enables two primary
|
||||||
|
* operations:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Importing a public key from a PEM-encoded X.509 certificate</li>
|
||||||
|
* <li>Generating and storing a key pair (public/private) for a specified
|
||||||
|
* owner</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* The keystore is backed by a flat file managed by
|
||||||
|
* {@link UniversalKeyStoreFile}, and keys are stored using symbolic owner
|
||||||
|
* identifiers.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The following command-line options are supported:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code -k, --keystore <file>}: Path to the keystore file (required)</li>
|
||||||
|
* <li>{@code -o, --owner <name>}: Owner name used as the key identifier
|
||||||
|
* (required)</li>
|
||||||
|
* <li>{@code -c, --cert <pem-file>}: Path to a PEM-encoded X.509 certificate
|
||||||
|
* file</li>
|
||||||
|
* <li>{@code -g, --generate}: Flag indicating that a key pair should be
|
||||||
|
* generated</li>
|
||||||
|
* <li>{@code -a, --algorithm <type>}: Algorithm and key size (e.g.,
|
||||||
|
* {@code RSA:2048}, {@code EC:256}). Defaults to {@code RSA:2048}</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* At least one of {@code --cert} or {@code --generate} must be specified.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This class is not meant to be instantiated.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Leo Galambos
|
||||||
|
*/
|
||||||
|
public final class KeyStoreManagement {
|
||||||
|
private static final Logger LOG = Logger.getLogger(KeyStoreManagement.class.getName());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to prevent instantiation of this utility class.
|
||||||
|
*/
|
||||||
|
private KeyStoreManagement() {
|
||||||
|
// Utility class; prevent instantiation.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for executing keystore operations via CLI.
|
||||||
|
*
|
||||||
|
* @param args the command-line arguments to parse
|
||||||
|
* @param options the Apache Commons CLI {@link Options} object to populate with
|
||||||
|
* supported options
|
||||||
|
* @return exit code: {@code 0} on success, {@code 2} on I/O error or processing
|
||||||
|
* failure
|
||||||
|
* @throws ParseException if argument parsing fails
|
||||||
|
* @throws NoSuchAlgorithmException if the specified key generation
|
||||||
|
* algorithm is unavailable
|
||||||
|
* @throws NoSuchProviderException if the Bouncy Castle provider is
|
||||||
|
* not registered
|
||||||
|
* @throws InvalidAlgorithmParameterException if the specified key size of
|
||||||
|
* EC-algorithms is invalid
|
||||||
|
*/
|
||||||
|
public static int main(String[] args, final Options options) throws ParseException, NoSuchAlgorithmException,
|
||||||
|
NoSuchProviderException, InvalidAlgorithmParameterException {
|
||||||
|
final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("file")
|
||||||
|
.desc("Path to keystore file").required().build();
|
||||||
|
|
||||||
|
final Option OWNER_OPTION = Option.builder("o").longOpt("owner").hasArg().argName("name")
|
||||||
|
.desc("Owner name for key").required().build();
|
||||||
|
|
||||||
|
final Option CERT_OPTION = Option.builder("c").longOpt("cert").hasArg().argName("pem-file")
|
||||||
|
.desc("PEM certificate file to extract public key").build();
|
||||||
|
|
||||||
|
final Option GENERATE_OPTION = Option.builder("g").longOpt("generate").desc("Generate key pair for the owner")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final Option ALGORITHM_OPTION = Option.builder("a").longOpt("algorithm").hasArg().argName("type")
|
||||||
|
.desc("Key pair algorithm (e.g., RSA:4096, EC:256). Default is RSA:2048.").build();
|
||||||
|
|
||||||
|
options.addOption(KEYSTORE_OPTION);
|
||||||
|
options.addOption(OWNER_OPTION);
|
||||||
|
options.addOption(CERT_OPTION);
|
||||||
|
options.addOption(GENERATE_OPTION);
|
||||||
|
options.addOption(ALGORITHM_OPTION);
|
||||||
|
|
||||||
|
final CommandLineParser parser = new DefaultParser();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final CommandLine cmd = parser.parse(options, args);
|
||||||
|
|
||||||
|
final String keystorePath = cmd.getOptionValue(KEYSTORE_OPTION);
|
||||||
|
final String owner = cmd.getOptionValue(OWNER_OPTION);
|
||||||
|
final String certPath = cmd.getOptionValue(CERT_OPTION);
|
||||||
|
final boolean generate = cmd.hasOption(GENERATE_OPTION);
|
||||||
|
|
||||||
|
if (certPath == null && !generate) {
|
||||||
|
throw new ParseException("You must specify either --cert or --generate");
|
||||||
|
}
|
||||||
|
|
||||||
|
final UniversalKeyStoreFile keystore = new UniversalKeyStoreFile(Paths.get(keystorePath));
|
||||||
|
|
||||||
|
if (certPath != null) {
|
||||||
|
final Certificate cert = X509Support.loadCertificate(certPath);
|
||||||
|
keystore.addPubKey(owner, cert.getPublicKey());
|
||||||
|
LOG.log(Level.INFO, "Public key added for owner: {0}", owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generate) {
|
||||||
|
final String algorithmString = cmd.getOptionValue(ALGORITHM_OPTION, "RSA:2048");
|
||||||
|
final KeyPair keyPair = KeyPairAlgorithm.fromString(algorithmString).generateKeyPair();
|
||||||
|
keystore.addPubKey(owner, keyPair.getPublic());
|
||||||
|
keystore.addPrivKey(owner, keyPair.getPrivate());
|
||||||
|
LOG.log(Level.INFO, "Key pair generated and added for owner: {0}", owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (final IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Unexpected error", e);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
361
app/src/main/java/zeroecho/MultiRecipientAes.java
Normal file
361
app/src/main/java/zeroecho/MultiRecipientAes.java
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.CommandLine;
|
||||||
|
import org.apache.commons.cli.CommandLineParser;
|
||||||
|
import org.apache.commons.cli.DefaultParser;
|
||||||
|
import org.apache.commons.cli.Option;
|
||||||
|
import org.apache.commons.cli.OptionGroup;
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.apache.commons.cli.ParseException;
|
||||||
|
|
||||||
|
import zeroecho.builder.AesBuilder;
|
||||||
|
import zeroecho.builder.AesRandomBuilder;
|
||||||
|
import zeroecho.builder.DataContentChainBuilder;
|
||||||
|
import zeroecho.builder.MultiRecipientCryptorBuilder;
|
||||||
|
import zeroecho.builder.PlainFileBuilder;
|
||||||
|
import zeroecho.data.DataContent;
|
||||||
|
import zeroecho.util.KeyPairAlgorithm;
|
||||||
|
import zeroecho.util.UniversalKeyStoreFile;
|
||||||
|
import zeroecho.util.aes.AesCipherType;
|
||||||
|
import zeroecho.util.aes.AesMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for multi-recipient AES encryption and decryption of files.
|
||||||
|
* <p>
|
||||||
|
* This class provides a command-line interface (CLI) entry point to encrypt or
|
||||||
|
* decrypt files using AES with multiple recipients’ public keys or a single
|
||||||
|
* private key from a keystore.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* Supported command-line options:
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>-e / --encrypt <inputFile></b>: File to encrypt.</li>
|
||||||
|
* <li><b>-d / --decrypt <inputFile></b>: File to decrypt.</li>
|
||||||
|
* <li><b>-k / --keystore <keystoreFile></b>: Path to keystore file
|
||||||
|
* (required).</li>
|
||||||
|
* <li><b>-p / --pubkeys <usernames></b>: List of recipient usernames for
|
||||||
|
* encryption.</li>
|
||||||
|
* <li><b>-s / --privkey <username></b>: Username with private key for
|
||||||
|
* decryption.</li>
|
||||||
|
* <li><b>-o / --output <outputFile></b>: Output file path (optional;
|
||||||
|
* defaults to inputFile.enc or inputFile.dec).</li>
|
||||||
|
* <li><b>-m / --mode <aesMode></b>: AES mode (AES-128, AES-192, AES-256).
|
||||||
|
* Default is AES-256.</li>
|
||||||
|
* <li><b>-c / --cipher <cipherType></b>: Cipher transformation
|
||||||
|
* (AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding). Default is
|
||||||
|
* AES/CBC/PKCS7Padding.</li>
|
||||||
|
* <li><b>-x / --decoys <algorithms></b>: List of decoy key algorithms to
|
||||||
|
* include (e.g., RSA:2048, EC:256).</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* This is a utility class and cannot be instantiated.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Leo Galambos
|
||||||
|
*/
|
||||||
|
public final class MultiRecipientAes {
|
||||||
|
private static final Logger LOG = Logger.getLogger(MultiRecipientAes.class.getName());
|
||||||
|
|
||||||
|
private MultiRecipientAes() {
|
||||||
|
// Utility class; prevent instantiation.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses command-line arguments and executes either encryption or decryption
|
||||||
|
* accordingly.
|
||||||
|
* <p>
|
||||||
|
* Encryption requires:
|
||||||
|
* <ul>
|
||||||
|
* <li>Option {@code -e} with input file path to encrypt.</li>
|
||||||
|
* <li>Option {@code -p} with one or more recipient usernames whose public keys
|
||||||
|
* are used.</li>
|
||||||
|
* <li>Option {@code -k} with the keystore file containing recipient keys.</li>
|
||||||
|
* </ul>
|
||||||
|
* Decryption requires:
|
||||||
|
* <ul>
|
||||||
|
* <li>Option {@code -d} with input file path to decrypt.</li>
|
||||||
|
* <li>Option {@code -s} with the username whose private key will decrypt the
|
||||||
|
* file.</li>
|
||||||
|
* <li>Option {@code -k} with the keystore file containing the private key.</li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* Optional parameters include the output file path, AES mode, and cipher type.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param args Command-line arguments specifying operation mode and
|
||||||
|
* parameters.
|
||||||
|
* @param options Pre-configured Apache Commons CLI {@link Options} object to
|
||||||
|
* which this method will add supported options.
|
||||||
|
* @return Exit code {@code 0} if operation was successful; {@code 1} if an
|
||||||
|
* error occurred.
|
||||||
|
* @throws ParseException If parsing the command-line arguments fails due to
|
||||||
|
* invalid or missing options.
|
||||||
|
*/
|
||||||
|
public static int main(final String[] args, final Options options) throws ParseException { // NOPMD
|
||||||
|
// Define and register CLI options
|
||||||
|
final Option ENCRYPT_OPTION = Option.builder("e").longOpt("encrypt").hasArg().argName("inputFile")
|
||||||
|
.desc("Encrypt the given file").build();
|
||||||
|
final Option DECRYPT_OPTION = Option.builder("d").longOpt("decrypt").hasArg().argName("inputFile")
|
||||||
|
.desc("Decrypt the given file").build();
|
||||||
|
final Option OUTPUT_OPTION = Option.builder("o").longOpt("output").hasArg().argName("outputFile")
|
||||||
|
.desc("Output file path (default: inputFile + .enc or .dec)").build();
|
||||||
|
final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("keystoreFile")
|
||||||
|
.desc("File with keys (ZeroEcho's *.keystore)").required().build();
|
||||||
|
final Option PUB_OPTION = Option.builder("p").longOpt("pubkeys").hasArgs().argName("list of usernames")
|
||||||
|
.desc("Recipients' usernames list (in keystore)").build();
|
||||||
|
final Option PRIV_OPTION = Option.builder("s").longOpt("privkey").hasArgs().argName("username")
|
||||||
|
.desc("Username with private key for decryption (in keystore)").build();
|
||||||
|
final Option MODE_OPTION = Option.builder("m").longOpt("mode").hasArg().argName("aesMode")
|
||||||
|
.desc("AES mode: AES-128, AES-192, AES-256 (default: AES-256)").build();
|
||||||
|
final Option CIPHER_TYPE_OPTION = Option.builder("c").longOpt("cipher").hasArg().argName("cipherType").desc(
|
||||||
|
"Cipher type: AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding (default: AES/CBC/PKCS7Padding)")
|
||||||
|
.build();
|
||||||
|
final Option DECOY_OPTION = Option.builder("x").longOpt("decoys").hasArgs().argName("keyAlgorithms")
|
||||||
|
.desc("List of decoy key algorithms (e.g., RSA:2048, EC:256)").build();
|
||||||
|
|
||||||
|
final OptionGroup OPERATION_GROUP = new OptionGroup();
|
||||||
|
OPERATION_GROUP.addOption(ENCRYPT_OPTION);
|
||||||
|
OPERATION_GROUP.addOption(DECRYPT_OPTION);
|
||||||
|
OPERATION_GROUP.setRequired(true);
|
||||||
|
|
||||||
|
options.addOptionGroup(OPERATION_GROUP);
|
||||||
|
options.addOption(OUTPUT_OPTION);
|
||||||
|
options.addOption(KEYSTORE_OPTION);
|
||||||
|
options.addOption(PUB_OPTION);
|
||||||
|
options.addOption(PRIV_OPTION);
|
||||||
|
options.addOption(MODE_OPTION);
|
||||||
|
options.addOption(CIPHER_TYPE_OPTION);
|
||||||
|
options.addOption(DECOY_OPTION);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final CommandLineParser parser = new DefaultParser();
|
||||||
|
final CommandLine cmd = parser.parse(options, args);
|
||||||
|
|
||||||
|
final boolean isEncryption = cmd.hasOption(ENCRYPT_OPTION.getOpt());
|
||||||
|
final boolean isDecryption = cmd.hasOption(DECRYPT_OPTION.getOpt());
|
||||||
|
|
||||||
|
final String inputPath = cmd.getOptionValue(ENCRYPT_OPTION.getOpt(),
|
||||||
|
cmd.getOptionValue(DECRYPT_OPTION.getOpt()));
|
||||||
|
final String outputPath = cmd.getOptionValue(OUTPUT_OPTION, inputPath + (isEncryption ? ".enc" : ".dec"));
|
||||||
|
final String keystorePath = cmd.getOptionValue(KEYSTORE_OPTION);
|
||||||
|
final AesMode mode = AesMode.fromString(cmd.getOptionValue(MODE_OPTION, "AES-256"));
|
||||||
|
final AesCipherType cipher = AesCipherType
|
||||||
|
.fromString(cmd.getOptionValue(CIPHER_TYPE_OPTION, "AES/CBC/PKCS7Padding"));
|
||||||
|
|
||||||
|
final UniversalKeyStoreFile db = new UniversalKeyStoreFile(keystorePath);
|
||||||
|
|
||||||
|
if (isEncryption) {
|
||||||
|
final String[] pub = cmd.getOptionValues(PUB_OPTION);
|
||||||
|
if (pub == null) {
|
||||||
|
throw new ParseException("Option is required for encryption: " + PUB_OPTION.getOpt() + " ("
|
||||||
|
+ PUB_OPTION.getLongOpt() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Collection<PublicKey> recipient = getPublicKeys(db, pub);
|
||||||
|
final Collection<PublicKey> decoy = cmd.hasOption(DECOY_OPTION)
|
||||||
|
? getDecoyKeys(cmd.getOptionValues(DECOY_OPTION))
|
||||||
|
: new HashSet<>();
|
||||||
|
|
||||||
|
performEncryption(recipient, decoy, mode, cipher, inputPath, outputPath);
|
||||||
|
} else if (isDecryption) {
|
||||||
|
final String priv = cmd.getOptionValue(PRIV_OPTION);
|
||||||
|
if (priv == null) {
|
||||||
|
throw new ParseException("Option is required for decryption: " + PRIV_OPTION.getOpt() + " ("
|
||||||
|
+ PRIV_OPTION.getLongOpt() + ")");
|
||||||
|
}
|
||||||
|
performDecryption(db.loadPrivateKey(priv), mode, cipher, inputPath, outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
|
||||||
|
if (LOG.isLoggable(Level.SEVERE)) {
|
||||||
|
LOG.log(Level.SEVERE, "Error during encryption/decryption: " + e.toString());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts the input file for multiple recipients using AES and writes the
|
||||||
|
* encrypted data to output file.
|
||||||
|
*
|
||||||
|
* @param recipient Collection of recipients' public keys to encrypt the AES
|
||||||
|
* key.
|
||||||
|
* @param decoy Collection of recipients' public keys (decoys) to encrypt
|
||||||
|
* the false AES key.
|
||||||
|
* @param mode AES mode (e.g., AES-128, AES-256).
|
||||||
|
* @param cipher Cipher type (e.g., AES/CBC/PKCS7Padding).
|
||||||
|
* @param inputPath Path to the plaintext input file.
|
||||||
|
* @param outputPath Path where the encrypted output file will be saved.
|
||||||
|
* @throws IOException If file reading or writing fails.
|
||||||
|
*/
|
||||||
|
private static void performEncryption(final Collection<PublicKey> recipient, final Collection<PublicKey> decoy,
|
||||||
|
final AesMode mode, final AesCipherType cipher, final String inputPath, final String outputPath)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
DataContent encrypted = DataContentChainBuilder.encrypt()
|
||||||
|
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL())).add(AesBuilder.builder())
|
||||||
|
.add(MultiRecipientCryptorBuilder.builder().addRecipients(recipient).addDecoys(decoy))
|
||||||
|
.add(AesRandomBuilder.builder().mode(mode).cipherType(cipher)).build();
|
||||||
|
|
||||||
|
try (InputStream encryptedStream = encrypted.getStream();
|
||||||
|
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
|
||||||
|
encryptedStream.transferTo(fileOut);
|
||||||
|
System.err.println("Encryption complete. Output saved to: " + outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the input file using the specified recipient's private key and
|
||||||
|
* writes the plaintext to output file.
|
||||||
|
*
|
||||||
|
* @param recipient Private key of the recipient used to unwrap AES key and
|
||||||
|
* decrypt the file.
|
||||||
|
* @param mode AES mode (e.g., AES-128, AES-256).
|
||||||
|
* @param cipher Cipher type (e.g., AES/CBC/PKCS7Padding).
|
||||||
|
* @param inputPath Path to the encrypted input file.
|
||||||
|
* @param outputPath Path where the decrypted output file will be saved.
|
||||||
|
* @throws IOException If file reading or writing fails.
|
||||||
|
*/
|
||||||
|
private static void performDecryption(final PrivateKey recipient, final AesMode mode, final AesCipherType cipher,
|
||||||
|
final String inputPath, final String outputPath) throws IOException {
|
||||||
|
|
||||||
|
DataContent decrypted = DataContentChainBuilder.decrypt()
|
||||||
|
.add(PlainFileBuilder.builder().url(Path.of(inputPath).toUri().toURL()))
|
||||||
|
.add(AesRandomBuilder.builder().mode(mode).cipherType(cipher))
|
||||||
|
.add(MultiRecipientCryptorBuilder.builder().privateKey(recipient)).add(AesBuilder.builder()).build();
|
||||||
|
|
||||||
|
try (InputStream decryptedStream = decrypted.getStream();
|
||||||
|
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
|
||||||
|
decryptedStream.transferTo(fileOut);
|
||||||
|
System.err.println("Decryption complete. Output saved to: " + outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a collection of public keys for the specified usernames from the
|
||||||
|
* keystore.
|
||||||
|
*
|
||||||
|
* @param db The {@link UniversalKeyStoreFile} instance representing the
|
||||||
|
* keystore.
|
||||||
|
* @param usernames The list of usernames whose public keys should be fetched.
|
||||||
|
* @return Collection of {@link PublicKey} instances corresponding to the given
|
||||||
|
* usernames.
|
||||||
|
* @throws NoSuchElementException If any username is not found or their public
|
||||||
|
* key cannot be constructed.
|
||||||
|
*/
|
||||||
|
private static Collection<PublicKey> getPublicKeys(final UniversalKeyStoreFile db, final String... usernames) {
|
||||||
|
final Collection<PublicKey> set = new HashSet<>();
|
||||||
|
for (String user : usernames) {
|
||||||
|
try {
|
||||||
|
set.add(db.loadPublicKey(user));
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
LOG.logp(Level.FINE, "MultiRecipientAes", "getPublicKeys", "Exception", e);
|
||||||
|
|
||||||
|
throw new NoSuchElementException("unknown user <" + user + ">", e);
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidKeySpecException | IOException e) {
|
||||||
|
LOG.logp(Level.FINE, "MultiRecipientAes", "getPublicKeys", "Exception", e);
|
||||||
|
|
||||||
|
throw new NoSuchElementException("cannot construct public key for <" + user + ">", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a collection of public keys based on the specified decoy key
|
||||||
|
* algorithms.
|
||||||
|
* <p>
|
||||||
|
* Each algorithm string should correspond to a recognized
|
||||||
|
* {@link KeyPairAlgorithm} format, such as "RSA:2048" or "EC:256". For each
|
||||||
|
* algorithm string provided, this method attempts to generate a new key pair
|
||||||
|
* and collect the public key.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* These keys can be used as decoys in multi-recipient encryption schemes to
|
||||||
|
* enhance security by adding plausible but non-functional recipients.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param decoyAlgorithms an array of strings specifying the key pair algorithms
|
||||||
|
* for decoy key generation (e.g., "RSA:2048", "EC:256")
|
||||||
|
* @return a {@link Collection} of generated {@link PublicKey} instances
|
||||||
|
* corresponding to the decoy algorithms provided
|
||||||
|
* @throws NoSuchElementException if any of the specified algorithms is invalid
|
||||||
|
* or if key generation fails for any algorithm,
|
||||||
|
* wrapping the underlying cause
|
||||||
|
*/
|
||||||
|
private static Collection<PublicKey> getDecoyKeys(final String... decoyAlgorithms) {
|
||||||
|
final Collection<PublicKey> set = new HashSet<>();
|
||||||
|
for (String algStr : decoyAlgorithms) {
|
||||||
|
try {
|
||||||
|
final KeyPairAlgorithm alg = KeyPairAlgorithm.fromString(algStr);
|
||||||
|
final KeyPair pair = alg.generateKeyPair();
|
||||||
|
set.add(pair.getPublic());
|
||||||
|
} catch (IllegalArgumentException | NoSuchAlgorithmException | NoSuchProviderException
|
||||||
|
| InvalidAlgorithmParameterException e) {
|
||||||
|
LOG.log(Level.FINE, "Failed to generate decoy key for algorithm: {0}", algStr);
|
||||||
|
|
||||||
|
throw new NoSuchElementException("cannot construct key pair for <" + algStr + ">", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
}
|
||||||
294
app/src/main/java/zeroecho/PasswordBasedAes.java
Normal file
294
app/src/main/java/zeroecho/PasswordBasedAes.java
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import java.io.Console;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.CommandLine;
|
||||||
|
import org.apache.commons.cli.CommandLineParser;
|
||||||
|
import org.apache.commons.cli.DefaultParser;
|
||||||
|
import org.apache.commons.cli.Option;
|
||||||
|
import org.apache.commons.cli.OptionGroup;
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.apache.commons.cli.ParseException;
|
||||||
|
|
||||||
|
import zeroecho.data.DataContent;
|
||||||
|
import zeroecho.data.processing.PasswordBasedAesDecryptor;
|
||||||
|
import zeroecho.data.processing.PasswordBasedAesEncryptor;
|
||||||
|
import zeroecho.data.processing.PlainFile;
|
||||||
|
import zeroecho.util.aes.AesCipherType;
|
||||||
|
import zeroecho.util.aes.AesMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class providing AES encryption and decryption based on a password
|
||||||
|
* using PBKDF2 key derivation. This class exposes a command-line interface
|
||||||
|
* (CLI) entry point for encrypting and decrypting files with configurable
|
||||||
|
* options.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The supported command-line parameters are as follows:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>-e / --encrypt <inputFile></b>: Specifies the file to
|
||||||
|
* encrypt.</li>
|
||||||
|
* <li><b>-d / --decrypt <inputFile></b>: Specifies the file to
|
||||||
|
* decrypt.</li>
|
||||||
|
* <li><b>-p / --password <password></b>: Password for
|
||||||
|
* encryption/decryption.</li>
|
||||||
|
* <li><b>-o / --output <outputFile></b>: Output file path (optional,
|
||||||
|
* defaults to input file with .enc or .dec extension).</li>
|
||||||
|
* <li><b>-m / --mode <aesMode></b>: AES key size mode (AES-128, AES-192,
|
||||||
|
* AES-256). Defaults to AES-256.</li>
|
||||||
|
* <li><b>-c / --cipher <cipherType></b>: Cipher transformation
|
||||||
|
* (AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding). Defaults to
|
||||||
|
* AES/CBC/PKCS7Padding.</li>
|
||||||
|
* <li><b>-n / --iterations <iterations></b>: PBKDF2 iteration count.
|
||||||
|
* Defaults to 100,000.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This class cannot be instantiated.
|
||||||
|
*/
|
||||||
|
public final class PasswordBasedAes {
|
||||||
|
|
||||||
|
private static final Logger LOG = Logger.getLogger(PasswordBasedAes.class.getName());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to prevent instantiation.
|
||||||
|
*/
|
||||||
|
private PasswordBasedAes() {
|
||||||
|
// Utility class; prevent instantiation.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main method that parses command-line arguments and performs encryption or
|
||||||
|
* decryption.
|
||||||
|
*
|
||||||
|
* @param args Command-line arguments.
|
||||||
|
* @param options Predefined CLI options to configure.
|
||||||
|
* @return Exit code: 0 if successful, 1 if an error occurred.
|
||||||
|
* @throws ParseException If argument parsing fails.
|
||||||
|
*/
|
||||||
|
public static int main(final String[] args, final Options options) throws ParseException {
|
||||||
|
// Define and register CLI options
|
||||||
|
final Option ENCRYPT_OPTION = Option.builder("e").longOpt("encrypt").hasArg().argName("inputFile")
|
||||||
|
.desc("Encrypt the given file").build();
|
||||||
|
final Option DECRYPT_OPTION = Option.builder("d").longOpt("decrypt").hasArg().argName("inputFile")
|
||||||
|
.desc("Decrypt the given file").build();
|
||||||
|
final Option PASSWORD_OPTION = Option.builder("p").longOpt("password").hasArg().argName("password")
|
||||||
|
.desc("Password used for encryption/decryption").build();
|
||||||
|
final Option OUTPUT_OPTION = Option.builder("o").longOpt("output").hasArg().argName("outputFile")
|
||||||
|
.desc("Output file path (default: inputFile + .enc or .dec)").build();
|
||||||
|
final Option MODE_OPTION = Option.builder("m").longOpt("mode").hasArg().argName("aesMode")
|
||||||
|
.desc("AES mode: AES-128, AES-192, AES-256 (default: AES-256)").build();
|
||||||
|
final Option CIPHER_TYPE_OPTION = Option.builder("c").longOpt("cipher").hasArg().argName("cipherType").desc(
|
||||||
|
"Cipher type: AES/CBC/PKCS7Padding, AES/GCM/NoPadding, AES/CTR/NoPadding (default: AES/CBC/PKCS7Padding)")
|
||||||
|
.build();
|
||||||
|
final Option ITER_OPTION = Option.builder("n").longOpt("iterations").hasArg().argName("iterations")
|
||||||
|
.desc("PBKDF2 iteration count (default: 100000)").build();
|
||||||
|
final Option AAD_OPTION = Option.builder("a").longOpt("aad").hasArg().argName("aadHex")
|
||||||
|
.desc("Additional Authenticated Data (AAD) as hex string (optional)").build();
|
||||||
|
|
||||||
|
final OptionGroup OPERATION_GROUP = new OptionGroup();
|
||||||
|
OPERATION_GROUP.addOption(ENCRYPT_OPTION);
|
||||||
|
OPERATION_GROUP.addOption(DECRYPT_OPTION);
|
||||||
|
OPERATION_GROUP.setRequired(true);
|
||||||
|
|
||||||
|
options.addOptionGroup(OPERATION_GROUP);
|
||||||
|
options.addOption(PASSWORD_OPTION);
|
||||||
|
options.addOption(OUTPUT_OPTION);
|
||||||
|
options.addOption(MODE_OPTION);
|
||||||
|
options.addOption(CIPHER_TYPE_OPTION);
|
||||||
|
options.addOption(ITER_OPTION);
|
||||||
|
options.addOption(AAD_OPTION);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final CommandLineParser parser = new DefaultParser();
|
||||||
|
final CommandLine cmd = parser.parse(options, args);
|
||||||
|
|
||||||
|
final boolean isEncryption = cmd.hasOption(ENCRYPT_OPTION.getOpt());
|
||||||
|
final boolean isDecryption = cmd.hasOption(DECRYPT_OPTION.getOpt());
|
||||||
|
|
||||||
|
final String inputPath = cmd.getOptionValue(ENCRYPT_OPTION.getOpt(),
|
||||||
|
cmd.getOptionValue(DECRYPT_OPTION.getOpt()));
|
||||||
|
final String outputPath = cmd.getOptionValue(OUTPUT_OPTION, inputPath + (isEncryption ? ".enc" : ".dec"));
|
||||||
|
final AesMode mode = AesMode.fromString(cmd.getOptionValue(MODE_OPTION, "AES-256"));
|
||||||
|
final AesCipherType cipher = AesCipherType
|
||||||
|
.fromString(cmd.getOptionValue(CIPHER_TYPE_OPTION, "AES/CBC/PKCS7Padding"));
|
||||||
|
final int iterations = Integer.parseInt(cmd.getOptionValue(ITER_OPTION, "100000"));
|
||||||
|
|
||||||
|
String password = cmd.getOptionValue(PASSWORD_OPTION);
|
||||||
|
if (password == null) {
|
||||||
|
password = promptForPassword(isEncryption);
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] aad = cmd.hasOption(AAD_OPTION.getOpt())
|
||||||
|
? HexFormat.of().parseHex(cmd.getOptionValue(AAD_OPTION.getOpt()))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final DataContent fileIn = new PlainFile(Path.of(inputPath).toUri().toURL());
|
||||||
|
|
||||||
|
if (isEncryption) {
|
||||||
|
performEncryption(password, iterations, aad, mode, cipher, fileIn, outputPath);
|
||||||
|
} else if (isDecryption) {
|
||||||
|
performDecryption(password, aad, mode, cipher, fileIn, outputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
|
||||||
|
if (LOG.isLoggable(Level.SEVERE)) {
|
||||||
|
LOG.log(Level.SEVERE, "Error during encryption/decryption: {0}", e.toString());
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompts the user to enter a password (optionally with confirmation).
|
||||||
|
*
|
||||||
|
* @param confirm Whether to ask for password confirmation (encryption mode).
|
||||||
|
* @return The confirmed password as a string.
|
||||||
|
* @throws ParseException If user input is invalid or confirmation fails.
|
||||||
|
* @throws IOException If the system console is not available.
|
||||||
|
*/
|
||||||
|
private static String promptForPassword(final boolean confirm) throws ParseException, IOException { // NOPMD
|
||||||
|
final Console console = System.console();
|
||||||
|
if (console == null) {
|
||||||
|
throw new IOException("No console available for password input. Please use the --password option.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final char[] pwdArray;
|
||||||
|
if (confirm) {
|
||||||
|
pwdArray = console.readPassword("Enter password for encryption: ");
|
||||||
|
final char[] confirmArray = console.readPassword("Confirm password: ");
|
||||||
|
if (pwdArray == null || confirmArray == null || pwdArray.length == 0) {
|
||||||
|
throw new ParseException("No password entered.");
|
||||||
|
}
|
||||||
|
if (!Arrays.equals(pwdArray, confirmArray)) {
|
||||||
|
throw new ParseException("Passwords do not match.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pwdArray = console.readPassword("Enter password for decryption: ");
|
||||||
|
if (pwdArray == null || pwdArray.length == 0) {
|
||||||
|
throw new ParseException("No password entered.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String password = new String(pwdArray);
|
||||||
|
Arrays.fill(pwdArray, ' '); // Clear sensitive data
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts a file using password-based AES encryption with the specified
|
||||||
|
* parameters.
|
||||||
|
*
|
||||||
|
* @param password the password used for key derivation; must not be
|
||||||
|
* {@code null}
|
||||||
|
* @param iterations the number of PBKDF2 iterations to use; must be positive
|
||||||
|
* @param aad additional authenticated data (AAD) for authenticated
|
||||||
|
* encryption modes; may be {@code null}
|
||||||
|
* @param mode the AES mode specifying key length (e.g., AES-128,
|
||||||
|
* AES-256); must not be {@code null}
|
||||||
|
* @param cipher the AES cipher configuration (e.g., CBC, GCM); must not be
|
||||||
|
* {@code null}
|
||||||
|
* @param fileIn the input data content representing the plaintext file;
|
||||||
|
* must not be {@code null}
|
||||||
|
* @param outputPath the path to write the encrypted output file; must not be
|
||||||
|
* {@code null} or empty
|
||||||
|
* @throws IOException if an I/O error occurs during reading or
|
||||||
|
* writing files
|
||||||
|
* @throws GeneralSecurityException if an error occurs during encryption or key
|
||||||
|
* derivation
|
||||||
|
*/
|
||||||
|
private static void performEncryption(final String password, final int iterations, final byte[] aad,
|
||||||
|
final AesMode mode, final AesCipherType cipher, final DataContent fileIn, final String outputPath)
|
||||||
|
throws IOException, GeneralSecurityException {
|
||||||
|
final PasswordBasedAesEncryptor encryptor = new PasswordBasedAesEncryptor(password, iterations, aad, mode,
|
||||||
|
cipher);
|
||||||
|
encryptor.setInput(fileIn);
|
||||||
|
|
||||||
|
try (InputStream encryptedStream = encryptor.getStream();
|
||||||
|
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
|
||||||
|
encryptedStream.transferTo(fileOut);
|
||||||
|
System.err.println("Encryption complete. Output saved to: " + outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts a file using password-based AES decryption with the specified
|
||||||
|
* parameters.
|
||||||
|
*
|
||||||
|
* @param password the password used for key derivation; must not be
|
||||||
|
* {@code null}
|
||||||
|
* @param aad additional authenticated data (AAD) for authenticated
|
||||||
|
* encryption modes; may be {@code null}
|
||||||
|
* @param mode the AES mode specifying key length (e.g., AES-128,
|
||||||
|
* AES-256); must not be {@code null}
|
||||||
|
* @param cipher the AES cipher configuration (e.g., CBC, GCM); must not be
|
||||||
|
* {@code null}
|
||||||
|
* @param fileIn the input data content representing the encrypted file;
|
||||||
|
* must not be {@code null}
|
||||||
|
* @param outputPath the path to write the decrypted output file; must not be
|
||||||
|
* {@code null} or empty
|
||||||
|
* @throws IOException if an I/O error occurs during reading or
|
||||||
|
* writing files
|
||||||
|
* @throws GeneralSecurityException if an error occurs during decryption or key
|
||||||
|
* derivation
|
||||||
|
*/
|
||||||
|
private static void performDecryption(final String password, final byte[] aad, final AesMode mode,
|
||||||
|
final AesCipherType cipher, final DataContent fileIn, final String outputPath)
|
||||||
|
throws IOException, GeneralSecurityException {
|
||||||
|
final PasswordBasedAesDecryptor decryptor = new PasswordBasedAesDecryptor(password, aad, mode, cipher);
|
||||||
|
decryptor.setInput(fileIn);
|
||||||
|
|
||||||
|
try (InputStream decryptedStream = decryptor.getStream();
|
||||||
|
OutputStream fileOut = Files.newOutputStream(Paths.get(outputPath))) {
|
||||||
|
decryptedStream.transferTo(fileOut);
|
||||||
|
System.err.println("Decryption complete. Output saved to: " + outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
208
app/src/main/java/zeroecho/ZeroEcho.java
Normal file
208
app/src/main/java/zeroecho/ZeroEcho.java
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.CommandLineParser;
|
||||||
|
import org.apache.commons.cli.DefaultParser;
|
||||||
|
import org.apache.commons.cli.HelpFormatter;
|
||||||
|
import org.apache.commons.cli.MissingOptionException;
|
||||||
|
import org.apache.commons.cli.Option;
|
||||||
|
import org.apache.commons.cli.OptionGroup;
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.apache.commons.cli.ParseException;
|
||||||
|
|
||||||
|
import zeroecho.util.BouncyCastleActivator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ZeroEcho is a command-line utility for managing asymmetric keys and
|
||||||
|
* certificates, primarily focusing on Certificate Authority (CA) operations
|
||||||
|
* such as issuing, revoking, and listing certificates.
|
||||||
|
* <p>
|
||||||
|
* It supports command-line options for asymmetric key management, including
|
||||||
|
* issuing certificates from CSRs or subject names, revoking certificates, and
|
||||||
|
* retrieving all certificates for a user.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* This class initializes Bouncy Castle security provider and uses Apache
|
||||||
|
* Commons CLI for command-line parsing.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author Leo Galambos
|
||||||
|
*/
|
||||||
|
public class ZeroEcho { // NOPMD by Leo Galambos on 6/1/25, 1:04 PM
|
||||||
|
/**
|
||||||
|
* Logger instance for the {@code ZeroEcho} class used to log messages and
|
||||||
|
* events.
|
||||||
|
* <p>
|
||||||
|
* This logger is configured with the name of the {@code ZeroEcho} class,
|
||||||
|
* allowing for fine-grained logging control specific to this class.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public static final Logger LOG = Logger.getLogger(ZeroEcho.class.getName());
|
||||||
|
|
||||||
|
static {
|
||||||
|
BouncyCastleActivator.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor for ZeroEcho.
|
||||||
|
* <p>
|
||||||
|
* This constructor does not perform any initialization since all operations are
|
||||||
|
* handled via static methods and blocks.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private ZeroEcho() {
|
||||||
|
// No initialization needed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for the ZeroEcho application. Parses command-line arguments
|
||||||
|
* and dispatches to the appropriate subcommand for asymmetric key management or
|
||||||
|
* prints the help message.
|
||||||
|
*
|
||||||
|
* @param args command-line arguments passed to the program
|
||||||
|
*/
|
||||||
|
public static void main(final String[] args) {
|
||||||
|
final int errorCode = mainProcess(args);
|
||||||
|
|
||||||
|
if (errorCode == 0) {
|
||||||
|
System.out.println("OK");
|
||||||
|
} else {
|
||||||
|
System.out.println("ERR: " + errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point for the ZeroEcho application. Parses command-line arguments and
|
||||||
|
* dispatches to the appropriate subcommand for asymmetric key management or
|
||||||
|
* prints the help message.
|
||||||
|
*
|
||||||
|
* @param args command-line arguments passed to the program
|
||||||
|
* @return error-code
|
||||||
|
*/
|
||||||
|
public static int mainProcess(final String[] args) { // NOPMD by Leo Galambos on 6/11/25, 10:25 PM
|
||||||
|
final Option ASYMETRIC_OPTION = Option.builder("A").longOpt("asm").desc("asymetric keys management").build();
|
||||||
|
final Option KEM_OPTION = Option.builder("E").longOpt("kem").desc("KEM encryption/decryption").build();
|
||||||
|
final Option MULTI_AES_OPTION = Option.builder("M").longOpt("multi-aes").desc("AES for multiple recipients")
|
||||||
|
.build();
|
||||||
|
final Option KEYSTORE_OPTION = Option.builder("K").longOpt("ksm").desc("key store management").build();
|
||||||
|
final Option AES_PSW_OPTION = Option.builder("P").longOpt("aes-psw").desc("AES with password").build();
|
||||||
|
|
||||||
|
final OptionGroup OPERATION_GROUP = new OptionGroup();
|
||||||
|
OPERATION_GROUP.addOption(ASYMETRIC_OPTION);
|
||||||
|
OPERATION_GROUP.addOption(AES_PSW_OPTION);
|
||||||
|
OPERATION_GROUP.addOption(MULTI_AES_OPTION);
|
||||||
|
OPERATION_GROUP.addOption(KEYSTORE_OPTION);
|
||||||
|
OPERATION_GROUP.addOption(KEM_OPTION);
|
||||||
|
OPERATION_GROUP.setRequired(true); // At least one required
|
||||||
|
|
||||||
|
Options options = new Options();
|
||||||
|
options.addOptionGroup(OPERATION_GROUP);
|
||||||
|
|
||||||
|
final CommandLineParser parser = new DefaultParser();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// parse the command line arguments (allow remaining arguments for subcommands)
|
||||||
|
parser.parse(options, args, true);
|
||||||
|
|
||||||
|
switch (OPERATION_GROUP.getSelected()) {
|
||||||
|
case "A" -> {
|
||||||
|
return AsymetricKeysManagement.main(args, options = new Options().addOption(ASYMETRIC_OPTION));
|
||||||
|
}
|
||||||
|
case "E" -> {
|
||||||
|
return KEMAes.main(args, options = new Options().addOption(KEM_OPTION));
|
||||||
|
}
|
||||||
|
case "P" -> {
|
||||||
|
return PasswordBasedAes.main(args, options = new Options().addOption(AES_PSW_OPTION));
|
||||||
|
}
|
||||||
|
case "M" -> {
|
||||||
|
return MultiRecipientAes.main(args, options = new Options().addOption(MULTI_AES_OPTION));
|
||||||
|
}
|
||||||
|
case "K" -> {
|
||||||
|
return KeyStoreManagement.main(args, options = new Options().addOption(KEYSTORE_OPTION));
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (MissingOptionException ex) {
|
||||||
|
if (LOG.isLoggable(Level.SEVERE)) {
|
||||||
|
LOG.log(Level.SEVERE, ex.getMessage());
|
||||||
|
}
|
||||||
|
return help(options);
|
||||||
|
|
||||||
|
} catch (ParseException ex) {
|
||||||
|
if (LOG.isLoggable(Level.WARNING)) {
|
||||||
|
LOG.log(Level.WARNING, "Unexpected exception", ex.getMessage());
|
||||||
|
}
|
||||||
|
return help(options);
|
||||||
|
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
|
||||||
|
LOG.logp(Level.WARNING, "ZeroEcho", "mainProcess", e.getMessage(), e);
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
LOG.log(Level.INFO, "Completed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints the usage help message for the command-line interface of the ZeroEcho
|
||||||
|
* application.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This method automatically generates and displays a help statement based on
|
||||||
|
* the provided command-line {@link Options}. It then terminates the program
|
||||||
|
* with exit status {@code 1}.
|
||||||
|
*
|
||||||
|
* @param options The {@link Options} instance defining the available
|
||||||
|
* command-line options.
|
||||||
|
* @return always {@code 1}
|
||||||
|
*/
|
||||||
|
protected static int help(final Options options) {
|
||||||
|
// automatically generate the help statement
|
||||||
|
final HelpFormatter formatter = new HelpFormatter();
|
||||||
|
formatter.printHelp(ZeroEcho.class.getName(), options);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
208
app/src/test/java/zeroecho/AsymetricKeysManagementTest.java
Normal file
208
app/src/test/java/zeroecho/AsymetricKeysManagementTest.java
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyPairGenerator;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.apache.commons.cli.ParseException;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
|
||||||
|
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import zeroecho.util.BouncyCastleActivator;
|
||||||
|
|
||||||
|
class AsymetricKeysManagementTest {
|
||||||
|
|
||||||
|
static {
|
||||||
|
BouncyCastleActivator.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private static final String USERNAME = "realUser";
|
||||||
|
private static final String SUBJECT_DN = "CN=Real Subject, O=RealOrg";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIssueWithRealCSR() throws Exception {
|
||||||
|
System.out.println("Running testIssueWithRealCSR...");
|
||||||
|
Path csrFile = generateCSRFile(SUBJECT_DN);
|
||||||
|
|
||||||
|
String[] args = { "-d", tempDir.toString(), "-u", USERNAME, "-i", csrFile.toString() };
|
||||||
|
Options options = new Options();
|
||||||
|
AsymetricKeysManagement.main(args, options);
|
||||||
|
|
||||||
|
// Validate output (e.g., check that a certificate was generated) - it is
|
||||||
|
// printed out to STDOUT only for now
|
||||||
|
// Path certPath = tempDir.resolve(USERNAME + ".crt");
|
||||||
|
// assertTrue(Files.exists(certPath), "Certificate should be created");
|
||||||
|
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testIssueWithGeneratedKeypair() throws Exception {
|
||||||
|
System.out.println("Running testIssueWithGeneratedKeypair...");
|
||||||
|
String[] args = { "-d", tempDir.toString(), "-u", USERNAME, "-s", SUBJECT_DN, "-i" };
|
||||||
|
Options options = new Options();
|
||||||
|
AsymetricKeysManagement.main(args, options);
|
||||||
|
|
||||||
|
// Validate output (certificate and private key) - it is printed out to STDOUT
|
||||||
|
// only for now
|
||||||
|
// Path certPath = tempDir.resolve(USERNAME + ".crt");
|
||||||
|
// Path keyPath = tempDir.resolve(USERNAME + ".key");
|
||||||
|
// assertTrue(Files.exists(certPath), "Certificate should be created");
|
||||||
|
// assertTrue(Files.exists(keyPath), "Private key should be created");
|
||||||
|
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRevokeRealCertificate() throws Exception {
|
||||||
|
System.out.println("Running testRevokeRealCertificate...");
|
||||||
|
|
||||||
|
// Generate key pair and CSR within this test
|
||||||
|
KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
|
||||||
|
X500Name subject = new X500Name(SUBJECT_DN);
|
||||||
|
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
|
||||||
|
PKCS10CertificationRequest csr = new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic())
|
||||||
|
.build(signer);
|
||||||
|
|
||||||
|
// Save CSR to file within tempDir
|
||||||
|
Path csrFile = tempDir.resolve("tempRevokeTest.csr");
|
||||||
|
try (JcaPEMWriter pemWriter = new JcaPEMWriter(Files.newBufferedWriter(csrFile))) {
|
||||||
|
pemWriter.writeObject(csr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue certificate
|
||||||
|
String[] issueArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-i", csrFile.toString() };
|
||||||
|
Options issueOptions = new Options();
|
||||||
|
AsymetricKeysManagement.main(issueArgs, issueOptions);
|
||||||
|
|
||||||
|
// Since ZeroEcho likely prints the cert but does not store it, we simulate
|
||||||
|
// loading it back
|
||||||
|
// Let's generate a self-signed cert to match the issued one (or ideally
|
||||||
|
// ZeroEcho should return it)
|
||||||
|
// Here we re-use the CSR to simulate a real certificate loading
|
||||||
|
X509Certificate issuedCert = generateSelfSignedCertificate(subject, keyPair);
|
||||||
|
|
||||||
|
// Save the issued cert to a temporary file
|
||||||
|
Path certFile = tempDir.resolve("tempIssuedCert.pem");
|
||||||
|
try (JcaPEMWriter pemWriter = new JcaPEMWriter(Files.newBufferedWriter(certFile))) {
|
||||||
|
pemWriter.writeObject(issuedCert);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now revoke the issued certificate
|
||||||
|
String[] revokeArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-r", certFile.toString() };
|
||||||
|
Options revokeOptions = new Options();
|
||||||
|
AsymetricKeysManagement.main(revokeArgs, revokeOptions);
|
||||||
|
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509Certificate generateSelfSignedCertificate(X500Name subject, KeyPair keyPair) throws Exception {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Date notBefore = new Date(now - 1000L * 60);
|
||||||
|
Date notAfter = new Date(now + (1000L * 60 * 60 * 24));
|
||||||
|
|
||||||
|
BigInteger serial = BigInteger.valueOf(now);
|
||||||
|
|
||||||
|
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
|
||||||
|
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(subject, serial, notBefore, notAfter,
|
||||||
|
subject, keyPair.getPublic());
|
||||||
|
|
||||||
|
return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.getCertificate(certBuilder.build(contentSigner));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetAllCertificates() throws Exception {
|
||||||
|
System.out.println("Running testGetAllCertificates...");
|
||||||
|
// Issue certificate first
|
||||||
|
String[] issueArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-s", SUBJECT_DN, "-i" };
|
||||||
|
AsymetricKeysManagement.main(issueArgs, new Options());
|
||||||
|
|
||||||
|
// Get all certificates for USERNAME
|
||||||
|
String[] getAllArgs = { "-d", tempDir.toString(), "-u", USERNAME, "-a" };
|
||||||
|
AsymetricKeysManagement.main(getAllArgs, new Options());
|
||||||
|
|
||||||
|
// (Optional) You might want to capture System.out and validate contents
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMissingUsernameThrows() {
|
||||||
|
System.out.println("Running testMissingUsernameThrows...");
|
||||||
|
String[] args = { "-d", tempDir.toString(), "-s", SUBJECT_DN, "-i" };
|
||||||
|
Options options = new Options();
|
||||||
|
assertThrows(ParseException.class, () -> AsymetricKeysManagement.main(args, options));
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: Generate a real CSR PEM file for testing.
|
||||||
|
*/
|
||||||
|
private Path generateCSRFile(String subjectDN) throws Exception {
|
||||||
|
KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
|
||||||
|
X500Name subject = new X500Name(subjectDN);
|
||||||
|
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
|
||||||
|
PKCS10CertificationRequest csr = new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic())
|
||||||
|
.build(signer);
|
||||||
|
|
||||||
|
Path csrFile = tempDir.resolve("test.csr");
|
||||||
|
try (JcaPEMWriter pemWriter = new JcaPEMWriter(Files.newBufferedWriter(csrFile))) {
|
||||||
|
pemWriter.writeObject(csr);
|
||||||
|
}
|
||||||
|
return csrFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
156
app/src/test/java/zeroecho/KEMAesTest.java
Normal file
156
app/src/test/java/zeroecho/KEMAesTest.java
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import zeroecho.util.BouncyCastleActivator;
|
||||||
|
import zeroecho.util.KeyPairAlgorithm;
|
||||||
|
import zeroecho.util.UniversalKeyStoreFile;
|
||||||
|
|
||||||
|
class KEMAesTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private Path keystorePath;
|
||||||
|
private UniversalKeyStoreFile keystore;
|
||||||
|
|
||||||
|
private static final int DATA_SIZE = 8 * 1024;
|
||||||
|
private static final int TOTAL_USERS = 10;
|
||||||
|
private Map<String, KeyPair> kemUsers;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void initializeCrypto() {
|
||||||
|
BouncyCastleActivator.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setupKeystore() throws Exception {
|
||||||
|
System.out.println();
|
||||||
|
keystorePath = tempDir.resolve("kem_test.keystore");
|
||||||
|
keystore = new UniversalKeyStoreFile(keystorePath);
|
||||||
|
kemUsers = new HashMap<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < TOTAL_USERS; i++) {
|
||||||
|
String username = "user-" + i + "-" + UUID.randomUUID();
|
||||||
|
String[] args = { "-k", keystorePath.toString(), "-o", username, "-g", "-a",
|
||||||
|
KeyPairAlgorithm.KYBER_512.toString() };
|
||||||
|
|
||||||
|
int exit = KeyStoreManagement.main(args, new Options());
|
||||||
|
assertEquals(0, exit, "Key generation failed for " + username);
|
||||||
|
|
||||||
|
KeyPair kp = new KeyPair(keystore.loadPublicKey(username), keystore.loadPrivateKey(username));
|
||||||
|
kemUsers.put(username, kp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testKEMAesEncryptionDecryption() throws Exception {
|
||||||
|
System.out.println("testKEMAesEncryptionDecryption");
|
||||||
|
|
||||||
|
// Write random input
|
||||||
|
Path inputFile = tempDir.resolve("plain_input.dat");
|
||||||
|
byte[] inputContent = new byte[DATA_SIZE];
|
||||||
|
new Random().nextBytes(inputContent);
|
||||||
|
Files.write(inputFile, inputContent);
|
||||||
|
|
||||||
|
// Encrypt using the first user's public key
|
||||||
|
String encryptUser = kemUsers.keySet().iterator().next();
|
||||||
|
Path encryptedFile = tempDir.resolve("encrypted_output.dat");
|
||||||
|
|
||||||
|
System.out.println("Encrypting as recipient: " + encryptUser);
|
||||||
|
|
||||||
|
String[] encryptArgs = { "-e", inputFile.toString(), "-k", keystorePath.toString(), "-p", encryptUser, "-o",
|
||||||
|
encryptedFile.toString() };
|
||||||
|
|
||||||
|
int encryptExit = KEMAes.main(encryptArgs, new Options());
|
||||||
|
assertEquals(0, encryptExit, "Encryption failed");
|
||||||
|
assertTrue(Files.exists(encryptedFile), "Encrypted file not created");
|
||||||
|
|
||||||
|
// Decrypt using corresponding private key
|
||||||
|
Path decryptedFile = tempDir.resolve("decrypted_output.dat");
|
||||||
|
|
||||||
|
String[] decryptArgs = { "-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s", encryptUser, "-o",
|
||||||
|
decryptedFile.toString() };
|
||||||
|
|
||||||
|
System.out.println("Decrypting as recipient: " + encryptUser);
|
||||||
|
|
||||||
|
int decryptExit = KEMAes.main(decryptArgs, new Options());
|
||||||
|
assertEquals(0, decryptExit, "Decryption failed");
|
||||||
|
assertTrue(Files.exists(decryptedFile), "Decrypted file not created");
|
||||||
|
|
||||||
|
byte[] decrypted = Files.readAllBytes(decryptedFile);
|
||||||
|
assertArrayEquals(inputContent, decrypted, "Decrypted content does not match");
|
||||||
|
|
||||||
|
// Attempt decryption with wrong private key (should fail)
|
||||||
|
List<String> others = kemUsers.keySet().stream().filter(k -> !k.equals(encryptUser))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
assertFalse(others.isEmpty(), "No alternative user for invalid decryption test");
|
||||||
|
for (String wrongUser : others) {
|
||||||
|
Path wrongOutput = tempDir.resolve("wrong_decrypt_output.dat");
|
||||||
|
|
||||||
|
String[] wrongDecryptArgs = { "-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s",
|
||||||
|
wrongUser, "-o", wrongOutput.toString() };
|
||||||
|
|
||||||
|
System.out.println("Attempting decryption as wrong user: " + wrongUser);
|
||||||
|
|
||||||
|
int wrongDecryptExit = KEMAes.main(wrongDecryptArgs, new Options());
|
||||||
|
assertEquals(1, wrongDecryptExit, "Expected decryption failure with wrong key");
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("...test completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
130
app/src/test/java/zeroecho/KeyStoreManagementTest.java
Normal file
130
app/src/test/java/zeroecho/KeyStoreManagementTest.java
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import zeroecho.util.BouncyCastleActivator;
|
||||||
|
import zeroecho.util.KeyPairAlgorithm;
|
||||||
|
import zeroecho.util.CryptoAlgorithmsNames;
|
||||||
|
import zeroecho.util.UniversalKeyStoreFile;
|
||||||
|
|
||||||
|
class KeyStoreManagementTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private static final Map<String, KeyPairAlgorithm> ownerToAlgorithm = new HashMap<>();
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void setupCryptoProvider() {
|
||||||
|
BouncyCastleActivator.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGenerateAndVerifyAllKeyPairAlgorithms() throws Exception {
|
||||||
|
System.out.println("testGenerateAndVerifyAllKeyPairAlgorithms");
|
||||||
|
|
||||||
|
Path keystorePath = tempDir.resolve("test-keystore.txt");
|
||||||
|
List<String> argsList = new ArrayList<>();
|
||||||
|
Options options = new Options();
|
||||||
|
|
||||||
|
for (KeyPairAlgorithm algorithm : KeyPairAlgorithm.SERIALIZABLE_ALGORITHMS) {
|
||||||
|
if (algorithm == KeyPairAlgorithm.ELGAMAL_2048) {
|
||||||
|
// too slow => skip over
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (algorithm.getAlgorithmName().equals(CryptoAlgorithmsNames.FRODO.displayName())) {
|
||||||
|
// unsupported
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String owner = "owner_" + UUID.randomUUID();
|
||||||
|
ownerToAlgorithm.put(owner, algorithm);
|
||||||
|
|
||||||
|
System.out.println("...generate new user " + owner + " with " + algorithm.toString());
|
||||||
|
|
||||||
|
argsList.clear();
|
||||||
|
argsList.add("--keystore");
|
||||||
|
argsList.add(keystorePath.toString());
|
||||||
|
argsList.add("--owner");
|
||||||
|
argsList.add(owner);
|
||||||
|
argsList.add("--generate");
|
||||||
|
argsList.add("--algorithm");
|
||||||
|
argsList.add(algorithm.toString());
|
||||||
|
|
||||||
|
int result = KeyStoreManagement.main(argsList.toArray(new String[0]), options);
|
||||||
|
assertEquals(0, result, "KeyStoreManagement should return 0 for success");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify contents of keystore
|
||||||
|
UniversalKeyStoreFile keystore = new UniversalKeyStoreFile(keystorePath);
|
||||||
|
|
||||||
|
for (Map.Entry<String, KeyPairAlgorithm> entry : ownerToAlgorithm.entrySet()) {
|
||||||
|
String owner = entry.getKey();
|
||||||
|
|
||||||
|
System.out.println("...loading keys for " + owner + " algorithm(" + entry.getValue() + ")");
|
||||||
|
|
||||||
|
PublicKey publicKey = keystore.loadPublicKey(owner);
|
||||||
|
PrivateKey privateKey = keystore.loadPrivateKey(owner);
|
||||||
|
|
||||||
|
assertNotNull(publicKey, "Public key should be present for " + owner);
|
||||||
|
assertNotNull(privateKey, "Private key should be present for " + owner);
|
||||||
|
|
||||||
|
// Optional: Check algorithm consistency
|
||||||
|
assertEquals(entry.getValue().getAlgorithmName(), CryptoAlgorithmsNames.fromString(publicKey.getAlgorithm()).displayName(),
|
||||||
|
"Public key algorithm mismatch for " + owner);
|
||||||
|
assertEquals(entry.getValue().getAlgorithmName(), CryptoAlgorithmsNames.fromString(privateKey.getAlgorithm()).displayName(),
|
||||||
|
"Private key algorithm mismatch for " + owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
}
|
||||||
203
app/src/test/java/zeroecho/MultiRecipientAesTest.java
Normal file
203
app/src/test/java/zeroecho/MultiRecipientAesTest.java
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import zeroecho.util.BouncyCastleActivator;
|
||||||
|
import zeroecho.util.UniversalKeyStoreFile;
|
||||||
|
|
||||||
|
class MultiRecipientAesTest {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private Path keystorePath;
|
||||||
|
private UniversalKeyStoreFile keystore;
|
||||||
|
|
||||||
|
private static final int DATA_SIZE = 16 * 1024;
|
||||||
|
private static final int TOTAL_OWNERS = 20;
|
||||||
|
private static final int RECIPIENTS_COUNT = 10;
|
||||||
|
private Map<String, KeyPair> ownerKeyPairs;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void setupCryptoProvider() {
|
||||||
|
BouncyCastleActivator.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setupKeystore() throws Exception {
|
||||||
|
System.out.println("");
|
||||||
|
|
||||||
|
keystorePath = tempDir.resolve("test.keystore");
|
||||||
|
keystore = new UniversalKeyStoreFile(keystorePath);
|
||||||
|
|
||||||
|
ownerKeyPairs = new HashMap<>();
|
||||||
|
|
||||||
|
// Use KeyStoreManagement to generate 20 RSA:2048 key pairs for random owners
|
||||||
|
Options options = new Options();
|
||||||
|
for (int i = 0; i < TOTAL_OWNERS; i++) {
|
||||||
|
String owner = "owner-" + i + "-" + UUID.randomUUID();
|
||||||
|
|
||||||
|
System.out.println("");
|
||||||
|
|
||||||
|
String[] args = { "-k", keystorePath.toString(), "-o", owner, "-g", "-a", "RSA:2048" };
|
||||||
|
|
||||||
|
int exitCode = KeyStoreManagement.main(args, options);
|
||||||
|
assertEquals(0, exitCode, "Key generation failed for owner " + owner);
|
||||||
|
|
||||||
|
// Reload keys after each addition
|
||||||
|
KeyPair kp = new KeyPair(keystore.loadPublicKey(owner), keystore.loadPrivateKey(owner));
|
||||||
|
ownerKeyPairs.put(owner, kp);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEncryptDecryptWithMultiRecipientAes() throws Exception {
|
||||||
|
System.out.println("testEncryptDecryptWithMultiRecipientAes");
|
||||||
|
|
||||||
|
// Prepare test file with random data
|
||||||
|
Path inputFile = tempDir.resolve("plaintext.dat");
|
||||||
|
byte[] originalContent = new byte[DATA_SIZE]; // 1 KB random data
|
||||||
|
|
||||||
|
System.out.println("...preparing test data, size=" + DATA_SIZE + " bytes");
|
||||||
|
|
||||||
|
new Random().nextBytes(originalContent);
|
||||||
|
Files.write(inputFile, originalContent);
|
||||||
|
|
||||||
|
// Pick 10 random owners for encryption recipients
|
||||||
|
List<String> allOwners = new ArrayList<>(ownerKeyPairs.keySet());
|
||||||
|
Collections.shuffle(allOwners);
|
||||||
|
List<String> recipientOwners = allOwners.subList(0, RECIPIENTS_COUNT);
|
||||||
|
|
||||||
|
System.out.println("...encrypting for " + RECIPIENTS_COUNT + " recipients");
|
||||||
|
|
||||||
|
// Encrypt with public keys of the 10 recipients
|
||||||
|
Path encryptedFile = tempDir.resolve("encrypted.dat");
|
||||||
|
|
||||||
|
List<String> encryptArgs = new ArrayList<>();
|
||||||
|
encryptArgs.add("-e");
|
||||||
|
encryptArgs.add(inputFile.toString());
|
||||||
|
|
||||||
|
encryptArgs.add("-k");
|
||||||
|
encryptArgs.add(keystorePath.toString());
|
||||||
|
|
||||||
|
encryptArgs.add("-p");
|
||||||
|
encryptArgs.addAll(recipientOwners);
|
||||||
|
|
||||||
|
encryptArgs.add("-o");
|
||||||
|
encryptArgs.add(encryptedFile.toString());
|
||||||
|
|
||||||
|
int encryptExit = MultiRecipientAes.main(encryptArgs.toArray(new String[0]), new Options());
|
||||||
|
assertEquals(0, encryptExit, "Encryption failed");
|
||||||
|
|
||||||
|
assertTrue(Files.exists(encryptedFile), "Encrypted file not created");
|
||||||
|
|
||||||
|
// Pick one owner from recipients to decrypt
|
||||||
|
String decryptOwner = recipientOwners.get(0);
|
||||||
|
Path decryptedFile = tempDir.resolve("decrypted.dat");
|
||||||
|
|
||||||
|
System.out.println("...testing decryption for the user " + decryptOwner);
|
||||||
|
|
||||||
|
List<String> decryptArgs = List.of("-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s",
|
||||||
|
decryptOwner, "-o", decryptedFile.toString());
|
||||||
|
|
||||||
|
int decryptExit = MultiRecipientAes.main(decryptArgs.toArray(new String[0]), new Options());
|
||||||
|
assertEquals(0, decryptExit, "Decryption failed for recipient owner");
|
||||||
|
|
||||||
|
System.out.println("...verifying decrypted content matches original");
|
||||||
|
|
||||||
|
// Verify decrypted content matches original
|
||||||
|
byte[] decryptedContent = Files.readAllBytes(decryptedFile);
|
||||||
|
assertArrayEquals(originalContent, decryptedContent, "Decrypted content does not match original");
|
||||||
|
|
||||||
|
// Pick owner NOT in recipients and test that decryption produces incorrect
|
||||||
|
// output or fails
|
||||||
|
List<String> nonRecipients = allOwners.stream().filter(o -> !recipientOwners.contains(o))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
assertFalse(nonRecipients.isEmpty(), "No non-recipient owners to test");
|
||||||
|
|
||||||
|
String nonRecipientOwner = nonRecipients.get(0);
|
||||||
|
Path decryptedWrongFile = tempDir.resolve("decrypted_wrong.dat");
|
||||||
|
|
||||||
|
System.out.println("...testing whether decryption fails for the user " + nonRecipientOwner
|
||||||
|
+ " who was not amongst recipients");
|
||||||
|
|
||||||
|
List<String> decryptWrongArgs = List.of("-d", encryptedFile.toString(), "-k", keystorePath.toString(), "-s",
|
||||||
|
nonRecipientOwner, "-o", decryptedWrongFile.toString());
|
||||||
|
|
||||||
|
int wrongDecryptExit = MultiRecipientAes.main(decryptWrongArgs.toArray(new String[0]), new Options());
|
||||||
|
// Decryption may or may not fail gracefully, but output should not match
|
||||||
|
// original content
|
||||||
|
assertEquals(1, wrongDecryptExit, "Decryption with non-recipient owner failed unexpectedly");
|
||||||
|
|
||||||
|
// The decryptedWrongFile should NOT exist because decryption failed (no output
|
||||||
|
// file created)
|
||||||
|
assertFalse(Files.exists(decryptedWrongFile), "Decrypted file should NOT exist for non-recipient owner");
|
||||||
|
|
||||||
|
// Trying to read should throw NoSuchFileException
|
||||||
|
assertThrows(NoSuchFileException.class, () -> {
|
||||||
|
Files.readAllBytes(decryptedWrongFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
}
|
||||||
135
app/src/test/java/zeroecho/PasswordBasedAesTest.java
Normal file
135
app/src/test/java/zeroecho/PasswordBasedAesTest.java
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.apache.commons.cli.Options;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import zeroecho.util.BouncyCastleActivator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link PasswordBasedAes} using its static CLI-based main
|
||||||
|
* method.
|
||||||
|
*/
|
||||||
|
class PasswordBasedAesTest {
|
||||||
|
|
||||||
|
static {
|
||||||
|
BouncyCastleActivator.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private static final String PASSWORD = "secure-password";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that encryption followed by decryption restores the original plain
|
||||||
|
* text.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testEncryptAndDecryptShortText() throws Exception {
|
||||||
|
String content = "This is a simple secret message.";
|
||||||
|
Path inputFile = writeTempFile("short.txt", content);
|
||||||
|
Path encryptedFile = tempDir.resolve("short.txt.enc");
|
||||||
|
Path decryptedFile = tempDir.resolve("short.txt.dec");
|
||||||
|
|
||||||
|
runEncryption(inputFile, encryptedFile);
|
||||||
|
runDecryption(encryptedFile, decryptedFile);
|
||||||
|
|
||||||
|
String decrypted = Files.readString(decryptedFile, StandardCharsets.UTF_8);
|
||||||
|
assertEquals(content, decrypted.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests encryption/decryption of a longer file to ensure streaming works.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testEncryptAndDecryptLongFile() throws Exception {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (int i = 0; i < 10000; i++) {
|
||||||
|
builder.append("Line ").append(i).append(": This is a long test line.\n");
|
||||||
|
}
|
||||||
|
String longText = builder.toString();
|
||||||
|
|
||||||
|
Path inputFile = writeTempFile("long.txt", longText);
|
||||||
|
Path encryptedFile = tempDir.resolve("long.txt.enc");
|
||||||
|
Path decryptedFile = tempDir.resolve("long.txt.dec");
|
||||||
|
|
||||||
|
runEncryption(inputFile, encryptedFile);
|
||||||
|
runDecryption(encryptedFile, decryptedFile);
|
||||||
|
|
||||||
|
String decrypted = Files.readString(decryptedFile, StandardCharsets.UTF_8);
|
||||||
|
assertEquals(longText, decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that a nonexistent input file causes no crash (is caught internally).
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testNonexistentInputHandledGracefully() {
|
||||||
|
String[] args = { "-e", "nonexistent.txt", "-p", PASSWORD, "-o", "out.bin" };
|
||||||
|
assertDoesNotThrow(() -> PasswordBasedAes.main(args, new Options()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper methods ---
|
||||||
|
|
||||||
|
private Path writeTempFile(String name, String content) throws IOException {
|
||||||
|
Path file = tempDir.resolve(name);
|
||||||
|
Files.writeString(file, content, StandardCharsets.UTF_8);
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runEncryption(Path input, Path output) throws Exception {
|
||||||
|
String[] args = { "-e", input.toString(), "-p", PASSWORD, "-o", output.toString() };
|
||||||
|
PasswordBasedAes.main(args, new Options());
|
||||||
|
assertTrue(Files.exists(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runDecryption(Path input, Path output) throws Exception {
|
||||||
|
String[] args = { "-d", input.toString(), "-p", PASSWORD, "-o", output.toString() };
|
||||||
|
PasswordBasedAes.main(args, new Options());
|
||||||
|
assertTrue(Files.exists(output));
|
||||||
|
}
|
||||||
|
}
|
||||||
74
app/src/test/java/zeroecho/ZeroEchoTest.java
Normal file
74
app/src/test/java/zeroecho/ZeroEchoTest.java
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ZeroEchoTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAsymetricOptionWithoutParamsReturnsOne() {
|
||||||
|
System.out.println("testAsymetricOptionWithoutParamsReturnsOne");
|
||||||
|
int result = ZeroEcho.mainProcess(new String[] { "-A" });
|
||||||
|
assertEquals(1, result, "Asymetric option without parameters should return 1 (error/help)");
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAesPswOptionWithoutParamsReturnsOne() {
|
||||||
|
System.out.println("testAesPswOptionWithoutParamsReturnsOne");
|
||||||
|
int result = ZeroEcho.mainProcess(new String[] { "-P" });
|
||||||
|
assertEquals(1, result, "AES-PSW option without parameters should return 1 (error/help)");
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNoOptionReturnsOne() {
|
||||||
|
System.out.println("testNoOptionReturnsOne");
|
||||||
|
int result = ZeroEcho.mainProcess(new String[] {});
|
||||||
|
assertEquals(1, result, "No options should return 1 (error/help)");
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInvalidOptionReturnsOne() {
|
||||||
|
System.out.println("testInvalidOptionReturnsOne");
|
||||||
|
int result = ZeroEcho.mainProcess(new String[] { "-X" });
|
||||||
|
assertEquals(1, result, "Invalid option should return 1 (error/help)");
|
||||||
|
System.out.println("...ok");
|
||||||
|
}
|
||||||
|
}
|
||||||
9
buildSrc/build.gradle
Normal file
9
buildSrc/build.gradle
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
plugins {
|
||||||
|
// Support convention plugins written in Groovy. Convention plugins are build scripts in 'src/main' that automatically become available as plugins in the main build.
|
||||||
|
id 'groovy-gradle-plugin'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
// Use the plugin portal to apply community plugins in convention plugins.
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
8
buildSrc/settings.gradle
Normal file
8
buildSrc/settings.gradle
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
dependencyResolutionManagement {
|
||||||
|
// Reuse version catalog from the main build.
|
||||||
|
versionCatalogs {
|
||||||
|
create('libs', { from(files("../gradle/libs.versions.toml")) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = 'buildSrc'
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* This file was generated by the Gradle 'init' task.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
// Apply the common convention plugin for shared build configuration between library and application projects.
|
||||||
|
id 'buildlogic.java-common-conventions'
|
||||||
|
|
||||||
|
// Apply the application plugin to add support for building a CLI application in Java.
|
||||||
|
id 'application'
|
||||||
|
}
|
||||||
@@ -0,0 +1,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 © 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
|
||||||
|
}
|
||||||
@@ -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
1
gradle.properties
Normal file
@@ -0,0 +1 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
|
||||||
2
gradle/libs.versions.toml
Normal file
2
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# This file was generated by the Gradle 'init' task.
|
||||||
|
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
252
gradlew
vendored
Executable file
252
gradlew
vendored
Executable file
@@ -0,0 +1,252 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
94
gradlew.bat
vendored
Normal file
94
gradlew.bat
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
26
lib/.classpath
Normal file
26
lib/.classpath
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="src" output="bin/main" path="src/main/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="main"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="main,test"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="bin/test" path="src/test/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="test"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="test"/>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="bin/test" path="src/test/resources">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="gradle_scope" value="test"/>
|
||||||
|
<attribute name="gradle_used_by_scope" value="test"/>
|
||||||
|
<attribute name="test" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
|
||||||
|
<classpathentry kind="output" path="bin/default"/>
|
||||||
|
</classpath>
|
||||||
23
lib/.project
Normal file
23
lib/.project
Normal 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
31
lib/LICENSE
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Copyright (C) 2025, Leo Galambos
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. All advertising materials mentioning features or use of this software must
|
||||||
|
display the following acknowledgement:
|
||||||
|
This product includes software developed by the Egothor project.
|
||||||
|
|
||||||
|
4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
13
lib/build.gradle
Normal file
13
lib/build.gradle
Normal 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'
|
||||||
|
}
|
||||||
93
lib/src/main/java/zeroecho/builder/AesBuilder.java
Normal file
93
lib/src/main/java/zeroecho/builder/AesBuilder.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
197
lib/src/main/java/zeroecho/builder/AesRandomBuilder.java
Normal file
197
lib/src/main/java/zeroecho/builder/AesRandomBuilder.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
lib/src/main/java/zeroecho/builder/DataContentBuilder.java
Normal file
78
lib/src/main/java/zeroecho/builder/DataContentBuilder.java
Normal 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);
|
||||||
|
}
|
||||||
158
lib/src/main/java/zeroecho/builder/DataContentChainBuilder.java
Normal file
158
lib/src/main/java/zeroecho/builder/DataContentChainBuilder.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
257
lib/src/main/java/zeroecho/builder/KEMAesParametersBuilder.java
Normal file
257
lib/src/main/java/zeroecho/builder/KEMAesParametersBuilder.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
lib/src/main/java/zeroecho/builder/PlainBytesBuilder.java
Normal file
110
lib/src/main/java/zeroecho/builder/PlainBytesBuilder.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
lib/src/main/java/zeroecho/builder/PlainFileBuilder.java
Normal file
105
lib/src/main/java/zeroecho/builder/PlainFileBuilder.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
101
lib/src/main/java/zeroecho/builder/PlainStringBuilder.java
Normal file
101
lib/src/main/java/zeroecho/builder/PlainStringBuilder.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
lib/src/main/java/zeroecho/builder/package-info.java
Normal file
89
lib/src/main/java/zeroecho/builder/package-info.java
Normal 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;
|
||||||
178
lib/src/main/java/zeroecho/covert/TextualCodec.java
Normal file
178
lib/src/main/java/zeroecho/covert/TextualCodec.java
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
166
lib/src/main/java/zeroecho/covert/jpeg/JpegExifEmbedder.java
Normal file
166
lib/src/main/java/zeroecho/covert/jpeg/JpegExifEmbedder.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
140
lib/src/main/java/zeroecho/covert/jpeg/JpegExifExtractor.java
Normal file
140
lib/src/main/java/zeroecho/covert/jpeg/JpegExifExtractor.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
93
lib/src/main/java/zeroecho/covert/jpeg/SlotType.java
Normal file
93
lib/src/main/java/zeroecho/covert/jpeg/SlotType.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
38
lib/src/main/java/zeroecho/covert/jpeg/package-info.java
Normal file
38
lib/src/main/java/zeroecho/covert/jpeg/package-info.java
Normal 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;
|
||||||
61
lib/src/main/java/zeroecho/covert/package-info.java
Normal file
61
lib/src/main/java/zeroecho/covert/package-info.java
Normal 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;
|
||||||
131
lib/src/main/java/zeroecho/data/DataContent.java
Normal file
131
lib/src/main/java/zeroecho/data/DataContent.java
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
45
lib/src/main/java/zeroecho/data/EncryptedContent.java
Normal file
45
lib/src/main/java/zeroecho/data/EncryptedContent.java
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
99
lib/src/main/java/zeroecho/data/ExportableDataContent.java
Normal file
99
lib/src/main/java/zeroecho/data/ExportableDataContent.java
Normal 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);
|
||||||
|
}
|
||||||
44
lib/src/main/java/zeroecho/data/PlainContent.java
Normal file
44
lib/src/main/java/zeroecho/data/PlainContent.java
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
44
lib/src/main/java/zeroecho/data/SecretContent.java
Normal file
44
lib/src/main/java/zeroecho/data/SecretContent.java
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
194
lib/src/main/java/zeroecho/data/output/Base64Stream.java
Normal file
194
lib/src/main/java/zeroecho/data/output/Base64Stream.java
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
lib/src/main/java/zeroecho/data/output/package-info.java
Normal file
62
lib/src/main/java/zeroecho/data/output/package-info.java
Normal 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;
|
||||||
53
lib/src/main/java/zeroecho/data/package-info.java
Normal file
53
lib/src/main/java/zeroecho/data/package-info.java
Normal 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;
|
||||||
139
lib/src/main/java/zeroecho/data/processing/AesCommon.java
Normal file
139
lib/src/main/java/zeroecho/data/processing/AesCommon.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
lib/src/main/java/zeroecho/data/processing/AesDecryptor.java
Normal file
109
lib/src/main/java/zeroecho/data/processing/AesDecryptor.java
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
lib/src/main/java/zeroecho/data/processing/AesEncryptor.java
Normal file
111
lib/src/main/java/zeroecho/data/processing/AesEncryptor.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
lib/src/main/java/zeroecho/data/processing/KEMDecryptor.java
Normal file
144
lib/src/main/java/zeroecho/data/processing/KEMDecryptor.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
174
lib/src/main/java/zeroecho/data/processing/KEMEncryptor.java
Normal file
174
lib/src/main/java/zeroecho/data/processing/KEMEncryptor.java
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
package zeroecho.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
135
lib/src/main/java/zeroecho/data/processing/PlainBytes.java
Normal file
135
lib/src/main/java/zeroecho/data/processing/PlainBytes.java
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
lib/src/main/java/zeroecho/data/processing/PlainFile.java
Normal file
110
lib/src/main/java/zeroecho/data/processing/PlainFile.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
126
lib/src/main/java/zeroecho/data/processing/PlainString.java
Normal file
126
lib/src/main/java/zeroecho/data/processing/PlainString.java
Normal 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 class’s operations.
|
||||||
|
*
|
||||||
|
* The logger is initialized with the name of the {@code PlainString} class,
|
||||||
|
* enabling targeted and organized logging output.
|
||||||
|
*/
|
||||||
|
private static final Logger LOG = Logger.getLogger(PlainString.class.getName());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plain text content represented by this instance.
|
||||||
|
*/
|
||||||
|
protected String str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a PlainString with the given string.
|
||||||
|
*
|
||||||
|
* @param str the plain text content; must not be null
|
||||||
|
*/
|
||||||
|
public PlainString(final String str) {
|
||||||
|
Objects.requireNonNull(str, "plain string must not be null");
|
||||||
|
|
||||||
|
this.str = str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the original string content.
|
||||||
|
*
|
||||||
|
* @return the string
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toText() {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* {@code PlainString} represents the start of a {@link DataContent} chain and
|
||||||
|
* therefore must not have any input. Calling this method with a
|
||||||
|
* non-{@code null} argument will result in an exception.
|
||||||
|
*
|
||||||
|
* @param input the preceding {@link DataContent}, which must be {@code null}
|
||||||
|
* @throws IllegalArgumentException if {@code input} is not {@code null}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setInput(final DataContent input) {
|
||||||
|
if (input != null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
getClass().getName() + " must be the first element in a DataContent chain; it cannot accept input");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an {@link InputStream} of the UTF-8 encoded string.
|
||||||
|
*
|
||||||
|
* @return a {@link ByteArrayInputStream}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public InputStream getStream() {
|
||||||
|
LOG.log(Level.FINE, "opening \"{0}\"", str);
|
||||||
|
return new ByteArrayInputStream(toBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the UTF-8 encoded byte array of the string.
|
||||||
|
*
|
||||||
|
* @return a byte array representation
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public byte[] toBytes() {
|
||||||
|
return str.getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
165
lib/src/main/java/zeroecho/data/processing/SecretAesRandom.java
Normal file
165
lib/src/main/java/zeroecho/data/processing/SecretAesRandom.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
148
lib/src/main/java/zeroecho/data/processing/SecretRandom.java
Normal file
148
lib/src/main/java/zeroecho/data/processing/SecretRandom.java
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
lib/src/main/java/zeroecho/data/processing/package-info.java
Normal file
79
lib/src/main/java/zeroecho/data/processing/package-info.java
Normal 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;
|
||||||
51
lib/src/main/java/zeroecho/operations/Decryption.java
Normal file
51
lib/src/main/java/zeroecho/operations/Decryption.java
Normal 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 {
|
||||||
|
}
|
||||||
56
lib/src/main/java/zeroecho/operations/Deployment.java
Normal file
56
lib/src/main/java/zeroecho/operations/Deployment.java
Normal 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 {
|
||||||
|
}
|
||||||
46
lib/src/main/java/zeroecho/operations/Encryption.java
Normal file
46
lib/src/main/java/zeroecho/operations/Encryption.java
Normal 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 {
|
||||||
|
}
|
||||||
64
lib/src/main/java/zeroecho/operations/package-info.java
Normal file
64
lib/src/main/java/zeroecho/operations/package-info.java
Normal 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;
|
||||||
106
lib/src/main/java/zeroecho/util/BouncyCastleActivator.java
Normal file
106
lib/src/main/java/zeroecho/util/BouncyCastleActivator.java
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* Copyright (C) 2025, Leo Galambos
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions are met:
|
||||||
|
*
|
||||||
|
* 1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer.
|
||||||
|
*
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
* this list of conditions and the following disclaimer in the documentation
|
||||||
|
* and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* 3. All advertising materials mentioning features or use of this software must
|
||||||
|
* display the following acknowledgement:
|
||||||
|
* This product includes software developed by the Egothor project.
|
||||||
|
*
|
||||||
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
* may be used to endorse or promote products derived from this software
|
||||||
|
* without specific prior written permission.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
* POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
******************************************************************************/
|
||||||
|
package zeroecho.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
|
||||||
|
}
|
||||||
|
}
|
||||||
217
lib/src/main/java/zeroecho/util/CryptoAlgorithmsNames.java
Normal file
217
lib/src/main/java/zeroecho/util/CryptoAlgorithmsNames.java
Normal 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"),
|
||||||
|
|
||||||
|
/** Naccache–Stern 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
286
lib/src/main/java/zeroecho/util/IOUtil.java
Normal file
286
lib/src/main/java/zeroecho/util/IOUtil.java
Normal 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:30 PM
|
||||||
|
/**
|
||||||
|
* 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:32 PM
|
||||||
|
buf[i] = (byte) (val & 0xff);
|
||||||
|
val = val >>> 8; // NOPMD by Leo Galambos on 6/1/25, 4:33 PM
|
||||||
|
}
|
||||||
|
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:35 PM
|
||||||
|
}
|
||||||
|
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:37 PM
|
||||||
|
}
|
||||||
|
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:38 PM
|
||||||
|
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:38 PM
|
||||||
|
return result & 0x7fL;
|
||||||
|
}
|
||||||
|
int i;
|
||||||
|
for (i = in.read(); i < 0x80; i = in.read()) {
|
||||||
|
result = (result << 7) | i;
|
||||||
|
}
|
||||||
|
return (result << 7) | (i & 0x7f);
|
||||||
|
}
|
||||||
|
}
|
||||||
516
lib/src/main/java/zeroecho/util/KeyPairAlgorithm.java
Normal file
516
lib/src/main/java/zeroecho/util/KeyPairAlgorithm.java
Normal 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),
|
||||||
|
|
||||||
|
/// Naccache–Stern 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
425
lib/src/main/java/zeroecho/util/KeySupport.java
Normal file
425
lib/src/main/java/zeroecho/util/KeySupport.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
182
lib/src/main/java/zeroecho/util/OutputToInputStreamAdapter.java
Normal file
182
lib/src/main/java/zeroecho/util/OutputToInputStreamAdapter.java
Normal 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:09 PM
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
147
lib/src/main/java/zeroecho/util/Password.java
Normal file
147
lib/src/main/java/zeroecho/util/Password.java
Normal 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 33–126
|
||||||
|
* @throws IllegalArgumentException if {@code length} is less than or equal to
|
||||||
|
* zero
|
||||||
|
*/
|
||||||
|
public static String generatePrintablePassword(final int length) {
|
||||||
|
if (length <= 0) {
|
||||||
|
throw new IllegalArgumentException("Password length must be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
final StringBuilder password = new StringBuilder(length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
final int ascii = RandomSupport.getRandom().nextInt('~' - '!' + 1) + '!';
|
||||||
|
password.append((char) ascii);
|
||||||
|
}
|
||||||
|
|
||||||
|
return password.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
131
lib/src/main/java/zeroecho/util/RandomSupport.java
Normal file
131
lib/src/main/java/zeroecho/util/RandomSupport.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
523
lib/src/main/java/zeroecho/util/SymmetricStreamBuilder.java
Normal file
523
lib/src/main/java/zeroecho/util/SymmetricStreamBuilder.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
265
lib/src/main/java/zeroecho/util/UniversalKeyStoreFile.java
Normal file
265
lib/src/main/java/zeroecho/util/UniversalKeyStoreFile.java
Normal 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
Reference in New Issue
Block a user